From 92b433bf9a55299fcc04cff464a5fdf1bd10ed1f Mon Sep 17 00:00:00 2001 From: ajaysi Date: Mon, 21 Apr 2025 21:07:35 +0530 Subject: [PATCH] Docs & Roadmap updates --- Getting Started/README.md | 167 + install.bat => Getting Started/install.bat | 0 setup.py => Getting Started/setup.py | 0 README.md | 488 +-- Roadmap TBDs/CONTRIBUTING.md | 202 + Roadmap TBDs/GOOGLE_INTEGRATION_PLAN.md | 2586 ++++++++++++ Roadmap TBDs/ONBOARDING_IMPROVEMENTS.md | 631 +++ Roadmap TBDs/ROADMAP.md | 250 ++ Roadmap TBDs/TWITTER_IMPLEMENTATION_PLAN.md | 4018 +++++++++++++++++++ lib/workspace/Dockerfile | 13 - lib/workspace/docker-compose.yml | 11 - 11 files changed, 8073 insertions(+), 293 deletions(-) create mode 100644 Getting Started/README.md rename install.bat => Getting Started/install.bat (100%) rename setup.py => Getting Started/setup.py (100%) create mode 100644 Roadmap TBDs/CONTRIBUTING.md create mode 100644 Roadmap TBDs/GOOGLE_INTEGRATION_PLAN.md create mode 100644 Roadmap TBDs/ONBOARDING_IMPROVEMENTS.md create mode 100644 Roadmap TBDs/ROADMAP.md create mode 100644 Roadmap TBDs/TWITTER_IMPLEMENTATION_PLAN.md delete mode 100644 lib/workspace/Dockerfile delete mode 100644 lib/workspace/docker-compose.yml diff --git a/Getting Started/README.md b/Getting Started/README.md new file mode 100644 index 00000000..6cb73585 --- /dev/null +++ b/Getting Started/README.md @@ -0,0 +1,167 @@ +## Easy Installation Guide for Content Creators + +### Step 1: Install Python 3.11 +1. Download Python 3.11 installer: + - Visit [Python 3.11.6 Download Page](https://www.python.org/downloads/release/python-3116/) + - Scroll down and click on "Windows installer (64-bit)" + - Save the file to your computer + +2. Run the installer: + - Double click the downloaded file + - βœ… IMPORTANT: Check "Add Python 3.11 to PATH" + - Click "Install Now" + - Wait for installation to complete + - Click "Close" + +### Step 2: Install ALwrity +1. Download this project: + - Click the green "Code" button above + - Select "Download ZIP" + - Extract the ZIP file to your desired location + +2. Open Command Prompt: + - Press Windows + R + - Type "cmd" and press Enter + - Navigate to the extracted folder: + ``` + cd path\to\ALwrity + ``` + +3. Run the automatic installer: + ``` + python setup.py install + ``` + +### Troubleshooting +If you encounter any issues: +1. Make sure Python 3.11 is installed correctly: + - Open Command Prompt + - Type: `python --version` + - Should show: `Python 3.11.x` + +2. Common Issues: + - If you see "Python is not recognized": Restart your computer + - If you get package errors: Run `pip install --upgrade pip` first + +Need help? [Open an issue](../../issues) and we'll assist you! + +## For Developers +If you're a developer or want to contribute: +```bash +# Clone the repository +git clone https://github.com/yourusername/ALwrity.git + +# Create virtual environment +python -m venv venv + +# Activate virtual environment +# On Windows: +.\venv\Scripts\activate +# On Mac/Linux: +source venv/bin/activate + +# Install dependencies +pip install -r requirements.txt +``` + +# ALwrity - AI Content Writing Assistant + +## Quick Start Guide for Non-Technical Users + +### Option 1: One-Click Installation (Recommended) +1. Download this project: + - Click the green "Code" button above + - Select "Download ZIP" + - Extract the ZIP file to your desired location (e.g., Desktop) + +2. Run the installer: + - Double-click `install.bat` in the extracted folder + - If Windows asks for permission, click "Yes" + - Follow the on-screen instructions + - Wait for the installation to complete + +3. Start ALwrity: + - Open Command Prompt (Windows + R, type "cmd", press Enter) + - Navigate to the ALwrity folder: + ``` + cd path\to\ALwrity + ``` + - Type `alwrity` and press Enter + +### Option 2: Manual Installation +If the one-click installer doesn't work, follow these steps: + +1. Install Python 3.11: + - Visit [Python 3.11.6 Download Page](https://www.python.org/downloads/release/python-3116/) + - Click "Windows installer (64-bit)" + - Run the installer + - βœ… IMPORTANT: Check "Add Python 3.11 to PATH" + - Click "Install Now" + - Wait for installation to complete + +2. Install ALwrity: + - Open Command Prompt (Windows + R, type "cmd", press Enter) + - Navigate to the ALwrity folder: + ``` + cd path\to\ALwrity + ``` + - Run the installation: + ``` + python setup.py install + ``` + - Follow any on-screen instructions + +3. Start ALwrity: + - In the same Command Prompt window, type: + ``` + alwrity + ``` + - Press Enter + +### Troubleshooting Guide + +#### Common Issues and Solutions: + +1. "Python is not recognized" + - Solution: Restart your computer after installing Python + - Make sure you checked "Add Python 3.11 to PATH" during installation + +2. "Visual C++ Build Tools not found" + - Solution: Run this command in an administrative PowerShell: + ``` + winget install Microsoft.VisualStudio.2022.BuildTools --silent --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" + ``` + +3. "Rust compiler not found" + - Solution: Run these commands in PowerShell: + ``` + Invoke-WebRequest -Uri https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe -OutFile rustup-init.exe + ./rustup-init.exe -y + ``` + +4. Installation Errors + - Check the `install_errors.log` file in the ALwrity folder + - Share the error message with our support team + +#### Need Help? +- Open an issue on GitHub +- Join our support community +- Contact our support team + +### System Requirements +- Windows 10 or later +- Python 3.11.x +- At least 4GB RAM +- 2GB free disk space + +### First-Time Setup +After installation: +1. The first time you run ALwrity, it will ask for your API keys +2. Follow the on-screen instructions to enter your keys +3. Your keys will be saved securely for future use + +### Updating ALwrity +To update to the latest version: +1. Download the latest release +2. Run `install.bat` again +3. Follow the on-screen instructions \ No newline at end of file diff --git a/install.bat b/Getting Started/install.bat similarity index 100% rename from install.bat rename to Getting Started/install.bat diff --git a/setup.py b/Getting Started/setup.py similarity index 100% rename from setup.py rename to Getting Started/setup.py diff --git a/README.md b/README.md index 2fdb144a..7f059b6b 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,105 @@ # Alwrity: Redefining Content Lifecycle with AI ### πŸš€ **ALwrity: Your All-in-One Content Platform** 🌟 +![Alwrity Logo](https://github.com/AJaySi/AI-Writer/blob/main/lib/workspace/alwrity_logo.png) + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) +[![Streamlit](https://img.shields.io/badge/Streamlit-1.28+-red.svg)](https://streamlit.io/) + > **NOTE** -> *Alwrity is a comprehensive content lifecycle platform tailored for content creators, digital marketers, and writers β€” no prior AI knowledge required.* +> *Alwrity is a comprehensive content lifecycle platform tailored for content creators, digital marketers, and writers β€” no prior AI knowledge required.* -Alwrity streamlines every phase of the content lifecycle, from **planning and research** to **personalized content generation**, **SEO audits**, **publishing**, and **analytics**. Our mission is to empower creators with AI-driven tools that simplify content creation while maintaining quality. +## πŸ“‹ Table of Contents -Explore the building blocks of Alwrity and contribute to shaping the future of AI-driven content creation. -> **New Release** -> [*ALwrity AI story writer App*](https://storyme.app.io/) -Learn more about our journey: [What's Alwrity Up To?](https://www.alwrity.com/post/whats-alwrity-upto) +- [Overview](#overview) +- [Key Features](#key-features) +- [System Architecture](#system-architecture) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Configuration](#configuration) +- [Usage Guide](#usage-guide) + - [AI Writers](#ai-writers) + - [SEO Tools](#seo-tools) + - [Social Media Tools](#social-media-tools) + - [Content Planning](#content-planning) +- [API Documentation](#api-documentation) +- [Contributing](#contributing) +- [Roadmap](#roadmap) +- [License](#license) +- [Acknowledgements](#acknowledgements) +--- + +## 🌟 Overview + +Alwrity streamlines every phase of the content lifecycle, from **planning and research** to **personalized content generation**, **SEO audits**, **publishing**, and **analytics**. Our mission is to empower creators with AI-driven tools that simplify content creation while maintaining quality. + +The platform integrates state-of-the-art AI technologies to provide a seamless content creation experience: + +| **Content Category** | **Technologies/Models** | +|--------------------------|-------------------------------------------| +| Text Generation Models | OpenAI, Gemini, Anthropic | +| Image Creation Tools | Stability.ai | +| Speech-to-Text Systems | Whisper, AssemblyAI | +| AI-Powered Web Research | Tavily AI, exa AI, Serper.dev | --- + +## πŸš€ Key Features + +### AI Writer Tools + +- **AI Blog Writer**: Generate blog content based on web research +- **AI YouTube to Content Writer**: Transform YouTube videos into written content +- **AI Long Form Content**: Create detailed articles with proper structure +- **AI Story Writer**: Craft engaging narratives and stories +- **AI Email Writer**: Generate professional and business emails +- **AI LinkedIn Post Generator**: Create optimized LinkedIn content +- **AI Product Description Generator**: Write compelling product descriptions + +### SEO Tools + +- **Rich Snippet Generator**: Create structured data for better SERP visibility +- **On-Page SEO Analyzer**: Evaluate and optimize web pages +- **URL SEO Checker**: Assess URL structure and performance +- **Backlinking Tool**: Discover high-quality backlink opportunities +- **Image Alt Text Generator**: Create accessible image descriptions +- **Meta Description Generator**: Generate SEO-friendly meta descriptions + +### Social Media Tools + +- **X Tweet Generator**: Create engaging tweets +- **Instagram Caption Generator**: Write compelling Instagram captions +- **Facebook Post Generator**: Develop Facebook-optimized content +- **YouTube Content Tools**: Generate titles, descriptions, and scripts + +### Content Planning + +- **Content Calendar**: Plan content for weeks or months ahead +- **Blog Image Creation**: Generate images to complement your content +- **Agentic Content Creation**: Use AI agents for specialized content tasks +- **Web Research Integration**: Gather factual information for your content + +## πŸ—οΈ System Architecture + +Alwrity is built with a modular architecture that enables flexibility and extensibility: + +![Architecture Diagram](https://github.com/AJaySi/AI-Writer/docs/architecture/diagrams/high_level_architecture.png) + +The platform consists of several key components: + +1. **User Interface Layer**: Streamlit-based web interface +2. **Core Services Layer**: AI Writers, Web Research, SEO Tools, Analytics +3. **Data Storage Layer**: Vector Database (ChromaDB), Relational Database (SQLite) +4. **External Integrations Layer**: LLM Providers, Search Providers, Image Generation, Publishing Platforms + +For more detailed architecture information, see the [Architecture Documentation](docs/architecture/index.rst). + ## Getting Started with ALwrity: "AI at Every Stage of Content Lifecycle." Alwrity empowers content creators, solopreneurs and digital marketers with cutting-edge tools for keyword research, AI-driven writing, and social media content generation. From creating high-quality copywriting frameworks to crafting engaging YouTube scripts, our platform simplifies every step of your content creation journey. -We seamlessly integrate state-of-the-art AI technologies, including: -| **Content Category** | **Technologies/Models** | -|--------------------------|-------------------------------------------| -| Text Generation Models | OpenAI, Gemini, Anthropic | -| Image Creation Tools | Stability.ai | -| Speech-to-Text Systems | Whisper, AssemblyAI | -| AI-Powered Web Research | Tavily AI, exa AI, Serper.dev | - -By bringing these powerful solutions together, Alwrity ensures a streamlined workflow and exceptional output with minimal effort. Get ready to supercharge your content creation process! - --- > ![](https://github.com/AJaySi/AI-Writer/blob/main/lib/workspace/keyword_blog.gif) --- @@ -46,48 +120,6 @@ By bringing these powerful solutions together, Alwrity ensures a streamlined wor >
> See Details > -## Prerequisites - -### Windows -- Python 3.10+ (3.12 recommended) -- Microsoft Visual C++ Build Tools 14.0 or greater - - Install with: `winget install Microsoft.VisualStudio.2022.BuildTools --silent --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"` -- Rust Compiler - - Install with: `Invoke-WebRequest -Uri https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe -OutFile rustup-init.exe; ./rustup-init.exe -y` - -### Linux -- Python 3.10+ (3.12 recommended) -- C/C++ compiler and development tools - - Install with: `sudo apt update && sudo apt install build-essential python3-dev` -- Rust Compiler - - Install with: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y; source $HOME/.cargo/env` - -> ``` -> 1). git clone https://github.com/AJaySi/AI-Writer.git -> 2). pip install -r requirements.txt -> 3). streamlit run alwrity.py -> -> 4). Visit Alwrity UI in a Browser & Start generation AI personalized content. -> ``` -> -> ### System Dependencies -> ``` -> You can run the install_dependencies.py script to check for system dependencies and assist with installation: -> -> python install_dependencies.py -> ``` -> This script will help verify that all necessary system dependencies are installed and guide you through the installation process. -> ### Updating to latest Code: (Existing users) -> ``` -> 1). Git pull -> 2). pip install -U -r requirements.txt -> 3). streamlit run alwrity.py -> ``` ->
->

---- -Still stuck, [Open issue here](https://github.com/AJaySi/AI-Writer/issues) & Someone will bail you out. -> ![Alwrity Documentation is here for more details](https://github.com/AJaySi/AI-Writer/wiki) ### ![List of all AI Tools & Features of Alwrity](https://github.com/AJaySi/AI-Writer/wiki/Features-of-ALwrity-AI-writer) --- @@ -96,8 +128,74 @@ Still stuck, [Open issue here](https://github.com/AJaySi/AI-Writer/issues) & Som - ![Specilized AI writers for every need & platform](https://github.com/AJaySi/AI-Writer/wiki/Features-of-ALwrity-AI-writer) - ![ALwrity AI SEO Tools](https://github.com/AJaySi/AI-Writer/wiki/ALwrity-AI-SEO-Tools) - ![ALwrity Web Researcher](https://github.com/AJaySi/AI-Writer/wiki/Alwrity-AI-Web-Research-Details-for-content-writing) -- RoadMap - Coming Soon.. -- Present Focus: Nextjs react Alwrity App - Coming Soon.. + +--- + +## πŸ“ Usage Guide + +### AI Writers + +1. **Blog Writer**: + - Enter your target keywords + - Select blog type and length + - Choose whether to include web research + - Generate and edit your blog content + + ![Blog Writer Demo](https://github.com/AJaySi/AI-Writer/blob/main/lib/workspace/keyword_blog.gif) + +2. **Long Form Content**: + - Provide a detailed topic + - Select content structure + - Generate comprehensive content with proper sections + - Edit and refine as needed + +3. **Email Writer**: + - Select email type (professional, business, etc.) + - Enter recipient and purpose + - Generate appropriate email content + - Customize tone and style + +### SEO Tools + +1. **Rich Snippet Generator**: + - Enter your URL or content + - Select snippet type (FAQ, Product, etc.) + - Generate structured data + - Copy and implement on your website + +2. **On-Page SEO Analyzer**: + - Enter your URL + - Receive comprehensive SEO analysis + - Get actionable recommendations + - Implement suggested changes + +### Social Media Tools + +1. **X Tweet Generator**: + - Enter topic or keywords + - Select tweet style + - Generate engaging tweets + - Schedule or post directly + +2. **YouTube Script Generator**: + - Enter video topic + - Select video length and style + - Generate complete script with sections + - Export for recording + +### Content Planning + +1. **Content Calendar**: + - Enter your niche or industry + - Select timeframe (weeks/months) + - Generate content ideas with titles + - Export to your preferred format + +2. **Web Research**: + - Enter research topic + - Select research depth + - Receive comprehensive research results + - Use in your content generation --- @@ -238,216 +336,68 @@ Still stuck, [Open issue here](https://github.com/AJaySi/AI-Writer/issues) & Som --- -

Notes from underground:

- 1). Focus is on writing/generating highly unique, SEO optimized blog content. - 2). Models: Openai, gemini, ollama are interesting. Minstral API is also worth exploring. Cohere API is purpose made. - Focus is getting the prompts right. Shit in, shit out, irrespective of dollars and cutting edge models. - Pydantically speakng, Due to experimental nature of prompting, its getting expensive soon enough. Gemini is free for now. - 3). Missing frontend: A smart backend will enable a good frontend. WIP, backend. So, frontend; coming soon. - 4).Getting AI agents to 'brainstrom' blog ideas seems more pressing. CrewAI seems more straightforward than autogen. - 5). Too Many APIs floating around: The implementation is using tools that dont depend on API keys and rather scrape them. - Duh, scraping wont scale, that is GPT vision based scraping will come in handy. - 6). firecrawl is interesting, gpt-researcher is providing local docsqa. - 7). Had to provide streamlit UI as Alwrity's audience arent comfortable with commandline. - 8). Local folder RAG and Chat with your content, website is on the cards. - 9). AI models are better, not sure until when 'Free' APIs will be "Free". - 10). The code is always a mess, lot of changes happening.. - 11). Focus is to stop making any more AI content tools, but rather revisit & improve user experience & content quality. - 12). To Err is Human & AI.... - 13). .... -
+## πŸ“š API Documentation + +Alwrity provides a comprehensive API for programmatic access to its features. The API documentation is available at: + +- [API Reference](docs/api/index.rst) +- [API Examples](docs/api/examples.rst) + +## 🀝 Contributing + +We welcome contributions to Alwrity! Please see our [Contributing Guide](CONTRIBUTING.md) for details on how to get started. + +## πŸ—ΊοΈ Roadmap + +- [Read Detailed Roadmap Here](Roadmap TBDs/ROADMAP.md) +- [ALwrity Roadmap](docs/roadmap.rst) + +Our development roadmap includes: + +- **Short-term (0-3 months)**: + - Enhanced multi-language support + - Improved image generation capabilities + - Additional AI model integrations + +- **Medium-term (3-6 months)**: + - Advanced analytics dashboard + - Content performance tracking + - Collaborative editing features + +- **Long-term (6+ months)**: + - NextJS React Alwrity App + - API-first architecture + - Enterprise features for teams + +## πŸ“„ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## πŸ™ Acknowledgements + +Alwrity stands on the shoulders of giants: + +- **APIs**: + - [Exa API](https://exa.ai/): Semantic search capabilities + - [Tavily API](https://tavily.com/): AI-powered web search + - [SerperDev API](https://serper.dev/): Search engine results + - [YOU.com](https://you.com/): Enhanced web search + - [Stability AI](https://stability.ai/): Image generation + - [OpenAI API](https://openai.com/): LLM capabilities + - [Gemini API](https://gemini.google.com/app): Google's LLM + - [Ollama](https://ollama.com/): Local LLM provider + - [CrewAI](https://www.crewai.com/): Collaborative AI agents + +## πŸ“ž Support + +If you encounter any issues or have questions, please [open an issue](https://github.com/AJaySi/AI-Writer/issues) on GitHub. --- -#### LICENSE -> [!NOTE]

-> MIT License -> -> Copyright (c) 2024 Alwrity -> -> Permission is hereby granted, free of charge, to any person obtaining a copy -> of this software and associated documentation files (the "Software"), to deal -> in the Software without restriction, including without limitation the rights -> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -> copies of the Software, and to permit persons to whom the Software is -> furnished to do so, subject to the following conditions: -> -> The above copyright notice and this permission notice shall be included in all -> copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -> SOFTWARE. ->

+

+ Visit Alwrity.com β€’ + Try Free Tools β€’ + Wiki +

-## Easy Installation Guide for Content Creators - -### Step 1: Install Python 3.11 -1. Download Python 3.11 installer: - - Visit [Python 3.11.6 Download Page](https://www.python.org/downloads/release/python-3116/) - - Scroll down and click on "Windows installer (64-bit)" - - Save the file to your computer - -2. Run the installer: - - Double click the downloaded file - - βœ… IMPORTANT: Check "Add Python 3.11 to PATH" - - Click "Install Now" - - Wait for installation to complete - - Click "Close" - -### Step 2: Install ALwrity -1. Download this project: - - Click the green "Code" button above - - Select "Download ZIP" - - Extract the ZIP file to your desired location - -2. Open Command Prompt: - - Press Windows + R - - Type "cmd" and press Enter - - Navigate to the extracted folder: - ``` - cd path\to\ALwrity - ``` - -3. Run the automatic installer: - ``` - python setup.py install - ``` - -### Troubleshooting -If you encounter any issues: -1. Make sure Python 3.11 is installed correctly: - - Open Command Prompt - - Type: `python --version` - - Should show: `Python 3.11.x` - -2. Common Issues: - - If you see "Python is not recognized": Restart your computer - - If you get package errors: Run `pip install --upgrade pip` first - -Need help? [Open an issue](../../issues) and we'll assist you! - -## For Developers -If you're a developer or want to contribute: -```bash -# Clone the repository -git clone https://github.com/yourusername/ALwrity.git - -# Create virtual environment -python -m venv venv - -# Activate virtual environment -# On Windows: -.\venv\Scripts\activate -# On Mac/Linux: -source venv/bin/activate - -# Install dependencies -pip install -r requirements.txt -``` - -# ALwrity - AI Content Writing Assistant - -## Quick Start Guide for Non-Technical Users - -### Option 1: One-Click Installation (Recommended) -1. Download this project: - - Click the green "Code" button above - - Select "Download ZIP" - - Extract the ZIP file to your desired location (e.g., Desktop) - -2. Run the installer: - - Double-click `install.bat` in the extracted folder - - If Windows asks for permission, click "Yes" - - Follow the on-screen instructions - - Wait for the installation to complete - -3. Start ALwrity: - - Open Command Prompt (Windows + R, type "cmd", press Enter) - - Navigate to the ALwrity folder: - ``` - cd path\to\ALwrity - ``` - - Type `alwrity` and press Enter - -### Option 2: Manual Installation -If the one-click installer doesn't work, follow these steps: - -1. Install Python 3.11: - - Visit [Python 3.11.6 Download Page](https://www.python.org/downloads/release/python-3116/) - - Click "Windows installer (64-bit)" - - Run the installer - - βœ… IMPORTANT: Check "Add Python 3.11 to PATH" - - Click "Install Now" - - Wait for installation to complete - -2. Install ALwrity: - - Open Command Prompt (Windows + R, type "cmd", press Enter) - - Navigate to the ALwrity folder: - ``` - cd path\to\ALwrity - ``` - - Run the installation: - ``` - python setup.py install - ``` - - Follow any on-screen instructions - -3. Start ALwrity: - - In the same Command Prompt window, type: - ``` - alwrity - ``` - - Press Enter - -### Troubleshooting Guide - -#### Common Issues and Solutions: - -1. "Python is not recognized" - - Solution: Restart your computer after installing Python - - Make sure you checked "Add Python 3.11 to PATH" during installation - -2. "Visual C++ Build Tools not found" - - Solution: Run this command in an administrative PowerShell: - ``` - winget install Microsoft.VisualStudio.2022.BuildTools --silent --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" - ``` - -3. "Rust compiler not found" - - Solution: Run these commands in PowerShell: - ``` - Invoke-WebRequest -Uri https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe -OutFile rustup-init.exe - ./rustup-init.exe -y - ``` - -4. Installation Errors - - Check the `install_errors.log` file in the ALwrity folder - - Share the error message with our support team - -#### Need Help? -- Open an issue on GitHub -- Join our support community -- Contact our support team - -### System Requirements -- Windows 10 or later -- Python 3.11.x -- At least 4GB RAM -- 2GB free disk space - -### First-Time Setup -After installation: -1. The first time you run ALwrity, it will ask for your API keys -2. Follow the on-screen instructions to enter your keys -3. Your keys will be saved securely for future use - -### Updating ALwrity -To update to the latest version: -1. Download the latest release -2. Run `install.bat` again -3. Follow the on-screen instructions +--- diff --git a/Roadmap TBDs/CONTRIBUTING.md b/Roadmap TBDs/CONTRIBUTING.md new file mode 100644 index 00000000..1bd507a8 --- /dev/null +++ b/Roadmap TBDs/CONTRIBUTING.md @@ -0,0 +1,202 @@ +# Contributing to AI-Writer + +Thank you for your interest in contributing to AI-Writer! This document provides guidelines and instructions for contributing to the project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Environment](#development-environment) +- [Coding Standards](#coding-standards) +- [Pull Request Process](#pull-request-process) +- [Testing Guidelines](#testing-guidelines) +- [Documentation](#documentation) +- [Community](#community) + +## Code of Conduct + +By participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing. + +## Getting Started + +### Issues + +- Check existing issues to see if your problem or idea has already been addressed. +- For bugs, create a new issue with a clear description, steps to reproduce, and relevant information about your environment. +- For feature requests, describe the feature, its benefits, and potential implementation approaches. +- Use issue templates when available. + +### Feature Branches + +- Fork the repository and create a feature branch from `main`. +- Use descriptive branch names: `feature/your-feature-name` or `fix/issue-description`. +- Keep branches focused on a single issue or feature. + +## Development Environment + +### Prerequisites + +- Python 3.9 or higher +- Git +- A code editor (VS Code, PyCharm, etc.) +- Docker (optional, for containerized development) + +### Setup + +1. Clone the repository: + ```bash + git clone https://github.com/AJaySi/AI-Writer.git + cd AI-Writer + ``` + +2. Create a virtual environment: + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +3. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +4. Set up environment variables: + - Create a `.env` file in the project root + - Add necessary API keys and configuration (see `.env.example` for reference) + +5. Initialize the database: + ```bash + python -c "from lib.database.db_manager import init_db; init_db()" + ``` + +6. Run the application: + ```bash + streamlit run alwrity.py + ``` + +## Coding Standards + +### Style Guide + +- Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) for Python code. +- Use 4 spaces for indentation (no tabs). +- Maximum line length is 100 characters. +- Use meaningful variable and function names. + +### Documentation + +- Use Google-style docstrings for all modules, classes, and functions. +- Include type hints in function signatures. +- Keep comments up-to-date with code changes. + +Example: + +```python +def generate_content(prompt: str, max_tokens: int = 100) -> str: + """Generate content using the AI model. + + Args: + prompt: The input prompt for content generation. + max_tokens: Maximum number of tokens to generate. + + Returns: + The generated content as a string. + + Raises: + ValueError: If the prompt is empty or max_tokens is negative. + """ + # Implementation... +``` + +### Error Handling + +- Use specific exception types rather than generic exceptions. +- Include meaningful error messages. +- Log exceptions with appropriate context. + +### Imports + +- Group imports in the following order: + 1. Standard library imports + 2. Related third-party imports + 3. Local application/library specific imports +- Within each group, imports should be sorted alphabetically. + +## Pull Request Process + +1. Ensure your code follows the project's coding standards. +2. Update documentation as necessary. +3. Add or update tests to cover your changes. +4. Ensure all tests pass. +5. Submit a pull request with a clear description of the changes and any relevant issue numbers. +6. Wait for review and address any feedback. + +### Commit Messages + +Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: + +- `feat`: A new feature +- `fix`: A bug fix +- `docs`: Documentation changes +- `style`: Code style changes (formatting, etc.) +- `refactor`: Code changes that neither fix bugs nor add features +- `test`: Adding or updating tests +- `chore`: Changes to the build process or auxiliary tools + +Example: `feat: add support for Google Gemini models` + +## Testing Guidelines + +### Writing Tests + +- Write unit tests for all new functions and classes. +- Place tests in the `tests/` directory, mirroring the package structure. +- Use descriptive test names that explain what is being tested. +- Aim for at least 80% test coverage for new code. + +### Running Tests + +```bash +# Run all tests +pytest + +# Run specific tests +pytest tests/path/to/test_file.py + +# Run with coverage +pytest --cov=lib +``` + +## Documentation + +### Code Documentation + +- Document all public modules, classes, and functions. +- Keep docstrings up-to-date with code changes. +- Use type hints consistently. + +### Project Documentation + +- Update README.md with new features or changes. +- Update installation and usage instructions as needed. +- For significant changes, update the documentation in the `docs/` directory. + +## Community + +### Communication Channels + +- GitHub Issues: For bug reports and feature requests +- Discussions: For general questions and discussions +- Pull Requests: For code contributions + +### Recognition + +All contributors will be recognized in the project's CONTRIBUTORS.md file. + +## Additional Resources + +- [Project Roadmap](docs/roadmap.rst) +- [Architecture Documentation](docs/architecture/index.rst) +- [API Reference](docs/api/index.rst) + +Thank you for contributing to AI-Writer! \ No newline at end of file diff --git a/Roadmap TBDs/GOOGLE_INTEGRATION_PLAN.md b/Roadmap TBDs/GOOGLE_INTEGRATION_PLAN.md new file mode 100644 index 00000000..fa261e4b --- /dev/null +++ b/Roadmap TBDs/GOOGLE_INTEGRATION_PLAN.md @@ -0,0 +1,2586 @@ +# Google Integration Implementation Plan + +This document outlines the step-by-step implementation plan for integrating Google login and Google Search Console (GSC) with AI-Writer to enhance content creation with real user insights. + +## Overview + +The integration will allow users to: + +1. Sign in with their Google account +2. Connect to Google Search Console +3. Access search analytics data for content optimization +4. Use real keyword data for content creation +5. Track content performance over time + +## Implementation Steps + +### Phase 1: Google OAuth Integration + +#### Step 1: Set Up Google Cloud Project + +1. **Create a Google Cloud Project** + - Go to [Google Cloud Console](https://console.cloud.google.com/) + - Create a new project named "AI-Writer" + - Note the Project ID for configuration + +2. **Configure OAuth Consent Screen** + - Navigate to "APIs & Services" > "OAuth consent screen" + - Select "External" user type + - Fill in application information: + - App name: "AI-Writer" + - User support email: support@alwrity.com + - Developer contact information + - Add scopes: + - `https://www.googleapis.com/auth/userinfo.email` + - `https://www.googleapis.com/auth/userinfo.profile` + - `https://www.googleapis.com/auth/webmasters.readonly` + - Add test users for development + +3. **Create OAuth Credentials** + - Navigate to "APIs & Services" > "Credentials" + - Click "Create Credentials" > "OAuth client ID" + - Select "Web application" as application type + - Set name to "AI-Writer Web Client" + - Add authorized JavaScript origins: + - `http://localhost:8501` (for development) + - `https://your-production-domain.com` (for production) + - Add authorized redirect URIs: + - `http://localhost:8501/oauth/callback` (for development) + - `https://your-production-domain.com/oauth/callback` (for production) + - Save and note the Client ID and Client Secret + +4. **Enable Required APIs** + - Navigate to "APIs & Services" > "Library" + - Search for and enable: + - Google Search Console API + - Google OAuth2 API + - Google People API + +#### Step 2: Implement Backend Authentication + +1. **Install Required Packages** + +```bash +pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client +``` + +2. **Create Authentication Module** + +Create a new file at `/workspace/AI-Writer/lib/integrations/google/auth.py`: + +```python +"""Google authentication module for AI-Writer.""" + +import os +import json +from pathlib import Path +from typing import Dict, Optional, Tuple, Any +import streamlit as st +from google_auth_oauthlib.flow import Flow +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build +from loguru import logger + +# Define scopes needed for the application +SCOPES = [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/webmasters.readonly' +] + +# Configuration directory +CONFIG_DIR = Path(__file__).parent.parent.parent.parent / 'config' +CREDENTIALS_FILE = CONFIG_DIR / 'google_credentials.json' +TOKENS_DIR = CONFIG_DIR / 'tokens' + +def get_google_auth_url() -> str: + """Generate Google OAuth authorization URL. + + Returns: + str: Authorization URL for Google OAuth + """ + try: + # Ensure config directory exists + CONFIG_DIR.mkdir(exist_ok=True) + TOKENS_DIR.mkdir(exist_ok=True) + + # Load client configuration + client_config = st.secrets.get("google_oauth", None) + + if not client_config: + logger.error("Google OAuth client configuration not found in secrets") + return "" + + # Create OAuth flow instance + flow = Flow.from_client_config( + client_config=client_config, + scopes=SCOPES, + redirect_uri=client_config["web"]["redirect_uris"][0] + ) + + # Generate authorization URL + auth_url, _ = flow.authorization_url( + access_type='offline', + include_granted_scopes='true', + prompt='consent' + ) + + # Store flow in session state for later use + st.session_state.google_auth_flow = flow + + return auth_url + except Exception as e: + logger.error(f"Error generating Google auth URL: {str(e)}") + return "" + +def handle_auth_callback(code: str) -> Tuple[bool, Optional[Dict[str, Any]]]: + """Handle OAuth callback and exchange code for tokens. + + Args: + code: Authorization code from Google + + Returns: + Tuple[bool, Optional[Dict]]: Success status and user info if successful + """ + try: + # Get flow from session state + flow = st.session_state.get("google_auth_flow") + if not flow: + logger.error("Auth flow not found in session state") + return False, None + + # Exchange code for tokens + flow.fetch_token(code=code) + + # Get credentials + credentials = flow.credentials + + # Save credentials + save_credentials(credentials) + + # Get user info + user_info = get_user_info(credentials) + + # Store in session state + st.session_state.google_credentials = credentials_to_dict(credentials) + st.session_state.google_user_info = user_info + + return True, user_info + except Exception as e: + logger.error(f"Error handling auth callback: {str(e)}") + return False, None + +def save_credentials(credentials: Credentials) -> bool: + """Save Google credentials to file. + + Args: + credentials: Google OAuth credentials + + Returns: + bool: Success status + """ + try: + # Convert credentials to dict + creds_dict = credentials_to_dict(credentials) + + # Get user email from credentials + user_info = get_user_info(credentials) + user_email = user_info.get("email", "unknown") + + # Create user-specific token file + token_file = TOKENS_DIR / f"{user_email}.json" + + # Save credentials to file + with open(token_file, 'w') as f: + json.dump(creds_dict, f) + + logger.info(f"Saved credentials for {user_email}") + return True + except Exception as e: + logger.error(f"Error saving credentials: {str(e)}") + return False + +def load_credentials(user_email: str) -> Optional[Credentials]: + """Load Google credentials from file. + + Args: + user_email: Email of the user + + Returns: + Optional[Credentials]: Google credentials if found + """ + try: + # Get user-specific token file + token_file = TOKENS_DIR / f"{user_email}.json" + + # Check if file exists + if not token_file.exists(): + logger.warning(f"No credentials found for {user_email}") + return None + + # Load credentials from file + with open(token_file, 'r') as f: + creds_dict = json.load(f) + + # Create credentials object + credentials = Credentials.from_authorized_user_info(creds_dict, SCOPES) + + # Check if credentials are valid + if credentials.expired and credentials.refresh_token: + credentials.refresh(Request()) + save_credentials(credentials) + + return credentials + except Exception as e: + logger.error(f"Error loading credentials: {str(e)}") + return None + +def get_user_info(credentials: Credentials) -> Dict[str, Any]: + """Get user information from Google. + + Args: + credentials: Google OAuth credentials + + Returns: + Dict[str, Any]: User information + """ + try: + # Build people API service + service = build('people', 'v1', credentials=credentials) + + # Get user profile + profile = service.people().get( + resourceName='people/me', + personFields='names,emailAddresses,photos' + ).execute() + + # Extract relevant information + user_info = { + "email": profile.get("emailAddresses", [{}])[0].get("value", ""), + "name": profile.get("names", [{}])[0].get("displayName", ""), + "picture": profile.get("photos", [{}])[0].get("url", "") + } + + return user_info + except Exception as e: + logger.error(f"Error getting user info: {str(e)}") + return {} + +def credentials_to_dict(credentials: Credentials) -> Dict[str, Any]: + """Convert Google credentials to dictionary. + + Args: + credentials: Google OAuth credentials + + Returns: + Dict[str, Any]: Credentials as dictionary + """ + return { + 'token': credentials.token, + 'refresh_token': credentials.refresh_token, + 'token_uri': credentials.token_uri, + 'client_id': credentials.client_id, + 'client_secret': credentials.client_secret, + 'scopes': credentials.scopes + } + +def is_authenticated() -> bool: + """Check if user is authenticated with Google. + + Returns: + bool: True if authenticated, False otherwise + """ + return 'google_credentials' in st.session_state and 'google_user_info' in st.session_state + +def logout() -> None: + """Log out user from Google.""" + if 'google_credentials' in st.session_state: + del st.session_state.google_credentials + if 'google_user_info' in st.session_state: + del st.session_state.google_user_info + if 'google_auth_flow' in st.session_state: + del st.session_state.google_auth_flow +``` + +3. **Create OAuth Callback Handler** + +Create a new file at `/workspace/AI-Writer/lib/integrations/google/callback_handler.py`: + +```python +"""Google OAuth callback handler for Streamlit.""" + +import streamlit as st +from urllib.parse import parse_qs, urlparse +from .auth import handle_auth_callback + +def handle_oauth_callback(): + """Handle OAuth callback in Streamlit.""" + # Get current URL + query_params = st.experimental_get_query_params() + + # Check if this is a callback + if 'code' in query_params: + code = query_params['code'][0] + + # Exchange code for tokens + success, user_info = handle_auth_callback(code) + + if success: + # Clear query parameters to avoid reprocessing + st.experimental_set_query_params() + + # Show success message + st.success(f"Successfully logged in as {user_info.get('name', 'User')}") + + # Redirect to main page + st.experimental_rerun() + else: + st.error("Failed to authenticate with Google. Please try again.") + + # Check for error + if 'error' in query_params: + error = query_params['error'][0] + st.error(f"Authentication error: {error}") + st.experimental_set_query_params() +``` + +#### Step 3: Implement Google Search Console API + +1. **Create GSC API Module** + +Create a new file at `/workspace/AI-Writer/lib/integrations/google/search_console.py`: + +```python +"""Google Search Console API integration for AI-Writer.""" + +from typing import Dict, List, Optional, Any +from datetime import datetime, timedelta +import streamlit as st +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build +from loguru import logger + +def get_search_console_service(credentials: Dict[str, Any]) -> Any: + """Build Google Search Console API service. + + Args: + credentials: Google OAuth credentials dictionary + + Returns: + Any: Search Console API service + """ + try: + # Convert dict to credentials object + creds = Credentials.from_authorized_user_info(credentials) + + # Build service + service = build('searchconsole', 'v1', credentials=creds) + + return service + except Exception as e: + logger.error(f"Error building Search Console service: {str(e)}") + return None + +def get_site_list() -> List[Dict[str, Any]]: + """Get list of sites from Search Console. + + Returns: + List[Dict[str, Any]]: List of sites + """ + try: + # Check if user is authenticated + if 'google_credentials' not in st.session_state: + logger.warning("User not authenticated with Google") + return [] + + # Get credentials + credentials = st.session_state.google_credentials + + # Build service + service = get_search_console_service(credentials) + if not service: + return [] + + # Get site list + sites = service.sites().list().execute() + + # Extract site entries + site_entries = sites.get('siteEntry', []) + + # Filter for verified sites + verified_sites = [ + { + 'site_url': site.get('siteUrl', ''), + 'permission_level': site.get('permissionLevel', ''), + 'site_type': 'Web' if site.get('siteUrl', '').startswith('http') else 'Domain Property' + } + for site in site_entries + if site.get('permissionLevel') in ['siteOwner', 'siteFullUser'] + ] + + return verified_sites + except Exception as e: + logger.error(f"Error getting site list: {str(e)}") + return [] + +def get_search_analytics( + site_url: str, + start_date: datetime, + end_date: datetime, + dimensions: List[str] = ['query'], + row_limit: int = 1000 +) -> Dict[str, Any]: + """Get search analytics data from Search Console. + + Args: + site_url: URL of the site + start_date: Start date for data + end_date: End date for data + dimensions: Dimensions to include (query, page, device, country, date) + row_limit: Maximum number of rows to return + + Returns: + Dict[str, Any]: Search analytics data + """ + try: + # Check if user is authenticated + if 'google_credentials' not in st.session_state: + logger.warning("User not authenticated with Google") + return {'rows': []} + + # Get credentials + credentials = st.session_state.google_credentials + + # Build service + service = get_search_console_service(credentials) + if not service: + return {'rows': []} + + # Format dates + start_date_str = start_date.strftime('%Y-%m-%d') + end_date_str = end_date.strftime('%Y-%m-%d') + + # Prepare request body + request_body = { + 'startDate': start_date_str, + 'endDate': end_date_str, + 'dimensions': dimensions, + 'rowLimit': row_limit, + 'startRow': 0, + 'searchType': 'web' + } + + # Execute request + response = service.searchanalytics().query( + siteUrl=site_url, + body=request_body + ).execute() + + return response + except Exception as e: + logger.error(f"Error getting search analytics: {str(e)}") + return {'rows': []} + +def get_top_keywords(site_url: str, days: int = 30, limit: int = 100) -> List[Dict[str, Any]]: + """Get top keywords for a site. + + Args: + site_url: URL of the site + days: Number of days to include + limit: Maximum number of keywords to return + + Returns: + List[Dict[str, Any]]: List of top keywords with metrics + """ + try: + # Calculate date range + end_date = datetime.now() + start_date = end_date - timedelta(days=days) + + # Get search analytics data + analytics = get_search_analytics( + site_url=site_url, + start_date=start_date, + end_date=end_date, + dimensions=['query'], + row_limit=limit + ) + + # Process rows + rows = analytics.get('rows', []) + + # Format results + keywords = [] + for row in rows: + keywords.append({ + 'keyword': row.get('keys', [''])[0], + 'clicks': row.get('clicks', 0), + 'impressions': row.get('impressions', 0), + 'ctr': row.get('ctr', 0) * 100, # Convert to percentage + 'position': row.get('position', 0) + }) + + # Sort by clicks (descending) + keywords.sort(key=lambda x: x['clicks'], reverse=True) + + return keywords + except Exception as e: + logger.error(f"Error getting top keywords: {str(e)}") + return [] + +def get_top_pages(site_url: str, days: int = 30, limit: int = 100) -> List[Dict[str, Any]]: + """Get top pages for a site. + + Args: + site_url: URL of the site + days: Number of days to include + limit: Maximum number of pages to return + + Returns: + List[Dict[str, Any]]: List of top pages with metrics + """ + try: + # Calculate date range + end_date = datetime.now() + start_date = end_date - timedelta(days=days) + + # Get search analytics data + analytics = get_search_analytics( + site_url=site_url, + start_date=start_date, + end_date=end_date, + dimensions=['page'], + row_limit=limit + ) + + # Process rows + rows = analytics.get('rows', []) + + # Format results + pages = [] + for row in rows: + pages.append({ + 'page': row.get('keys', [''])[0], + 'clicks': row.get('clicks', 0), + 'impressions': row.get('impressions', 0), + 'ctr': row.get('ctr', 0) * 100, # Convert to percentage + 'position': row.get('position', 0) + }) + + # Sort by clicks (descending) + pages.sort(key=lambda x: x['clicks'], reverse=True) + + return pages + except Exception as e: + logger.error(f"Error getting top pages: {str(e)}") + return [] + +def get_keyword_insights(keyword: str, site_url: str, days: int = 90) -> Dict[str, Any]: + """Get detailed insights for a specific keyword. + + Args: + keyword: Keyword to analyze + site_url: URL of the site + days: Number of days to include + + Returns: + Dict[str, Any]: Keyword insights + """ + try: + # Calculate date range + end_date = datetime.now() + start_date = end_date - timedelta(days=days) + + # Get search analytics data with date dimension + analytics = get_search_analytics( + site_url=site_url, + start_date=start_date, + end_date=end_date, + dimensions=['query', 'date'], + row_limit=1000 + ) + + # Process rows + rows = analytics.get('rows', []) + + # Filter for the specific keyword + keyword_rows = [ + row for row in rows + if row.get('keys', ['', ''])[0].lower() == keyword.lower() + ] + + # Prepare time series data + time_series = [] + for row in keyword_rows: + date_str = row.get('keys', ['', ''])[1] + time_series.append({ + 'date': date_str, + 'clicks': row.get('clicks', 0), + 'impressions': row.get('impressions', 0), + 'ctr': row.get('ctr', 0) * 100, + 'position': row.get('position', 0) + }) + + # Sort by date + time_series.sort(key=lambda x: x['date']) + + # Get pages ranking for this keyword + page_analytics = get_search_analytics( + site_url=site_url, + start_date=start_date, + end_date=end_date, + dimensions=['query', 'page'], + row_limit=100 + ) + + # Filter for the specific keyword + keyword_pages = [ + { + 'page': row.get('keys', ['', ''])[1], + 'clicks': row.get('clicks', 0), + 'impressions': row.get('impressions', 0), + 'ctr': row.get('ctr', 0) * 100, + 'position': row.get('position', 0) + } + for row in page_analytics.get('rows', []) + if row.get('keys', ['', ''])[0].lower() == keyword.lower() + ] + + # Sort by position (ascending) + keyword_pages.sort(key=lambda x: x['position']) + + # Calculate totals + total_clicks = sum(row.get('clicks', 0) for row in keyword_rows) + total_impressions = sum(row.get('impressions', 0) for row in keyword_rows) + avg_ctr = (sum(row.get('ctr', 0) for row in keyword_rows) / len(keyword_rows)) * 100 if keyword_rows else 0 + avg_position = sum(row.get('position', 0) for row in keyword_rows) / len(keyword_rows) if keyword_rows else 0 + + # Compile insights + insights = { + 'keyword': keyword, + 'total_clicks': total_clicks, + 'total_impressions': total_impressions, + 'avg_ctr': avg_ctr, + 'avg_position': avg_position, + 'time_series': time_series, + 'pages': keyword_pages + } + + return insights + except Exception as e: + logger.error(f"Error getting keyword insights: {str(e)}") + return { + 'keyword': keyword, + 'total_clicks': 0, + 'total_impressions': 0, + 'avg_ctr': 0, + 'avg_position': 0, + 'time_series': [], + 'pages': [] + } +``` + +#### Step 4: Create UI Components + +1. **Create Login Component** + +Create a new file at `/workspace/AI-Writer/lib/integrations/google/components.py`: + +```python +"""UI components for Google integration.""" + +import streamlit as st +from .auth import get_google_auth_url, is_authenticated, logout +from .search_console import get_site_list, get_top_keywords, get_top_pages, get_keyword_insights + +def render_google_login_button(): + """Render Google login button.""" + if not is_authenticated(): + auth_url = get_google_auth_url() + + st.markdown(""" + + + +
+ +
+ Sign in with Google +
+ """.format(auth_url=auth_url), unsafe_allow_html=True) + else: + user_info = st.session_state.google_user_info + + # Display user info + col1, col2 = st.columns([1, 3]) + + with col1: + st.image(user_info.get('picture', ''), width=50) + + with col2: + st.markdown(f"**{user_info.get('name', 'User')}**") + st.markdown(f"{user_info.get('email', '')}") + + # Logout button + if st.button("Sign Out"): + logout() + st.experimental_rerun() + +def render_site_selector(): + """Render Google Search Console site selector.""" + if not is_authenticated(): + st.warning("Please sign in with Google to access Search Console data.") + return None + + # Get site list + sites = get_site_list() + + if not sites: + st.warning("No verified sites found in your Search Console account.") + return None + + # Create site options + site_options = [site['site_url'] for site in sites] + + # Select site + selected_site = st.selectbox( + "Select a website", + options=site_options, + index=0 if site_options else None, + format_func=lambda x: x.replace("sc-domain:", "") + ) + + return selected_site + +def render_search_console_dashboard(site_url): + """Render Google Search Console dashboard. + + Args: + site_url: URL of the selected site + """ + if not site_url: + return + + st.markdown("## Search Console Insights") + + # Create tabs + tab1, tab2, tab3 = st.tabs(["Top Keywords", "Top Pages", "Keyword Research"]) + + with tab1: + st.markdown("### Top Keywords") + + # Date range selector + days = st.slider("Time period (days)", min_value=7, max_value=90, value=30, step=1) + + # Get top keywords + with st.spinner("Loading keyword data..."): + keywords = get_top_keywords(site_url, days=days, limit=100) + + if not keywords: + st.info("No keyword data available for this site.") + return + + # Display keywords table + st.dataframe( + keywords, + column_config={ + "keyword": "Keyword", + "clicks": st.column_config.NumberColumn("Clicks", format="%d"), + "impressions": st.column_config.NumberColumn("Impressions", format="%d"), + "ctr": st.column_config.NumberColumn("CTR", format="%.2f%%"), + "position": st.column_config.NumberColumn("Position", format="%.1f") + }, + hide_index=True + ) + + with tab2: + st.markdown("### Top Pages") + + # Date range selector + days = st.slider("Time period (days)", min_value=7, max_value=90, value=30, step=1, key="pages_days") + + # Get top pages + with st.spinner("Loading page data..."): + pages = get_top_pages(site_url, days=days, limit=100) + + if not pages: + st.info("No page data available for this site.") + return + + # Display pages table + st.dataframe( + pages, + column_config={ + "page": "Page URL", + "clicks": st.column_config.NumberColumn("Clicks", format="%d"), + "impressions": st.column_config.NumberColumn("Impressions", format="%d"), + "ctr": st.column_config.NumberColumn("CTR", format="%.2f%%"), + "position": st.column_config.NumberColumn("Position", format="%.1f") + }, + hide_index=True + ) + + with tab3: + st.markdown("### Keyword Research") + + # Keyword input + keyword = st.text_input("Enter a keyword to analyze") + + if keyword: + # Get keyword insights + with st.spinner(f"Analyzing '{keyword}'..."): + insights = get_keyword_insights(keyword, site_url, days=90) + + # Display insights + col1, col2, col3, col4 = st.columns(4) + + with col1: + st.metric("Clicks", f"{insights['total_clicks']:,}") + + with col2: + st.metric("Impressions", f"{insights['total_impressions']:,}") + + with col3: + st.metric("Avg. CTR", f"{insights['avg_ctr']:.2f}%") + + with col4: + st.metric("Avg. Position", f"{insights['avg_position']:.1f}") + + # Time series chart + if insights['time_series']: + st.markdown("#### Performance Over Time") + + # Prepare data for chart + chart_data = { + 'date': [item['date'] for item in insights['time_series']], + 'clicks': [item['clicks'] for item in insights['time_series']], + 'impressions': [item['impressions'] for item in insights['time_series']], + 'position': [item['position'] for item in insights['time_series']] + } + + # Create tabs for different metrics + chart_tab1, chart_tab2, chart_tab3 = st.tabs(["Clicks", "Impressions", "Position"]) + + with chart_tab1: + st.line_chart(chart_data, x='date', y='clicks') + + with chart_tab2: + st.line_chart(chart_data, x='date', y='impressions') + + with chart_tab3: + # Invert position axis (lower is better) + position_chart = { + 'date': chart_data['date'], + 'position': [-pos for pos in chart_data['position']] # Invert values + } + st.line_chart(position_chart, x='date', y='position') + st.caption("Note: Position axis is inverted (higher is better)") + + # Ranking pages + if insights['pages']: + st.markdown("#### Pages Ranking for this Keyword") + + st.dataframe( + insights['pages'], + column_config={ + "page": "Page URL", + "clicks": st.column_config.NumberColumn("Clicks", format="%d"), + "impressions": st.column_config.NumberColumn("Impressions", format="%d"), + "ctr": st.column_config.NumberColumn("CTR", format="%.2f%%"), + "position": st.column_config.NumberColumn("Position", format="%.1f") + }, + hide_index=True + ) + + # Content suggestions + st.markdown("#### Content Suggestions") + + if st.button("Generate Content Ideas"): + with st.spinner("Generating content ideas..."): + # This would connect to your AI content generation system + # For now, we'll just show a placeholder + st.info("This would connect to your AI content generation system to create content ideas based on this keyword's performance data.") +``` + +2. **Create Main Integration Page** + +Create a new file at `/workspace/AI-Writer/lib/integrations/google/page.py`: + +```python +"""Google integration page for AI-Writer.""" + +import streamlit as st +from .components import render_google_login_button, render_site_selector, render_search_console_dashboard +from .callback_handler import handle_oauth_callback + +def render_google_integration_page(): + """Render the Google integration page.""" + st.title("Google Integration") + + # Handle OAuth callback if present + handle_oauth_callback() + + # Display login/user info + st.markdown("### Google Account") + render_google_login_button() + + # Separator + st.markdown("---") + + # Search Console section + st.markdown("### Google Search Console") + st.markdown(""" + Connect to Google Search Console to access search analytics data for your websites. + This data will help you optimize your content for better search visibility. + """) + + # Site selector + selected_site = render_site_selector() + + # Display dashboard if site is selected + if selected_site: + render_search_console_dashboard(selected_site) +``` + +#### Step 5: Update Main Application + +1. **Add Google Integration to Sidebar** + +Update the sidebar in the main application file (`alwrity.py`): + +```python +# Add to imports +from lib.integrations.google.page import render_google_integration_page + +# Add to sidebar menu +with st.sidebar: + st.title("AI-Writer") + + # Existing menu items... + + # Add Google Integration option + if st.sidebar.selectbox("Integrations", ["None", "Google"]) == "Google": + page = "google_integration" + + # Existing sidebar code... + +# Add to page router +if page == "google_integration": + render_google_integration_page() +``` + +2. **Add Google Credentials to Streamlit Secrets** + +Create a file at `/.streamlit/secrets.toml`: + +```toml +[google_oauth] +web = { + "client_id": "YOUR_CLIENT_ID", + "project_id": "ai-writer", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_secret": "YOUR_CLIENT_SECRET", + "redirect_uris": ["http://localhost:8501/oauth/callback"], + "javascript_origins": ["http://localhost:8501"] +} +``` + +### Phase 2: Content Enhancement with GSC Data + +#### Step 1: Create Keyword Research Module + +Create a new file at `/workspace/AI-Writer/lib/integrations/google/keyword_research.py`: + +```python +"""Keyword research module using Google Search Console data.""" + +from typing import Dict, List, Any, Optional +import streamlit as st +from loguru import logger +from .search_console import get_top_keywords, get_keyword_insights + +def get_content_opportunities(site_url: str, min_impressions: int = 100, max_position: float = 20.0) -> List[Dict[str, Any]]: + """Find content opportunities based on GSC data. + + Args: + site_url: URL of the site + min_impressions: Minimum impressions threshold + max_position: Maximum position threshold + + Returns: + List[Dict[str, Any]]: List of content opportunities + """ + try: + # Get top keywords + keywords = get_top_keywords(site_url, days=90, limit=1000) + + # Filter for opportunities + opportunities = [ + keyword for keyword in keywords + if keyword['impressions'] >= min_impressions + and keyword['position'] <= max_position + and keyword['position'] > 3 # Not already in top 3 + ] + + # Sort by potential (impressions * (11 - position)/10) + # This prioritizes keywords with high impressions and positions 4-10 + for opp in opportunities: + opp['potential'] = opp['impressions'] * (11 - min(opp['position'], 10)) / 10 + + opportunities.sort(key=lambda x: x['potential'], reverse=True) + + return opportunities[:100] # Return top 100 opportunities + except Exception as e: + logger.error(f"Error finding content opportunities: {str(e)}") + return [] + +def get_keyword_clusters(keywords: List[Dict[str, Any]], threshold: int = 3) -> List[Dict[str, Any]]: + """Group keywords into clusters based on common words. + + Args: + keywords: List of keywords with metrics + threshold: Minimum number of keywords to form a cluster + + Returns: + List[Dict[str, Any]]: List of keyword clusters + """ + try: + # Extract keyword strings + keyword_strings = [k['keyword'].lower() for k in keywords] + + # Create word frequency map + word_map = {} + for kw in keyword_strings: + words = kw.split() + for word in words: + if len(word) > 3: # Ignore short words + if word not in word_map: + word_map[word] = [] + word_map[word].append(kw) + + # Find clusters + clusters = [] + for word, kws in word_map.items(): + if len(kws) >= threshold: + # Get full keyword data for each keyword in cluster + cluster_data = [ + k for k in keywords + if k['keyword'].lower() in kws + ] + + # Calculate cluster metrics + total_impressions = sum(k['impressions'] for k in cluster_data) + total_clicks = sum(k['clicks'] for k in cluster_data) + avg_position = sum(k['position'] for k in cluster_data) / len(cluster_data) + + clusters.append({ + 'topic': word, + 'keywords': cluster_data, + 'keyword_count': len(cluster_data), + 'total_impressions': total_impressions, + 'total_clicks': total_clicks, + 'avg_position': avg_position + }) + + # Sort clusters by total impressions + clusters.sort(key=lambda x: x['total_impressions'], reverse=True) + + return clusters + except Exception as e: + logger.error(f"Error creating keyword clusters: {str(e)}") + return [] + +def generate_content_brief(keyword: str, site_url: str) -> Dict[str, Any]: + """Generate a content brief for a keyword based on GSC data. + + Args: + keyword: Target keyword + site_url: URL of the site + + Returns: + Dict[str, Any]: Content brief + """ + try: + # Get keyword insights + insights = get_keyword_insights(keyword, site_url, days=90) + + # Get top keywords (for related keywords) + all_keywords = get_top_keywords(site_url, days=90, limit=1000) + + # Find related keywords + related_keywords = [ + k for k in all_keywords + if keyword.lower() in k['keyword'].lower() and k['keyword'].lower() != keyword.lower() + ] + + # Sort related keywords by impressions + related_keywords.sort(key=lambda x: x['impressions'], reverse=True) + + # Create content brief + brief = { + 'primary_keyword': keyword, + 'search_metrics': { + 'monthly_impressions': insights['total_impressions'] // 3, # Approximate monthly + 'monthly_clicks': insights['total_clicks'] // 3, + 'avg_position': insights['avg_position'], + 'avg_ctr': insights['avg_ctr'] + }, + 'related_keywords': related_keywords[:20], # Top 20 related keywords + 'competing_pages': insights['pages'], + 'recommended_headings': [], # Will be filled by AI + 'content_suggestions': [] # Will be filled by AI + } + + return brief + except Exception as e: + logger.error(f"Error generating content brief: {str(e)}") + return { + 'primary_keyword': keyword, + 'search_metrics': { + 'monthly_impressions': 0, + 'monthly_clicks': 0, + 'avg_position': 0, + 'avg_ctr': 0 + }, + 'related_keywords': [], + 'competing_pages': [], + 'recommended_headings': [], + 'content_suggestions': [] + } +``` + +#### Step 2: Integrate with Content Generation + +1. **Create GSC-Enhanced Content Generator** + +Create a new file at `/workspace/AI-Writer/lib/integrations/google/content_generator.py`: + +```python +"""Content generator enhanced with Google Search Console data.""" + +from typing import Dict, List, Any, Optional +import streamlit as st +from loguru import logger +from .keyword_research import generate_content_brief +from ...ai_writers.blog_writer import generate_blog_post + +def generate_seo_optimized_content( + keyword: str, + site_url: str, + content_type: str = "blog", + length: str = "medium" +) -> Dict[str, Any]: + """Generate SEO-optimized content using GSC data. + + Args: + keyword: Target keyword + site_url: URL of the site + content_type: Type of content to generate + length: Content length + + Returns: + Dict[str, Any]: Generated content + """ + try: + # Generate content brief + brief = generate_content_brief(keyword, site_url) + + # Extract related keywords + related_keywords = [k['keyword'] for k in brief['related_keywords']] + + # Prepare prompt enhancements + prompt_enhancements = { + 'primary_keyword': brief['primary_keyword'], + 'related_keywords': related_keywords, + 'search_position': brief['search_metrics']['avg_position'], + 'monthly_impressions': brief['search_metrics']['monthly_impressions'], + 'competing_urls': [p['page'] for p in brief['competing_pages'][:5]] + } + + # Generate content with enhanced prompt + content = generate_blog_post( + keyword=keyword, + content_type=content_type, + length=length, + prompt_enhancements=prompt_enhancements + ) + + # Add brief data to the result + content['brief'] = brief + + return content + except Exception as e: + logger.error(f"Error generating SEO-optimized content: {str(e)}") + return { + 'title': f"Error generating content for '{keyword}'", + 'content': f"An error occurred: {str(e)}", + 'brief': {} + } +``` + +2. **Create Content Optimization Component** + +Create a new file at `/workspace/AI-Writer/lib/integrations/google/content_optimizer.py`: + +```python +"""Content optimization using Google Search Console data.""" + +from typing import Dict, List, Any, Optional +import streamlit as st +from loguru import logger +from .search_console import get_keyword_insights +from ...seo_tools.analyzer import analyze_content_for_seo + +def optimize_existing_content( + content: str, + keyword: str, + site_url: str +) -> Dict[str, Any]: + """Optimize existing content using GSC data. + + Args: + content: Existing content + keyword: Target keyword + site_url: URL of the site + + Returns: + Dict[str, Any]: Optimization suggestions + """ + try: + # Get keyword insights + insights = get_keyword_insights(keyword, site_url, days=90) + + # Analyze content for SEO + seo_analysis = analyze_content_for_seo(content, keyword) + + # Generate optimization suggestions + suggestions = [] + + # Check keyword density + if seo_analysis['keyword_density'] < 0.5: + suggestions.append({ + 'type': 'keyword_density', + 'severity': 'high', + 'message': f"Keyword density is too low ({seo_analysis['keyword_density']:.2f}%). Aim for 1-2%.", + 'action': "Add more instances of the keyword in a natural way." + }) + elif seo_analysis['keyword_density'] > 3: + suggestions.append({ + 'type': 'keyword_density', + 'severity': 'medium', + 'message': f"Keyword density is too high ({seo_analysis['keyword_density']:.2f}%). Aim for 1-2%.", + 'action': "Reduce keyword usage to avoid keyword stuffing." + }) + + # Check title + if keyword.lower() not in seo_analysis['title'].lower(): + suggestions.append({ + 'type': 'title', + 'severity': 'high', + 'message': "Primary keyword is missing from the title.", + 'action': f"Add '{keyword}' to the title in a natural way." + }) + + # Check headings + if not any(keyword.lower() in h.lower() for h in seo_analysis['headings']): + suggestions.append({ + 'type': 'headings', + 'severity': 'medium', + 'message': "Primary keyword is missing from all headings.", + 'action': f"Add '{keyword}' to at least one heading (preferably H2)." + }) + + # Check content length + if seo_analysis['word_count'] < 300: + suggestions.append({ + 'type': 'content_length', + 'severity': 'high', + 'message': f"Content is too short ({seo_analysis['word_count']} words). Aim for at least 800 words.", + 'action': "Expand the content with more valuable information." + }) + elif seo_analysis['word_count'] < 800: + suggestions.append({ + 'type': 'content_length', + 'severity': 'medium', + 'message': f"Content is relatively short ({seo_analysis['word_count']} words). Aim for 1000+ words for competitive keywords.", + 'action': "Consider adding more depth to the content." + }) + + # Check readability + if seo_analysis['readability_score'] < 50: + suggestions.append({ + 'type': 'readability', + 'severity': 'medium', + 'message': f"Content readability is low ({seo_analysis['readability_score']}/100).", + 'action': "Simplify sentences, use shorter paragraphs, and avoid jargon." + }) + + # Check competing pages + competing_pages = insights['pages'] + if competing_pages: + top_competing_page = competing_pages[0] + suggestions.append({ + 'type': 'competition', + 'severity': 'info', + 'message': f"Your top competing page ranks at position {top_competing_page['position']:.1f}.", + 'action': f"Review the content at {top_competing_page['page']} for insights." + }) + + # Compile results + result = { + 'keyword': keyword, + 'search_metrics': { + 'monthly_impressions': insights['total_impressions'] // 3, + 'monthly_clicks': insights['total_clicks'] // 3, + 'avg_position': insights['avg_position'], + 'avg_ctr': insights['avg_ctr'] + }, + 'seo_analysis': seo_analysis, + 'suggestions': suggestions + } + + return result + except Exception as e: + logger.error(f"Error optimizing content: {str(e)}") + return { + 'keyword': keyword, + 'search_metrics': {}, + 'seo_analysis': {}, + 'suggestions': [{ + 'type': 'error', + 'severity': 'high', + 'message': f"Error analyzing content: {str(e)}", + 'action': "Please try again or contact support." + }] + } +``` + +#### Step 3: Create Content Planning Component + +Create a new file at `/workspace/AI-Writer/lib/integrations/google/content_planner.py`: + +```python +"""Content planning using Google Search Console data.""" + +import streamlit as st +from typing import Dict, List, Any, Optional +from datetime import datetime, timedelta +from loguru import logger +from .keyword_research import get_content_opportunities, get_keyword_clusters +from .search_console import get_top_keywords + +def render_content_planner(site_url: str): + """Render content planning interface using GSC data. + + Args: + site_url: URL of the site + """ + st.markdown("## Content Planning") + st.markdown(""" + Use your Google Search Console data to plan your content strategy. + Identify opportunities, organize topics, and create a content calendar. + """) + + # Create tabs + tab1, tab2, tab3 = st.tabs(["Content Opportunities", "Topic Clusters", "Content Calendar"]) + + with tab1: + st.markdown("### Content Opportunities") + st.markdown(""" + Find keywords where you're ranking on page 1-2 but not in the top 3. + These are opportunities to improve existing content or create new content. + """) + + # Filters + col1, col2 = st.columns(2) + + with col1: + min_impressions = st.slider( + "Minimum monthly impressions", + min_value=10, + max_value=1000, + value=100, + step=10 + ) + + with col2: + max_position = st.slider( + "Maximum position", + min_value=5.0, + max_value=30.0, + value=20.0, + step=1.0 + ) + + # Get opportunities + if st.button("Find Opportunities"): + with st.spinner("Analyzing search data..."): + opportunities = get_content_opportunities( + site_url=site_url, + min_impressions=min_impressions, + max_position=max_position + ) + + if not opportunities: + st.info("No content opportunities found with the current filters.") + else: + # Display opportunities + st.markdown(f"Found {len(opportunities)} content opportunities:") + + # Create dataframe + st.dataframe( + opportunities, + column_config={ + "keyword": "Keyword", + "clicks": st.column_config.NumberColumn("Clicks", format="%d"), + "impressions": st.column_config.NumberColumn("Impressions", format="%d"), + "ctr": st.column_config.NumberColumn("CTR", format="%.2f%%"), + "position": st.column_config.NumberColumn("Position", format="%.1f"), + "potential": st.column_config.NumberColumn("Potential", format="%.1f") + }, + hide_index=True + ) + + # Action buttons for selected opportunity + st.markdown("### Take Action") + selected_keyword = st.selectbox( + "Select a keyword to take action on", + options=[o['keyword'] for o in opportunities] + ) + + col1, col2 = st.columns(2) + + with col1: + if st.button("Create New Content", key="create_new"): + st.session_state.selected_keyword = selected_keyword + st.session_state.action = "create_content" + st.experimental_rerun() + + with col2: + if st.button("Optimize Existing Content", key="optimize"): + st.session_state.selected_keyword = selected_keyword + st.session_state.action = "optimize_content" + st.experimental_rerun() + + with tab2: + st.markdown("### Topic Clusters") + st.markdown(""" + Group your keywords into topic clusters to organize your content strategy. + This helps identify themes and create comprehensive content around topics. + """) + + # Cluster settings + min_cluster_size = st.slider( + "Minimum cluster size", + min_value=2, + max_value=10, + value=3, + step=1 + ) + + # Get clusters + if st.button("Generate Topic Clusters"): + with st.spinner("Analyzing keywords..."): + # Get top keywords + keywords = get_top_keywords(site_url, days=90, limit=1000) + + # Generate clusters + clusters = get_keyword_clusters( + keywords=keywords, + threshold=min_cluster_size + ) + + if not clusters: + st.info("No topic clusters found with the current settings.") + else: + # Display clusters + st.markdown(f"Found {len(clusters)} topic clusters:") + + for i, cluster in enumerate(clusters): + with st.expander(f"Cluster: {cluster['topic']} ({cluster['keyword_count']} keywords)"): + st.markdown(f"**Total Impressions:** {cluster['total_impressions']:,}") + st.markdown(f"**Total Clicks:** {cluster['total_clicks']:,}") + st.markdown(f"**Average Position:** {cluster['avg_position']:.1f}") + + # Display keywords in cluster + st.dataframe( + cluster['keywords'], + column_config={ + "keyword": "Keyword", + "clicks": st.column_config.NumberColumn("Clicks", format="%d"), + "impressions": st.column_config.NumberColumn("Impressions", format="%d"), + "position": st.column_config.NumberColumn("Position", format="%.1f") + }, + hide_index=True + ) + + # Action button + if st.button("Create Cluster Content", key=f"cluster_{i}"): + st.session_state.selected_cluster = cluster + st.session_state.action = "create_cluster_content" + st.experimental_rerun() + + with tab3: + st.markdown("### Content Calendar") + st.markdown(""" + Create a content calendar based on your search data and opportunities. + Plan your content strategy for the coming weeks or months. + """) + + # Calendar settings + num_weeks = st.slider( + "Number of weeks to plan", + min_value=1, + max_value=12, + value=4, + step=1 + ) + + posts_per_week = st.slider( + "Posts per week", + min_value=1, + max_value=7, + value=2, + step=1 + ) + + # Generate calendar + if st.button("Generate Content Calendar"): + with st.spinner("Creating content calendar..."): + # Get opportunities + opportunities = get_content_opportunities( + site_url=site_url, + min_impressions=50, + max_position=30.0 + ) + + # Sort by potential + opportunities.sort(key=lambda x: x['potential'], reverse=True) + + # Calculate total posts needed + total_posts = num_weeks * posts_per_week + + # Limit opportunities to needed posts + selected_opportunities = opportunities[:total_posts] + + # Create calendar + calendar = [] + start_date = datetime.now() + timedelta(days=7) # Start next week + + for week in range(num_weeks): + week_start = start_date + timedelta(weeks=week) + + for day in range(posts_per_week): + opp_index = week * posts_per_week + day + + if opp_index < len(selected_opportunities): + post_date = week_start + timedelta(days=day) + + calendar.append({ + 'date': post_date.strftime('%Y-%m-%d'), + 'keyword': selected_opportunities[opp_index]['keyword'], + 'impressions': selected_opportunities[opp_index]['impressions'], + 'position': selected_opportunities[opp_index]['position'], + 'potential': selected_opportunities[opp_index]['potential'] + }) + + if not calendar: + st.info("Could not generate content calendar. Try adjusting your filters.") + else: + # Display calendar + st.markdown(f"Generated content calendar with {len(calendar)} posts:") + + # Group by week + weeks = {} + for post in calendar: + post_date = datetime.strptime(post['date'], '%Y-%m-%d') + week_num = post_date.isocalendar()[1] + week_start = post_date - timedelta(days=post_date.weekday()) + week_key = f"Week {week_num} ({week_start.strftime('%b %d')})" + + if week_key not in weeks: + weeks[week_key] = [] + + weeks[week_key].append(post) + + # Display by week + for week_key, posts in weeks.items(): + with st.expander(week_key, expanded=True): + for post in posts: + st.markdown(f"**{post['date']}:** {post['keyword']}") + st.markdown(f"Impressions: {post['impressions']:,} | Position: {post['position']:.1f}") + st.markdown("---") + + # Export button + if st.download_button( + label="Export Calendar (CSV)", + data="\n".join([ + "date,keyword,impressions,position,potential", + *[f"{post['date']},{post['keyword']},{post['impressions']},{post['position']},{post['potential']}" for post in calendar] + ]), + file_name="content_calendar.csv", + mime="text/csv" + ): + st.success("Calendar exported successfully!") +``` + +#### Step 4: Integrate with Content Writers + +1. **Update Blog Writer** + +Modify the existing blog writer to accept GSC data: + +```python +# Add to blog_writer.py + +def generate_blog_post( + keyword: str, + content_type: str = "blog", + length: str = "medium", + prompt_enhancements: Dict[str, Any] = None +) -> Dict[str, Any]: + """Generate a blog post with optional GSC data enhancement. + + Args: + keyword: Target keyword + content_type: Type of content to generate + length: Content length + prompt_enhancements: Optional GSC data for prompt enhancement + + Returns: + Dict[str, Any]: Generated content + """ + # Existing code... + + # Enhance prompt with GSC data if available + if prompt_enhancements: + # Add related keywords to prompt + if 'related_keywords' in prompt_enhancements: + related_kw_str = ", ".join(prompt_enhancements['related_keywords'][:5]) + prompt += f"\nInclude these related keywords naturally: {related_kw_str}" + + # Add search position context + if 'search_position' in prompt_enhancements: + position = prompt_enhancements['search_position'] + if position > 10: + prompt += f"\nThis keyword currently ranks at position {position:.1f}, which is on page 2. Create comprehensive content to improve ranking." + else: + prompt += f"\nThis keyword currently ranks at position {position:.1f}. Enhance the content to improve ranking further." + + # Add impression data + if 'monthly_impressions' in prompt_enhancements: + impressions = prompt_enhancements['monthly_impressions'] + prompt += f"\nThis keyword gets approximately {impressions} monthly impressions." + + # Add competing URLs + if 'competing_urls' in prompt_enhancements: + prompt += "\nMake sure your content is more comprehensive than competing pages." + + # Continue with existing code... +``` + +2. **Add GSC Data to SEO Tools** + +Update the SEO analyzer to incorporate GSC data: + +```python +# Add to seo_tools/analyzer.py + +def analyze_content_with_gsc( + content: str, + keyword: str, + gsc_data: Dict[str, Any] +) -> Dict[str, Any]: + """Analyze content with GSC data enhancement. + + Args: + content: Content to analyze + keyword: Target keyword + gsc_data: Google Search Console data + + Returns: + Dict[str, Any]: Enhanced analysis + """ + # Get basic SEO analysis + analysis = analyze_content_for_seo(content, keyword) + + # Enhance with GSC data + if gsc_data: + # Add search metrics + analysis['search_metrics'] = { + 'impressions': gsc_data.get('impressions', 0), + 'clicks': gsc_data.get('clicks', 0), + 'position': gsc_data.get('position', 0), + 'ctr': gsc_data.get('ctr', 0) + } + + # Add competition analysis + if 'competing_pages' in gsc_data: + analysis['competition'] = { + 'competing_pages': gsc_data['competing_pages'], + 'ranking_gap': analysis['word_count'] - gsc_data.get('avg_competitor_length', 0) + } + + # Add keyword opportunity score + if 'position' in gsc_data and 'impressions' in gsc_data: + position = gsc_data['position'] + impressions = gsc_data['impressions'] + + # Calculate opportunity score (higher for keywords with good impressions but not yet in top 3) + if position <= 3: + opportunity_score = 30 # Already ranking well + elif position <= 10: + opportunity_score = 70 # On first page but not top 3 + elif position <= 20: + opportunity_score = 50 # On second page + else: + opportunity_score = 30 # Beyond second page + + # Adjust for impressions + if impressions > 1000: + opportunity_score += 30 + elif impressions > 500: + opportunity_score += 20 + elif impressions > 100: + opportunity_score += 10 + + # Cap at 100 + opportunity_score = min(opportunity_score, 100) + + analysis['opportunity_score'] = opportunity_score + + return analysis +``` + +### Phase 3: Testing Plan + +#### Step 1: Unit Testing + +1. **Create Test Files** + +Create a new file at `/workspace/AI-Writer/tests/integrations/google/test_auth.py`: + +```python +"""Unit tests for Google authentication module.""" + +import unittest +from unittest.mock import patch, MagicMock +import os +import sys +import json +from pathlib import Path + +# Add project root to path +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + +from lib.integrations.google.auth import ( + get_google_auth_url, + handle_auth_callback, + save_credentials, + load_credentials, + get_user_info, + credentials_to_dict, + is_authenticated +) + +class TestGoogleAuth(unittest.TestCase): + """Test cases for Google authentication module.""" + + def setUp(self): + """Set up test environment.""" + # Mock session state + self.session_state_patch = patch('streamlit.session_state', {}) + self.mock_session_state = self.session_state_patch.start() + + # Mock secrets + self.secrets_patch = patch('streamlit.secrets', { + 'google_oauth': { + 'web': { + 'client_id': 'test_client_id', + 'client_secret': 'test_client_secret', + 'redirect_uris': ['http://localhost:8501/oauth/callback'] + } + } + }) + self.mock_secrets = self.secrets_patch.start() + + def tearDown(self): + """Clean up after tests.""" + self.session_state_patch.stop() + self.secrets_patch.stop() + + @patch('lib.integrations.google.auth.Flow') + def test_get_google_auth_url(self, mock_flow): + """Test generating Google auth URL.""" + # Mock flow instance + mock_flow_instance = MagicMock() + mock_flow_instance.authorization_url.return_value = ('https://test-auth-url.com', None) + mock_flow.from_client_config.return_value = mock_flow_instance + + # Call function + url = get_google_auth_url() + + # Assertions + self.assertEqual(url, 'https://test-auth-url.com') + mock_flow.from_client_config.assert_called_once() + mock_flow_instance.authorization_url.assert_called_once() + + @patch('lib.integrations.google.auth.get_user_info') + def test_handle_auth_callback(self, mock_get_user_info): + """Test handling auth callback.""" + # Mock flow in session state + mock_flow = MagicMock() + mock_flow.credentials = MagicMock() + self.mock_session_state['google_auth_flow'] = mock_flow + + # Mock user info + mock_get_user_info.return_value = {'email': 'test@example.com', 'name': 'Test User'} + + # Call function + success, user_info = handle_auth_callback('test_code') + + # Assertions + self.assertTrue(success) + self.assertEqual(user_info['email'], 'test@example.com') + mock_flow.fetch_token.assert_called_once_with(code='test_code') + mock_get_user_info.assert_called_once() + + @patch('builtins.open', new_callable=unittest.mock.mock_open) + @patch('json.dump') + @patch('lib.integrations.google.auth.get_user_info') + def test_save_credentials(self, mock_get_user_info, mock_json_dump, mock_open): + """Test saving credentials.""" + # Mock credentials + mock_credentials = MagicMock() + mock_credentials.token = 'test_token' + mock_credentials.refresh_token = 'test_refresh_token' + + # Mock user info + mock_get_user_info.return_value = {'email': 'test@example.com'} + + # Call function + result = save_credentials(mock_credentials) + + # Assertions + self.assertTrue(result) + mock_get_user_info.assert_called_once() + mock_open.assert_called_once() + mock_json_dump.assert_called_once() + + def test_credentials_to_dict(self): + """Test converting credentials to dictionary.""" + # Mock credentials + mock_credentials = MagicMock() + mock_credentials.token = 'test_token' + mock_credentials.refresh_token = 'test_refresh_token' + mock_credentials.token_uri = 'test_token_uri' + mock_credentials.client_id = 'test_client_id' + mock_credentials.client_secret = 'test_client_secret' + mock_credentials.scopes = ['test_scope'] + + # Call function + result = credentials_to_dict(mock_credentials) + + # Assertions + self.assertEqual(result['token'], 'test_token') + self.assertEqual(result['refresh_token'], 'test_refresh_token') + self.assertEqual(result['client_id'], 'test_client_id') + self.assertEqual(result['scopes'], ['test_scope']) + + def test_is_authenticated(self): + """Test authentication check.""" + # Test not authenticated + self.assertFalse(is_authenticated()) + + # Test authenticated + self.mock_session_state['google_credentials'] = {'token': 'test'} + self.mock_session_state['google_user_info'] = {'email': 'test@example.com'} + self.assertTrue(is_authenticated()) + +if __name__ == '__main__': + unittest.main() +``` + +2. **Create Test for Search Console API** + +Create a new file at `/workspace/AI-Writer/tests/integrations/google/test_search_console.py`: + +```python +"""Unit tests for Google Search Console API module.""" + +import unittest +from unittest.mock import patch, MagicMock +import os +import sys +from pathlib import Path +from datetime import datetime, timedelta + +# Add project root to path +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + +from lib.integrations.google.search_console import ( + get_search_console_service, + get_site_list, + get_search_analytics, + get_top_keywords, + get_top_pages, + get_keyword_insights +) + +class TestSearchConsole(unittest.TestCase): + """Test cases for Google Search Console API module.""" + + def setUp(self): + """Set up test environment.""" + # Mock session state + self.session_state_patch = patch('streamlit.session_state', { + 'google_credentials': { + 'token': 'test_token', + 'refresh_token': 'test_refresh_token', + 'token_uri': 'test_token_uri', + 'client_id': 'test_client_id', + 'client_secret': 'test_client_secret', + 'scopes': ['test_scope'] + } + }) + self.mock_session_state = self.session_state_patch.start() + + def tearDown(self): + """Clean up after tests.""" + self.session_state_patch.stop() + + @patch('lib.integrations.google.search_console.build') + @patch('lib.integrations.google.search_console.Credentials') + def test_get_search_console_service(self, mock_credentials, mock_build): + """Test building Search Console service.""" + # Mock credentials + mock_credentials_instance = MagicMock() + mock_credentials.from_authorized_user_info.return_value = mock_credentials_instance + + # Mock service + mock_service = MagicMock() + mock_build.return_value = mock_service + + # Call function + service = get_search_console_service(self.mock_session_state['google_credentials']) + + # Assertions + self.assertEqual(service, mock_service) + mock_credentials.from_authorized_user_info.assert_called_once() + mock_build.assert_called_once_with('searchconsole', 'v1', credentials=mock_credentials_instance) + + @patch('lib.integrations.google.search_console.get_search_console_service') + def test_get_site_list(self, mock_get_service): + """Test getting site list.""" + # Mock service + mock_service = MagicMock() + mock_sites = MagicMock() + mock_sites.list().execute.return_value = { + 'siteEntry': [ + { + 'siteUrl': 'https://example.com/', + 'permissionLevel': 'siteOwner' + }, + { + 'siteUrl': 'sc-domain:example.org', + 'permissionLevel': 'siteFullUser' + }, + { + 'siteUrl': 'https://example.net/', + 'permissionLevel': 'siteRestrictedUser' # Should be filtered out + } + ] + } + mock_service.sites.return_value = mock_sites + mock_get_service.return_value = mock_service + + # Call function + sites = get_site_list() + + # Assertions + self.assertEqual(len(sites), 2) # Only 2 sites with sufficient permissions + self.assertEqual(sites[0]['site_url'], 'https://example.com/') + self.assertEqual(sites[0]['permission_level'], 'siteOwner') + self.assertEqual(sites[0]['site_type'], 'Web') + self.assertEqual(sites[1]['site_url'], 'sc-domain:example.org') + self.assertEqual(sites[1]['site_type'], 'Domain Property') + + @patch('lib.integrations.google.search_console.get_search_console_service') + def test_get_search_analytics(self, mock_get_service): + """Test getting search analytics data.""" + # Mock service + mock_service = MagicMock() + mock_analytics = MagicMock() + mock_analytics.query().execute.return_value = { + 'rows': [ + { + 'keys': ['test keyword'], + 'clicks': 100, + 'impressions': 1000, + 'ctr': 0.1, + 'position': 5.5 + } + ] + } + mock_service.searchanalytics.return_value = mock_analytics + mock_get_service.return_value = mock_service + + # Call function + start_date = datetime.now() - timedelta(days=30) + end_date = datetime.now() + result = get_search_analytics( + site_url='https://example.com/', + start_date=start_date, + end_date=end_date, + dimensions=['query'], + row_limit=100 + ) + + # Assertions + self.assertEqual(len(result['rows']), 1) + mock_analytics.query.assert_called_once() + call_args = mock_analytics.query.call_args[1] + self.assertEqual(call_args['siteUrl'], 'https://example.com/') + self.assertEqual(call_args['body']['dimensions'], ['query']) + self.assertEqual(call_args['body']['rowLimit'], 100) + + @patch('lib.integrations.google.search_console.get_search_analytics') + def test_get_top_keywords(self, mock_get_analytics): + """Test getting top keywords.""" + # Mock analytics response + mock_get_analytics.return_value = { + 'rows': [ + { + 'keys': ['keyword1'], + 'clicks': 100, + 'impressions': 1000, + 'ctr': 0.1, + 'position': 5.5 + }, + { + 'keys': ['keyword2'], + 'clicks': 200, + 'impressions': 2000, + 'ctr': 0.1, + 'position': 3.2 + } + ] + } + + # Call function + keywords = get_top_keywords( + site_url='https://example.com/', + days=30, + limit=100 + ) + + # Assertions + self.assertEqual(len(keywords), 2) + self.assertEqual(keywords[0]['keyword'], 'keyword1') + self.assertEqual(keywords[0]['clicks'], 100) + self.assertEqual(keywords[0]['impressions'], 1000) + self.assertEqual(keywords[0]['ctr'], 10.0) # Converted to percentage + self.assertEqual(keywords[0]['position'], 5.5) + mock_get_analytics.assert_called_once() + +if __name__ == '__main__': + unittest.main() +``` + +#### Step 2: Integration Testing + +1. **Create Integration Test Script** + +Create a new file at `/workspace/AI-Writer/tests/integrations/google/test_integration.py`: + +```python +"""Integration tests for Google integration.""" + +import unittest +import os +import sys +from pathlib import Path +import streamlit as st +from unittest.mock import patch, MagicMock + +# Add project root to path +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + +from lib.integrations.google.auth import get_google_auth_url, handle_auth_callback +from lib.integrations.google.search_console import get_site_list, get_top_keywords +from lib.integrations.google.keyword_research import get_content_opportunities +from lib.integrations.google.content_generator import generate_seo_optimized_content + +class TestGoogleIntegration(unittest.TestCase): + """Integration tests for Google integration.""" + + @classmethod + def setUpClass(cls): + """Set up test environment.""" + # Check if we have real credentials for testing + cls.has_real_credentials = os.environ.get('GOOGLE_TEST_CREDENTIALS') is not None + + if cls.has_real_credentials: + # Load real credentials from environment + import json + credentials_json = os.environ.get('GOOGLE_TEST_CREDENTIALS') + cls.test_credentials = json.loads(credentials_json) + + # Set up session state with real credentials + st.session_state['google_credentials'] = cls.test_credentials + st.session_state['google_user_info'] = { + 'email': os.environ.get('GOOGLE_TEST_EMAIL', 'test@example.com'), + 'name': 'Test User' + } + + def test_auth_flow(self): + """Test authentication flow.""" + # Skip if no real credentials + if not self.has_real_credentials: + self.skipTest("No real credentials available for testing") + + # Get auth URL + auth_url = get_google_auth_url() + + # Verify URL format + self.assertTrue(auth_url.startswith('https://accounts.google.com/o/oauth2/auth')) + self.assertIn('client_id=', auth_url) + self.assertIn('redirect_uri=', auth_url) + self.assertIn('scope=', auth_url) + + def test_search_console_access(self): + """Test Search Console API access.""" + # Skip if no real credentials + if not self.has_real_credentials: + self.skipTest("No real credentials available for testing") + + # Get site list + sites = get_site_list() + + # Verify we got some sites + self.assertIsInstance(sites, list) + + # If we have sites, test keyword data + if sites: + site_url = sites[0]['site_url'] + + # Get top keywords + keywords = get_top_keywords(site_url, days=30, limit=10) + + # Verify we got keyword data + self.assertIsInstance(keywords, list) + + # If we have keywords, test content opportunities + if keywords: + # Get content opportunities + opportunities = get_content_opportunities(site_url, min_impressions=10, max_position=50.0) + + # Verify we got opportunities + self.assertIsInstance(opportunities, list) + + @patch('lib.integrations.google.content_generator.generate_blog_post') + def test_content_generation(self, mock_generate_blog_post): + """Test content generation with GSC data.""" + # Mock blog post generation + mock_generate_blog_post.return_value = { + 'title': 'Test Blog Post', + 'content': 'This is a test blog post.' + } + + # Generate content + content = generate_seo_optimized_content( + keyword='test keyword', + site_url='https://example.com/', + content_type='blog', + length='medium' + ) + + # Verify content was generated + self.assertEqual(content['title'], 'Test Blog Post') + self.assertEqual(content['content'], 'This is a test blog post.') + + # Verify prompt enhancements were passed + mock_generate_blog_post.assert_called_once() + call_args = mock_generate_blog_post.call_args[1] + self.assertEqual(call_args['keyword'], 'test keyword') + self.assertEqual(call_args['content_type'], 'blog') + self.assertEqual(call_args['length'], 'medium') + self.assertIn('prompt_enhancements', call_args) + +if __name__ == '__main__': + unittest.main() +``` + +#### Step 3: End-to-End Testing + +1. **Create Manual Test Plan** + +Create a new file at `/workspace/AI-Writer/tests/integrations/google/manual_test_plan.md`: + +```markdown +# Google Integration Manual Test Plan + +This document outlines the manual testing procedures for the Google integration in AI-Writer. + +## Prerequisites + +- Google account with access to Search Console +- Website verified in Google Search Console +- AI-Writer development environment set up + +## Test Cases + +### 1. Authentication Flow + +#### 1.1 Google Login + +1. Navigate to the Google Integration page +2. Click "Sign in with Google" +3. Complete Google authentication flow +4. Verify user is redirected back to AI-Writer +5. Verify user info is displayed correctly + +**Expected Result:** User is successfully authenticated and user info is displayed. + +#### 1.2 Logout + +1. Click "Sign Out" button +2. Verify user is logged out +3. Verify login button is displayed again + +**Expected Result:** User is successfully logged out. + +### 2. Search Console Access + +#### 2.1 Site Selection + +1. Authenticate with Google +2. Verify site selector displays user's verified sites +3. Select a site from the dropdown +4. Verify Search Console dashboard loads + +**Expected Result:** User's verified sites are displayed and can be selected. + +#### 2.2 Top Keywords + +1. Select a site +2. Navigate to "Top Keywords" tab +3. Adjust time period slider +4. Verify keyword data loads and updates + +**Expected Result:** Keyword data is displayed and updates when time period changes. + +#### 2.3 Top Pages + +1. Select a site +2. Navigate to "Top Pages" tab +3. Adjust time period slider +4. Verify page data loads and updates + +**Expected Result:** Page data is displayed and updates when time period changes. + +#### 2.4 Keyword Research + +1. Select a site +2. Navigate to "Keyword Research" tab +3. Enter a keyword +4. Verify keyword insights are displayed +5. Verify performance charts are displayed +6. Verify ranking pages are displayed + +**Expected Result:** Keyword insights and related data are displayed correctly. + +### 3. Content Planning + +#### 3.1 Content Opportunities + +1. Navigate to Content Planning +2. Select "Content Opportunities" tab +3. Adjust filters +4. Click "Find Opportunities" +5. Verify opportunities are displayed +6. Select a keyword and click "Create New Content" +7. Verify redirection to content creation + +**Expected Result:** Content opportunities are found and can be used for content creation. + +#### 3.2 Topic Clusters + +1. Navigate to Content Planning +2. Select "Topic Clusters" tab +3. Adjust cluster settings +4. Click "Generate Topic Clusters" +5. Verify clusters are displayed +6. Expand a cluster to view keywords +7. Click "Create Cluster Content" +8. Verify redirection to content creation + +**Expected Result:** Topic clusters are generated and can be used for content creation. + +#### 3.3 Content Calendar + +1. Navigate to Content Planning +2. Select "Content Calendar" tab +3. Adjust calendar settings +4. Click "Generate Content Calendar" +5. Verify calendar is displayed +6. Verify export functionality works + +**Expected Result:** Content calendar is generated and can be exported. + +### 4. Content Creation + +#### 4.1 SEO-Optimized Content + +1. Select a keyword from opportunities +2. Click "Create New Content" +3. Verify GSC data is incorporated in the prompt +4. Generate content +5. Verify content includes related keywords +6. Verify content addresses search intent + +**Expected Result:** Generated content is optimized based on GSC data. + +#### 4.2 Content Optimization + +1. Select a keyword +2. Click "Optimize Existing Content" +3. Enter or paste existing content +4. Click "Analyze Content" +5. Verify optimization suggestions are displayed +6. Apply suggestions +7. Verify content improvements + +**Expected Result:** Content optimization suggestions are provided and can be applied. + +## Test Execution Checklist + +| Test Case | Status | Notes | +|-----------|--------|-------| +| 1.1 Google Login | | | +| 1.2 Logout | | | +| 2.1 Site Selection | | | +| 2.2 Top Keywords | | | +| 2.3 Top Pages | | | +| 2.4 Keyword Research | | | +| 3.1 Content Opportunities | | | +| 3.2 Topic Clusters | | | +| 3.3 Content Calendar | | | +| 4.1 SEO-Optimized Content | | | +| 4.2 Content Optimization | | | +``` + +#### Step 4: Automated Testing Setup + +1. **Create GitHub Actions Workflow** + +Create a new file at `/.github/workflows/google_integration_tests.yml`: + +```yaml +name: Google Integration Tests + +on: + push: + branches: [ main ] + paths: + - 'lib/integrations/google/**' + - 'tests/integrations/google/**' + pull_request: + branches: [ main ] + paths: + - 'lib/integrations/google/**' + - 'tests/integrations/google/**' + workflow_dispatch: + +jobs: + unit-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install pytest pytest-cov + + - name: Run unit tests + run: | + pytest tests/integrations/google/test_auth.py tests/integrations/google/test_search_console.py -v + + - name: Generate coverage report + run: | + pytest --cov=lib.integrations.google tests/integrations/google/ --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: google-integration + fail_ci_if_error: false + + integration-tests: + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install pytest + + - name: Run integration tests + env: + GOOGLE_TEST_CREDENTIALS: ${{ secrets.GOOGLE_TEST_CREDENTIALS }} + GOOGLE_TEST_EMAIL: ${{ secrets.GOOGLE_TEST_EMAIL }} + run: | + pytest tests/integrations/google/test_integration.py -v +``` + +## Deployment Plan + +### Phase 1: Development Environment + +1. **Initial Setup** + - Set up Google Cloud Project + - Configure OAuth consent screen + - Create OAuth credentials + - Enable required APIs + - Implement authentication module + +2. **Local Testing** + - Test authentication flow + - Test Search Console API access + - Test content generation with GSC data + - Run unit tests + +### Phase 2: Staging Environment + +1. **Configuration** + - Create staging Google Cloud Project + - Configure OAuth for staging domain + - Update redirect URIs + - Set up staging environment variables + +2. **Integration Testing** + - Deploy to staging environment + - Test end-to-end flow + - Verify data accuracy + - Test with multiple users and sites + +3. **Performance Testing** + - Test with large datasets + - Optimize API calls + - Implement caching if needed + - Monitor API usage limits + +### Phase 3: Production Deployment + +1. **Final Configuration** + - Create production Google Cloud Project + - Configure OAuth for production domain + - Set up production environment variables + - Verify API quotas and limits + +2. **Deployment** + - Deploy to production + - Monitor for errors + - Verify authentication flow + - Test with real user accounts + +3. **Post-Deployment** + - Monitor API usage + - Collect user feedback + - Address any issues + - Document the integration + +## User Documentation + +### User Guide + +Create a user guide that explains: + +1. How to connect Google account +2. How to access Search Console data +3. How to use content opportunities +4. How to create SEO-optimized content +5. How to use the content calendar + +### Video Tutorial + +Create a video tutorial demonstrating: + +1. Initial setup process +2. Finding content opportunities +3. Creating content with GSC data +4. Optimizing existing content +5. Planning content with the calendar + +## Conclusion + +This implementation plan provides a comprehensive approach to integrating Google login and Google Search Console with AI-Writer. By following these steps, you'll enhance the platform with real user insights for content creation, improving the relevance and effectiveness of generated content. + +The integration will provide significant value to users by: + +1. Leveraging real search data for content creation +2. Identifying content opportunities based on actual performance +3. Optimizing content for keywords that matter to the user's audience +4. Creating a data-driven content strategy +5. Measuring content performance over time + +This feature will differentiate AI-Writer from competitors by providing a closed-loop system for content creation, optimization, and performance tracking. \ No newline at end of file diff --git a/Roadmap TBDs/ONBOARDING_IMPROVEMENTS.md b/Roadmap TBDs/ONBOARDING_IMPROVEMENTS.md new file mode 100644 index 00000000..88099474 --- /dev/null +++ b/Roadmap TBDs/ONBOARDING_IMPROVEMENTS.md @@ -0,0 +1,631 @@ +# Onboarding Process Improvements + +This document outlines a comprehensive plan to improve the user onboarding experience in AI-Writer, focusing on the API key management and initial setup process. + +## Current Issues + +After analyzing the current onboarding process in `utils.api_key_manager`, several issues were identified: + +### User Experience Issues + +- **Complex Multi-step Process**: The onboarding is split across multiple steps without clear indication of progress or purpose +- **Confusing Navigation**: Users can get lost between steps with no clear path forward +- **Required vs. Optional**: No clear distinction between required and optional API keys +- **No Skip Option**: Users must go through all steps even if some are not relevant to them +- **Limited Guidance**: Insufficient contextual help for users unfamiliar with API keys + +### Technical Issues + +- **Inconsistent State Management**: Wizard state is initialized in multiple places +- **Basic Validation**: API keys are only checked for non-emptiness, not actual validity +- **Environment Variable Handling**: Not robust across different environments +- **Error Handling**: Inconsistent error handling and user feedback +- **No Testing Mechanism**: No way to test API keys during setup + +### UI/Design Issues + +- **Inconsistent Styling**: Visual inconsistency across different components +- **Poor Mobile Experience**: Limited responsiveness for mobile users +- **Visual Hierarchy**: Lack of clear visual distinction for important elements +- **Help Text Visibility**: Instructions and help text are not prominent enough + +## Proposed Improvements + +### 1. Redesigned Onboarding Flow + +#### Welcome Screen + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ Welcome to AI-Writer! πŸ‘‹ β”‚ +β”‚ β”‚ +β”‚ Let's get you set up in just a few minutes. β”‚ +β”‚ β”‚ +β”‚ What would you like to do? β”‚ +β”‚ β”‚ +β”‚ β—‹ Quick Start (minimal setup) β”‚ +β”‚ β—‹ Complete Setup (all features) β”‚ +β”‚ β—‹ Import Configuration β”‚ +β”‚ β”‚ +β”‚ [Start Setup] β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +#### API Key Setup Screen + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ Step 1 of 4: Connect AI Models β”‚ +β”‚ ━━━━━━━━━━○○○○ β”‚ +β”‚ β”‚ +β”‚ Required (choose at least one): β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ⭐ OpenAI API Key β”‚ β”‚ +β”‚ β”‚ [β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’] β”‚ β”‚ +β”‚ β”‚ βœ“ Validated successfully! β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ⭐ Google Gemini API Key β”‚ β”‚ +β”‚ β”‚ [ ] β”‚ β”‚ +β”‚ β”‚ ℹ️ Not configured (optional) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Optional: β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Anthropic API Key (Coming Soon) β”‚ β”‚ +β”‚ β”‚ [ ] β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ [Skip Optional] [Test Keys] [Continue β†’] β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +#### Progress Summary Screen + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ Setup Complete! πŸŽ‰ β”‚ +β”‚ β”‚ +β”‚ Here's your configuration: β”‚ +β”‚ β”‚ +β”‚ AI Models: β”‚ +β”‚ βœ… OpenAI API - Connected β”‚ +β”‚ ❌ Google Gemini - Not configured β”‚ +β”‚ β”‚ +β”‚ Research Tools: β”‚ +β”‚ βœ… Tavily Search - Connected β”‚ +β”‚ ❌ Serper API - Not configured β”‚ +β”‚ β”‚ +β”‚ Publishing: β”‚ +β”‚ ❌ WordPress - Not configured β”‚ +β”‚ β”‚ +β”‚ You can change these settings anytime from β”‚ +β”‚ the Settings menu. β”‚ +β”‚ β”‚ +β”‚ [Edit Configuration] [Start Using AI-Writer] β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 2. Technical Improvements + +#### Unified State Management + +```python +# Create a dedicated state manager class +class OnboardingStateManager: + """Manages the state of the onboarding process.""" + + def __init__(self): + """Initialize the onboarding state.""" + if 'onboarding_state' not in st.session_state: + st.session_state.onboarding_state = { + 'current_step': 1, + 'total_steps': 4, + 'completed_steps': set(), + 'api_keys': {}, + 'validated_keys': {}, + 'setup_mode': 'quick_start', # or 'complete' + 'setup_complete': False + } + + def get_state(self): + """Get the current onboarding state.""" + return st.session_state.onboarding_state + + def update_state(self, updates): + """Update the onboarding state.""" + st.session_state.onboarding_state.update(updates) + + def next_step(self): + """Move to the next step.""" + current = st.session_state.onboarding_state['current_step'] + total = st.session_state.onboarding_state['total_steps'] + + if current < total: + st.session_state.onboarding_state['current_step'] += 1 + st.session_state.onboarding_state['completed_steps'].add(current) + + def previous_step(self): + """Move to the previous step.""" + if st.session_state.onboarding_state['current_step'] > 1: + st.session_state.onboarding_state['current_step'] -= 1 + + def skip_to_step(self, step): + """Skip to a specific step.""" + if 1 <= step <= st.session_state.onboarding_state['total_steps']: + st.session_state.onboarding_state['current_step'] = step + + def mark_complete(self): + """Mark the onboarding as complete.""" + st.session_state.onboarding_state['setup_complete'] = True +``` + +#### Enhanced API Key Validation + +```python +async def validate_api_key(service, key): + """Validate an API key by making a test request.""" + try: + if service == "openai": + # Test OpenAI API key with a minimal request + import openai + client = openai.OpenAI(api_key=key) + response = await client.models.list() + return {"valid": True, "models": [model.id for model in response.data[:5]]} + + elif service == "gemini": + # Test Google Gemini API key + import google.generativeai as genai + genai.configure(api_key=key) + models = genai.list_models() + return {"valid": True, "models": [model.name for model in models]} + + elif service == "tavily": + # Test Tavily API key + import requests + response = requests.get( + "https://api.tavily.com/health", + headers={"x-api-key": key} + ) + if response.status_code == 200: + return {"valid": True, "status": "healthy"} + else: + return {"valid": False, "error": f"Status code: {response.status_code}"} + + # Add more services as needed + + except Exception as e: + return {"valid": False, "error": str(e)} +``` + +#### Secure API Key Storage + +```python +def save_api_keys(keys_dict): + """Save API keys securely.""" + try: + # 1. Save to .env file + env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env') + + # Read existing .env file + env_contents = {} + if os.path.exists(env_path): + with open(env_path, 'r') as f: + for line in f: + if '=' in line: + key, value = line.strip().split('=', 1) + env_contents[key] = value + + # Update with new keys + for key_name, key_value in keys_dict.items(): + if key_value: # Only save non-empty keys + env_key = f"{key_name.upper()}_API_KEY" + env_contents[env_key] = key_value + + # Write back to .env file + with open(env_path, 'w') as f: + for key, value in env_contents.items(): + f.write(f"{key}={value}\n") + + # 2. Also store in session state for immediate use + for key_name, key_value in keys_dict.items(): + if key_value: + st.session_state[f"{key_name}_api_key"] = key_value + + # 3. Set environment variables for current session + for key_name, key_value in keys_dict.items(): + if key_value: + os.environ[f"{key_name.upper()}_API_KEY"] = key_value + + return True + except Exception as e: + logger.error(f"Error saving API keys: {str(e)}") + return False +``` + +### 3. UI/UX Improvements + +#### Responsive Design + +```python +def render_responsive_layout(): + """Render a responsive layout that works on mobile and desktop.""" + # Check viewport width + st.markdown(""" + + + + """, unsafe_allow_html=True) +``` + +#### Visual Hierarchy for Required vs Optional + +```python +def render_api_key_input(label, key_name, required=False, help_text=""): + """Render an API key input with clear visual hierarchy.""" + + # Add required indicator if needed + display_label = f"{label} {'*' if required else '(optional)'}" + + # Get existing value from session state or environment + existing_value = st.session_state.get(f"{key_name}_api_key", "") or os.getenv(f"{key_name.upper()}_API_KEY", "") + + # Render the input with appropriate styling + st.markdown(f""" +
+ +
{help_text}
+
+ """, unsafe_allow_html=True) + + # The actual input field + value = st.text_input( + label="", # Empty because we use custom label above + value=existing_value, + type="password", + key=f"input_{key_name}", + label_visibility="collapsed" + ) + + # Validation status + if value: + is_valid = key_name in st.session_state.get("validated_keys", {}) + if is_valid: + st.success(f"βœ“ {label} validated successfully") + else: + st.info(f"⚠️ {label} not validated yet") + + return value +``` + +#### Interactive Help and Tooltips + +```python +def render_help_section(service): + """Render an interactive help section for getting API keys.""" + + help_content = { + "openai": { + "title": "How to get your OpenAI API key", + "steps": [ + "Go to [OpenAI's website](https://platform.openai.com)", + "Sign up or log in to your account", + "Navigate to the API section", + "Click 'Create new secret key'", + "Copy the generated key and paste it here" + ], + "note": "Keep your API key secure and never share it publicly.", + "pricing": "$0.002 per 1K tokens for GPT-3.5, $0.06 per 1K tokens for GPT-4", + "link": "https://platform.openai.com/account/api-keys" + }, + "gemini": { + "title": "How to get your Google Gemini API key", + "steps": [ + "Visit [Google AI Studio](https://makersuite.google.com/app/apikey)", + "Sign in with your Google account", + "Click 'Create API key'", + "Copy the generated key and paste it here" + ], + "note": "Make sure to enable the Gemini API in your Google Cloud Console.", + "pricing": "Free tier available, then $0.0025 per 1K tokens", + "link": "https://makersuite.google.com/app/apikey" + } + # Add more services as needed + } + + if service in help_content: + content = help_content[service] + + with st.expander(f"πŸ“‹ {content['title']}", expanded=False): + st.markdown("**Step-by-step guide:**") + for i, step in enumerate(content["steps"], 1): + st.markdown(f"{i}. {step}") + + st.markdown(f"**Note:** {content['note']}") + st.markdown(f"**Pricing:** {content['pricing']}") + st.markdown(f"[Get your API key here]({content['link']})") +``` + +### 4. Implementation Plan + +#### Phase 1: Core Improvements + +1. **Create Unified State Manager** + - Implement the `OnboardingStateManager` class + - Refactor existing code to use the new state manager + - Add proper state persistence + +2. **Enhance API Key Validation** + - Implement real validation for each service + - Add visual feedback for validation status + - Create a "Test All Keys" function + +3. **Improve Navigation** + - Redesign step indicator with clear labels + - Add skip options for optional steps + - Implement a "Quick Start" mode + +#### Phase 2: UI/UX Enhancements + +1. **Redesign Input Components** + - Create clear visual hierarchy + - Add responsive design for mobile + - Implement interactive help sections + +2. **Create Summary Screens** + - Add welcome screen with setup options + - Implement completion summary screen + - Add configuration export/import + +3. **Enhance Visual Design** + - Update color scheme for better accessibility + - Add animations for transitions + - Implement progress indicators + +#### Phase 3: Advanced Features + +1. **Guided Tours** + - Add interactive tutorials + - Create contextual help popups + - Implement feature discovery + +2. **Smart Defaults** + - Suggest configurations based on user needs + - Implement templates for common use cases + - Add recommended settings + +3. **Troubleshooting Assistance** + - Add automatic error detection + - Create guided troubleshooting flows + - Implement self-healing for common issues + +## Code Implementation Examples + +### Welcome Screen Component + +```python +def render_welcome_screen(): + """Render the welcome screen for onboarding.""" + st.markdown(""" +
+

Welcome to AI-Writer! πŸ‘‹

+

Let's get you set up in just a few minutes.

+
+ """, unsafe_allow_html=True) + + # Setup mode selection + setup_mode = st.radio( + "What would you like to do?", + options=["Quick Start (minimal setup)", + "Complete Setup (all features)", + "Import Configuration"], + index=0, + key="setup_mode_selection" + ) + + # Store the selection in state + if "onboarding_state" in st.session_state: + st.session_state.onboarding_state["setup_mode"] = setup_mode.split(" ")[0].lower() + + # Start button + if st.button("Start Setup", use_container_width=True, type="primary"): + if "onboarding_state" in st.session_state: + st.session_state.onboarding_state["current_step"] = 1 + st.rerun() +``` + +### API Key Manager Component + +```python +def render_api_key_manager(): + """Render the improved API key manager.""" + # Get state manager + state_manager = OnboardingStateManager() + state = state_manager.get_state() + + # Render step indicator + render_step_indicator(state["current_step"], state["total_steps"]) + + # Render appropriate step based on current_step + if state["current_step"] == 1: + render_ai_providers_step(state_manager) + elif state["current_step"] == 2: + render_research_tools_step(state_manager) + elif state["current_step"] == 3: + render_publishing_step(state_manager) + elif state["current_step"] == 4: + render_summary_step(state_manager) + + # Render navigation buttons + render_navigation_buttons(state_manager) +``` + +### Improved AI Providers Step + +```python +def render_ai_providers_step(state_manager): + """Render the improved AI providers setup step.""" + st.markdown("## Step 1: Connect AI Models") + st.markdown("Configure the AI models you want to use for content generation.") + + # Create tabs for required vs optional + tab1, tab2 = st.tabs(["Required (at least one)", "Optional Models"]) + + with tab1: + col1, col2 = st.columns(2) + + with col1: + # OpenAI + openai_key = render_api_key_input( + "OpenAI API Key", + "openai", + required=True, + help_text="Powers GPT-3.5 and GPT-4 models" + ) + render_help_section("openai") + + with col2: + # Google Gemini + gemini_key = render_api_key_input( + "Google Gemini API Key", + "gemini", + required=False, + help_text="Powers Gemini Pro models" + ) + render_help_section("gemini") + + with tab2: + col1, col2 = st.columns(2) + + with col1: + # Anthropic + anthropic_key = render_api_key_input( + "Anthropic API Key", + "anthropic", + required=False, + help_text="Powers Claude models" + ) + render_help_section("anthropic") + + with col2: + # Mistral + mistral_key = render_api_key_input( + "Mistral API Key", + "mistral", + required=False, + help_text="Powers Mistral models" + ) + render_help_section("mistral") + + # Test keys button + if st.button("Test API Keys", use_container_width=True): + with st.spinner("Testing API keys..."): + # Test each provided key + results = {} + if openai_key: + results["openai"] = asyncio.run(validate_api_key("openai", openai_key)) + if gemini_key: + results["gemini"] = asyncio.run(validate_api_key("gemini", gemini_key)) + if anthropic_key: + results["anthropic"] = asyncio.run(validate_api_key("anthropic", anthropic_key)) + if mistral_key: + results["mistral"] = asyncio.run(validate_api_key("mistral", mistral_key)) + + # Store validation results + state_manager.update_state({"validated_keys": results}) + + # Display results + for service, result in results.items(): + if result.get("valid", False): + st.success(f"βœ… {service.title()} API key is valid") + else: + st.error(f"❌ {service.title()} API key is invalid: {result.get('error', 'Unknown error')}") + + # Save keys to state + api_keys = { + "openai": openai_key, + "gemini": gemini_key, + "anthropic": anthropic_key, + "mistral": mistral_key + } + state_manager.update_state({"api_keys": api_keys}) + + # Check if we have at least one valid key + has_valid_key = any([ + openai_key and state_manager.get_state().get("validated_keys", {}).get("openai", {}).get("valid", False), + gemini_key and state_manager.get_state().get("validated_keys", {}).get("gemini", {}).get("valid", False) + ]) + + if not has_valid_key and (openai_key or gemini_key): + st.warning("Please test your API keys before continuing") +``` + +## Benefits of Improved Onboarding + +1. **Increased User Retention** + - Smoother onboarding leads to higher completion rates + - Clear guidance reduces frustration and abandonment + - Faster time-to-value improves user satisfaction + +2. **Reduced Support Burden** + - Better self-service options decrease support tickets + - Clearer instructions prevent common setup issues + - Automated validation catches problems early + +3. **Higher Feature Adoption** + - Users understand available features better + - Guided setup encourages exploration of capabilities + - Contextual help improves feature discovery + +4. **Improved User Experience** + - Consistent design creates a professional impression + - Responsive layout works across all devices + - Intuitive navigation reduces cognitive load + +5. **Better Data Quality** + - Proper validation ensures working API keys + - Clear requirements improve data completeness + - Structured setup leads to better configuration + +## Implementation Timeline + +- **Week 1**: Design and prototype core improvements +- **Week 2**: Implement unified state management and API validation +- **Week 3**: Develop UI components and responsive design +- **Week 4**: Create welcome and summary screens +- **Week 5**: Add help content and contextual assistance +- **Week 6**: Testing, refinement, and documentation + +## Conclusion + +The proposed improvements to the onboarding process will significantly enhance the user experience for new AI-Writer users. By implementing a more intuitive, guided, and responsive setup flow, we can increase user retention, reduce support needs, and help users get value from the platform faster. + +These changes represent a comprehensive overhaul of the current system, addressing both technical and user experience issues while maintaining compatibility with the existing codebase. \ No newline at end of file diff --git a/Roadmap TBDs/ROADMAP.md b/Roadmap TBDs/ROADMAP.md new file mode 100644 index 00000000..e35573e2 --- /dev/null +++ b/Roadmap TBDs/ROADMAP.md @@ -0,0 +1,250 @@ +# AI-Writer Public Roadmap + +This roadmap outlines the planned features and improvements for the AI-Writer platform. It provides transparency into our development priorities and gives users insight into upcoming capabilities. + +## 🚦 Roadmap Status Indicators + +- 🟒 **In Progress**: Currently being developed +- 🟑 **Planned**: Scheduled for upcoming development cycles +- πŸ”΅ **Researching**: Under investigation and evaluation +- βœ… **Completed**: Released and available + +## πŸ—“οΈ Q2 2025 (April - June) + +### Core Platform + +- 🟒 **Performance Optimization** + - Reduce content generation time by 30% + - Optimize memory usage for large content pieces + - Implement caching for frequently used research data + +- 🟑 **Multi-language Support** + - Add support for Spanish, French, and German content generation + - Implement language-specific research capabilities + - Create language-specific SEO optimization + +- 🟑 **User Interface Refresh** + - Redesign main dashboard for improved usability + - Implement dark mode + - Add customizable workspace layouts + +### AI Writers + +- 🟒 **Enhanced Blog Writer** + - Add support for more blog formats (listicles, how-to guides, etc.) + - Implement advanced outline generation + - Add competitor content analysis + +- 🟑 **AI Script Writer** + - Create specialized writer for video scripts + - Support multiple video formats (YouTube, TikTok, Instagram) + - Add scene breakdown and shot suggestions + +- 🟑 **Technical Content Writer** + - Specialized writer for technical documentation + - Code snippet generation and formatting + - Technical accuracy verification + +### Research & SEO + +- 🟒 **Advanced Web Research** + - Implement multi-source research aggregation + - Add research depth controls + - Improve citation and source tracking + +- 🟑 **Semantic SEO Tools** + - Entity-based content optimization + - Topic cluster mapping + - Natural language query optimization + +- πŸ”΅ **Competitive Analysis Tools** + - Analyze top-ranking content for target keywords + - Identify content gaps and opportunities + - Generate differentiation strategies + +## πŸ—“οΈ Q3 2025 (July - September) + +### Core Platform + +- 🟑 **Collaboration Features** + - Multi-user editing capabilities + - Role-based access control + - Comment and feedback system + +- 🟑 **Content Versioning** + - Track content revisions + - Compare different versions + - Restore previous versions + +- πŸ”΅ **Analytics Dashboard** + - Content performance tracking + - Usage statistics and insights + - AI model performance metrics + +### AI Writers + +- 🟑 **E-commerce Content Suite** + - Enhanced product description generator + - Category page content creator + - Product comparison generator + +- 🟑 **AI Newsletter Writer** + - Email newsletter templates + - Subscriber segmentation support + - A/B testing headline generator + +- πŸ”΅ **Interactive Content Generator** + - Quiz and poll creator + - Interactive calculator generator + - Decision tree content builder + +### Research & SEO + +- 🟑 **AI-Powered Content Audit** + - Analyze existing content + - Identify improvement opportunities + - Generate update recommendations + +- 🟑 **Local SEO Tools** + - Location-based content optimization + - Local business schema generator + - Regional keyword research + +- πŸ”΅ **Content Distribution Planner** + - Channel-specific content adaptation + - Publishing schedule optimizer + - Cross-platform content strategy + +## πŸ—“οΈ Q4 2025 (October - December) + +### Core Platform + +- 🟑 **API Expansion** + - Comprehensive REST API + - Webhook integrations + - Developer documentation and SDKs + +- 🟑 **Enterprise Features** + - SSO integration + - Advanced security controls + - Custom branding options + +- πŸ”΅ **AI Workflow Automation** + - Custom workflow builder + - Scheduled content generation + - Conditional content processing + +### AI Writers + +- 🟑 **AI Book Writer** + - Long-form content organization + - Chapter planning and generation + - Book formatting and structure + +- 🟑 **AI Course Creator** + - Educational content generator + - Lesson plan development + - Quiz and assessment creator + +- πŸ”΅ **Multimedia Content Generator** + - Integrated image generation + - Infographic creator + - Audio content generator + +### Research & SEO + +- 🟑 **AI Research Assistant** + - Conversational research interface + - Deep research capabilities + - Research summarization and extraction + +- 🟑 **International SEO Tools** + - Multi-language keyword research + - International content optimization + - Hreflang tag generator + +- πŸ”΅ **Predictive Content Performance** + - AI-powered performance prediction + - Content improvement recommendations + - Trend analysis and forecasting + +## πŸ—“οΈ 2026 and Beyond + +### Core Platform + +- πŸ”΅ **NextJS React Application** + - Complete frontend rebuild + - Enhanced performance and responsiveness + - Progressive web app capabilities + +- πŸ”΅ **AI Agent Ecosystem** + - Specialized AI agents for different tasks + - Agent collaboration framework + - Custom agent creation + +- πŸ”΅ **Advanced Personalization** + - User behavior-based recommendations + - Personalized content generation + - Learning from user preferences + +### AI Writers + +- πŸ”΅ **Multimodal Content Creation** + - Integrated text, image, and video generation + - Cross-format content consistency + - Single-prompt multi-format generation + +- πŸ”΅ **Industry-Specific Writers** + - Legal content generator + - Medical content writer + - Financial content creator + +- πŸ”΅ **Real-time Collaborative Writing** + - Multi-user simultaneous editing + - AI-assisted collaboration + - Role-based collaborative workflows + +### Research & SEO + +- πŸ”΅ **Real-time Content Optimization** + - Live SEO feedback during writing + - Instant research integration + - Dynamic content suggestions + +- πŸ”΅ **Comprehensive Analytics Suite** + - Advanced content performance tracking + - Conversion attribution + - ROI calculation and reporting + +- πŸ”΅ **AI-Driven Content Strategy** + - Content gap analysis + - Opportunity identification + - Automated content planning + +## βœ… Recently Completed + +- βœ… **Google Gemini Integration** - Added support for Google's Gemini Pro model +- βœ… **AI News Article Writer** - Specialized writer for news content with citation support +- βœ… **ChromaDB Vector Storage** - Implemented vector database for semantic search capabilities +- βœ… **Tavily AI Research Integration** - Added support for AI-powered web research +- βœ… **Streamlit UI Improvements** - Enhanced user interface with better navigation and controls + +## 🀝 Community Contributions + +We welcome community contributions to the AI-Writer platform! If you're interested in contributing to any of the features on our roadmap or have ideas for new features, please: + +1. Check our [Contributing Guidelines](CONTRIBUTING.md) +2. Open an issue to discuss your proposed feature or improvement +3. Submit a pull request with your implementation + +## πŸ“ Feedback + +Your feedback is essential in shaping the future of AI-Writer. If you have feature requests, suggestions, or feedback on existing features, please: + +- Open an issue on GitHub +- Join our [community forum](https://alwrity.com) +- Contact us directly at info@alwrity.com + +--- + +*Note: This roadmap is subject to change based on user feedback, technological developments, and strategic priorities. Last updated: April 18, 2025* \ No newline at end of file diff --git a/Roadmap TBDs/TWITTER_IMPLEMENTATION_PLAN.md b/Roadmap TBDs/TWITTER_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..e43052b4 --- /dev/null +++ b/Roadmap TBDs/TWITTER_IMPLEMENTATION_PLAN.md @@ -0,0 +1,4018 @@ +# Twitter Integration Implementation Plan + +This document outlines a comprehensive plan to implement and enhance the Twitter features in AI-Writer, transforming the "coming soon" features into fully functional components using Tweepy and user-provided API keys. + +## Current State Analysis + +The current Twitter functionality in AI-Writer includes: +- A Twitter dashboard with multiple planned features +- Only the "Smart Tweet Generator" is currently active +- The tweet generator creates content but lacks real Twitter integration +- Performance metrics are simulated rather than based on real data +- No actual posting, scheduling, or analytics capabilities + +## Implementation Strategy + +We'll implement the Twitter features in multiple phases, focusing on delivering value incrementally while building toward a comprehensive Twitter management solution. + +### Phase 1: Twitter Authentication & Basic Integration + +#### 1.1 Twitter API Authentication System + +Create a secure system for users to connect their Twitter accounts: + +```python +# lib/integrations/twitter/auth.py + +import tweepy +import streamlit as st +from typing import Dict, Optional, Any +import os +import json +from pathlib import Path +import time +from loguru import logger + +# Configuration paths +CONFIG_DIR = Path(__file__).parent.parent.parent.parent / 'config' / 'twitter' +CONFIG_DIR.mkdir(exist_ok=True, parents=True) + +def get_twitter_auth_url() -> str: + """Generate Twitter OAuth URL for user authentication.""" + try: + # Get API keys from session state or environment + consumer_key = st.session_state.get('twitter_consumer_key', os.getenv('TWITTER_CONSUMER_KEY', '')) + consumer_secret = st.session_state.get('twitter_consumer_secret', os.getenv('TWITTER_CONSUMER_SECRET', '')) + + if not consumer_key or not consumer_secret: + logger.error("Twitter API keys not found") + return "" + + # Initialize OAuth handler + oauth1_user_handler = tweepy.OAuth1UserHandler( + consumer_key, consumer_secret, + callback="http://localhost:8501/twitter/callback" + ) + + # Get authorization URL + auth_url = oauth1_user_handler.get_authorization_url() + + # Store OAuth handler in session state + st.session_state.twitter_oauth_handler = oauth1_user_handler + + return auth_url + except Exception as e: + logger.error(f"Error generating Twitter auth URL: {str(e)}") + return "" + +def handle_twitter_callback(oauth_verifier: str) -> bool: + """Handle Twitter OAuth callback.""" + try: + # Get OAuth handler from session state + oauth_handler = st.session_state.get('twitter_oauth_handler') + if not oauth_handler: + logger.error("OAuth handler not found in session state") + return False + + # Get access tokens + access_token, access_token_secret = oauth_handler.get_access_token(oauth_verifier) + + # Store tokens in session state + st.session_state.twitter_access_token = access_token + st.session_state.twitter_access_token_secret = access_token_secret + + # Create API client + api = create_twitter_api() + + # Get user info + user = api.verify_credentials() + + # Store user info + st.session_state.twitter_user = { + 'id': user.id, + 'screen_name': user.screen_name, + 'name': user.name, + 'profile_image_url': user.profile_image_url, + 'followers_count': user.followers_count, + 'friends_count': user.friends_count + } + + # Save credentials to file + save_twitter_credentials( + user.id, + access_token, + access_token_secret + ) + + return True + except Exception as e: + logger.error(f"Error handling Twitter callback: {str(e)}") + return False + +def create_twitter_api() -> tweepy.API: + """Create Twitter API client.""" + try: + # Get API keys and tokens + consumer_key = st.session_state.get('twitter_consumer_key', os.getenv('TWITTER_CONSUMER_KEY', '')) + consumer_secret = st.session_state.get('twitter_consumer_secret', os.getenv('TWITTER_CONSUMER_SECRET', '')) + access_token = st.session_state.get('twitter_access_token', '') + access_token_secret = st.session_state.get('twitter_access_token_secret', '') + + # Create auth handler + auth = tweepy.OAuth1UserHandler( + consumer_key, consumer_secret, + access_token, access_token_secret + ) + + # Create API client + api = tweepy.API(auth) + + return api + except Exception as e: + logger.error(f"Error creating Twitter API client: {str(e)}") + raise + +def create_twitter_client() -> tweepy.Client: + """Create Twitter API v2 client.""" + try: + # Get API keys and tokens + consumer_key = st.session_state.get('twitter_consumer_key', os.getenv('TWITTER_CONSUMER_KEY', '')) + consumer_secret = st.session_state.get('twitter_consumer_secret', os.getenv('TWITTER_CONSUMER_SECRET', '')) + access_token = st.session_state.get('twitter_access_token', '') + access_token_secret = st.session_state.get('twitter_access_token_secret', '') + bearer_token = st.session_state.get('twitter_bearer_token', os.getenv('TWITTER_BEARER_TOKEN', '')) + + # Create client + client = tweepy.Client( + bearer_token=bearer_token, + consumer_key=consumer_key, + consumer_secret=consumer_secret, + access_token=access_token, + access_token_secret=access_token_secret + ) + + return client + except Exception as e: + logger.error(f"Error creating Twitter client: {str(e)}") + raise + +def save_twitter_credentials(user_id: int, access_token: str, access_token_secret: str) -> bool: + """Save Twitter credentials to file.""" + try: + # Create user-specific credentials file + creds_file = CONFIG_DIR / f"{user_id}.json" + + # Save credentials + with open(creds_file, 'w') as f: + json.dump({ + 'access_token': access_token, + 'access_token_secret': access_token_secret, + 'timestamp': time.time() + }, f) + + return True + except Exception as e: + logger.error(f"Error saving Twitter credentials: {str(e)}") + return False + +def load_twitter_credentials(user_id: int) -> Dict[str, str]: + """Load Twitter credentials from file.""" + try: + # Get user-specific credentials file + creds_file = CONFIG_DIR / f"{user_id}.json" + + # Check if file exists + if not creds_file.exists(): + logger.warning(f"No credentials found for user {user_id}") + return {} + + # Load credentials + with open(creds_file, 'r') as f: + creds = json.load(f) + + # Update session state + st.session_state.twitter_access_token = creds['access_token'] + st.session_state.twitter_access_token_secret = creds['access_token_secret'] + + return creds + except Exception as e: + logger.error(f"Error loading Twitter credentials: {str(e)}") + return {} + +def is_authenticated() -> bool: + """Check if user is authenticated with Twitter.""" + return ( + 'twitter_access_token' in st.session_state and + 'twitter_access_token_secret' in st.session_state and + 'twitter_user' in st.session_state + ) + +def logout() -> None: + """Log out user from Twitter.""" + if 'twitter_access_token' in st.session_state: + del st.session_state.twitter_access_token + if 'twitter_access_token_secret' in st.session_state: + del st.session_state.twitter_access_token_secret + if 'twitter_user' in st.session_state: + del st.session_state.twitter_user + if 'twitter_oauth_handler' in st.session_state: + del st.session_state.twitter_oauth_handler +``` + +#### 1.2 Twitter API Key Management UI + +Create a UI for users to enter and manage their Twitter API keys: + +```python +# lib/integrations/twitter/api_key_manager.py + +import streamlit as st +from typing import Dict, Optional +import os +from loguru import logger + +def render_twitter_api_key_manager() -> None: + """Render Twitter API key management UI.""" + st.markdown("## Twitter API Configuration") + st.markdown(""" + To use Twitter integration features, you need to provide your Twitter API keys. + You can get these by creating a Twitter Developer account and setting up a project. + """) + + # Create tabs for different authentication methods + tab1, tab2 = st.tabs(["Basic Authentication", "Advanced Settings"]) + + with tab1: + # Get existing keys from session state or environment + consumer_key = st.session_state.get('twitter_consumer_key', os.getenv('TWITTER_CONSUMER_KEY', '')) + consumer_secret = st.session_state.get('twitter_consumer_secret', os.getenv('TWITTER_CONSUMER_SECRET', '')) + bearer_token = st.session_state.get('twitter_bearer_token', os.getenv('TWITTER_BEARER_TOKEN', '')) + + # Input fields for API keys + new_consumer_key = st.text_input( + "Consumer Key (API Key)", + value=consumer_key, + type="password", + help="Your Twitter API consumer key" + ) + + new_consumer_secret = st.text_input( + "Consumer Secret (API Secret)", + value=consumer_secret, + type="password", + help="Your Twitter API consumer secret" + ) + + new_bearer_token = st.text_input( + "Bearer Token", + value=bearer_token, + type="password", + help="Your Twitter API bearer token" + ) + + # Save button + if st.button("Save API Keys", use_container_width=True): + # Update session state + st.session_state.twitter_consumer_key = new_consumer_key + st.session_state.twitter_consumer_secret = new_consumer_secret + st.session_state.twitter_bearer_token = new_bearer_token + + # Show success message + st.success("Twitter API keys saved successfully!") + + with tab2: + st.markdown("### Advanced API Settings") + + # Callback URL + st.text_input( + "Callback URL", + value="http://localhost:8501/twitter/callback", + disabled=True, + help="Use this URL in your Twitter Developer Portal" + ) + + # API usage limits + st.info(""" + **Twitter API Rate Limits:** + - Standard API: 500,000 tweets/month + - Essential API: 10,000 tweets/month + + Make sure your Twitter Developer account has the appropriate access level for your needs. + """) + + # Help resources + st.markdown(""" + **Need help?** + - [Twitter Developer Portal](https://developer.twitter.com) + - [API Documentation](https://developer.twitter.com/en/docs) + - [Rate Limits](https://developer.twitter.com/en/docs/twitter-api/rate-limits) + """) +``` + +#### 1.3 Twitter Account Connection UI + +Create a UI for users to connect their Twitter accounts: + +```python +# lib/integrations/twitter/account_manager.py + +import streamlit as st +from typing import Dict, Optional +from .auth import get_twitter_auth_url, is_authenticated, logout, create_twitter_api +import tweepy +from loguru import logger + +def render_twitter_account_manager() -> None: + """Render Twitter account management UI.""" + st.markdown("## Twitter Account") + + # Check if API keys are configured + if not st.session_state.get('twitter_consumer_key') or not st.session_state.get('twitter_consumer_secret'): + st.warning("Please configure your Twitter API keys first.") + return + + # Check if user is authenticated + if is_authenticated(): + # Get user info + user = st.session_state.twitter_user + + # Display user info + col1, col2, col3 = st.columns([1, 3, 1]) + + with col1: + st.image(user['profile_image_url'], width=80) + + with col2: + st.markdown(f"**{user['name']}** (@{user['screen_name']})") + st.markdown(f"Followers: {user['followers_count']} | Following: {user['friends_count']}") + + with col3: + if st.button("Disconnect", key="disconnect_twitter"): + logout() + st.experimental_rerun() + + # Account status + st.success("βœ… Your Twitter account is connected and ready to use.") + + # Account actions + st.markdown("### Quick Actions") + col1, col2 = st.columns(2) + + with col1: + if st.button("View Profile", use_container_width=True): + try: + # Get API client + api = create_twitter_api() + + # Get user timeline + tweets = api.user_timeline(count=5) + + # Display tweets + st.markdown("#### Recent Tweets") + for tweet in tweets: + st.markdown(f""" +
+

{tweet.text}

+ Posted on {tweet.created_at.strftime('%Y-%m-%d %H:%M')} +
+ """, unsafe_allow_html=True) + except Exception as e: + logger.error(f"Error fetching tweets: {str(e)}") + st.error(f"Error fetching tweets: {str(e)}") + + with col2: + if st.button("Check API Limits", use_container_width=True): + try: + # Get API client + api = create_twitter_api() + + # Get rate limit status + limits = api.rate_limit_status() + + # Display limits + st.markdown("#### API Rate Limits") + + # Timeline limits + timeline_limit = limits['resources']['statuses']['/statuses/user_timeline'] + st.markdown(f"**Timeline:** {timeline_limit['remaining']}/{timeline_limit['limit']} requests remaining") + + # Search limits + search_limit = limits['resources']['search']['/search/tweets'] + st.markdown(f"**Search:** {search_limit['remaining']}/{search_limit['limit']} requests remaining") + + # Tweet posting limits + st.markdown("**Tweet Posting:** Limited by your Twitter API access level") + except Exception as e: + logger.error(f"Error checking API limits: {str(e)}") + st.error(f"Error checking API limits: {str(e)}") + else: + # Get auth URL + auth_url = get_twitter_auth_url() + + if auth_url: + # Display connect button + st.markdown(""" + +
+ + + + Connect Twitter Account +
+
+ """.format(auth_url=auth_url), unsafe_allow_html=True) + + st.info("Click the button above to connect your Twitter account.") + else: + st.error("Failed to generate authentication URL. Please check your API keys.") +``` + +#### 1.4 Twitter Callback Handler + +Create a handler for the Twitter OAuth callback: + +```python +# lib/integrations/twitter/callback_handler.py + +import streamlit as st +from urllib.parse import parse_qs, urlparse +from .auth import handle_twitter_callback +from loguru import logger + +def handle_oauth_callback(): + """Handle Twitter OAuth callback in Streamlit.""" + # Get current URL + query_params = st.experimental_get_query_params() + + # Check if this is a callback + if 'oauth_token' in query_params and 'oauth_verifier' in query_params: + oauth_verifier = query_params['oauth_verifier'][0] + + # Handle callback + success = handle_twitter_callback(oauth_verifier) + + if success: + # Clear query parameters to avoid reprocessing + st.experimental_set_query_params() + + # Show success message + st.success("Successfully connected Twitter account!") + + # Redirect to main page + st.experimental_rerun() + else: + st.error("Failed to authenticate with Twitter. Please try again.") + + # Check for error + if 'denied' in query_params: + st.error("Twitter authentication was denied.") + st.experimental_set_query_params() +``` + +### Phase 2: Enhanced Tweet Generator with Real Twitter Integration + +#### 2.1 Improved Tweet Generator with Real Data + +Enhance the tweet generator to use real Twitter data: + +```python +# lib/ai_writers/twitter_writers/tweet_generator/enhanced_tweet_generator.py + +import streamlit as st +import re +import json +import time +from typing import Dict, List, Tuple, Optional +import random +import emoji +from datetime import datetime +import tweepy + +from .....gpt_providers.text_generation.main_text_generation import llm_text_gen +from ....integrations.twitter.auth import create_twitter_api, create_twitter_client, is_authenticated + +# Constants +MAX_TWEET_LENGTH = 280 +EMOJI_CATEGORIES = { + "Humorous": ["πŸ˜„", "πŸ˜‚", "🀣", "😊", "πŸ˜‰", "😎", "πŸ€ͺ", "😜", "πŸ€“", "πŸ˜‡"], + "Informative": ["πŸ“š", "πŸ“Š", "πŸ“ˆ", "πŸ”", "πŸ’‘", "πŸ“", "πŸ“‹", "πŸ”Ž", "πŸ“–", "πŸ“‘"], + "Inspirational": ["✨", "🌟", "πŸ’«", "⭐", "πŸ”₯", "πŸ’ͺ", "πŸ™Œ", "πŸ‘", "πŸ’―", "🎯"], + "Serious": ["πŸ€”", "πŸ’­", "🧐", "πŸ“’", "πŸ””", "βš–οΈ", "πŸŽ“", "πŸ“Š", "πŸ”¬", "πŸ“°"], + "Casual": ["πŸ‘‹", "πŸ‘", "πŸ™‹", "πŸ’", "πŸ€—", "πŸ‘Œ", "✌️", "🀝", "πŸ‘Š", "πŸ™"] +} + +def count_characters(text: str) -> int: + """Count characters in tweet, accounting for emojis.""" + return len(text) + +def extract_hashtags(text: str) -> List[str]: + """Extract hashtags from tweet text.""" + return re.findall(r'#\w+', text) + +def get_trending_hashtags(location_id: int = 1) -> List[str]: + """Get trending hashtags from Twitter API.""" + try: + if not is_authenticated(): + return [] + + # Get API client + api = create_twitter_api() + + # Get trending topics + trends = api.get_place_trends(location_id) + + # Extract hashtags + hashtags = [trend['name'] for trend in trends[0]['trends'] if trend['name'].startswith('#')] + + return hashtags[:10] # Return top 10 hashtags + except Exception as e: + st.error(f"Error fetching trending hashtags: {str(e)}") + return [] + +def suggest_hashtags(topic: str, tone: str, use_trending: bool = False) -> List[str]: + """Suggest relevant hashtags based on topic and tone.""" + # Enhanced hashtag suggestions based on topic and tone + base_hashtags = { + "professional": ["#Business", "#Leadership", "#Innovation"], + "casual": ["#Life", "#Fun", "#Trending"], + "informative": ["#Learn", "#Tips", "#HowTo"], + "humorous": ["#Funny", "#LOL", "#Humor"], + "inspirational": ["#Motivation", "#Success", "#Growth"] + } + + topic_hashtags = { + "tech": ["#Technology", "#TechNews", "#Innovation"], + "business": ["#Business", "#Entrepreneurship", "#Startup"], + "marketing": ["#Marketing", "#DigitalMarketing", "#SocialMedia"], + "education": ["#Education", "#Learning", "#Knowledge"], + "health": ["#Health", "#Wellness", "#Fitness"] + } + + # Combine base and topic hashtags + suggested = base_hashtags.get(tone.lower(), []) + topic_hashtags.get(topic.lower(), []) + + # Add trending hashtags if requested + if use_trending and is_authenticated(): + trending = get_trending_hashtags() + suggested = list(set(suggested + trending)) + + return list(set(suggested))[:5] # Return unique hashtags, max 5 + +def get_optimal_posting_time() -> Dict[str, Any]: + """Get optimal posting time based on follower activity.""" + try: + if not is_authenticated(): + return { + "time": datetime.now().strftime("%H:%M"), + "day": datetime.now().strftime("%A"), + "confidence": "low" + } + + # Get API client + api = create_twitter_api() + + # Get user timeline + tweets = api.user_timeline(count=100) + + # Analyze engagement by hour and day + hour_engagement = {} + day_engagement = {} + + for tweet in tweets: + hour = tweet.created_at.hour + day = tweet.created_at.strftime("%A") + engagement = tweet.favorite_count + tweet.retweet_count + + if hour not in hour_engagement: + hour_engagement[hour] = [] + hour_engagement[hour].append(engagement) + + if day not in day_engagement: + day_engagement[day] = [] + day_engagement[day].append(engagement) + + # Calculate average engagement by hour and day + hour_avg_engagement = {h: sum(e)/len(e) for h, e in hour_engagement.items() if e} + day_avg_engagement = {d: sum(e)/len(e) for d, e in day_engagement.items() if e} + + # Find optimal hour and day + optimal_hour = max(hour_avg_engagement.items(), key=lambda x: x[1])[0] if hour_avg_engagement else datetime.now().hour + optimal_day = max(day_avg_engagement.items(), key=lambda x: x[1])[0] if day_avg_engagement else datetime.now().strftime("%A") + + # Calculate confidence + confidence = "high" if len(tweets) >= 50 else "medium" if len(tweets) >= 20 else "low" + + return { + "time": f"{optimal_hour:02d}:00", + "day": optimal_day, + "confidence": confidence + } + except Exception as e: + st.error(f"Error calculating optimal posting time: {str(e)}") + return { + "time": datetime.now().strftime("%H:%M"), + "day": datetime.now().strftime("%A"), + "confidence": "low" + } + +def analyze_audience() -> Dict[str, Any]: + """Analyze Twitter audience for content targeting.""" + try: + if not is_authenticated(): + return { + "follower_count": 0, + "engagement_rate": 0, + "top_interests": [], + "active_hours": [] + } + + # Get API client + api = create_twitter_api() + + # Get user info + user = api.verify_credentials() + + # Get user timeline + tweets = api.user_timeline(count=100) + + # Calculate engagement rate + total_engagement = sum(tweet.favorite_count + tweet.retweet_count for tweet in tweets) + engagement_rate = total_engagement / (len(tweets) * user.followers_count) if tweets and user.followers_count else 0 + + # Analyze active hours + hour_activity = {} + for tweet in tweets: + hour = tweet.created_at.hour + if hour not in hour_activity: + hour_activity[hour] = 0 + hour_activity[hour] += 1 + + active_hours = sorted(hour_activity.items(), key=lambda x: x[1], reverse=True)[:3] + active_hours = [f"{hour:02d}:00" for hour, _ in active_hours] + + # Get follower interests (simplified) + top_interests = ["Technology", "Business", "Marketing"] # Would require more complex analysis + + return { + "follower_count": user.followers_count, + "engagement_rate": engagement_rate * 100, # Convert to percentage + "top_interests": top_interests, + "active_hours": active_hours + } + except Exception as e: + st.error(f"Error analyzing audience: {str(e)}") + return { + "follower_count": 0, + "engagement_rate": 0, + "top_interests": [], + "active_hours": [] + } + +def generate_tweet_variations( + hook: str, + target_audience: str, + tone: str, + call_to_action: str = "", + keywords: str = "", + length: str = "medium", + num_variations: int = 3, + use_ai: bool = True +) -> List[Dict]: + """Generate multiple tweet variations with AI and Twitter data.""" + # Enhanced prompt template with Twitter-specific guidance + prompt_template = f""" + Create {num_variations} engaging tweet variations with the following parameters: + - Hook/Topic: {hook} + - Target Audience: {target_audience} + - Tone: {tone} + - Call to Action: {call_to_action} + - Keywords: {keywords} + - Length: {length} + + Each tweet should: + 1. Start with an attention-grabbing hook + 2. Include relevant hashtags (max 2-3) + 3. Use appropriate emojis (1-2 max) + 4. End with a clear call-to-action + 5. Stay within Twitter's 280 character limit + 6. Match the specified tone and audience + + Format each tweet as a JSON object with: + - text: The tweet content + - hashtags: List of suggested hashtags + - emojis: List of suggested emojis + """ + + if use_ai: + # Use AI to generate tweets + try: + response = llm_text_gen(prompt_template) + + # Parse JSON response + try: + tweets = json.loads(response) + if not isinstance(tweets, list): + tweets = [tweets] + except: + # Handle non-JSON response by extracting tweet text + tweet_texts = re.findall(r'"text"\s*:\s*"([^"]+)"', response) + tweets = [{"text": text, "hashtags": extract_hashtags(text), "emojis": []} for text in tweet_texts] + + # Ensure we have the requested number of variations + while len(tweets) < num_variations: + tweets.append({ + "text": f"{hook} {call_to_action}", + "hashtags": suggest_hashtags(keywords, tone), + "emojis": [] + }) + + # Add engagement score + for tweet in tweets: + tweet["engagement_score"] = random.randint(60, 95) + + return tweets[:num_variations] + except Exception as e: + st.error(f"Error generating tweets with AI: {str(e)}") + # Fall back to template-based generation + + # Template-based generation (fallback) + templates = [ + "{emoji} {hook} {hashtags} {cta}", + "{hook} {emoji} {hashtags} {cta}", + "{hashtags} {hook} {emoji} {cta}" + ] + + tweets = [] + for i in range(num_variations): + template = templates[i % len(templates)] + emoji_list = EMOJI_CATEGORIES.get(tone.capitalize(), EMOJI_CATEGORIES["Casual"]) + emoji_str = random.choice(emoji_list) + hashtag_list = suggest_hashtags(keywords, tone) + hashtag_str = " ".join(hashtag_list[:2]) + + tweet_text = template.format( + emoji=emoji_str, + hook=hook, + hashtags=hashtag_str, + cta=call_to_action + ) + + tweets.append({ + "text": tweet_text, + "hashtags": hashtag_list, + "emojis": [emoji_str], + "engagement_score": random.randint(60, 95) + }) + + return tweets + +def post_tweet(tweet_text: str) -> Dict[str, Any]: + """Post a tweet to Twitter.""" + try: + if not is_authenticated(): + return {"success": False, "error": "Not authenticated with Twitter"} + + # Get API client + api = create_twitter_api() + + # Post tweet + status = api.update_status(tweet_text) + + return { + "success": True, + "tweet_id": status.id, + "created_at": status.created_at.isoformat() + } + except Exception as e: + return {"success": False, "error": str(e)} + +def schedule_tweet(tweet_text: str, scheduled_time: datetime) -> Dict[str, Any]: + """Schedule a tweet for later posting.""" + try: + # Store scheduled tweet in session state + if "scheduled_tweets" not in st.session_state: + st.session_state.scheduled_tweets = [] + + tweet_id = f"scheduled_{int(time.time())}" + + st.session_state.scheduled_tweets.append({ + "id": tweet_id, + "text": tweet_text, + "scheduled_time": scheduled_time.isoformat(), + "status": "scheduled" + }) + + return { + "success": True, + "id": tweet_id, + "scheduled_time": scheduled_time.isoformat() + } + except Exception as e: + return {"success": False, "error": str(e)} + +def enhanced_tweet_generator(): + """Enhanced Smart Tweet Generator with Twitter integration.""" + st.title("✨ Enhanced Tweet Generator") + st.markdown("Create and post engaging tweets with AI and Twitter data") + + # Check if connected to Twitter + twitter_connected = is_authenticated() + + # Twitter connection status + if twitter_connected: + user = st.session_state.twitter_user + st.success(f"Connected as @{user['screen_name']}") + + # Show audience insights + with st.expander("Audience Insights", expanded=False): + audience = analyze_audience() + + col1, col2, col3 = st.columns(3) + with col1: + st.metric("Followers", f"{audience['follower_count']:,}") + with col2: + st.metric("Engagement Rate", f"{audience['engagement_rate']:.2f}%") + with col3: + st.metric("Active Hours", ", ".join(audience['active_hours']) if audience['active_hours'] else "Unknown") + + if audience['top_interests']: + st.markdown(f"**Top Interests:** {', '.join(audience['top_interests'])}") + else: + st.warning("Connect your Twitter account to access advanced features") + + # Input section with improved UI + with st.expander("Tweet Parameters", expanded=True): + col1, col2 = st.columns(2) + + with col1: + hook = st.text_area("Tweet Hook/Topic", + placeholder="Enter your main message or topic...", + help="The main message or topic of your tweet") + + target_audience = st.selectbox( + "Target Audience", + ["Professionals", "Students", "General"], + help="Select your target audience" + ) + + tone = st.radio( + "Tweet Tone", + ["Professional", "Casual", "Informative", "Humorous", "Inspirational"], + horizontal=True, + help="Choose the tone for your tweet" + ) + + with col2: + call_to_action = st.text_input( + "Call to Action", + placeholder="e.g., Learn more, Follow us...", + help="What action do you want your audience to take?" + ) + + keywords = st.text_input( + "Keywords/Hashtags", + placeholder="Enter keywords separated by commas", + help="Keywords to include in your tweet" + ) + + length = st.select_slider( + "Tweet Length", + options=["short", "medium", "long"], + value="medium", + help="Choose your desired tweet length" + ) + + num_variations = st.slider( + "Number of Variations", + min_value=1, + max_value=5, + value=3, + help="How many tweet variations would you like to generate?" + ) + + # Advanced options + with st.expander("Advanced Options", expanded=False): + use_trending = st.checkbox("Include trending hashtags", value=False) + + if twitter_connected: + # Get optimal posting time + optimal_time = get_optimal_posting_time() + + st.markdown(f""" + **Optimal Posting Time:** {optimal_time['day']} at {optimal_time['time']} + (Confidence: {optimal_time['confidence']}) + """) + + # Generate button with loading state + if st.button("Generate Tweets", use_container_width=True): + with st.spinner("Generating tweet variations..."): + tweets = generate_tweet_variations( + hook, target_audience, tone, + call_to_action, keywords, length, + num_variations, use_ai=True + ) + + # Store tweets in session state + st.session_state.generated_tweets = tweets + + # Display tweets + st.markdown("### Generated Tweets") + + for i, tweet in enumerate(tweets): + with st.container(): + st.markdown(f""" +
+

Tweet Variation {i + 1}

+

{tweet['text']}

+
+ + Score: {tweet['engagement_score']}% + + + {count_characters(tweet['text'])}/280 chars + +
+
+ """, unsafe_allow_html=True) + + # Action buttons + col1, col2, col3 = st.columns(3) + + with col1: + if st.button(f"Copy Tweet {i + 1}", key=f"copy_{i}"): + st.code(tweet['text']) + st.success("Tweet copied to clipboard!") + + with col2: + if twitter_connected and st.button(f"Post Now {i + 1}", key=f"post_{i}"): + result = post_tweet(tweet['text']) + if result['success']: + st.success("Tweet posted successfully!") + else: + st.error(f"Error posting tweet: {result['error']}") + + with col3: + if twitter_connected and st.button(f"Schedule {i + 1}", key=f"schedule_{i}"): + # Show scheduling options + st.session_state.tweet_to_schedule = tweet['text'] + st.session_state.scheduling_tweet_index = i + st.experimental_rerun() + + # Export options + st.markdown("### πŸ“₯ Export Options") + col1, col2 = st.columns(2) + with col1: + if st.button("Export as JSON"): + st.download_button( + "Download JSON", + data=json.dumps(tweets, indent=2), + file_name=f"tweets_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", + mime="application/json" + ) + with col2: + if st.button("Copy All Tweets"): + tweet_texts = "\n\n".join(tweet["text"] for tweet in tweets) + st.code(tweet_texts) + + # Handle tweet scheduling + if "tweet_to_schedule" in st.session_state: + st.markdown("### Schedule Tweet") + st.markdown(f"**Tweet to schedule:** {st.session_state.tweet_to_schedule}") + + col1, col2 = st.columns(2) + with col1: + schedule_date = st.date_input("Date", value=datetime.now().date()) + with col2: + schedule_time = st.time_input("Time", value=datetime.now().time()) + + scheduled_datetime = datetime.combine(schedule_date, schedule_time) + + if st.button("Confirm Schedule"): + result = schedule_tweet(st.session_state.tweet_to_schedule, scheduled_datetime) + if result['success']: + st.success(f"Tweet scheduled for {scheduled_datetime.strftime('%Y-%m-%d %H:%M')}") + del st.session_state.tweet_to_schedule + del st.session_state.scheduling_tweet_index + else: + st.error(f"Error scheduling tweet: {result['error']}") +``` + +#### 2.2 Tweet Performance Predictor + +Implement the Tweet Performance Predictor feature: + +```python +# lib/ai_writers/twitter_writers/tweet_performance/performance_predictor.py + +import streamlit as st +import pandas as pd +import numpy as np +from typing import Dict, List, Any +import json +from datetime import datetime, timedelta +import plotly.express as px +import plotly.graph_objects as go +from sklearn.ensemble import RandomForestRegressor +from sklearn.preprocessing import OneHotEncoder +import pickle +import os +from pathlib import Path + +from ....integrations.twitter.auth import create_twitter_api, is_authenticated + +# Constants +MODEL_DIR = Path(__file__).parent.parent.parent.parent.parent / 'models' / 'twitter' +MODEL_DIR.mkdir(exist_ok=True, parents=True) +MODEL_PATH = MODEL_DIR / 'performance_predictor.pkl' + +def get_tweet_history() -> pd.DataFrame: + """Get tweet history from Twitter API.""" + try: + if not is_authenticated(): + return pd.DataFrame() + + # Get API client + api = create_twitter_api() + + # Get user timeline + tweets = api.user_timeline(count=200, tweet_mode='extended') + + # Create DataFrame + data = [] + for tweet in tweets: + # Skip retweets + if hasattr(tweet, 'retweeted_status'): + continue + + # Extract tweet data + tweet_data = { + 'id': tweet.id, + 'text': tweet.full_text, + 'created_at': tweet.created_at, + 'likes': tweet.favorite_count, + 'retweets': tweet.retweet_count, + 'replies': 0, # Not available in standard API + 'impressions': 0, # Not available in standard API + 'hashtags': [h['text'] for h in tweet.entities.get('hashtags', [])], + 'mentions': [m['screen_name'] for m in tweet.entities.get('user_mentions', [])], + 'urls': [u['expanded_url'] for u in tweet.entities.get('urls', [])], + 'media': 1 if 'media' in tweet.entities else 0, + 'hour': tweet.created_at.hour, + 'day': tweet.created_at.weekday(), + 'length': len(tweet.full_text) + } + + # Calculate engagement + tweet_data['engagement'] = tweet_data['likes'] + tweet_data['retweets'] + + data.append(tweet_data) + + # Create DataFrame + df = pd.DataFrame(data) + + return df + except Exception as e: + st.error(f"Error fetching tweet history: {str(e)}") + return pd.DataFrame() + +def train_performance_model(df: pd.DataFrame) -> Any: + """Train a model to predict tweet performance.""" + try: + if df.empty: + return None + + # Feature engineering + X = pd.DataFrame({ + 'hour': df['hour'], + 'day': df['day'], + 'length': df['length'], + 'hashtag_count': df['hashtags'].apply(len), + 'mention_count': df['mentions'].apply(len), + 'url_count': df['urls'].apply(len), + 'has_media': df['media'] + }) + + # Target variable + y = df['engagement'] + + # Train model + model = RandomForestRegressor(n_estimators=100, random_state=42) + model.fit(X, y) + + # Save model + with open(MODEL_PATH, 'wb') as f: + pickle.dump(model, f) + + return model + except Exception as e: + st.error(f"Error training model: {str(e)}") + return None + +def load_performance_model() -> Any: + """Load the performance prediction model.""" + try: + if MODEL_PATH.exists(): + with open(MODEL_PATH, 'rb') as f: + model = pickle.load(f) + return model + else: + return None + except Exception as e: + st.error(f"Error loading model: {str(e)}") + return None + +def predict_performance( + tweet_text: str, + hour: int, + day: int, + has_media: bool = False +) -> Dict[str, Any]: + """Predict tweet performance.""" + try: + # Load model + model = load_performance_model() + + if model is None: + # No model available, use heuristics + return predict_performance_heuristic(tweet_text, hour, day, has_media) + + # Feature extraction + hashtags = len([w for w in tweet_text.split() if w.startswith('#')]) + mentions = len([w for w in tweet_text.split() if w.startswith('@')]) + urls = len([w for w in tweet_text.split() if w.startswith('http')]) + length = len(tweet_text) + + # Create feature vector + X = pd.DataFrame({ + 'hour': [hour], + 'day': [day], + 'length': [length], + 'hashtag_count': [hashtags], + 'mention_count': [mentions], + 'url_count': [urls], + 'has_media': [1 if has_media else 0] + }) + + # Make prediction + engagement = model.predict(X)[0] + + # Calculate confidence + confidence = "high" if model.n_estimators >= 100 else "medium" + + return { + "predicted_engagement": engagement, + "confidence": confidence, + "factors": { + "time_factor": get_time_factor(hour, day), + "content_factor": get_content_factor(tweet_text), + "media_factor": 1.5 if has_media else 1.0 + } + } + except Exception as e: + st.error(f"Error predicting performance: {str(e)}") + return predict_performance_heuristic(tweet_text, hour, day, has_media) + +def predict_performance_heuristic( + tweet_text: str, + hour: int, + day: int, + has_media: bool = False +) -> Dict[str, Any]: + """Predict tweet performance using heuristics.""" + # Time factor (higher for business hours on weekdays) + time_factor = get_time_factor(hour, day) + + # Content factor + content_factor = get_content_factor(tweet_text) + + # Media factor + media_factor = 1.5 if has_media else 1.0 + + # Base engagement (arbitrary value) + base_engagement = 10 + + # Calculate predicted engagement + predicted_engagement = base_engagement * time_factor * content_factor * media_factor + + return { + "predicted_engagement": predicted_engagement, + "confidence": "low", + "factors": { + "time_factor": time_factor, + "content_factor": content_factor, + "media_factor": media_factor + } + } + +def get_time_factor(hour: int, day: int) -> float: + """Calculate time factor for engagement prediction.""" + # Weekday factor (higher for weekdays) + weekday_factor = 1.0 if day < 5 else 0.8 + + # Hour factor (higher for business hours) + if 9 <= hour <= 17: + hour_factor = 1.0 + elif 7 <= hour <= 8 or 18 <= hour <= 21: + hour_factor = 0.8 + else: + hour_factor = 0.6 + + return weekday_factor * hour_factor + +def get_content_factor(tweet_text: str) -> float: + """Calculate content factor for engagement prediction.""" + # Length factor (higher for medium-length tweets) + length = len(tweet_text) + if 70 <= length <= 140: + length_factor = 1.0 + elif length < 70: + length_factor = 0.8 + else: + length_factor = 0.9 + + # Hashtag factor (higher for 1-2 hashtags) + hashtags = len([w for w in tweet_text.split() if w.startswith('#')]) + if 1 <= hashtags <= 2: + hashtag_factor = 1.0 + elif hashtags == 0: + hashtag_factor = 0.8 + else: + hashtag_factor = 0.7 # Too many hashtags + + # Question factor (higher for tweets with questions) + question_factor = 1.2 if '?' in tweet_text else 1.0 + + # Call to action factor + cta_words = ['follow', 'retweet', 'like', 'share', 'comment', 'check', 'click', 'read'] + cta_factor = 1.1 if any(word in tweet_text.lower() for word in cta_words) else 1.0 + + return length_factor * hashtag_factor * question_factor * cta_factor + +def get_best_posting_times() -> List[Dict[str, Any]]: + """Get best posting times based on historical data.""" + try: + # Get tweet history + df = get_tweet_history() + + if df.empty: + # Return default recommendations + return [ + {"day": "Monday", "hour": 12, "confidence": "low"}, + {"day": "Wednesday", "hour": 15, "confidence": "low"}, + {"day": "Friday", "hour": 10, "confidence": "low"} + ] + + # Group by day and hour + df['day_name'] = df['created_at'].dt.day_name() + engagement_by_time = df.groupby(['day_name', 'hour'])['engagement'].mean().reset_index() + + # Sort by engagement + engagement_by_time = engagement_by_time.sort_values('engagement', ascending=False) + + # Get top 3 times + top_times = engagement_by_time.head(3) + + # Format results + results = [] + for _, row in top_times.iterrows(): + results.append({ + "day": row['day_name'], + "hour": row['hour'], + "confidence": "high" if len(df) >= 100 else "medium" if len(df) >= 50 else "low" + }) + + return results + except Exception as e: + st.error(f"Error getting best posting times: {str(e)}") + return [ + {"day": "Monday", "hour": 12, "confidence": "low"}, + {"day": "Wednesday", "hour": 15, "confidence": "low"}, + {"day": "Friday", "hour": 10, "confidence": "low"} + ] + +def render_performance_predictor(): + """Render the Tweet Performance Predictor UI.""" + st.title("πŸ“Š Tweet Performance Predictor") + st.markdown("Predict engagement and find the best time to post your tweets") + + # Check if connected to Twitter + twitter_connected = is_authenticated() + + if twitter_connected: + user = st.session_state.twitter_user + st.success(f"Connected as @{user['screen_name']}") + + # Get tweet history + with st.spinner("Analyzing your tweet history..."): + df = get_tweet_history() + + if not df.empty: + # Train model if needed + if not MODEL_PATH.exists(): + with st.spinner("Training prediction model..."): + train_performance_model(df) + + # Show historical performance + with st.expander("Your Tweet Performance", expanded=True): + # Engagement over time + st.markdown("#### Engagement Over Time") + fig = px.line( + df.sort_values('created_at'), + x='created_at', + y='engagement', + title="Tweet Engagement History" + ) + st.plotly_chart(fig, use_container_width=True) + + # Engagement by day and hour + st.markdown("#### Engagement by Day and Hour") + df['day_name'] = df['created_at'].dt.day_name() + pivot = df.pivot_table( + index='day_name', + columns='hour', + values='engagement', + aggfunc='mean' + ) + + fig = px.imshow( + pivot, + labels=dict(x="Hour of Day", y="Day of Week", color="Avg. Engagement"), + x=pivot.columns, + y=pivot.index, + color_continuous_scale="Viridis" + ) + st.plotly_chart(fig, use_container_width=True) + + # Content analysis + st.markdown("#### Content Analysis") + col1, col2 = st.columns(2) + + with col1: + # Engagement by tweet length + df['length_bin'] = pd.cut(df['length'], bins=[0, 70, 140, 210, 280], labels=['0-70', '71-140', '141-210', '211-280']) + length_engagement = df.groupby('length_bin')['engagement'].mean().reset_index() + + fig = px.bar( + length_engagement, + x='length_bin', + y='engagement', + title="Engagement by Tweet Length" + ) + st.plotly_chart(fig, use_container_width=True) + + with col2: + # Engagement by hashtag count + df['hashtag_count'] = df['hashtags'].apply(len) + df['hashtag_bin'] = pd.cut(df['hashtag_count'], bins=[-1, 0, 1, 2, 10], labels=['0', '1', '2', '3+']) + hashtag_engagement = df.groupby('hashtag_bin')['engagement'].mean().reset_index() + + fig = px.bar( + hashtag_engagement, + x='hashtag_bin', + y='engagement', + title="Engagement by Hashtag Count" + ) + st.plotly_chart(fig, use_container_width=True) + + # Best posting times + best_times = get_best_posting_times() + + st.markdown("### πŸ•’ Best Times to Post") + for i, time in enumerate(best_times): + st.markdown(f"**{i+1}.** {time['day']} at {time['hour']}:00 (Confidence: {time['confidence']})") + else: + st.warning("Connect your Twitter account to access performance prediction features") + + # Show demo data + st.markdown("### Demo Data") + st.markdown("Here's how the performance predictor works with sample data:") + + # Sample engagement chart + dates = pd.date_range(start='2023-01-01', periods=30) + engagements = np.random.normal(loc=20, scale=10, size=30) + engagements = np.abs(engagements) # Make all values positive + + df = pd.DataFrame({ + 'date': dates, + 'engagement': engagements + }) + + fig = px.line( + df, + x='date', + y='engagement', + title="Sample Tweet Engagement History" + ) + st.plotly_chart(fig, use_container_width=True) + + # Tweet input for prediction + st.markdown("### Predict Tweet Performance") + + tweet_text = st.text_area( + "Enter your tweet", + placeholder="Type your tweet here...", + max_chars=280 + ) + + col1, col2, col3 = st.columns(3) + + with col1: + day = st.selectbox( + "Day of week", + ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + ) + day_num = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"].index(day) + + with col2: + hour = st.slider("Hour of day", 0, 23, 12) + + with col3: + has_media = st.checkbox("Include media", value=False) + + if st.button("Predict Performance", use_container_width=True): + if tweet_text: + with st.spinner("Predicting performance..."): + prediction = predict_performance(tweet_text, hour, day_num, has_media) + + # Display prediction + st.markdown("### Prediction Results") + + col1, col2 = st.columns(2) + + with col1: + st.metric( + "Predicted Engagement", + f"{prediction['predicted_engagement']:.1f}", + delta=None + ) + + with col2: + st.markdown(f"**Confidence:** {prediction['confidence'].title()}") + + # Factors affecting prediction + st.markdown("#### Factors Affecting Prediction") + + factors = prediction['factors'] + factor_df = pd.DataFrame({ + 'Factor': ['Time of posting', 'Content quality', 'Media inclusion'], + 'Impact': [ + factors['time_factor'], + factors['content_factor'], + factors['media_factor'] + ] + }) + + fig = px.bar( + factor_df, + x='Factor', + y='Impact', + title="Impact Factors" + ) + st.plotly_chart(fig, use_container_width=True) + + # Recommendations + st.markdown("#### Recommendations to Improve Performance") + + recommendations = [] + + if factors['time_factor'] < 0.9: + recommendations.append("Consider posting during weekdays between 9 AM and 5 PM for better engagement.") + + if factors['content_factor'] < 0.9: + if len(tweet_text) > 140: + recommendations.append("Try shortening your tweet to 70-140 characters for optimal engagement.") + + hashtags = len([w for w in tweet_text.split() if w.startswith('#')]) + if hashtags == 0: + recommendations.append("Add 1-2 relevant hashtags to increase visibility.") + elif hashtags > 2: + recommendations.append("Reduce the number of hashtags to 1-2 for better engagement.") + + if '?' not in tweet_text: + recommendations.append("Consider adding a question to encourage responses.") + + cta_words = ['follow', 'retweet', 'like', 'share', 'comment', 'check', 'click', 'read'] + if not any(word in tweet_text.lower() for word in cta_words): + recommendations.append("Add a clear call-to-action to encourage engagement.") + + if not has_media: + recommendations.append("Including an image or video could increase engagement by up to 50%.") + + if recommendations: + for rec in recommendations: + st.info(rec) + else: + st.success("Your tweet is well-optimized for engagement!") + else: + st.error("Please enter a tweet to predict performance.") +``` + +### Phase 3: Content Strategy Tools + +#### 3.1 Content Calendar Generator + +Implement the Content Calendar Generator feature: + +```python +# lib/ai_writers/twitter_writers/content_strategy/calendar_generator.py + +import streamlit as st +import pandas as pd +import numpy as np +from typing import Dict, List, Any +import json +from datetime import datetime, timedelta +import plotly.express as px +import plotly.graph_objects as go +import calendar +import random +from io import BytesIO + +from ....integrations.twitter.auth import create_twitter_api, is_authenticated +from ....gpt_providers.text_generation.main_text_generation import llm_text_gen + +def generate_content_themes(industry: str, num_themes: int = 5) -> List[str]: + """Generate content themes based on industry.""" + # Predefined themes by industry + industry_themes = { + "Technology": [ + "Product Updates", "Tech Tips", "Industry News", "Customer Success Stories", + "Behind the Scenes", "Tech Trends", "How-To Guides", "Tech Humor", + "Industry Events", "Q&A Sessions" + ], + "Marketing": [ + "Marketing Tips", "Case Studies", "Industry Trends", "Tool Recommendations", + "Marketing Metrics", "Campaign Spotlights", "Marketing Humor", "Expert Interviews", + "Marketing Events", "Strategy Insights" + ], + "Education": [ + "Learning Resources", "Student Spotlights", "Education News", "Teaching Tips", + "Research Highlights", "Campus Events", "Educational Humor", "Alumni Stories", + "Faculty Spotlights", "Educational Trends" + ], + "Health": [ + "Health Tips", "Wellness Advice", "Medical News", "Patient Stories", + "Research Updates", "Health Myths Debunked", "Nutrition Tips", "Exercise Guides", + "Mental Health Awareness", "Healthcare Innovations" + ], + "Finance": [ + "Financial Tips", "Market Updates", "Investment Strategies", "Financial Education", + "Economic News", "Savings Guides", "Financial Planning", "Success Stories", + "Industry Trends", "Q&A Sessions" + ] + } + + # Get themes for the selected industry or use AI to generate them + if industry in industry_themes: + themes = industry_themes[industry] + return random.sample(themes, min(num_themes, len(themes))) + else: + # Use AI to generate themes + prompt = f""" + Generate {num_themes} content themes for Twitter posts in the {industry} industry. + Each theme should be a short phrase (2-4 words) that describes a category of content. + Return the themes as a JSON array of strings. + """ + + try: + response = llm_text_gen(prompt) + + # Try to parse as JSON + try: + themes = json.loads(response) + if isinstance(themes, list): + return themes[:num_themes] + except: + # Extract themes using regex + import re + themes = re.findall(r'"([^"]+)"', response) + return themes[:num_themes] + except Exception as e: + st.error(f"Error generating themes: {str(e)}") + + # Return generic themes + generic_themes = [ + "Industry News", "Tips & Tricks", "Behind the Scenes", + "Customer Stories", "Product Highlights" + ] + return generic_themes[:num_themes] + +def generate_content_ideas(theme: str, industry: str, num_ideas: int = 3) -> List[str]: + """Generate content ideas for a theme.""" + # Use AI to generate content ideas + prompt = f""" + Generate {num_ideas} specific Twitter post ideas for the theme "{theme}" in the {industry} industry. + Each idea should be a brief description (not the actual tweet) of what the post will be about. + Keep each idea under 100 characters. + Return the ideas as a JSON array of strings. + """ + + try: + response = llm_text_gen(prompt) + + # Try to parse as JSON + try: + ideas = json.loads(response) + if isinstance(ideas, list): + return ideas[:num_ideas] + except: + # Extract ideas using regex + import re + ideas = re.findall(r'"([^"]+)"', response) + return ideas[:num_ideas] if ideas else [f"{theme} idea {i+1}" for i in range(num_ideas)] + except Exception as e: + st.error(f"Error generating ideas: {str(e)}") + + # Return generic ideas + return [f"{theme} idea {i+1}" for i in range(num_ideas)] + +def create_content_calendar( + start_date: datetime, + end_date: datetime, + posts_per_week: int, + themes: List[str], + industry: str +) -> pd.DataFrame: + """Create a content calendar with themes and ideas.""" + # Calculate date range + date_range = pd.date_range(start=start_date, end=end_date) + + # Filter for weekdays if needed + if posts_per_week <= 5: + date_range = date_range[date_range.dayofweek < 5] # Monday to Friday + + # Calculate posting frequency + if posts_per_week < 5: + # Select specific days + days_to_post = random.sample(range(5), posts_per_week) # Random weekdays + date_range = date_range[date_range.dayofweek.isin(days_to_post)] + + # Limit to the requested frequency + posting_dates = [] + current_week = -1 + posts_this_week = 0 + + for date in date_range: + week_num = date.isocalendar()[1] + + if week_num != current_week: + current_week = week_num + posts_this_week = 0 + + if posts_this_week < posts_per_week: + posting_dates.append(date) + posts_this_week += 1 + + # Create calendar dataframe + calendar_data = [] + + for date in posting_dates: + # Assign theme (rotate through themes) + theme_index = len(calendar_data) % len(themes) + theme = themes[theme_index] + + # Generate content ideas + ideas = generate_content_ideas(theme, industry, num_ideas=1) + + calendar_data.append({ + 'date': date, + 'day': date.day_name(), + 'week': date.isocalendar()[1], + 'theme': theme, + 'content_idea': ideas[0] if ideas else "", + 'status': 'Planned' + }) + + # Create DataFrame + df = pd.DataFrame(calendar_data) + + return df + +def export_calendar_to_excel(df: pd.DataFrame) -> BytesIO: + """Export content calendar to Excel.""" + # Create Excel writer + output = BytesIO() + writer = pd.ExcelWriter(output, engine='xlsxwriter') + + # Write DataFrame to Excel + df.to_excel(writer, sheet_name='Content Calendar', index=False) + + # Get workbook and worksheet + workbook = writer.book + worksheet = writer.sheets['Content Calendar'] + + # Add formats + header_format = workbook.add_format({ + 'bold': True, + 'text_wrap': True, + 'valign': 'top', + 'fg_color': '#D7E4BC', + 'border': 1 + }) + + date_format = workbook.add_format({ + 'num_format': 'yyyy-mm-dd', + 'border': 1 + }) + + cell_format = workbook.add_format({ + 'text_wrap': True, + 'border': 1 + }) + + # Write headers with format + for col_num, value in enumerate(df.columns.values): + worksheet.write(0, col_num, value, header_format) + + # Format columns + worksheet.set_column('A:A', 12, date_format) # Date column + worksheet.set_column('B:B', 10, cell_format) # Day column + worksheet.set_column('C:C', 8, cell_format) # Week column + worksheet.set_column('D:D', 15, cell_format) # Theme column + worksheet.set_column('E:E', 40, cell_format) # Content idea column + worksheet.set_column('F:F', 10, cell_format) # Status column + + # Close the writer + writer.close() + + # Return the Excel file + output.seek(0) + return output + +def render_calendar_generator(): + """Render the Content Calendar Generator UI.""" + st.title("πŸ—“οΈ Content Calendar Generator") + st.markdown("Create a strategic Twitter content calendar with themes and ideas") + + # Check if connected to Twitter + twitter_connected = is_authenticated() + + if twitter_connected: + user = st.session_state.twitter_user + st.success(f"Connected as @{user['screen_name']}") + + # Calendar settings + st.markdown("### Calendar Settings") + + col1, col2 = st.columns(2) + + with col1: + start_date = st.date_input("Start Date", value=datetime.now().date()) + + industry = st.selectbox( + "Industry", + ["Technology", "Marketing", "Education", "Health", "Finance", "Other"], + index=0 + ) + + if industry == "Other": + industry = st.text_input("Specify Industry") + + with col2: + duration = st.selectbox( + "Duration", + ["1 week", "2 weeks", "1 month", "3 months"], + index=2 + ) + + # Calculate end date based on duration + if duration == "1 week": + end_date = start_date + timedelta(days=6) + elif duration == "2 weeks": + end_date = start_date + timedelta(days=13) + elif duration == "1 month": + end_date = start_date + timedelta(days=29) + else: # 3 months + end_date = start_date + timedelta(days=89) + + posts_per_week = st.slider( + "Posts per Week", + min_value=1, + max_value=7, + value=3 + ) + + # Theme settings + st.markdown("### Content Themes") + + num_themes = st.slider( + "Number of Themes", + min_value=1, + max_value=10, + value=5 + ) + + # Generate themes button + if st.button("Generate Themes", key="generate_themes"): + with st.spinner("Generating content themes..."): + themes = generate_content_themes(industry, num_themes) + st.session_state.content_themes = themes + + # Display and edit themes + if "content_themes" in st.session_state: + themes = st.session_state.content_themes + + # Create columns for themes + cols = st.columns(min(5, len(themes))) + + # Display each theme in a column + for i, theme in enumerate(themes): + col_index = i % len(cols) + with cols[col_index]: + new_theme = st.text_input(f"Theme {i+1}", value=theme, key=f"theme_{i}") + themes[i] = new_theme + + st.session_state.content_themes = themes + else: + st.info("Click 'Generate Themes' to create content themes for your calendar") + + # Generate calendar button + if st.button("Generate Calendar", use_container_width=True): + if "content_themes" in st.session_state and st.session_state.content_themes: + with st.spinner("Creating your content calendar..."): + # Create calendar + calendar_df = create_content_calendar( + start_date=start_date, + end_date=end_date, + posts_per_week=posts_per_week, + themes=st.session_state.content_themes, + industry=industry + ) + + # Store in session state + st.session_state.content_calendar = calendar_df + + # Display calendar + st.markdown("### Your Content Calendar") + + # Calendar view + calendar_view = st.radio( + "View", + ["Table", "Calendar View"], + horizontal=True + ) + + if calendar_view == "Table": + # Display as table + st.dataframe( + calendar_df, + column_config={ + "date": st.column_config.DateColumn("Date", format="YYYY-MM-DD"), + "day": "Day", + "week": "Week", + "theme": "Theme", + "content_idea": st.column_config.TextColumn("Content Idea", width="large"), + "status": "Status" + }, + hide_index=True + ) + else: + # Display as calendar + # Group by week + weeks = calendar_df.groupby('week') + + for week_num, week_data in weeks: + st.markdown(f"#### Week {week_num}") + + # Create a row for each day of the week + days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + cols = st.columns(7) + + # Add day headers + for i, day in enumerate(days): + with cols[i]: + st.markdown(f"**{day[:3]}**") + + # Add content for each day + for i, day in enumerate(days): + with cols[i]: + # Find posts for this day + day_posts = week_data[week_data['day'] == day] + + if not day_posts.empty: + for _, post in day_posts.iterrows(): + st.markdown(f""" +
+ {post['date'].strftime('%Y-%m-%d')} +

{post['theme']}

+

{post['content_idea']}

+
+ """, unsafe_allow_html=True) + else: + st.markdown("β€”") + + st.markdown("---") + + # Export options + st.markdown("### Export Options") + + col1, col2 = st.columns(2) + + with col1: + # Export as Excel + excel_file = export_calendar_to_excel(calendar_df) + st.download_button( + "Download Excel Calendar", + data=excel_file, + file_name=f"twitter_content_calendar_{datetime.now().strftime('%Y%m%d')}.xlsx", + mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + + with col2: + # Export as CSV + csv = calendar_df.to_csv(index=False) + st.download_button( + "Download CSV Calendar", + data=csv, + file_name=f"twitter_content_calendar_{datetime.now().strftime('%Y%m%d')}.csv", + mime="text/csv" + ) + else: + st.error("Please generate themes first") +``` + +#### 3.2 Hashtag Strategy Manager + +Implement the Hashtag Strategy Manager feature: + +```python +# lib/ai_writers/twitter_writers/content_strategy/hashtag_manager.py + +import streamlit as st +import pandas as pd +import numpy as np +from typing import Dict, List, Any +import json +from datetime import datetime, timedelta +import plotly.express as px +import plotly.graph_objects as go +import re +import tweepy +from collections import Counter + +from ....integrations.twitter.auth import create_twitter_api, create_twitter_client, is_authenticated +from ....gpt_providers.text_generation.main_text_generation import llm_text_gen + +def get_trending_hashtags(location_id: int = 1) -> List[Dict[str, Any]]: + """Get trending hashtags from Twitter API.""" + try: + if not is_authenticated(): + return [] + + # Get API client + api = create_twitter_api() + + # Get trending topics + trends = api.get_place_trends(location_id) + + # Extract hashtags + hashtags = [ + { + "name": trend["name"], + "volume": trend.get("tweet_volume", 0), + "is_hashtag": trend["name"].startswith("#") + } + for trend in trends[0]["trends"] + ] + + # Sort by volume + hashtags.sort(key=lambda x: x["volume"] if x["volume"] else 0, reverse=True) + + return hashtags + except Exception as e: + st.error(f"Error fetching trending hashtags: {str(e)}") + return [] + +def get_trending_locations() -> List[Dict[str, Any]]: + """Get available trending locations.""" + try: + if not is_authenticated(): + return [] + + # Get API client + api = create_twitter_api() + + # Get available trend locations + locations = api.available_trends() + + # Format locations + formatted_locations = [ + { + "woeid": location["woeid"], + "name": location["name"], + "country": location.get("countryCode", "") + } + for location in locations + ] + + # Sort by name + formatted_locations.sort(key=lambda x: x["name"]) + + return formatted_locations + except Exception as e: + st.error(f"Error fetching trending locations: {str(e)}") + return [] + +def analyze_hashtag(hashtag: str) -> Dict[str, Any]: + """Analyze a specific hashtag.""" + try: + if not is_authenticated(): + return { + "volume": 0, + "sentiment": "neutral", + "related_hashtags": [], + "sample_tweets": [] + } + + # Clean hashtag + if not hashtag.startswith("#"): + hashtag = f"#{hashtag}" + + # Get API client + api = create_twitter_api() + client = create_twitter_client() + + # Search for tweets with the hashtag + tweets = api.search_tweets(q=hashtag, count=100, tweet_mode="extended") + + # Extract data + sample_tweets = [] + all_hashtags = [] + sentiment_scores = [] + + for tweet in tweets: + # Skip retweets + if hasattr(tweet, "retweeted_status"): + continue + + # Add to sample tweets + sample_tweets.append({ + "text": tweet.full_text, + "created_at": tweet.created_at, + "likes": tweet.favorite_count, + "retweets": tweet.retweet_count + }) + + # Extract hashtags + tweet_hashtags = [h["text"] for h in tweet.entities.get("hashtags", [])] + all_hashtags.extend(tweet_hashtags) + + # Simple sentiment analysis + text = tweet.full_text.lower() + positive_words = ["good", "great", "awesome", "excellent", "love", "happy", "best", "amazing"] + negative_words = ["bad", "terrible", "awful", "hate", "worst", "poor", "disappointing"] + + positive_count = sum(1 for word in positive_words if word in text) + negative_count = sum(1 for word in negative_words if word in text) + + if positive_count > negative_count: + sentiment_scores.append(1) # Positive + elif negative_count > positive_count: + sentiment_scores.append(-1) # Negative + else: + sentiment_scores.append(0) # Neutral + + # Calculate related hashtags + hashtag_counts = Counter(all_hashtags) + related_hashtags = [ + {"name": f"#{h}", "count": count} + for h, count in hashtag_counts.most_common(10) + if h.lower() != hashtag[1:].lower() # Exclude the analyzed hashtag + ] + + # Calculate sentiment + avg_sentiment = sum(sentiment_scores) / len(sentiment_scores) if sentiment_scores else 0 + sentiment = "positive" if avg_sentiment > 0.2 else "negative" if avg_sentiment < -0.2 else "neutral" + + # Get volume (approximate) + volume = len(tweets) + + return { + "volume": volume, + "sentiment": sentiment, + "related_hashtags": related_hashtags, + "sample_tweets": sample_tweets[:5] # Return top 5 tweets + } + except Exception as e: + st.error(f"Error analyzing hashtag: {str(e)}") + return { + "volume": 0, + "sentiment": "neutral", + "related_hashtags": [], + "sample_tweets": [] + } + +def get_account_hashtags() -> Dict[str, int]: + """Get hashtags used in the user's account.""" + try: + if not is_authenticated(): + return {} + + # Get API client + api = create_twitter_api() + + # Get user timeline + tweets = api.user_timeline(count=200, tweet_mode="extended") + + # Extract hashtags + all_hashtags = [] + for tweet in tweets: + # Skip retweets + if hasattr(tweet, "retweeted_status"): + continue + + # Extract hashtags + tweet_hashtags = [h["text"] for h in tweet.entities.get("hashtags", [])] + all_hashtags.extend(tweet_hashtags) + + # Count hashtags + hashtag_counts = Counter(all_hashtags) + + return dict(hashtag_counts.most_common(20)) + except Exception as e: + st.error(f"Error getting account hashtags: {str(e)}") + return {} + +def generate_hashtag_recommendations(industry: str, topic: str) -> List[Dict[str, Any]]: + """Generate hashtag recommendations based on industry and topic.""" + # Use AI to generate recommendations + prompt = f""" + Generate 10 effective Twitter hashtags for the {industry} industry, specifically for content about {topic}. + For each hashtag, provide: + 1. The hashtag itself (including the # symbol) + 2. A brief description of why it's effective + 3. An estimate of how popular it is (high, medium, or low) + + Return the results as a JSON array of objects with the following structure: + [ + {{ + "hashtag": "#example", + "description": "Brief explanation of why this hashtag is effective", + "popularity": "high/medium/low" + }} + ] + """ + + try: + response = llm_text_gen(prompt) + + # Try to parse as JSON + try: + recommendations = json.loads(response) + if isinstance(recommendations, list): + return recommendations + except: + # Extract using regex + hashtags = re.findall(r'#\w+', response) + return [ + { + "hashtag": hashtag, + "description": f"Recommended hashtag for {topic} in {industry}", + "popularity": "medium" + } + for hashtag in hashtags[:10] + ] + except Exception as e: + st.error(f"Error generating hashtag recommendations: {str(e)}") + + # Return generic recommendations + return [ + { + "hashtag": f"#{industry.lower()}", + "description": f"Main industry hashtag", + "popularity": "high" + }, + { + "hashtag": f"#{topic.lower().replace(' ', '')}", + "description": f"Topic-specific hashtag", + "popularity": "medium" + } + ] + +def render_hashtag_manager(): + """Render the Hashtag Strategy Manager UI.""" + st.title("#️⃣ Hashtag Strategy Manager") + st.markdown("Research and manage trending hashtags for better reach") + + # Check if connected to Twitter + twitter_connected = is_authenticated() + + if twitter_connected: + user = st.session_state.twitter_user + st.success(f"Connected as @{user['screen_name']}") + + # Create tabs + tab1, tab2, tab3 = st.tabs(["Trending Hashtags", "Hashtag Analysis", "Hashtag Strategy"]) + + with tab1: + st.markdown("### Trending Hashtags") + + # Get trending locations + if twitter_connected: + locations = get_trending_locations() + + if locations: + # Create location selector + location_options = {f"{loc['name']}, {loc['country']}": loc["woeid"] for loc in locations} + selected_location = st.selectbox( + "Select Location", + options=list(location_options.keys()), + index=0 + ) + + location_id = location_options[selected_location] + + # Get trending hashtags + if st.button("Get Trending Hashtags", use_container_width=True): + with st.spinner("Fetching trending hashtags..."): + trends = get_trending_hashtags(location_id) + + if trends: + # Filter hashtags + show_all = st.checkbox("Show all trending topics (not just hashtags)") + + if not show_all: + trends = [t for t in trends if t["is_hashtag"]] + + # Create DataFrame + trends_df = pd.DataFrame(trends) + + # Display trends + st.dataframe( + trends_df, + column_config={ + "name": "Trend", + "volume": st.column_config.NumberColumn("Volume", format="%d"), + "is_hashtag": "Is Hashtag" + }, + hide_index=True + ) + + # Visualize top trends + st.markdown("#### Top Trends by Volume") + + # Filter out trends with no volume data + volume_trends = [t for t in trends if t["volume"]] + + if volume_trends: + # Sort by volume + volume_trends.sort(key=lambda x: x["volume"], reverse=True) + + # Take top 10 + top_trends = volume_trends[:10] + + # Create DataFrame + top_df = pd.DataFrame(top_trends) + + # Create bar chart + fig = px.bar( + top_df, + x="name", + y="volume", + title="Top Trending Topics by Volume", + labels={"name": "Trend", "volume": "Tweet Volume"} + ) + st.plotly_chart(fig, use_container_width=True) + else: + st.info("No volume data available for trending topics") + else: + st.warning("No trending topics found") + else: + st.warning("Could not fetch trending locations") + else: + st.warning("Connect your Twitter account to access trending hashtags") + + # Show sample data + st.markdown("### Sample Trending Hashtags") + sample_trends = [ + {"name": "#AI", "volume": 125000, "is_hashtag": True}, + {"name": "#MachineLearning", "volume": 78000, "is_hashtag": True}, + {"name": "#DataScience", "volume": 65000, "is_hashtag": True}, + {"name": "#Python", "volume": 52000, "is_hashtag": True}, + {"name": "#BigData", "volume": 48000, "is_hashtag": True} + ] + + # Create DataFrame + sample_df = pd.DataFrame(sample_trends) + + # Display trends + st.dataframe( + sample_df, + column_config={ + "name": "Trend", + "volume": st.column_config.NumberColumn("Volume", format="%d"), + "is_hashtag": "Is Hashtag" + }, + hide_index=True + ) + + with tab2: + st.markdown("### Hashtag Analysis") + + # Hashtag input + hashtag = st.text_input( + "Enter a hashtag to analyze", + placeholder="e.g., #AI" + ) + + if hashtag: + # Clean hashtag + if not hashtag.startswith("#"): + hashtag = f"#{hashtag}" + + # Analyze button + if st.button("Analyze Hashtag", use_container_width=True): + with st.spinner(f"Analyzing {hashtag}..."): + if twitter_connected: + # Get hashtag analysis + analysis = analyze_hashtag(hashtag) + + # Display results + st.markdown(f"#### Analysis Results for {hashtag}") + + # Metrics + col1, col2, col3 = st.columns(3) + + with col1: + st.metric("Volume", f"{analysis['volume']:,}") + + with col2: + sentiment = analysis["sentiment"].title() + sentiment_color = { + "Positive": "green", + "Neutral": "blue", + "Negative": "red" + }.get(sentiment, "blue") + + st.markdown(f""" +
+

Sentiment

+

{sentiment}

+
+ """, unsafe_allow_html=True) + + with col3: + related_count = len(analysis["related_hashtags"]) + st.metric("Related Hashtags", related_count) + + # Related hashtags + if analysis["related_hashtags"]: + st.markdown("#### Related Hashtags") + + # Create DataFrame + related_df = pd.DataFrame(analysis["related_hashtags"]) + + # Display related hashtags + st.dataframe( + related_df, + column_config={ + "name": "Hashtag", + "count": st.column_config.NumberColumn("Count", format="%d") + }, + hide_index=True + ) + + # Sample tweets + if analysis["sample_tweets"]: + st.markdown("#### Sample Tweets") + + for tweet in analysis["sample_tweets"]: + st.markdown(f""" +
+

{tweet['text']}

+
+ {tweet['created_at'].strftime('%Y-%m-%d %H:%M')} + ❀️ {tweet['likes']} | πŸ”„ {tweet['retweets']} +
+
+ """, unsafe_allow_html=True) + else: + st.warning("Connect your Twitter account to analyze hashtags") + + # Show sample analysis + st.markdown(f"#### Sample Analysis for {hashtag}") + + # Metrics + col1, col2, col3 = st.columns(3) + + with col1: + st.metric("Volume", "5,280") + + with col2: + st.markdown(""" +
+

Sentiment

+

Positive

+
+ """, unsafe_allow_html=True) + + with col3: + st.metric("Related Hashtags", 8) + + # Related hashtags + st.markdown("#### Related Hashtags") + + sample_related = [ + {"name": "#MachineLearning", "count": 45}, + {"name": "#DataScience", "count": 32}, + {"name": "#Python", "count": 28}, + {"name": "#DeepLearning", "count": 21}, + {"name": "#BigData", "count": 18} + ] + + # Create DataFrame + related_df = pd.DataFrame(sample_related) + + # Display related hashtags + st.dataframe( + related_df, + column_config={ + "name": "Hashtag", + "count": st.column_config.NumberColumn("Count", format="%d") + }, + hide_index=True + ) + + # Account hashtag analysis + st.markdown("### Your Hashtag Usage") + + if twitter_connected: + if st.button("Analyze My Hashtags", use_container_width=True): + with st.spinner("Analyzing your hashtag usage..."): + # Get account hashtags + account_hashtags = get_account_hashtags() + + if account_hashtags: + # Create DataFrame + hashtags_df = pd.DataFrame([ + {"hashtag": f"#{h}", "count": count} + for h, count in account_hashtags.items() + ]) + + # Display hashtags + st.dataframe( + hashtags_df, + column_config={ + "hashtag": "Hashtag", + "count": st.column_config.NumberColumn("Count", format="%d") + }, + hide_index=True + ) + + # Visualize top hashtags + st.markdown("#### Your Top Hashtags") + + # Create bar chart + fig = px.bar( + hashtags_df.head(10), + x="hashtag", + y="count", + title="Your Most Used Hashtags", + labels={"hashtag": "Hashtag", "count": "Usage Count"} + ) + st.plotly_chart(fig, use_container_width=True) + else: + st.info("No hashtags found in your recent tweets") + else: + st.warning("Connect your Twitter account to analyze your hashtag usage") + + with tab3: + st.markdown("### Hashtag Strategy") + + # Industry and topic inputs + col1, col2 = st.columns(2) + + with col1: + industry = st.selectbox( + "Industry", + ["Technology", "Marketing", "Education", "Health", "Finance", "Other"], + index=0 + ) + + if industry == "Other": + industry = st.text_input("Specify Industry") + + with col2: + topic = st.text_input( + "Content Topic", + placeholder="e.g., Artificial Intelligence" + ) + + # Generate recommendations + if st.button("Generate Hashtag Recommendations", use_container_width=True): + if topic: + with st.spinner("Generating hashtag recommendations..."): + # Get recommendations + recommendations = generate_hashtag_recommendations(industry, topic) + + if recommendations: + # Display recommendations + st.markdown("#### Recommended Hashtags") + + for rec in recommendations: + # Determine color based on popularity + color = { + "high": "#28a745", + "medium": "#ffc107", + "low": "#6c757d" + }.get(rec.get("popularity", "medium").lower(), "#6c757d") + + st.markdown(f""" +
+

{rec['hashtag']}

+

{rec.get('description', '')}

+ + {rec.get('popularity', 'medium').title()} Popularity + +
+ """, unsafe_allow_html=True) + + # Hashtag strategy tips + st.markdown("#### Hashtag Strategy Tips") + + st.markdown(""" + - **Use a mix of popular and niche hashtags** to balance reach and competition + - **Limit to 2-3 hashtags per tweet** for optimal engagement + - **Research before using trending hashtags** to ensure relevance + - **Create branded hashtags** for campaigns and tracking + - **Place hashtags within your tweet text** when possible, rather than at the end + - **Monitor performance** to identify which hashtags drive the most engagement + """) + + # Save hashtags + if st.button("Save to Hashtag Library"): + # Store in session state + if "hashtag_library" not in st.session_state: + st.session_state.hashtag_library = [] + + # Add new hashtags + for rec in recommendations: + if rec["hashtag"] not in [h["hashtag"] for h in st.session_state.hashtag_library]: + st.session_state.hashtag_library.append(rec) + + st.success("Hashtags saved to your library!") + else: + st.warning("Could not generate recommendations") + else: + st.error("Please enter a content topic") + + # Hashtag library + if "hashtag_library" in st.session_state and st.session_state.hashtag_library: + st.markdown("### Your Hashtag Library") + + # Create DataFrame + library_df = pd.DataFrame(st.session_state.hashtag_library) + + # Display library + st.dataframe( + library_df, + column_config={ + "hashtag": "Hashtag", + "description": st.column_config.TextColumn("Description", width="large"), + "popularity": "Popularity" + }, + hide_index=True + ) + + # Export button + if st.button("Export Hashtag Library"): + # Convert to CSV + csv = library_df.to_csv(index=False) + + # Download button + st.download_button( + "Download CSV", + data=csv, + file_name=f"hashtag_library_{datetime.now().strftime('%Y%m%d')}.csv", + mime="text/csv" + ) +``` + +### Phase 4: Visual Content Creation + +#### 4.1 Image Generator + +Implement the Image Generator feature: + +```python +# lib/ai_writers/twitter_writers/visual_content/image_generator.py + +import streamlit as st +import pandas as pd +import numpy as np +from typing import Dict, List, Any +import json +from datetime import datetime +import io +import base64 +from PIL import Image, ImageDraw, ImageFont +import requests +import os +from pathlib import Path + +# Constants +TEMPLATE_DIR = Path(__file__).parent.parent.parent.parent.parent / 'assets' / 'templates' +TEMPLATE_DIR.mkdir(exist_ok=True, parents=True) +FONT_DIR = Path(__file__).parent.parent.parent.parent.parent / 'assets' / 'fonts' +FONT_DIR.mkdir(exist_ok=True, parents=True) + +def generate_quote_image( + quote: str, + author: str = "", + template: str = "gradient", + color_scheme: str = "blue" +) -> Image.Image: + """Generate a quote image.""" + # Define templates + templates = { + "gradient": { + "width": 1200, + "height": 675, + "background": { + "blue": [(25, 84, 123), (142, 197, 252)], + "green": [(25, 123, 48), (142, 252, 169)], + "purple": [(84, 25, 123), (197, 142, 252)], + "red": [(123, 25, 25), (252, 142, 142)] + }, + "text_color": (255, 255, 255), + "author_color": (255, 255, 255, 200) + }, + "minimal": { + "width": 1200, + "height": 675, + "background": { + "blue": (240, 248, 255), + "green": (240, 255, 240), + "purple": (248, 240, 255), + "red": (255, 240, 240) + }, + "text_color": (50, 50, 50), + "author_color": (100, 100, 100) + }, + "bold": { + "width": 1200, + "height": 675, + "background": { + "blue": (25, 84, 123), + "green": (25, 123, 48), + "purple": (84, 25, 123), + "red": (123, 25, 25) + }, + "text_color": (255, 255, 255), + "author_color": (255, 255, 255, 200) + } + } + + # Get template settings + template_settings = templates.get(template, templates["gradient"]) + width = template_settings["width"] + height = template_settings["height"] + + # Create image + img = Image.new("RGB", (width, height), color=(255, 255, 255)) + draw = ImageDraw.Draw(img) + + # Draw background + if template == "gradient": + # Create gradient background + bg_colors = template_settings["background"][color_scheme] + for y in range(height): + r = int(bg_colors[0][0] + (bg_colors[1][0] - bg_colors[0][0]) * y / height) + g = int(bg_colors[0][1] + (bg_colors[1][1] - bg_colors[0][1]) * y / height) + b = int(bg_colors[0][2] + (bg_colors[1][2] - bg_colors[0][2]) * y / height) + draw.line([(0, y), (width, y)], fill=(r, g, b)) + else: + # Solid background + draw.rectangle([(0, 0), (width, height)], fill=template_settings["background"][color_scheme]) + + # Add quote + # Try to load font, fall back to default if not available + try: + quote_font = ImageFont.truetype(str(FONT_DIR / "Roboto-Bold.ttf"), 48) + author_font = ImageFont.truetype(str(FONT_DIR / "Roboto-Regular.ttf"), 32) + except: + quote_font = ImageFont.load_default() + author_font = ImageFont.load_default() + + # Wrap text + max_width = width - 200 + words = quote.split() + lines = [] + current_line = [] + + for word in words: + test_line = " ".join(current_line + [word]) + bbox = draw.textbbox((0, 0), test_line, font=quote_font) + text_width = bbox[2] - bbox[0] + + if text_width <= max_width: + current_line.append(word) + else: + lines.append(" ".join(current_line)) + current_line = [word] + + if current_line: + lines.append(" ".join(current_line)) + + # Draw quote + quote_text = "\n".join(lines) + quote_bbox = draw.textbbox((0, 0), quote_text, font=quote_font) + quote_width = quote_bbox[2] - quote_bbox[0] + quote_height = quote_bbox[3] - quote_bbox[1] + + quote_x = (width - quote_width) // 2 + quote_y = (height - quote_height) // 2 - 50 if author else (height - quote_height) // 2 + + # Add quote marks + draw.text((quote_x - 60, quote_y - 80), """, fill=template_settings["text_color"], font=quote_font) + + # Draw quote text + draw.text((quote_x, quote_y), quote_text, fill=template_settings["text_color"], font=quote_font, align="center") + + # Draw author + if author: + author_text = f"β€” {author}" + author_bbox = draw.textbbox((0, 0), author_text, font=author_font) + author_width = author_bbox[2] - author_bbox[0] + + author_x = (width - author_width) // 2 + author_y = quote_y + quote_height + 40 + + draw.text((author_x, author_y), author_text, fill=template_settings["author_color"], font=author_font) + + return img + +def generate_tweet_card( + text: str, + username: str = "", + profile_image: str = None, + theme: str = "light" +) -> Image.Image: + """Generate a tweet card image.""" + # Define themes + themes = { + "light": { + "background": (255, 255, 255), + "text": (20, 23, 26), + "username": (83, 100, 113), + "border": (235, 238, 240) + }, + "dark": { + "background": (21, 32, 43), + "text": (255, 255, 255), + "username": (136, 153, 166), + "border": (56, 68, 77) + }, + "black": { + "background": (0, 0, 0), + "text": (217, 217, 217), + "username": (110, 118, 125), + "border": (47, 51, 54) + } + } + + # Get theme settings + theme_settings = themes.get(theme, themes["light"]) + + # Create image + width = 1200 + height = 675 + img = Image.new("RGB", (width, height), color=theme_settings["background"]) + draw = ImageDraw.Draw(img) + + # Draw card background + card_width = 800 + card_height = 400 + card_x = (width - card_width) // 2 + card_y = (height - card_height) // 2 + + # Draw card border + draw.rectangle( + [(card_x, card_y), (card_x + card_width, card_y + card_height)], + outline=theme_settings["border"], + width=2 + ) + + # Try to load font, fall back to default if not available + try: + text_font = ImageFont.truetype(str(FONT_DIR / "Roboto-Regular.ttf"), 32) + username_font = ImageFont.truetype(str(FONT_DIR / "Roboto-Bold.ttf"), 28) + except: + text_font = ImageFont.load_default() + username_font = ImageFont.load_default() + + # Add profile image + profile_size = 80 + profile_x = card_x + 40 + profile_y = card_y + 40 + + if profile_image: + try: + # Try to load profile image + response = requests.get(profile_image) + profile_img = Image.open(io.BytesIO(response.content)) + profile_img = profile_img.resize((profile_size, profile_size)) + + # Create circular mask + mask = Image.new("L", (profile_size, profile_size), 0) + mask_draw = ImageDraw.Draw(mask) + mask_draw.ellipse((0, 0, profile_size, profile_size), fill=255) + + # Apply mask + profile_img.putalpha(mask) + + # Paste profile image + img.paste(profile_img, (profile_x, profile_y), profile_img) + except: + # Draw placeholder circle + draw.ellipse( + [(profile_x, profile_y), (profile_x + profile_size, profile_y + profile_size)], + fill=theme_settings["username"] + ) + else: + # Draw placeholder circle + draw.ellipse( + [(profile_x, profile_y), (profile_x + profile_size, profile_y + profile_size)], + fill=theme_settings["username"] + ) + + # Add username + if username: + username_x = profile_x + profile_size + 20 + username_y = profile_y + 10 + + draw.text((username_x, username_y), username, fill=theme_settings["text"], font=username_font) + draw.text((username_x, username_y + 40), f"@{username}", fill=theme_settings["username"], font=text_font) + + # Wrap text + max_width = card_width - 80 + words = text.split() + lines = [] + current_line = [] + + for word in words: + test_line = " ".join(current_line + [word]) + bbox = draw.textbbox((0, 0), test_line, font=text_font) + text_width = bbox[2] - bbox[0] + + if text_width <= max_width: + current_line.append(word) + else: + lines.append(" ".join(current_line)) + current_line = [word] + + if current_line: + lines.append(" ".join(current_line)) + + # Draw tweet text + tweet_text = "\n".join(lines) + text_x = card_x + 40 + text_y = profile_y + profile_size + 40 + + draw.text((text_x, text_y), tweet_text, fill=theme_settings["text"], font=text_font) + + return img + +def generate_infographic( + title: str, + items: List[str], + theme: str = "blue" +) -> Image.Image: + """Generate a simple infographic.""" + # Define themes + themes = { + "blue": { + "background": (240, 248, 255), + "title_bg": (25, 84, 123), + "title_text": (255, 255, 255), + "item_bg": [(142, 197, 252), (173, 216, 230)], + "item_text": (25, 25, 25) + }, + "green": { + "background": (240, 255, 240), + "title_bg": (25, 123, 48), + "title_text": (255, 255, 255), + "item_bg": [(142, 252, 169), (173, 230, 188)], + "item_text": (25, 25, 25) + }, + "purple": { + "background": (248, 240, 255), + "title_bg": (84, 25, 123), + "title_text": (255, 255, 255), + "item_bg": [(197, 142, 252), (216, 173, 230)], + "item_text": (25, 25, 25) + }, + "red": { + "background": (255, 240, 240), + "title_bg": (123, 25, 25), + "title_text": (255, 255, 255), + "item_bg": [(252, 142, 142), (230, 173, 173)], + "item_text": (25, 25, 25) + } + } + + # Get theme settings + theme_settings = themes.get(theme, themes["blue"]) + + # Create image + width = 1200 + height = 675 + img = Image.new("RGB", (width, height), color=theme_settings["background"]) + draw = ImageDraw.Draw(img) + + # Try to load font, fall back to default if not available + try: + title_font = ImageFont.truetype(str(FONT_DIR / "Roboto-Bold.ttf"), 48) + item_font = ImageFont.truetype(str(FONT_DIR / "Roboto-Regular.ttf"), 32) + except: + title_font = ImageFont.load_default() + item_font = ImageFont.load_default() + + # Draw title + title_height = 100 + draw.rectangle([(0, 0), (width, title_height)], fill=theme_settings["title_bg"]) + + title_bbox = draw.textbbox((0, 0), title, font=title_font) + title_width = title_bbox[2] - title_bbox[0] + title_x = (width - title_width) // 2 + title_y = (title_height - title_font.size) // 2 + + draw.text((title_x, title_y), title, fill=theme_settings["title_text"], font=title_font) + + # Draw items + item_height = 80 + item_padding = 20 + item_y = title_height + 50 + + for i, item in enumerate(items): + # Alternate background colors + bg_color = theme_settings["item_bg"][i % 2] + + # Draw item background + draw.rectangle( + [(50, item_y), (width - 50, item_y + item_height)], + fill=bg_color, + outline=theme_settings["title_bg"], + width=2 + ) + + # Draw item number + number_size = 60 + draw.ellipse( + [(70, item_y + (item_height - number_size) // 2), + (70 + number_size, item_y + (item_height - number_size) // 2 + number_size)], + fill=theme_settings["title_bg"] + ) + + number_text = str(i + 1) + number_bbox = draw.textbbox((0, 0), number_text, font=item_font) + number_width = number_bbox[2] - number_bbox[0] + number_height = number_bbox[3] - number_bbox[1] + + number_x = 70 + (number_size - number_width) // 2 + number_y = item_y + (item_height - number_height) // 2 + + draw.text((number_x, number_y), number_text, fill=theme_settings["title_text"], font=item_font) + + # Draw item text + item_text_x = 70 + number_size + 20 + item_text_y = item_y + (item_height - item_font.size) // 2 + + draw.text((item_text_x, item_text_y), item, fill=theme_settings["item_text"], font=item_font) + + # Update y position for next item + item_y += item_height + item_padding + + return img + +def render_image_generator(): + """Render the Image Generator UI.""" + st.title("πŸ–ΌοΈ Twitter Image Generator") + st.markdown("Create engaging visual content for your tweets") + + # Create tabs for different image types + tab1, tab2, tab3 = st.tabs(["Quote Cards", "Tweet Cards", "Infographics"]) + + with tab1: + st.markdown("### Quote Cards") + st.markdown("Create shareable quote images for Twitter") + + # Quote input + quote = st.text_area( + "Quote Text", + placeholder="Enter your quote here...", + height=100 + ) + + author = st.text_input( + "Author/Source", + placeholder="e.g., Albert Einstein" + ) + + # Design options + col1, col2 = st.columns(2) + + with col1: + template = st.selectbox( + "Template", + ["gradient", "minimal", "bold"], + index=0 + ) + + with col2: + color_scheme = st.selectbox( + "Color Scheme", + ["blue", "green", "purple", "red"], + index=0 + ) + + # Generate button + if st.button("Generate Quote Card", use_container_width=True, key="generate_quote"): + if quote: + with st.spinner("Generating quote card..."): + # Generate image + img = generate_quote_image(quote, author, template, color_scheme) + + # Convert to bytes + buf = io.BytesIO() + img.save(buf, format="PNG") + byte_im = buf.getvalue() + + # Display image + st.image(byte_im, caption="Generated Quote Card", use_column_width=True) + + # Download button + st.download_button( + "Download Image", + data=byte_im, + file_name=f"quote_card_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png", + mime="image/png" + ) + else: + st.error("Please enter a quote") + + with tab2: + st.markdown("### Tweet Cards") + st.markdown("Create images that showcase your tweets") + + # Tweet input + tweet_text = st.text_area( + "Tweet Text", + placeholder="Enter your tweet here...", + height=100 + ) + + username = st.text_input( + "Twitter Username", + placeholder="e.g., elonmusk" + ) + + # Design options + theme = st.selectbox( + "Theme", + ["light", "dark", "black"], + index=0 + ) + + # Profile image + profile_image = None + if username: + try: + # Try to fetch profile image + profile_image = f"https://unavatar.io/twitter/{username}" + except: + pass + + # Generate button + if st.button("Generate Tweet Card", use_container_width=True, key="generate_tweet"): + if tweet_text: + with st.spinner("Generating tweet card..."): + # Generate image + img = generate_tweet_card(tweet_text, username, profile_image, theme) + + # Convert to bytes + buf = io.BytesIO() + img.save(buf, format="PNG") + byte_im = buf.getvalue() + + # Display image + st.image(byte_im, caption="Generated Tweet Card", use_column_width=True) + + # Download button + st.download_button( + "Download Image", + data=byte_im, + file_name=f"tweet_card_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png", + mime="image/png" + ) + else: + st.error("Please enter tweet text") + + with tab3: + st.markdown("### Infographics") + st.markdown("Create simple infographics for Twitter") + + # Infographic input + title = st.text_input( + "Infographic Title", + placeholder="e.g., 5 Tips for Better Tweets" + ) + + # Items input + items = [] + for i in range(5): + item = st.text_input( + f"Item {i+1}", + placeholder=f"Enter item {i+1}...", + key=f"item_{i}" + ) + if item: + items.append(item) + + # Design options + theme = st.selectbox( + "Color Theme", + ["blue", "green", "purple", "red"], + index=0, + key="infographic_theme" + ) + + # Generate button + if st.button("Generate Infographic", use_container_width=True, key="generate_infographic"): + if title and items: + with st.spinner("Generating infographic..."): + # Generate image + img = generate_infographic(title, items, theme) + + # Convert to bytes + buf = io.BytesIO() + img.save(buf, format="PNG") + byte_im = buf.getvalue() + + # Display image + st.image(byte_im, caption="Generated Infographic", use_column_width=True) + + # Download button + st.download_button( + "Download Image", + data=byte_im, + file_name=f"infographic_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png", + mime="image/png" + ) + else: + st.error("Please enter a title and at least one item") +``` + +### Phase 5: Analytics & Optimization + +#### 5.1 Performance Analytics + +Implement the Performance Analytics feature: + +```python +# lib/ai_writers/twitter_writers/analytics/performance_analytics.py + +import streamlit as st +import pandas as pd +import numpy as np +from typing import Dict, List, Any +import json +from datetime import datetime, timedelta +import plotly.express as px +import plotly.graph_objects as go +from plotly.subplots import make_subplots + +from ....integrations.twitter.auth import create_twitter_api, create_twitter_client, is_authenticated + +def get_tweet_performance(days: int = 30) -> pd.DataFrame: + """Get tweet performance data from Twitter API.""" + try: + if not is_authenticated(): + return pd.DataFrame() + + # Get API client + api = create_twitter_api() + + # Get user timeline + tweets = api.user_timeline(count=200, tweet_mode="extended") + + # Filter by date + cutoff_date = datetime.now() - timedelta(days=days) + tweets = [t for t in tweets if t.created_at >= cutoff_date] + + # Create DataFrame + data = [] + for tweet in tweets: + # Skip retweets + if hasattr(tweet, "retweeted_status"): + continue + + # Extract tweet data + tweet_data = { + "id": tweet.id, + "text": tweet.full_text, + "created_at": tweet.created_at, + "likes": tweet.favorite_count, + "retweets": tweet.retweet_count, + "replies": 0, # Not available in standard API + "impressions": 0, # Not available in standard API + "engagement": tweet.favorite_count + tweet.retweet_count, + "hashtags": len(tweet.entities.get("hashtags", [])), + "mentions": len(tweet.entities.get("user_mentions", [])), + "urls": len(tweet.entities.get("urls", [])), + "media": 1 if "media" in tweet.entities else 0, + "hour": tweet.created_at.hour, + "day": tweet.created_at.strftime("%A"), + "length": len(tweet.full_text) + } + + data.append(tweet_data) + + # Create DataFrame + df = pd.DataFrame(data) + + return df + except Exception as e: + st.error(f"Error fetching tweet performance: {str(e)}") + return pd.DataFrame() + +def analyze_performance(df: pd.DataFrame) -> Dict[str, Any]: + """Analyze tweet performance data.""" + if df.empty: + return { + "total_engagement": 0, + "avg_engagement": 0, + "top_tweet": None, + "worst_tweet": None, + "best_time": None, + "best_day": None, + "media_impact": 0, + "hashtag_impact": 0, + "length_impact": 0 + } + + # Calculate metrics + total_engagement = df["engagement"].sum() + avg_engagement = df["engagement"].mean() + + # Find top and worst tweets + top_tweet = df.loc[df["engagement"].idxmax()] if not df.empty else None + worst_tweet = df.loc[df["engagement"].idxmin()] if not df.empty else None + + # Find best time and day + hour_engagement = df.groupby("hour")["engagement"].mean() + best_hour = hour_engagement.idxmax() if not hour_engagement.empty else None + + day_engagement = df.groupby("day")["engagement"].mean() + best_day = day_engagement.idxmax() if not day_engagement.empty else None + + # Calculate impact factors + media_impact = df[df["media"] == 1]["engagement"].mean() / avg_engagement if avg_engagement > 0 else 1 + + hashtag_impact = 0 + if not df.empty: + hashtag_groups = df.groupby("hashtags")["engagement"].mean() + optimal_hashtags = hashtag_groups.idxmax() if not hashtag_groups.empty else 0 + hashtag_impact = hashtag_groups.max() / avg_engagement if avg_engagement > 0 else 1 + + length_impact = 0 + if not df.empty: + df["length_bin"] = pd.cut(df["length"], bins=[0, 70, 140, 210, 280], labels=["0-70", "71-140", "141-210", "211-280"]) + length_groups = df.groupby("length_bin")["engagement"].mean() + optimal_length = length_groups.idxmax() if not length_groups.empty else "71-140" + length_impact = length_groups.max() / avg_engagement if avg_engagement > 0 else 1 + + return { + "total_engagement": total_engagement, + "avg_engagement": avg_engagement, + "top_tweet": top_tweet, + "worst_tweet": worst_tweet, + "best_time": best_hour, + "best_day": best_day, + "media_impact": media_impact, + "hashtag_impact": hashtag_impact, + "length_impact": length_impact + } + +def render_performance_analytics(): + """Render the Performance Analytics UI.""" + st.title("πŸ“Š Twitter Performance Analytics") + st.markdown("Track tweet performance and engagement metrics") + + # Check if connected to Twitter + twitter_connected = is_authenticated() + + if twitter_connected: + user = st.session_state.twitter_user + st.success(f"Connected as @{user['screen_name']}") + + # Date range selector + col1, col2 = st.columns(2) + + with col1: + days = st.slider( + "Time Period", + min_value=7, + max_value=90, + value=30, + step=1 + ) + + with col2: + refresh = st.button("Refresh Data", use_container_width=True) + + # Get performance data + with st.spinner("Fetching tweet performance data..."): + df = get_tweet_performance(days) + + if not df.empty: + # Store in session state + st.session_state.tweet_performance = df + + # Analyze performance + analysis = analyze_performance(df) + + # Display overview metrics + st.markdown("### Performance Overview") + + col1, col2, col3, col4 = st.columns(4) + + with col1: + st.metric("Total Tweets", len(df)) + + with col2: + st.metric("Total Engagement", f"{analysis['total_engagement']:,}") + + with col3: + st.metric("Avg. Engagement", f"{analysis['avg_engagement']:.1f}") + + with col4: + st.metric("Media Impact", f"{analysis['media_impact']:.2f}x") + + # Engagement over time + st.markdown("### Engagement Over Time") + + # Sort by date + df_sorted = df.sort_values("created_at") + + # Create time series chart + fig = px.line( + df_sorted, + x="created_at", + y="engagement", + title="Tweet Engagement History", + labels={"created_at": "Date", "engagement": "Engagement"} + ) + + # Add 7-day moving average + df_sorted["ma7"] = df_sorted["engagement"].rolling(7).mean() + + fig.add_trace( + go.Scatter( + x=df_sorted["created_at"], + y=df_sorted["ma7"], + mode="lines", + name="7-day Moving Average", + line=dict(color="red", width=2) + ) + ) + + st.plotly_chart(fig, use_container_width=True) + + # Engagement by day and hour + st.markdown("### Engagement by Day and Hour") + + # Create heatmap + pivot = df.pivot_table( + index="day", + columns="hour", + values="engagement", + aggfunc="mean" + ) + + # Reorder days + days_order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + pivot = pivot.reindex(days_order) + + fig = px.imshow( + pivot, + labels=dict(x="Hour of Day", y="Day of Week", color="Avg. Engagement"), + x=list(range(24)), + y=days_order, + color_continuous_scale="Viridis", + title="Engagement Heatmap by Day and Hour" + ) + + st.plotly_chart(fig, use_container_width=True) + + # Content analysis + st.markdown("### Content Analysis") + + col1, col2 = st.columns(2) + + with col1: + # Engagement by tweet length + df["length_bin"] = pd.cut(df["length"], bins=[0, 70, 140, 210, 280], labels=["0-70", "71-140", "141-210", "211-280"]) + length_engagement = df.groupby("length_bin")["engagement"].mean().reset_index() + + fig = px.bar( + length_engagement, + x="length_bin", + y="engagement", + title="Engagement by Tweet Length", + labels={"length_bin": "Character Count", "engagement": "Avg. Engagement"} + ) + st.plotly_chart(fig, use_container_width=True) + + with col2: + # Engagement by hashtag count + hashtag_engagement = df.groupby("hashtags")["engagement"].mean().reset_index() + + fig = px.bar( + hashtag_engagement, + x="hashtags", + y="engagement", + title="Engagement by Hashtag Count", + labels={"hashtags": "Number of Hashtags", "engagement": "Avg. Engagement"} + ) + st.plotly_chart(fig, use_container_width=True) + + # Media impact + st.markdown("### Media Impact") + + media_engagement = df.groupby("media")["engagement"].mean().reset_index() + media_engagement["media"] = media_engagement["media"].map({0: "No Media", 1: "With Media"}) + + fig = px.bar( + media_engagement, + x="media", + y="engagement", + title="Engagement by Media Inclusion", + labels={"media": "Media", "engagement": "Avg. Engagement"}, + color="media", + color_discrete_map={"No Media": "#1DA1F2", "With Media": "#17BF63"} + ) + st.plotly_chart(fig, use_container_width=True) + + # Top tweets + st.markdown("### Top Performing Tweets") + + # Sort by engagement + top_tweets = df.sort_values("engagement", ascending=False).head(5) + + for i, (_, tweet) in enumerate(top_tweets.iterrows()): + st.markdown(f""" +
+

{tweet['text']}

+
+ {tweet['created_at'].strftime('%Y-%m-%d %H:%M')} + ❀️ {tweet['likes']} | πŸ”„ {tweet['retweets']} | πŸ’¬ {tweet['replies']} +
+
+ """, unsafe_allow_html=True) + + # Recommendations + st.markdown("### Performance Recommendations") + + recommendations = [] + + # Best time to post + if analysis["best_time"] is not None and analysis["best_day"] is not None: + recommendations.append(f"Post on {analysis['best_day']} around {analysis['best_time']}:00 for optimal engagement") + + # Media recommendation + if analysis["media_impact"] > 1.2: + recommendations.append(f"Including media increases engagement by {(analysis['media_impact']-1)*100:.1f}%") + + # Hashtag recommendation + hashtag_groups = df.groupby("hashtags")["engagement"].mean() + if not hashtag_groups.empty: + optimal_hashtags = hashtag_groups.idxmax() + recommendations.append(f"Using {optimal_hashtags} hashtags yields the best engagement") + + # Length recommendation + length_groups = df.groupby("length_bin")["engagement"].mean() + if not length_groups.empty: + optimal_length = length_groups.idxmax() + recommendations.append(f"Tweets with {optimal_length} characters perform best") + + # Display recommendations + for rec in recommendations: + st.info(rec) + + # Export options + st.markdown("### Export Data") + + col1, col2 = st.columns(2) + + with col1: + # Export as CSV + csv = df.to_csv(index=False) + st.download_button( + "Download CSV", + data=csv, + file_name=f"tweet_performance_{datetime.now().strftime('%Y%m%d')}.csv", + mime="text/csv" + ) + + with col2: + # Export as JSON + json_str = df.to_json(orient="records", date_format="iso") + st.download_button( + "Download JSON", + data=json_str, + file_name=f"tweet_performance_{datetime.now().strftime('%Y%m%d')}.json", + mime="application/json" + ) + else: + st.warning("No tweets found in the selected time period") + else: + st.warning("Connect your Twitter account to access performance analytics") + + # Show sample data + st.markdown("### Sample Performance Data") + + # Create sample data + dates = pd.date_range(start=datetime.now() - timedelta(days=30), periods=30) + engagements = np.random.normal(loc=20, scale=10, size=30) + engagements = np.abs(engagements) # Make all values positive + + df = pd.DataFrame({ + "date": dates, + "engagement": engagements + }) + + # Sample metrics + col1, col2, col3, col4 = st.columns(4) + + with col1: + st.metric("Total Tweets", "42") + + with col2: + st.metric("Total Engagement", "1,254") + + with col3: + st.metric("Avg. Engagement", "29.8") + + with col4: + st.metric("Media Impact", "1.45x") + + # Sample chart + fig = px.line( + df, + x="date", + y="engagement", + title="Sample Tweet Engagement History" + ) + st.plotly_chart(fig, use_container_width=True) +``` + +### Phase 6: Integration and Dashboard Updates + +#### 6.1 Update Twitter Dashboard + +Update the Twitter dashboard to include the new features: + +```python +# lib/ai_writers/twitter_writers/twitter_dashboard.py + +import streamlit as st +import streamlit.components.v1 as components +from typing import Dict, List +import json +from datetime import datetime + +from .tweet_generator.enhanced_tweet_generator import enhanced_tweet_generator +from .tweet_performance.performance_predictor import render_performance_predictor +from .content_strategy.calendar_generator import render_calendar_generator +from .content_strategy.hashtag_manager import render_hashtag_manager +from .visual_content.image_generator import render_image_generator +from .analytics.performance_analytics import render_performance_analytics +from ...integrations.twitter.auth import is_authenticated +from ...integrations.twitter.account_manager import render_twitter_account_manager +from ...integrations.twitter.api_key_manager import render_twitter_api_key_manager +from ...integrations.twitter.callback_handler import handle_oauth_callback + +def load_feature_data() -> Dict: + """Load feature data from a structured format.""" + return { + "tweet_generation": { + "title": "Tweet Generation & Optimization", + "icon": "🐦", + "description": "Create and optimize engaging tweets with AI assistance", + "features": [ + { + "name": "Smart Tweet Generator", + "description": "Generate multiple tweet variations with optimal character count, hashtags, and emojis", + "status": "active", + "icon": "✨", + "function": enhanced_tweet_generator + }, + { + "name": "Tweet Performance Predictor", + "description": "Predict engagement rates and best posting times for maximum impact", + "status": "active", + "icon": "πŸ“Š", + "function": render_performance_predictor + } + ] + }, + "content_strategy": { + "title": "Content Strategy Tools", + "icon": "πŸ“…", + "description": "Plan and manage your Twitter content strategy effectively", + "features": [ + { + "name": "Content Calendar Generator", + "description": "Create weekly/monthly content plans with theme-based scheduling", + "status": "active", + "icon": "πŸ—“οΈ", + "function": render_calendar_generator + }, + { + "name": "Hashtag Strategy Manager", + "description": "Research and manage trending hashtags for better reach", + "status": "active", + "icon": "#️⃣", + "function": render_hashtag_manager + } + ] + }, + "visual_content": { + "title": "Visual Content Creation", + "icon": "🎨", + "description": "Create engaging visual content for your tweets", + "features": [ + { + "name": "Image Generator", + "description": "Create tweet cards, infographics, and quote designs", + "status": "active", + "icon": "πŸ–ΌοΈ", + "function": render_image_generator + }, + { + "name": "Video Content Assistant", + "description": "Generate video scripts and optimize captions", + "status": "coming_soon", + "icon": "πŸŽ₯" + } + ] + }, + "engagement": { + "title": "Engagement & Community", + "icon": "🀝", + "description": "Manage and enhance community engagement", + "features": [ + { + "name": "Reply Generator", + "description": "Generate context-aware responses with appropriate tone", + "status": "coming_soon", + "icon": "πŸ’¬" + }, + { + "name": "Community Tools", + "description": "Create polls and plan Q&A sessions", + "status": "coming_soon", + "icon": "πŸ‘₯" + } + ] + }, + "analytics": { + "title": "Analytics & Optimization", + "icon": "πŸ“ˆ", + "description": "Track performance and optimize your Twitter strategy", + "features": [ + { + "name": "Performance Analytics", + "description": "Track tweet performance and engagement metrics", + "status": "active", + "icon": "πŸ“Š", + "function": render_performance_analytics + }, + { + "name": "A/B Testing Assistant", + "description": "Test and optimize tweet variations for better results", + "status": "coming_soon", + "icon": "πŸ”" + } + ] + }, + "research": { + "title": "Research & Intelligence", + "icon": "πŸ”Ž", + "description": "Gain insights and stay ahead of trends", + "features": [ + { + "name": "Market Research", + "description": "Analyze competitors and track industry trends", + "status": "coming_soon", + "icon": "πŸ“Š" + }, + { + "name": "Content Inspiration", + "description": "Get trending topic suggestions and content ideas", + "status": "coming_soon", + "icon": "πŸ’‘" + } + ] + } + } + +def render_feature_card(feature: Dict) -> None: + """Render a single feature card with its details.""" + with st.container(): + st.markdown(f""" +
+

{feature['icon']} {feature['name']}

+

{feature['description']}

+ + {feature['status'].title()} + +
+ """, unsafe_allow_html=True) + +def render_category_section(category: Dict) -> None: + """Render a category section with all its features.""" + st.markdown(f"### {category['icon']} {category['title']}") + st.markdown(f"*{category['description']}*") + + col1, col2 = st.columns(2) + with col1: + render_feature_card(category['features'][0]) + if category['features'][0]['status'] == 'active': + if st.button(f"Launch {category['features'][0]['name']}", key=f"launch_{category['features'][0]['name']}"): + st.session_state.current_feature = category['features'][0]['function'] + st.experimental_rerun() + with col2: + render_feature_card(category['features'][1]) + if category['features'][1]['status'] == 'active': + if st.button(f"Launch {category['features'][1]['name']}", key=f"launch_{category['features'][1]['name']}"): + st.session_state.current_feature = category['features'][1]['function'] + st.experimental_rerun() + +def run_dashboard(): + """Main function to run the Twitter dashboard.""" + # Handle OAuth callback if present + handle_oauth_callback() + + # Check if a feature is currently active + if "current_feature" in st.session_state and callable(st.session_state.current_feature): + # Add back button + if st.button("← Back to Dashboard"): + del st.session_state.current_feature + st.experimental_rerun() + + # Run the current feature + st.session_state.current_feature() + return + + # Header + st.title("🐦 Twitter AI Writer Dashboard") + st.markdown(""" + Welcome to your all-in-one Twitter content creation and management platform. + Explore our AI-powered tools to enhance your Twitter marketing strategy. + """) + + # Twitter account status + twitter_connected = is_authenticated() + + if twitter_connected: + user = st.session_state.twitter_user + st.success(f"Connected as @{user['screen_name']}") + else: + st.warning("Connect your Twitter account to access all features") + + # Show account connection UI + with st.expander("Twitter Account Setup", expanded=True): + # First show API key manager + render_twitter_api_key_manager() + + # Then show account manager + render_twitter_account_manager() + + # Create tabs for different sections + tab1, tab2, tab3 = st.tabs(["🎯 Quick Actions", "πŸ“Š Analytics", "βš™οΈ Settings"]) + + with tab1: + st.markdown("### πŸš€ Quick Actions") + col1, col2, col3 = st.columns(3) + + with col1: + if st.button("πŸ“ Create New Tweet", use_container_width=True): + # Set the current feature to the enhanced tweet generator + st.session_state.current_feature = enhanced_tweet_generator + st.experimental_rerun() + with col2: + if st.button("πŸ“… Plan Content", use_container_width=True): + # Set the current feature to the calendar generator + st.session_state.current_feature = render_calendar_generator + st.experimental_rerun() + with col3: + if st.button("πŸ“Š View Analytics", use_container_width=True): + # Set the current feature to performance analytics + st.session_state.current_feature = render_performance_analytics + st.experimental_rerun() + + with tab2: + st.markdown("### πŸ“ˆ Analytics Dashboard") + + if twitter_connected: + # Show mini analytics dashboard + try: + from .analytics.performance_analytics import get_tweet_performance, analyze_performance + + # Get performance data + with st.spinner("Loading analytics..."): + df = get_tweet_performance(days=30) + + if not df.empty: + # Analyze performance + analysis = analyze_performance(df) + + # Display overview metrics + col1, col2, col3, col4 = st.columns(4) + + with col1: + st.metric("Total Tweets", len(df)) + + with col2: + st.metric("Total Engagement", f"{analysis['total_engagement']:,}") + + with col3: + st.metric("Avg. Engagement", f"{analysis['avg_engagement']:.1f}") + + with col4: + st.metric("Media Impact", f"{analysis['media_impact']:.2f}x") + + # Show engagement chart + import plotly.express as px + + # Sort by date + df_sorted = df.sort_values("created_at") + + # Create time series chart + fig = px.line( + df_sorted, + x="created_at", + y="engagement", + title="Recent Tweet Engagement", + labels={"created_at": "Date", "engagement": "Engagement"} + ) + + st.plotly_chart(fig, use_container_width=True) + + # View full analytics button + if st.button("View Full Analytics", use_container_width=True): + st.session_state.current_feature = render_performance_analytics + st.experimental_rerun() + else: + st.info("No recent tweets found. Start tweeting to see analytics!") + except Exception as e: + st.error(f"Error loading analytics: {str(e)}") + st.info("View full analytics for detailed insights") + else: + st.info("Connect your Twitter account to view analytics") + + with tab3: + st.markdown("### βš™οΈ Settings") + + # Twitter account settings + with st.expander("Twitter Account", expanded=True): + render_twitter_account_manager() + + # API key settings + with st.expander("API Configuration", expanded=False): + render_twitter_api_key_manager() + + # Preferences + with st.expander("Preferences", expanded=False): + st.markdown("#### Dashboard Preferences") + + # Theme preference + theme = st.selectbox( + "Theme", + ["Light", "Dark", "System"], + index=2 + ) + + # Default content type + content_type = st.selectbox( + "Default Content Type", + ["Informative", "Promotional", "Engaging", "Educational"], + index=0 + ) + + # Save preferences + if st.button("Save Preferences"): + st.session_state.twitter_preferences = { + "theme": theme, + "content_type": content_type + } + st.success("Preferences saved!") + + # Load feature data + features = load_feature_data() + + # Main content area + st.markdown("## πŸ› οΈ Available Tools") + + # Render each category + for category in features.values(): + render_category_section(category) + + # Footer + st.markdown("---") + st.markdown(""" +
+

Need help? Check out our documentation or contact support

+
+ """, unsafe_allow_html=True) + +if __name__ == "__main__": + run_dashboard() +``` + +## Testing Plan + +### Unit Testing + +1. **Authentication Tests** + - Test Twitter API key validation + - Test OAuth flow + - Test token storage and retrieval + +2. **API Integration Tests** + - Test tweet posting + - Test timeline retrieval + - Test hashtag search + - Test media upload + +3. **Feature Tests** + - Test tweet generation + - Test performance prediction + - Test content calendar creation + - Test image generation + +### Integration Testing + +1. **End-to-End Flow Tests** + - Test complete user journey from authentication to posting + - Test data flow between components + - Test error handling and recovery + +2. **Cross-Feature Tests** + - Test integration between tweet generator and performance predictor + - Test integration between content calendar and tweet scheduler + - Test integration between analytics and content recommendations + +### User Acceptance Testing + +1. **Usability Tests** + - Test with real users to gather feedback + - Evaluate UI/UX design + - Measure time to complete common tasks + +2. **Performance Tests** + - Test with large datasets + - Measure response times + - Identify bottlenecks + +## Implementation Timeline + +### Phase 1: Twitter Authentication & Basic Integration (2 weeks) +- Week 1: Set up Twitter API authentication system +- Week 2: Create account connection UI and API key management + +### Phase 2: Enhanced Tweet Generator (2 weeks) +- Week 3: Implement real Twitter data integration +- Week 4: Develop tweet performance predictor + +### Phase 3: Content Strategy Tools (3 weeks) +- Week 5: Implement content calendar generator +- Week 6-7: Develop hashtag strategy manager + +### Phase 4: Visual Content Creation (2 weeks) +- Week 8-9: Implement image generator for quotes, tweets, and infographics + +### Phase 5: Analytics & Optimization (2 weeks) +- Week 10-11: Implement performance analytics dashboard + +### Phase 6: Integration and Dashboard Updates (1 week) +- Week 12: Update Twitter dashboard and integrate all features + +## Conclusion + +This implementation plan provides a comprehensive approach to enhancing the Twitter features in AI-Writer. By leveraging the Tweepy library and user-provided API keys, we can transform the "coming soon" features into fully functional components that provide real value to users. + +The phased approach allows for incremental delivery of features, with each phase building on the previous one. This ensures that users can start benefiting from the enhancements early, while more advanced features are developed. + +Key benefits of this implementation: + +1. **Real Twitter Integration**: Users can connect their Twitter accounts and interact directly with the platform. +2. **Data-Driven Insights**: Performance analytics and predictions based on real Twitter data. +3. **Comprehensive Content Strategy**: Tools for planning, creating, and optimizing Twitter content. +4. **Visual Content Creation**: Easy-to-use tools for creating engaging visual content. +5. **Streamlined Workflow**: Integrated dashboard for managing all Twitter activities. + +By following this plan, AI-Writer will stand out against competitors by offering a complete Twitter content creation and management solution that leverages real data and AI to optimize performance. \ No newline at end of file diff --git a/lib/workspace/Dockerfile b/lib/workspace/Dockerfile deleted file mode 100644 index 1bcbd6f7..00000000 --- a/lib/workspace/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3.12 - -WORKDIR /app - -COPY requirements.txt . - -RUN pip install -r requirements.txt - -COPY . . - -EXPOSE 8501 - -CMD ["streamlit", "run", "alwrity.py"] diff --git a/lib/workspace/docker-compose.yml b/lib/workspace/docker-compose.yml deleted file mode 100644 index c2baee11..00000000 --- a/lib/workspace/docker-compose.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: '3.8' - -services: - app: - image: python:3.12 - working_dir: /app - volumes: - - .:/app - command: bash -c "pip install -r requirements.txt && streamlit run alwrity.py" - ports: - - "8501:8501"