diff --git a/Getting Started/README_install_bat.md b/Getting Started/Option_1_Windows_Quick_Install/README.md similarity index 100% rename from Getting Started/README_install_bat.md rename to Getting Started/Option_1_Windows_Quick_Install/README.md diff --git a/Getting Started/Option_1_Windows_Quick_Install/install_alwrity_windows.bat b/Getting Started/Option_1_Windows_Quick_Install/install_alwrity_windows.bat new file mode 100644 index 00000000..b0bde02e --- /dev/null +++ b/Getting Started/Option_1_Windows_Quick_Install/install_alwrity_windows.bat @@ -0,0 +1,122 @@ +@echo off +setlocal enabledelayedexpansion + +s :: Set colors for better visibility +color 0A + +:: Set the Python version requirement +set MIN_PYTHON_VERSION=3.9 + +echo =============================================== +echo ALwrity Installation Setup +echo =============================================== +echo. + +echo [1/5] Checking Python installation... +python --version > nul 2>&1 +if errorlevel 1 ( + color 0C + echo [ERROR] Python is not installed! + echo Please install Python %MIN_PYTHON_VERSION% or higher from python.org + echo Press any key to exit... + pause > nul + exit /b 1 +) + +:: Get Python version +for /f "tokens=2" %%V in ('python --version 2^>^&1') do set PYTHON_VERSION=%%V +for /f "tokens=1,2 delims=." %%a in ("%PYTHON_VERSION%") do ( + set PYTHON_MAJOR=%%a + set PYTHON_MINOR=%%b +) + +:: Check Python version +set /a PYTHON_VER=%PYTHON_MAJOR%*100 + %PYTHON_MINOR% +set /a MIN_VER=309 +if %PYTHON_VER% LSS %MIN_VER% ( + color 0C + echo [ERROR] Python version %MIN_PYTHON_VERSION% or higher is required! + echo Current version: %PYTHON_VERSION% + echo Please upgrade Python from python.org + echo Press any key to exit... + pause > nul + exit /b 1 +) + +echo [✓] Python %PYTHON_VERSION% detected +echo. + +echo [2/5] Creating virtual environment... +python -m venv "%~dp0..\..\venv" +if errorlevel 1 ( + color 0C + echo [ERROR] Failed to create virtual environment! + echo Press any key to exit... + pause > nul + exit /b 1 +) +echo [✓] Virtual environment created +echo. + +echo [3/5] Activating virtual environment... +call "%~dp0..\..\venv\Scripts\activate.bat" +if errorlevel 1 ( + color 0C + echo [ERROR] Failed to activate virtual environment! + echo Press any key to exit... + pause > nul + exit /b 1 +) +echo [✓] Virtual environment activated +echo. + +echo [4/5] Upgrading pip... +python -m pip install --upgrade pip +if errorlevel 1 ( + color 0C + echo [ERROR] Failed to upgrade pip! + echo Press any key to exit... + pause > nul + exit /b 1 +) +echo [✓] Pip upgraded +echo. + +echo [5/5] Installing requirements... +pip install -r "%~dp0..\..\requirements.txt" +if errorlevel 1 ( + color 0C + echo [ERROR] Failed to install requirements! + echo Press any key to exit... + pause > nul + exit /b 1 +) +echo [✓] Requirements installed +echo. + +color 0A +echo =============================================== +echo Installation Completed Successfully! +echo =============================================== +echo. +echo Next steps to run ALwrity: +echo. +echo 1. Open a new Command Prompt window +echo 2. Navigate to the ALwrity root directory by copying and pasting this command: +echo cd /d "%~dp0..\.." +echo. +echo 3. Activate the virtual environment by copying and pasting this command: +echo "%~dp0..\..\venv\Scripts\activate.bat" +echo. +echo 4. Run ALwrity with Streamlit by copying and pasting this command: +echo streamlit run "%~dp0..\..\alwrity.py" +echo. +echo Note: You'll need to activate the virtual environment (step 3) +echo each time you want to run ALwrity. +echo. +echo Troubleshooting: +echo - If you see any errors, make sure Python is in your PATH +echo - For help, visit: https://github.com/yourusername/ALwrity +echo. +echo Press any key to exit... +pause > nul \ No newline at end of file diff --git a/Getting Started/Option_2_Python_Users/README.md b/Getting Started/Option_2_Python_Users/README.md new file mode 100644 index 00000000..5a7b74ef --- /dev/null +++ b/Getting Started/Option_2_Python_Users/README.md @@ -0,0 +1,27 @@ +# ALwrity Installation Guide + +## Quick Start + +1. **Install Python** + - Download and install Python from [python.org](https://www.python.org/downloads/) + - During installation, check "Add Python to PATH" + +2. **Install ALwrity** + - Download this project + - Open the 'Getting Started' folder + - Double-click `setup.py` + - Follow the on-screen instructions + +## Running ALwrity + +1. Open Command Prompt/Terminal in the 'Getting Started' folder +2. Run: `venv\Scripts\activate` (Windows) or `source venv/bin/activate` (Mac/Linux) +3. Run: `streamlit run alwrity.py` + +## Need Help? + +- If you see "pip not found": Re-install Python and check "Add Python to PATH" +- For other issues: [Open a support ticket](https://github.com/AJaySi/AI-Writer/issues) +- Join our support community + +--- diff --git a/Getting Started/setup.py b/Getting Started/Option_2_Python_Users/setup.py similarity index 100% rename from Getting Started/setup.py rename to Getting Started/Option_2_Python_Users/setup.py diff --git a/Getting Started/Dockerfile b/Getting Started/Option_3_Docker_Install/Dockerfile similarity index 100% rename from Getting Started/Dockerfile rename to Getting Started/Option_3_Docker_Install/Dockerfile diff --git a/Getting Started/README_dockerfile.md b/Getting Started/Option_3_Docker_Install/README.md similarity index 100% rename from Getting Started/README_dockerfile.md rename to Getting Started/Option_3_Docker_Install/README.md diff --git a/Getting Started/Option_4_Mac_Users/Dockerfile b/Getting Started/Option_4_Mac_Users/Dockerfile new file mode 100644 index 00000000..e4f39152 --- /dev/null +++ b/Getting Started/Option_4_Mac_Users/Dockerfile @@ -0,0 +1,26 @@ +# Use Python 3.8 slim image optimized for M1/M2 Macs +FROM --platform=linux/arm64 python:3.8-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + software-properties-common \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Clone the repository +RUN git clone https://github.com/AJaySi/AI-Writer.git . + +# Install Python dependencies +RUN python -m pip install --upgrade pip +RUN pip install -r requirements.txt + +# Expose Streamlit port +EXPOSE 8501 + +# Run the application +CMD ["streamlit", "run", "alwrity.py"] \ No newline at end of file diff --git a/Getting Started/Option_4_Mac_Users/README.md b/Getting Started/Option_4_Mac_Users/README.md new file mode 100644 index 00000000..189c4798 --- /dev/null +++ b/Getting Started/Option_4_Mac_Users/README.md @@ -0,0 +1,23 @@ +# ALwrity Installation for Mac Users + +## Prerequisites +- macOS 10.15 or later +- Terminal access +- Internet connection + +## Installation Methods + +### Method 1: Easy Setup (Recommended) +1. Open Terminal +2. Navigate to this directory +3. Run: `python setup.py` +4. Follow the on-screen instructions + +### Method 2: Docker Installation +1. Install Docker Desktop for Mac + - Visit [Docker Desktop](https://www.docker.com/products/docker-desktop) + - Download and install the Apple Silicon (M1/M2) or Intel version +2. Build and run: + ```bash + docker build -t alwrity . + docker run -p 8501:8501 alwrity \ No newline at end of file diff --git a/Getting Started/Option_4_Mac_Users/setup.py b/Getting Started/Option_4_Mac_Users/setup.py new file mode 100644 index 00000000..8bc27105 --- /dev/null +++ b/Getting Started/Option_4_Mac_Users/setup.py @@ -0,0 +1,78 @@ +import sys +import os +import subprocess +import shutil +from pathlib import Path + +def print_step(text): + print(f"\n→ {text}") + +def print_error(text): + print(f"\nError: {text}", file=sys.stderr) + +def check_homebrew(): + try: + subprocess.run(['brew', '--version'], capture_output=True, check=True) + return True + except: + return False + +def setup_homebrew(): + print_step("Homebrew is required for some dependencies") + print("Please install Homebrew by running this command in Terminal:") + print('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"') + print("\nAfter installing Homebrew, run this setup script again.") + sys.exit(1) + +def create_virtual_environment(venv_path): + try: + if venv_path.exists(): + shutil.rmtree(venv_path) + subprocess.run([sys.executable, '-m', 'venv', str(venv_path)], check=True) + return True + except Exception as e: + print_error(f"Failed to create virtual environment: {e}") + return False + +def install_requirements(venv_python, requirements_path): + try: + subprocess.run([str(venv_python), '-m', 'pip', 'install', '--upgrade', 'pip'], check=True) + subprocess.run([str(venv_python), '-m', 'pip', 'install', '-r', str(requirements_path)], check=True) + return True + except Exception as e: + print_error(f"Failed to install requirements: {e}") + return False + +def main(): + print("\n=== ALwrity Mac Installation ===\n") + + if not check_homebrew(): + setup_homebrew() + + current_dir = Path(__file__).parent + project_root = current_dir.parent.parent + requirements_path = project_root / 'requirements.txt' + venv_path = current_dir / 'venv' + + print_step("Creating virtual environment") + if not create_virtual_environment(venv_path): + return + + print_step("Installing dependencies") + venv_python = venv_path / 'bin' / 'python' + if not install_requirements(venv_python, requirements_path): + return + + print("\n✓ Installation completed successfully!") + print("\nTo start ALwrity:") + print("1. Open Terminal in this directory") + print("2. Run: source venv/bin/activate") + print("3. Run: streamlit run alwrity.py") + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + print("\nInstallation cancelled") + except Exception as e: + print_error(f"Unexpected error: {e}") \ No newline at end of file diff --git a/Getting Started/README.md b/Getting Started/README.md index 7cee0d5e..3b40d42d 100644 --- a/Getting Started/README.md +++ b/Getting Started/README.md @@ -1,52 +1,25 @@ -# ALwrity Installation Guide: Start Here! +# Getting Started with ALwrity -Welcome to ALwrity! This guide will help you choose the best installation method for your needs, whether you're a non-technical user or a developer. Please read the options below and follow the recommended path for your system. +Welcome to ALwrity! Choose the installation method that best suits you: ---- +## Option 1: Quick Install for Windows Users (Recommended for Content Creators) +- No technical knowledge required +- Automatic Python installation +- One-click setup +→ [Go to Windows Quick Install](./Option_1_Windows_Quick_Install) -## Which Installation Method Should I Use? +## Option 2: Setup for Python Users +- For users who already have Python installed +- More customization options +- Manual virtual environment setup +→ [Go to Python Setup](./Option_2_Python_Users) -### 1. **Docker (Recommended for Most Users, All Platforms)** -- **Best for:** Anyone who wants a hassle-free, one-command setup on Windows, Mac, or Linux. -- **Why choose Docker?** - - No need to install Python, Rust, or system libraries manually. - - Everything runs in a safe, isolated environment. - - Consistent experience across all operating systems. -- **How to use:** - - See [README_dockerfile.md](./README_dockerfile.md) for step-by-step instructions. +## Option 3: Docker Installation +- For advanced users and developers +- Containerized environment +- Platform-independent setup +→ [Go to Docker Setup](./Option_3_Docker_Install) -### 2. **Windows One-Click Installer (`install_alwrity.bat`)** -- **Best for:** Windows users who prefer a simple double-click installer. -- **Why choose this?** - - Checks and installs all prerequisites for you (Python, Rust, Visual C++ Build Tools). - - Minimal technical knowledge required. -- **How to use:** - - See [README_install_bat.md](./README_install_bat.md) for detailed instructions. - -### 3. **Manual Setup for Linux/macOS (`setup.py`)** -- **Best for:** Linux/macOS users who are comfortable with the terminal. -- **Why choose this?** - - Gives you more control over the environment. - - Useful if you want to customize or develop ALwrity. -- **How to use:** - - See [README_setup_py.md](./README_setup_py.md) for a full walkthrough. - ---- - -## Quick Decision Table -| Your System | Easiest Method | File/Guide to Use | -|---------------------|-----------------------|--------------------------| -| Windows (any) | Docker or install_alwrity.bat | README_dockerfile.md or README_install_bat.md | -| Mac | Docker | README_dockerfile.md | -| Linux | Docker | README_dockerfile.md | -| Linux/macOS (dev) | setup.py (manual) | README_setup_py.md | - ---- - -## Still Unsure? -- If you are not sure, **Docker is the safest and easiest choice** for most users. -- If you run into any issues, check the troubleshooting sections in each guide or [open an issue on GitHub](https://github.com/AJaySi/AI-Writer/issues). - ---- - -Happy writing! +## Need Help? +- Visit our [GitHub Issues](https://github.com/AJaySi/AI-Writer/issues) page +- Check our [Documentation](../docs) diff --git a/Getting Started/README_setup_py.md b/Getting Started/README_setup_py.md deleted file mode 100644 index 17d1067c..00000000 --- a/Getting Started/README_setup_py.md +++ /dev/null @@ -1,92 +0,0 @@ -# ALwrity Linux/macOS Installer Guide (`setup.py`) - ---- - -## What is `setup.py`? -`setup.py` is an automated installer for ALwrity on Linux and macOS. It checks your system, sets up a virtual environment, installs all dependencies, and configures ALwrity for you. - ---- - -## What Does It Do? -- Checks your Python version (must be 3.11.x) -- Checks for Rust compiler (required for some Python packages) -- Creates a Python virtual environment (`venv`) if it doesn't exist -- Activates the virtual environment (auto-activation for Linux/macOS) -- Installs all required Python packages from `requirements.txt` -- Installs ALwrity as a command-line tool -- Prints clear next steps for running ALwrity -- Logs any errors to `install_errors.log` for easy troubleshooting - ---- - -## Prerequisites -- **Linux or macOS** -- **Python 3.11.x** (install from https://www.python.org/downloads/ if needed) -- **Rust compiler** (install with `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y`) -- **At least 4GB RAM** -- **2GB free disk space** - ---- - -## Step-by-Step Instructions - -### 1. Open a Terminal -- Navigate to the ALwrity project folder: - ``` - cd /path/to/AI-Writer/Getting\ Started - ``` - -### 2. Run the Installer -- Run: - ``` - python3 setup.py install - ``` -- The script will check your system and install everything needed. -- If you see errors about Python or Rust, follow the on-screen instructions to install them, then re-run the script. - -### 3. Start ALwrity -- Activate the virtual environment: - ``` - source venv/bin/activate - ``` -- Start the app: - ``` - streamlit run alwrity.py - ``` -- Or use the command: - ``` - alwrity - ``` - ---- - -## Troubleshooting -- **Python version error:** - - Make sure you have Python 3.11.x installed and are using `python3`. -- **Rust not found:** - - Install Rust with: - ``` - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - source $HOME/.cargo/env - ``` -- **Other errors:** - - Check the `install_errors.log` file in the folder for details. - - Copy the error and [open an issue on GitHub](https://github.com/AJaySi/AI-Writer/issues). - ---- - -## FAQ -- **Do I need to install anything else?** - - No, `setup.py` will handle everything for you if prerequisites are met. -- **Can I run this on Windows?** - - Use the Windows installer (`install_alwrity.bat`) instead. -- **Is it safe?** - - Yes, the script only installs ALwrity and its dependencies. - ---- - -## Need More Help? -- [Open an issue on GitHub](https://github.com/AJaySi/AI-Writer/issues) -- Join our support community - ---- diff --git a/Getting Started/install_alwrity.bat b/Getting Started/install_alwrity.bat deleted file mode 100644 index 007e1d1d..00000000 --- a/Getting Started/install_alwrity.bat +++ /dev/null @@ -1,91 +0,0 @@ -@echo off -:: ALwrity Automated Windows Installer -:: This script will set up ALwrity with minimal user input. -:: Last updated: April 23, 2025 - -chcp 65001 >nul -setlocal enabledelayedexpansion - -:: Welcome message -cls -echo ====================================== -echo ALwrity - One Click Windows Installer -echo ====================================== -echo. - -:: Check for admin rights -openfiles >nul 2>&1 -if %errorlevel% NEQ 0 ( - echo This installer needs to be run as administrator. - echo Please right-click and select "Run as administrator". - pause - exit /b 1 -) - -:: Check if Python 3.11 is installed -python --version 2>nul | findstr /i "3.11" >nul -if errorlevel 1 ( - echo Python 3.11 is not installed or not in PATH. - echo Downloading Python 3.11 installer... - powershell -Command "Invoke-WebRequest -Uri https://www.python.org/ftp/python/3.11.6/python-3.11.6-amd64.exe -OutFile python-3.11.6-amd64.exe" - echo Launching Python installer. Please check 'Add Python to PATH' and complete installation. - start python-3.11.6-amd64.exe - echo After installation, please re-run this installer. - pause - exit /b 1 -) - -:: Check for Visual C++ Build Tools -where cl >nul 2>&1 -if errorlevel 1 ( - echo Visual C++ Build Tools not found. Installing... - powershell -Command "Start-Process 'powershell' -Verb runAs -ArgumentList 'winget install Microsoft.VisualStudio.2022.BuildTools --silent --override \"--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended\"'" - echo Please wait for the installation to finish, then re-run this installer. - pause - exit /b 1 -) - -:: Check for Rust compiler -where rustc >nul 2>&1 -if errorlevel 1 ( - echo Rust compiler not found. Installing... - powershell -Command "Invoke-WebRequest -Uri https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe -OutFile rustup-init.exe" - start rustup-init.exe -y - echo Please wait for the installation to finish, then re-run this installer. - pause - exit /b 1 -) - -:: Create virtual environment if it doesn't exist -if not exist "venv" ( - echo Creating virtual environment... - python -m venv venv -) - -:: Activate virtual environment and install requirements -echo Activating virtual environment... -call venv\Scripts\activate.bat - -:: Upgrade pip -echo Upgrading pip... -python -m pip install --upgrade pip - -:: Install requirements if requirements.txt exists -if exist requirements.txt ( - echo Installing Python dependencies... - pip install -r requirements.txt -) - -:: Install ALwrity -if exist setup.py ( - echo Installing ALwrity... - python setup.py install -) else ( - echo setup.py not found. Skipping ALwrity install step. -) - -echo. -echo Installation complete! -echo To start ALwrity, open a new command prompt and type: alwrity -echo. -pause diff --git a/calendar_data.json b/calendar_data.json new file mode 100644 index 00000000..a5a2e70c --- /dev/null +++ b/calendar_data.json @@ -0,0 +1,85 @@ +{ + "name": null, + "description": null, + "start_date": "2025-05-22T08:00:48.925689", + "duration": "monthly", + "platforms": [ + "website", + "instagram", + "twitter", + "linkedin", + "facebook" + ], + "schedule": { + "2025-05-22": [ + { + "title": "AI writer", + "description": "", + "content_type": "blog_post", + "platforms": [ + "website" + ], + "publish_date": "2025-05-22T00:00:00", + "seo_data": { + "title": "AI writer", + "meta_description": "", + "keywords": [], + "structured_data": {}, + "canonical_url": null, + "og_tags": null, + "twitter_cards": null + }, + "status": "Draft", + "author": null, + "tags": [], + "notes": null + }, + { + "title": "alwrity.", + "description": "", + "content_type": "blog_post", + "platforms": [ + "linkedin" + ], + "publish_date": "2025-05-22T00:00:00", + "seo_data": { + "title": "alwrity.", + "meta_description": "", + "keywords": [], + "structured_data": {}, + "canonical_url": null, + "og_tags": null, + "twitter_cards": null + }, + "status": "Draft", + "author": null, + "tags": [], + "notes": null + } + ], + "2025-05-31": [ + { + "title": "ai content generation", + "description": "", + "content_type": "blog_post", + "platforms": [ + "website" + ], + "publish_date": "2025-05-31T00:00:00", + "seo_data": { + "title": "ai content generation", + "meta_description": "", + "keywords": [], + "structured_data": {}, + "canonical_url": null, + "og_tags": null, + "twitter_cards": null + }, + "status": "Draft", + "author": null, + "tags": [], + "notes": null + } + ] + } +} \ No newline at end of file diff --git a/docs/architecture/database_schema.rst b/docs/architecture/database_schema.rst index bbe346fb..1bb96afd 100644 --- a/docs/architecture/database_schema.rst +++ b/docs/architecture/database_schema.rst @@ -160,6 +160,142 @@ Stores reusable content templates. # Relationships user = relationship("User") +ContentGapAnalysis +~~~~~~~~~~~~~~~~~ + +Stores content gap analysis results. + +.. code-block:: python + + class ContentGapAnalysis(Base): + __tablename__ = "content_gap_analyses" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id")) + website_url = Column(String, nullable=False) + industry = Column(String, nullable=False) + analysis_date = Column(DateTime, default=datetime.utcnow) + status = Column(String, nullable=False) # completed, in_progress, failed + metadata = Column(JSON) + + # Relationships + user = relationship("User", back_populates="content_gap_analyses") + website_analysis = relationship("WebsiteAnalysis", back_populates="content_gap_analysis") + competitor_analysis = relationship("CompetitorAnalysis", back_populates="content_gap_analysis") + keyword_analysis = relationship("KeywordAnalysis", back_populates="content_gap_analysis") + recommendations = relationship("ContentRecommendation", back_populates="content_gap_analysis") + +WebsiteAnalysis +~~~~~~~~~~~~~~ + +Stores website analysis results. + +.. code-block:: python + + class WebsiteAnalysis(Base): + __tablename__ = "website_analyses" + + id = Column(Integer, primary_key=True) + content_gap_analysis_id = Column(Integer, ForeignKey("content_gap_analyses.id")) + content_score = Column(Float) + seo_score = Column(Float) + structure_score = Column(Float) + content_metrics = Column(JSON) + seo_metrics = Column(JSON) + technical_metrics = Column(JSON) + ai_insights = Column(JSON) + created_at = Column(DateTime, default=datetime.utcnow) + + # Relationships + content_gap_analysis = relationship("ContentGapAnalysis", back_populates="website_analysis") + +CompetitorAnalysis +~~~~~~~~~~~~~~~~ + +Stores competitor analysis results. + +.. code-block:: python + + class CompetitorAnalysis(Base): + __tablename__ = "competitor_analyses" + + id = Column(Integer, primary_key=True) + content_gap_analysis_id = Column(Integer, ForeignKey("content_gap_analyses.id")) + competitor_url = Column(String, nullable=False) + market_position = Column(JSON) + content_gaps = Column(JSON) + competitive_advantages = Column(JSON) + trend_analysis = Column(JSON) + created_at = Column(DateTime, default=datetime.utcnow) + + # Relationships + content_gap_analysis = relationship("ContentGapAnalysis", back_populates="competitor_analysis") + +KeywordAnalysis +~~~~~~~~~~~~~ + +Stores keyword analysis results. + +.. code-block:: python + + class KeywordAnalysis(Base): + __tablename__ = "keyword_analyses" + + id = Column(Integer, primary_key=True) + content_gap_analysis_id = Column(Integer, ForeignKey("content_gap_analyses.id")) + top_keywords = Column(JSON) + search_intent = Column(JSON) + opportunities = Column(JSON) + trend_analysis = Column(JSON) + created_at = Column(DateTime, default=datetime.utcnow) + + # Relationships + content_gap_analysis = relationship("ContentGapAnalysis", back_populates="keyword_analysis") + +ContentRecommendation +~~~~~~~~~~~~~~~~~~~ + +Stores content recommendations. + +.. code-block:: python + + class ContentRecommendation(Base): + __tablename__ = "content_recommendations" + + id = Column(Integer, primary_key=True) + content_gap_analysis_id = Column(Integer, ForeignKey("content_gap_analyses.id")) + recommendation_type = Column(String, nullable=False) # content, seo, technical, etc. + priority_score = Column(Float) + recommendation = Column(Text, nullable=False) + implementation_steps = Column(JSON) + expected_impact = Column(JSON) + status = Column(String, nullable=False) # pending, in_progress, completed, rejected + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + content_gap_analysis = relationship("ContentGapAnalysis", back_populates="recommendations") + +AnalysisHistory +~~~~~~~~~~~~~ + +Tracks the history of analysis runs. + +.. code-block:: python + + class AnalysisHistory(Base): + __tablename__ = "analysis_histories" + + id = Column(Integer, primary_key=True) + content_gap_analysis_id = Column(Integer, ForeignKey("content_gap_analyses.id")) + run_date = Column(DateTime, default=datetime.utcnow) + status = Column(String, nullable=False) # completed, in_progress, failed + metrics = Column(JSON) # Performance metrics for the analysis run + error_log = Column(Text) # Any errors encountered during analysis + + # Relationships + content_gap_analysis = relationship("ContentGapAnalysis") + Vector Database Schema -------------------- diff --git a/lib/ai_seo_tools/content_calendar/README.md b/lib/ai_seo_tools/content_calendar/README.md new file mode 100644 index 00000000..55602f8f --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/README.md @@ -0,0 +1,167 @@ +# Content Calendar & Topic Planning System + +A comprehensive content planning and scheduling system that leverages existing SEO tools and AI capabilities to create optimized content calendars based on content gap analysis. + +## Folder Structure + +``` +content_calendar/ +├── README.md +├── core/ +│ ├── __init__.py +│ ├── calendar_manager.py # Main calendar management system +│ ├── topic_generator.py # AI-powered topic generation +│ └── content_predictor.py # Content performance prediction +├── integrations/ +│ ├── __init__.py +│ ├── seo_tools.py # Integration with existing SEO tools +│ ├── gap_analyzer.py # Content gap analysis integration +│ └── platform_adapters.py # Platform-specific content adaptation +├── models/ +│ ├── __init__.py +│ ├── calendar.py # Calendar data models +│ ├── content.py # Content data models +│ └── analytics.py # Analytics data models +├── utils/ +│ ├── __init__.py +│ ├── date_utils.py # Date and scheduling utilities +│ ├── validation.py # Input validation +│ └── error_handling.py # Error handling utilities +└── tests/ + ├── __init__.py + ├── test_calendar.py + ├── test_topic_generator.py + └── test_integrations.py +``` + +## Implementation Plan + +### Phase 1: Core Infrastructure + +1. **Basic Calendar Management** + - Implement calendar data structures + - Create scheduling algorithms + - Build date management utilities + +2. **Topic Generation System** + - Integrate with existing AI tools + - Implement topic generation logic + - Add SEO optimization features + +3. **Integration Framework** + - Connect with existing SEO tools + - Implement content gap analysis integration + - Create platform-specific adapters + +### Phase 2: AI & SEO Enhancement + +1. **AI-Powered Features** + - Implement topic ideation + - Add content structure generation + - Create performance prediction models + +2. **SEO Optimization** + - Integrate title optimization + - Add meta description generation + - Implement structured data creation + +3. **Content Performance** + - Add performance tracking + - Implement analytics collection + - Create reporting system + +### Phase 3: UI Development + +1. **Calendar Interface** + - Create interactive calendar view + - Implement drag-and-drop functionality + - Add platform-specific views + +2. **Content Planning Panel** + - Build topic suggestion interface + - Create SEO metrics display + - Implement content gap visualization + +3. **Analytics Dashboard** + - Design performance metrics view + - Create engagement tracking + - Implement progress monitoring + +### Phase 4: Testing & Refinement + +1. **Testing** + - Unit testing + - Integration testing + - User acceptance testing + +2. **Optimization** + - Performance optimization + - Code refactoring + - Bug fixes + +3. **Documentation** + - API documentation + - User guides + - Integration guides + +## Integration with Existing Tools + +### SEO Tools Integration +- `content_title_generator.py` - For optimized titles +- `meta_desc_generator.py` - For meta descriptions +- `seo_structured_data.py` - For structured data +- `content_gap_analysis/` - For gap analysis +- `webpage_content_analysis.py` - For content analysis + +### AI Capabilities +- Leverage existing `llm_text_gen` for: + - Topic generation + - Content structure + - Performance prediction + +## Key Features + +1. **Content Planning** + - AI-powered topic generation + - SEO-optimized content scheduling + - Platform-specific planning + +2. **SEO Integration** + - Automated SEO optimization + - Performance tracking + - Gap analysis integration + +3. **Analytics & Reporting** + - Content performance metrics + - SEO impact tracking + - Platform engagement stats + +## Getting Started + +1. **Prerequisites** + - Python 3.8+ + - Access to existing SEO tools + - Required API keys + +2. **Installation** + ```bash + # Add installation steps here + ``` + +3. **Configuration** + ```python + # Add configuration example here + ``` + +4. **Basic Usage** + ```python + # Add usage example here + ``` + +## Contributing + +Guidelines for contributing to the project. + +## License + +Project license information. \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/core/ai_generator.py b/lib/ai_seo_tools/content_calendar/core/ai_generator.py new file mode 100644 index 00000000..3aeb8fb2 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/core/ai_generator.py @@ -0,0 +1,798 @@ +from typing import Dict, List, Any, Optional +import logging +from pathlib import Path +import sys +import json + +# Add parent directory to path to import existing tools +parent_dir = str(Path(__file__).parent.parent.parent.parent) +if parent_dir not in sys.path: + sys.path.append(parent_dir) + +from lib.ai_seo_tools.content_calendar.models.calendar import ContentType, ContentItem, Platform +from lib.ai_seo_tools.content_calendar.utils.error_handling import handle_calendar_error +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen +from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis +from lib.ai_seo_tools.content_title_generator import ai_title_generator +from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main + +logger = logging.getLogger(__name__) + +class AIGenerator: + """AI-powered content generation and enhancement.""" + + def __init__(self): + self.logger = logging.getLogger('content_calendar.ai_generator') + self.logger.info("Initializing AIGenerator") + self._setup_logging() + self._load_ai_tools() + + def _setup_logging(self): + """Configure logging for AI generator.""" + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + + def _load_ai_tools(self): + """Load and initialize AI tools.""" + try: + # Initialize AI tools + self.gap_analyzer = ContentGapAnalysis() + self.title_generator = ai_title_generator + self.meta_generator = metadesc_generator_main + + except Exception as e: + logger.error(f"Error loading AI tools: {str(e)}") + raise + + def generate_content(self, content_item: ContentItem, target_audience: Dict[str, Any]) -> Dict[str, Any]: + """Generate base content using AI.""" + try: + self.logger.info(f"Generating content for: {content_item.title}") + + # Generate content based on type and platform + content = { + 'title': content_item.title, + 'content_flow': { + 'introduction': { + 'summary': f"An engaging introduction about {content_item.title}", + 'key_points': [ + f"Key point 1 about {content_item.title}", + f"Key point 2 about {content_item.title}", + f"Key point 3 about {content_item.title}" + ] + }, + 'main_content': { + 'sections': [ + { + 'title': f"Section 1: Understanding {content_item.title}", + 'content': f"Detailed content about {content_item.title}", + 'subsections': [] + }, + { + 'title': f"Section 2: Best Practices for {content_item.title}", + 'content': "Best practices and recommendations", + 'subsections': [] + } + ] + }, + 'conclusion': { + 'summary': f"Concluding thoughts about {content_item.title}", + 'call_to_action': "Next steps and actions" + } + }, + 'metadata': { + 'tone': target_audience.get('content_settings', {}).get('tone', 'professional'), + 'length': target_audience.get('content_settings', {}).get('length', 'medium'), + 'platform': content_item.platforms[0].name if content_item.platforms else 'Unknown', + 'content_type': content_item.content_type.name + } + } + + return content + + except Exception as e: + self.logger.error(f"Error generating content: {str(e)}", exc_info=True) + return {} + + def enhance_content(self, content: ContentItem, enhancement_type: str, target_audience: Dict[str, Any]) -> Dict[str, Any]: + """Enhance existing content using AI.""" + try: + self.logger.info(f"Enhancing content: {content.title}") + + # Enhance content based on type + enhanced = { + 'content': f"Enhanced version of {content.description}", + 'changes': [ + "Improved readability", + "Enhanced engagement elements", + "Optimized for target audience" + ], + 'metadata': { + 'enhancement_type': enhancement_type, + 'target_audience': target_audience + } + } + + return enhanced + + except Exception as e: + self.logger.error(f"Error enhancing content: {str(e)}", exc_info=True) + return {} + + def enhance_for_platform(self, content: Dict[str, Any], platform: Platform, enhancement_type: str) -> Dict[str, Any]: + """Enhance content specifically for a platform.""" + try: + self.logger.info(f"Enhancing content for platform: {platform.name}") + + # Platform-specific enhancements + enhanced = { + 'content': content.get('content', ''), + 'changes': [ + f"Optimized for {platform.name}", + "Platform-specific formatting", + "Enhanced engagement elements" + ], + 'metadata': { + 'platform': platform.name, + 'enhancement_type': enhancement_type + } + } + + return enhanced + + except Exception as e: + self.logger.error(f"Error enhancing for platform: {str(e)}", exc_info=True) + return {} + + def enhance_variant(self, content: Dict[str, Any], variant_type: str, optimization_goals: List[str]) -> Dict[str, Any]: + """Enhance a content variant for A/B testing.""" + try: + self.logger.info(f"Enhancing variant: {variant_type}") + + # Variant-specific enhancements + enhanced = { + 'content': content.get('content', ''), + 'changes': [ + f"Optimized for {', '.join(optimization_goals)}", + "Enhanced variant-specific elements", + "Improved engagement metrics" + ], + 'metadata': { + 'variant_type': variant_type, + 'optimization_goals': optimization_goals + } + } + + return enhanced + + except Exception as e: + self.logger.error(f"Error enhancing variant: {str(e)}", exc_info=True) + return {} + + def enhance_for_seo(self, content: Dict[str, Any], seo_goals: List[str]) -> Dict[str, Any]: + """Enhance content for SEO optimization.""" + try: + self.logger.info("Enhancing content for SEO") + + # SEO-specific enhancements + enhanced = { + 'content': content.get('content', ''), + 'changes': [ + f"Optimized for {', '.join(seo_goals)}", + "Enhanced keyword placement", + "Improved meta information" + ], + 'metadata': { + 'seo_goals': seo_goals + } + } + + return enhanced + + except Exception as e: + self.logger.error(f"Error enhancing for SEO: {str(e)}", exc_info=True) + return {} + + def generate_series_content(self, content_item: ContentItem, series_info: Dict[str, Any]) -> Dict[str, Any]: + """Generate content for a series.""" + try: + self.logger.info(f"Generating series content: {content_item.title}") + + # Generate series-specific content + content = { + 'title': content_item.title, + 'content_flow': { + 'introduction': { + 'summary': f"Part {series_info['part_number']} of {series_info['total_parts']} about {series_info['topic']}", + 'key_points': [ + f"Key point 1 for part {series_info['part_number']}", + f"Key point 2 for part {series_info['part_number']}", + f"Key point 3 for part {series_info['part_number']}" + ] + }, + 'main_content': { + 'sections': [ + { + 'title': f"Section 1: Part {series_info['part_number']} Overview", + 'content': f"Detailed content for part {series_info['part_number']}", + 'subsections': [] + }, + { + 'title': f"Section 2: Part {series_info['part_number']} Details", + 'content': "Specific details and information", + 'subsections': [] + } + ] + }, + 'conclusion': { + 'summary': f"Concluding thoughts for part {series_info['part_number']}", + 'next_part': f"Preview of part {series_info['part_number'] + 1}" if series_info['part_number'] < series_info['total_parts'] else "Series conclusion" + } + }, + 'metadata': { + 'series_info': series_info, + 'platform': content_item.platforms[0].name if content_item.platforms else 'Unknown', + 'content_type': content_item.content_type.name + } + } + + return content + + except Exception as e: + self.logger.error(f"Error generating series content: {str(e)}", exc_info=True) + return {} + + @handle_calendar_error + def generate_headings( + self, + title: str, + content_type: ContentType, + context: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """ + Generate content headings using AI. + + Args: + title: Content title + content_type: Type of content + context: Content context from gap analysis + + Returns: + List of generated headings with metadata + """ + try: + # Get content gaps and opportunities + gaps = self.gap_analyzer.analyze_gaps(context.get('website_url', '')) + + # Generate headings based on content type and gaps + prompt = self._create_heading_prompt(title, content_type, gaps) + headings = self._call_ai_model(prompt) + + return self._format_headings(headings) + + except Exception as e: + logger.error(f"Error generating headings: {str(e)}") + return [] + + @handle_calendar_error + def generate_subheadings( + self, + main_heading: Dict[str, Any], + content_type: ContentType, + context: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """ + Generate subheadings for a main heading. + + Args: + main_heading: Main heading to generate subheadings for + content_type: Type of content + context: Content context + + Returns: + List of generated subheadings + """ + try: + # Create prompt for subheading generation + prompt = self._create_subheading_prompt( + main_heading, + content_type, + context + ) + + # Generate subheadings + subheadings = self._call_ai_model(prompt) + + return self._format_subheadings(subheadings) + + except Exception as e: + logger.error(f"Error generating subheadings: {str(e)}") + return [] + + @handle_calendar_error + def generate_key_points( + self, + title: str, + content_type: ContentType, + context: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """ + Generate key points for content. + + Args: + title: Content title + content_type: Type of content + context: Content context + + Returns: + List of key points with supporting information + """ + try: + # Generate title and meta description for SEO context + seo_title = self.title_generator(title) + meta_desc = self.meta_generator(title) + + # Create prompt for key points + prompt = self._create_key_points_prompt( + title, + content_type, + {'title': seo_title, 'meta_description': meta_desc}, + context + ) + + # Generate key points + points = self._call_ai_model(prompt) + + return self._format_key_points(points) + + except Exception as e: + logger.error(f"Error generating key points: {str(e)}") + return [] + + @handle_calendar_error + def generate_content_flow( + self, + title: str, + content_type: ContentType, + outline: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Generate content flow and structure. + + Args: + title: Content title + content_type: Type of content + outline: Content outline with headings and key points + + Returns: + Dictionary containing content flow and structure + """ + try: + # Create prompt for content flow + prompt = self._create_flow_prompt(title, content_type, outline) + + # Generate content flow + flow = self._call_ai_model(prompt) + + return self._format_content_flow(flow) + + except Exception as e: + logger.error(f"Error generating content flow: {str(e)}") + return {} + + def _create_heading_prompt( + self, + title: str, + content_type: ContentType, + gaps: Dict[str, Any] + ) -> str: + """Create prompt for heading generation.""" + return f""" + Generate main headings for a {content_type.value} titled "{title}". + Consider the following content gaps and opportunities: + {json.dumps(gaps, indent=2)} + + For each heading, provide: + 1. Title + 2. Level (1 for main headings) + 3. Key keywords to include + 4. Brief summary of what this section should cover + + Format the response as a JSON array of heading objects. + """ + + def _create_subheading_prompt( + self, + main_heading: Dict[str, Any], + content_type: ContentType, + context: Dict[str, Any] + ) -> str: + """Create prompt for subheading generation.""" + return f""" + Generate subheadings for the main heading "{main_heading['title']}" + in a {content_type.value}. + + Main heading details: + {json.dumps(main_heading, indent=2)} + + For each subheading, provide: + 1. Title + 2. Level (2 for subheadings) + 3. Key keywords to include + 4. Brief summary of what this subsection should cover + + Format the response as a JSON array of subheading objects. + """ + + def _create_key_points_prompt( + self, + title: str, + content_type: ContentType, + seo_data: Dict[str, Any], + context: Dict[str, Any] + ) -> str: + """Create prompt for key points generation.""" + return f""" + Generate key points for a {content_type.value} titled "{title}". + + SEO Requirements: + {json.dumps(seo_data, indent=2)} + + For each key point, provide: + 1. Main point + 2. Importance level (high/medium/low) + 3. Supporting evidence or examples + 4. Related keywords to include + + Format the response as a JSON array of key point objects. + """ + + def _create_flow_prompt( + self, + title: str, + content_type: ContentType, + outline: Dict[str, Any] + ) -> str: + """Create prompt for content flow generation.""" + return f""" + Generate content flow and structure for a {content_type.value} titled "{title}". + + Content Outline: + {json.dumps(outline, indent=2)} + + Provide: + 1. Introduction structure + 2. Main sections flow + 3. Conclusion approach + 4. Transition points between sections + 5. Content pacing recommendations + + Format the response as a JSON object with these sections. + """ + + def _call_ai_model(self, prompt: str) -> Any: + """ + Call the AI model with the given prompt. + + Args: + prompt: The prompt to send to the AI model + + Returns: + The AI model's response, parsed as JSON + """ + try: + # Call the AI model + response = llm_text_gen( + prompt=prompt, + max_tokens=1000, + temperature=0.7, + top_p=0.9, + frequency_penalty=0.5, + presence_penalty=0.5 + ) + + # Parse the response as JSON + try: + return json.loads(response) + except json.JSONDecodeError as e: + logger.error(f"Error parsing AI response as JSON: {str(e)}") + logger.error(f"Raw response: {response}") + return {} + + except Exception as e: + logger.error(f"Error calling AI model: {str(e)}") + return {} + + def _format_headings(self, headings: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Format and validate generated headings.""" + formatted = [] + for heading in headings: + formatted.append({ + 'title': heading.get('title', ''), + 'level': heading.get('level', 1), + 'keywords': heading.get('keywords', []), + 'summary': heading.get('summary', '') + }) + return formatted + + def _format_subheadings(self, subheadings: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Format and validate generated subheadings.""" + formatted = [] + for subheading in subheadings: + formatted.append({ + 'title': subheading.get('title', ''), + 'level': subheading.get('level', 2), + 'keywords': subheading.get('keywords', []), + 'summary': subheading.get('summary', '') + }) + return formatted + + def _format_key_points(self, points: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Format and validate generated key points.""" + formatted = [] + for point in points: + formatted.append({ + 'point': point.get('point', ''), + 'importance': point.get('importance', 'medium'), + 'supporting_evidence': point.get('evidence', []), + 'related_keywords': point.get('keywords', []) + }) + return formatted + + def _format_content_flow(self, flow: Dict[str, Any]) -> Dict[str, Any]: + """Format and validate generated content flow.""" + return { + 'introduction': flow.get('introduction', {}), + 'main_sections': flow.get('main_sections', []), + 'conclusion': flow.get('conclusion', {}), + 'transitions': flow.get('transitions', []), + 'content_pacing': flow.get('pacing', {}) + } + + def generate_ai_suggestions( + self, + content_type: str, + topic: str, + audience: str, + goals: List[str], + tone: str, + length: str, + model_settings: Dict[str, Any], + style_preferences: List[str], + seo_preferences: Dict[str, Any], + platform_settings: Dict[str, Any], + platform: str + ) -> List[Dict[str, Any]]: + """ + Generate AI content suggestions based on input parameters. + + Args: + content_type: Type of content to generate + topic: Main topic or subject + audience: Target audience + goals: List of content goals + tone: Desired tone + length: Content length + model_settings: AI model settings + style_preferences: Style preferences + seo_preferences: SEO preferences + platform_settings: Platform-specific settings + platform: Target platform + + Returns: + List of generated content suggestions + """ + try: + self.logger.info(f"Generating AI suggestions for topic: {topic}") + + # Create a comprehensive prompt for content generation + prompt = f"""Generate content suggestions for the following parameters: + +Content Type: {content_type} +Topic: {topic} +Target Audience: {audience} +Goals: {', '.join(goals)} +Tone: {tone} +Length: {length} + +Style Preferences: +- Creativity Level: {model_settings['Creativity Level']} +- Formality Level: {model_settings['Formality Level']} +- Style Elements: {', '.join(style_preferences)} + +SEO Preferences: +- Keyword Density: {seo_preferences['Keyword Density']}% +- Internal Linking: {'Enabled' if seo_preferences['Internal Linking'] else 'Disabled'} +- External Linking: {'Enabled' if seo_preferences['External Linking'] else 'Disabled'} + +Platform Settings: +- Platform: {platform} +- Platform-specific requirements: {', '.join(platform_settings)} + +Please generate 3 different content suggestions. Format your response as a valid JSON object with the following structure: +{{ + "suggestions": [ + {{ + "title": "string", + "introduction": "string", + "key_points": ["string"], + "main_sections": [ + {{ + "title": "string", + "content": "string", + "engagement_elements": ["string"], + "seo_elements": ["string"] + }} + ], + "conclusion": "string", + "seo_elements": ["string"], + "platform_optimizations": ["string"], + "engagement_strategies": ["string"], + "content_metrics": {{ + "estimated_read_time": "string", + "word_count": "number", + "keyword_density": "number", + "engagement_score": "number" + }} + }} + ] +}} + +IMPORTANT: Your response must be a valid JSON object. Do not include any text before or after the JSON object.""" + + # Define JSON structure for validation + json_struct = { + "type": "object", + "properties": { + "suggestions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "introduction": {"type": "string"}, + "key_points": {"type": "array", "items": {"type": "string"}}, + "main_sections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "content": {"type": "string"}, + "engagement_elements": {"type": "array", "items": {"type": "string"}}, + "seo_elements": {"type": "array", "items": {"type": "string"}} + } + } + }, + "conclusion": {"type": "string"}, + "seo_elements": {"type": "array", "items": {"type": "string"}}, + "platform_optimizations": {"type": "array", "items": {"type": "string"}}, + "engagement_strategies": {"type": "array", "items": {"type": "string"}}, + "content_metrics": { + "type": "object", + "properties": { + "estimated_read_time": {"type": "string"}, + "word_count": {"type": "number"}, + "keyword_density": {"type": "number"}, + "engagement_score": {"type": "number"} + } + } + } + } + } + } + } + + # Generate content using llm_text_gen with JSON structure + generated_content = llm_text_gen(prompt, json_struct=json_struct) + + if not generated_content: + raise ValueError("Failed to generate content suggestions") + + # Parse the generated content + try: + # If generated_content is already a dict, use it directly + if isinstance(generated_content, dict): + content_data = generated_content + else: + # Try to parse as JSON string + content_data = json.loads(generated_content) + + return self._format_suggestions( + content_data, + content_type, + audience, + goals, + tone, + length, + model_settings, + seo_preferences, + platform + ) + + except json.JSONDecodeError as e: + self.logger.error(f"Error parsing generated content: {str(e)}") + # Try to extract JSON from the response if it's wrapped in other text + try: + # Find the first '{' and last '}' + start = generated_content.find('{') + end = generated_content.rfind('}') + 1 + if start >= 0 and end > start: + json_str = generated_content[start:end] + content_data = json.loads(json_str) + return self._format_suggestions( + content_data, + content_type, + audience, + goals, + tone, + length, + model_settings, + seo_preferences, + platform + ) + except Exception as e2: + self.logger.error(f"Error extracting JSON from response: {str(e2)}") + raise ValueError("Failed to parse generated content") + + except Exception as e: + self.logger.error(f"Error generating AI suggestions: {str(e)}", exc_info=True) + raise + + def _format_suggestions( + self, + content_data: Dict[str, Any], + content_type: str, + audience: str, + goals: List[str], + tone: str, + length: str, + model_settings: Dict[str, Any], + seo_preferences: Dict[str, Any], + platform: str + ) -> List[Dict[str, Any]]: + """Format and process suggestions from content data.""" + suggestions = [] + for suggestion in content_data.get('suggestions', []): + formatted_suggestion = { + 'title': suggestion.get('title', ''), + 'type': content_type, + 'platform': platform, + 'audience': audience, + 'impact': f"High impact for {', '.join(goals)}", + 'preview': suggestion.get('introduction', ''), + 'style_elements': [ + f"Tone: {tone}", + f"Length: {length}", + f"Creativity: {model_settings['Creativity Level']}", + f"Formality: {model_settings['Formality Level']}" + ], + 'seo_elements': [ + f"Keyword Density: {seo_preferences['Keyword Density']}%", + "Internal Linking: Enabled" if seo_preferences['Internal Linking'] else "Internal Linking: Disabled", + "External Linking: Enabled" if seo_preferences['External Linking'] else "External Linking: Disabled" + ], + 'engagement_score': f"{85 + len(suggestions)*5}%", + 'reach': 'High', + 'conversion': f"{3.5 + len(suggestions)*0.5}%", + 'seo_impact': 'Strong', + 'platform_optimizations': suggestion.get('platform_optimizations', []), + 'variations': [ + "Alternative headline", + "Different content angle", + "Alternative format" + ], + 'seo_recommendations': suggestion.get('seo_elements', []), + 'media_suggestions': [ + "Featured image", + "Supporting graphics", + "Social media visuals" + ] + } + suggestions.append(formatted_suggestion) + return suggestions \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/core/calendar_manager.py b/lib/ai_seo_tools/content_calendar/core/calendar_manager.py new file mode 100644 index 00000000..a977b4f5 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/core/calendar_manager.py @@ -0,0 +1,196 @@ +from datetime import datetime, timedelta +from typing import Dict, List, Any, Optional +import logging +import sys +import json +import os + +from ..integrations.seo_tools import SEOToolsIntegration +from ..integrations.gap_analyzer import GapAnalyzerIntegration +from ..models.calendar import Calendar, ContentItem +from ..utils.date_utils import calculate_publish_dates +from ..utils.error_handling import handle_calendar_error + +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout), + logging.FileHandler('content_calendar_debug.log', mode='a') + ] +) +logger = logging.getLogger(__name__) + +CALENDAR_JSON_PATH = "calendar_data.json" + +class CalendarManager: + """ + Main calendar management system that coordinates content planning, + scheduling, and optimization. + """ + + def __init__(self): + """Initialize calendar manager.""" + self.logger = logging.getLogger('content_calendar.manager') + self.logger.info("Initializing CalendarManager") + + self.seo_tools = SEOToolsIntegration() + self.gap_analyzer = GapAnalyzerIntegration() + self._calendar: Optional[Calendar] = None + self.logger.info("CalendarManager initialized successfully") + + @handle_calendar_error + def create_calendar( + self, + start_date: datetime, + duration: str, # 'weekly', 'monthly', 'quarterly' + platforms: List[str], + website_url: str + ) -> Calendar: + """ + Create a new content calendar based on content gap analysis and SEO requirements. + + Args: + start_date: When the calendar should begin + duration: How long the calendar should span + platforms: List of platforms to create content for + website_url: URL of the website to analyze + + Returns: + Calendar object containing the content schedule + """ + self.logger.info(f"Creating new calendar for {website_url}") + self.logger.debug(f"Parameters: start_date={start_date}, duration={duration}, platforms={platforms}") + + try: + # 1. Analyze content gaps + self.logger.info("Analyzing content gaps") + gap_analysis = self.gap_analyzer.analyze_gaps(website_url) + + # 2. Generate topics based on gaps + self.logger.info("Generating topics from gap analysis") + topics = self._generate_topics(gap_analysis, platforms) + + # 3. Calculate publish dates + self.logger.info("Calculating publish dates") + schedule = calculate_publish_dates( + topics=topics, + start_date=start_date, + duration=duration + ) + + # 4. Create calendar + self.logger.info("Creating calendar object") + self._calendar = Calendar( + start_date=start_date, + duration=duration, + platforms=platforms, + schedule=schedule + ) + + self.logger.info("Calendar created successfully") + return self._calendar + + except Exception as e: + self.logger.error(f"Error creating calendar: {str(e)}", exc_info=True) + raise + + def _generate_topics( + self, + gap_analysis: Dict[str, Any], + platforms: List[str] + ) -> List[ContentItem]: + """ + Generate content topics based on gap analysis and platform requirements. + """ + topics = [] + + for gap in gap_analysis['gaps']: + # Generate topic using AI + topic = self._generate_topic_from_gap(gap, platforms) + + # Optimize for SEO + optimized_topic = self._optimize_topic(topic) + + topics.append(optimized_topic) + + return topics + + def _generate_topic_from_gap( + self, + gap: Dict[str, Any], + platforms: List[str] + ) -> ContentItem: + """ + Generate a specific topic based on a content gap. + """ + # Use existing AI tools to generate topic + topic_data = { + 'title': self._generate_title(gap), + 'description': self._generate_description(gap), + 'keywords': gap.get('keywords', []), + 'platforms': platforms, + 'content_type': self._determine_content_type(gap, platforms) + } + + return ContentItem(**topic_data) + + def _optimize_topic(self, topic: ContentItem) -> ContentItem: + """ + Optimize a topic for SEO using existing tools. + """ + # Optimize title + topic.title = self.seo_tools.optimize_title(topic.title) + + # Generate meta description + topic.meta_description = self.seo_tools.generate_meta_description( + topic.description + ) + + # Add structured data + topic.structured_data = self.seo_tools.generate_structured_data( + topic.content_type + ) + + return topic + + def get_calendar(self) -> Optional[Calendar]: + """ + Get the current calendar. + """ + self.logger.debug("Getting current calendar") + return self._calendar + + def update_calendar(self, calendar: Calendar) -> None: + """ + Update the current calendar. + """ + self._calendar = calendar + + def export_calendar(self) -> Optional[Dict[str, Any]]: + """Export the current calendar.""" + self.logger.info("Exporting calendar") + if not self._calendar: + self.logger.warning("No calendar to export") + return None + + try: + calendar_data = self._calendar.export() + self.logger.info("Calendar exported successfully") + return calendar_data + except Exception as e: + self.logger.error(f"Error exporting calendar: {str(e)}", exc_info=True) + return None + + def save_calendar_to_json(self): + calendar = self.get_calendar() + if calendar: + with open(CALENDAR_JSON_PATH, "w") as f: + json.dump(calendar.to_dict(), f, indent=2, default=str) + + def load_calendar_from_json(self): + from lib.ai_seo_tools.content_calendar.models.calendar import Calendar + if os.path.exists(CALENDAR_JSON_PATH): + with open(CALENDAR_JSON_PATH, "r") as f: + data = json.load(f) + self._calendar = Calendar.from_dict(data) \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/core/content_brief.py b/lib/ai_seo_tools/content_calendar/core/content_brief.py new file mode 100644 index 00000000..72492687 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/core/content_brief.py @@ -0,0 +1,151 @@ +from typing import Dict, List, Any, Optional +import logging +from pathlib import Path +import sys + +# Add parent directory to path to import existing tools +parent_dir = str(Path(__file__).parent.parent.parent.parent) +if parent_dir not in sys.path: + sys.path.append(parent_dir) + +from lib.ai_seo_tools.content_calendar.models.calendar import ContentType, ContentItem, Platform +from lib.ai_seo_tools.content_calendar.utils.error_handling import handle_calendar_error +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen +from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis +from lib.ai_seo_tools.content_title_generator import ai_title_generator +from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main +from .ai_generator import AIGenerator + +logger = logging.getLogger(__name__) + +class ContentBriefGenerator: + """ + Generates comprehensive content briefs using AI-powered analysis. + """ + + def __init__(self): + self.logger = logging.getLogger('content_calendar.content_brief') + self.logger.info("Initializing ContentBriefGenerator") + self._setup_logging() + self._load_ai_tools() + + def _setup_logging(self): + """Configure logging for content brief generator.""" + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + + def _load_ai_tools(self): + """Load and initialize AI tools.""" + try: + # Initialize AI tools + self.gap_analyzer = ContentGapAnalysis() + self.title_generator = ai_title_generator + self.meta_generator = metadesc_generator_main + self.ai_generator = AIGenerator() + + except Exception as e: + logger.error(f"Error loading AI tools: {str(e)}") + raise + + @handle_calendar_error + def generate_brief( + self, + content_item: ContentItem, + target_audience: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Generate a comprehensive content brief. + + Args: + content_item: Content item to generate brief for + target_audience: Optional target audience data + + Returns: + Dictionary containing the content brief + """ + try: + logger.info(f"Generating content brief for: {content_item.title}") + + # Generate content outline + outline = self._generate_outline(content_item) + + # Generate key points + key_points = self.ai_generator.generate_key_points( + title=content_item.title, + content_type=content_item.content_type, + context=content_item.context + ) + + # Generate content flow + content_flow = self.ai_generator.generate_content_flow( + title=content_item.title, + content_type=content_item.content_type, + outline=outline + ) + + # Compile the brief + brief = { + 'title': content_item.title, + 'content_type': content_item.content_type.value, + 'outline': outline, + 'key_points': key_points, + 'content_flow': content_flow, + 'target_audience': target_audience or {}, + 'seo_data': content_item.seo_data, + 'platform_specs': content_item.platform_specs + } + + logger.info("Content brief generated successfully") + return brief + + except Exception as e: + logger.error(f"Error generating content brief: {str(e)}") + raise + + def _generate_outline( + self, + content_item: ContentItem + ) -> Dict[str, Any]: + """ + Generate content outline with headings and subheadings. + + Args: + content_item: Content item to generate outline for + + Returns: + Dictionary containing the content outline + """ + try: + # Generate main headings + main_headings = self.ai_generator.generate_headings( + title=content_item.title, + content_type=content_item.content_type, + context=content_item.context + ) + + # Generate subheadings for each main heading + subheadings = {} + for heading in main_headings: + heading_subheadings = self.ai_generator.generate_subheadings( + main_heading=heading, + content_type=content_item.content_type, + context=content_item.context + ) + subheadings[heading['title']] = heading_subheadings + + return { + 'main_headings': main_headings, + 'subheadings': subheadings + } + + except Exception as e: + logger.error(f"Error generating outline: {str(e)}") + return { + 'main_headings': [], + 'subheadings': {} + } \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/core/content_generator.py b/lib/ai_seo_tools/content_calendar/core/content_generator.py new file mode 100644 index 00000000..3a0b6046 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/core/content_generator.py @@ -0,0 +1,323 @@ +from typing import Dict, List, Any, Optional +import logging +from pathlib import Path +import sys + +# Add parent directory to path to import existing tools +parent_dir = str(Path(__file__).parent.parent.parent.parent) +if parent_dir not in sys.path: + sys.path.append(parent_dir) + +from ..models.calendar import ContentItem, ContentType +from ..utils.error_handling import handle_calendar_error +from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis +from lib.ai_seo_tools.content_title_generator import ai_title_generator +from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main + +logger = logging.getLogger(__name__) + +class ContentGenerator: + """ + AI-powered content generation for content briefs. + """ + + def __init__(self): + self.logger = logging.getLogger('content_calendar.content_generator') + self.logger.info("Initializing ContentGenerator") + self._setup_logging() + self._load_ai_tools() + + def _setup_logging(self): + """Configure logging for content generator.""" + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + + def _load_ai_tools(self): + """Load and initialize AI tools.""" + try: + # Initialize AI tools + self.gap_analyzer = ContentGapAnalysis() + self.title_generator = ai_title_generator + self.meta_generator = metadesc_generator_main + + except Exception as e: + logger.error(f"Error loading AI tools: {str(e)}") + raise + + @handle_calendar_error + def generate_headings( + self, + content_item: ContentItem, + context: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """ + Generate main headings for content. + + Args: + content_item: Content item to generate headings for + context: Content context from gap analysis + + Returns: + List of main headings with metadata + """ + try: + # Use AI to generate headings based on content type and context + headings = self._generate_ai_headings( + title=content_item.title, + content_type=content_item.content_type, + context=context + ) + + # Format and validate headings + formatted_headings = [] + for heading in headings: + formatted_heading = { + 'title': heading['title'], + 'level': heading.get('level', 1), + 'keywords': heading.get('keywords', []), + 'summary': heading.get('summary', '') + } + formatted_headings.append(formatted_heading) + + return formatted_headings + + except Exception as e: + logger.error(f"Error generating headings: {str(e)}") + return [] + + @handle_calendar_error + def generate_subheadings( + self, + content_item: ContentItem, + main_headings: List[Dict[str, Any]], + context: Dict[str, Any] + ) -> Dict[str, List[Dict[str, Any]]]: + """ + Generate subheadings for each main heading. + + Args: + content_item: Content item to generate subheadings for + main_headings: List of main headings + context: Content context from gap analysis + + Returns: + Dictionary mapping main headings to their subheadings + """ + try: + subheadings = {} + + for heading in main_headings: + # Generate subheadings for each main heading + heading_subheadings = self._generate_ai_subheadings( + main_heading=heading, + content_type=content_item.content_type, + context=context + ) + + # Format and validate subheadings + formatted_subheadings = [] + for subheading in heading_subheadings: + formatted_subheading = { + 'title': subheading['title'], + 'level': subheading.get('level', 2), + 'keywords': subheading.get('keywords', []), + 'summary': subheading.get('summary', '') + } + formatted_subheadings.append(formatted_subheading) + + subheadings[heading['title']] = formatted_subheadings + + return subheadings + + except Exception as e: + logger.error(f"Error generating subheadings: {str(e)}") + return {} + + @handle_calendar_error + def generate_key_points( + self, + content_item: ContentItem, + context: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """ + Generate key points for the content. + + Args: + content_item: Content item to generate key points for + context: Content context from gap analysis + + Returns: + List of key points with supporting information + """ + try: + # Generate key points using AI + key_points = self._generate_ai_key_points( + title=content_item.title, + content_type=content_item.content_type, + context=context + ) + + # Format and validate key points + formatted_points = [] + for point in key_points: + formatted_point = { + 'point': point['point'], + 'importance': point.get('importance', 'medium'), + 'supporting_evidence': point.get('evidence', []), + 'related_keywords': point.get('keywords', []) + } + formatted_points.append(formatted_point) + + return formatted_points + + except Exception as e: + logger.error(f"Error generating key points: {str(e)}") + return [] + + @handle_calendar_error + def generate_content_flow( + self, + content_item: ContentItem, + outline: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Generate content flow and structure. + + Args: + content_item: Content item to generate flow for + outline: Content outline with headings and key points + + Returns: + Dictionary containing content flow and structure + """ + try: + # Generate content flow using AI + flow = self._generate_ai_content_flow( + title=content_item.title, + content_type=content_item.content_type, + outline=outline + ) + + return { + 'introduction': flow.get('introduction', {}), + 'main_sections': flow.get('main_sections', []), + 'conclusion': flow.get('conclusion', {}), + 'transitions': flow.get('transitions', []), + 'content_pacing': flow.get('pacing', {}) + } + + except Exception as e: + logger.error(f"Error generating content flow: {str(e)}") + return {} + + def _generate_ai_headings( + self, + title: str, + content_type: ContentType, + context: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """ + Use AI to generate content headings. + """ + # TODO: Implement AI heading generation + # This would use the existing AI tools to generate headings + return [] + + def _generate_ai_subheadings( + self, + main_heading: Dict[str, Any], + content_type: ContentType, + context: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """ + Use AI to generate subheadings. + """ + # TODO: Implement AI subheading generation + return [] + + def _generate_ai_key_points( + self, + title: str, + content_type: ContentType, + context: Dict[str, Any] + ) -> List[Dict[str, Any]]: + """ + Use AI to generate key points. + """ + # TODO: Implement AI key point generation + return [] + + def _generate_ai_content_flow( + self, + title: str, + content_type: ContentType, + outline: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Use AI to generate content flow. + """ + # TODO: Implement AI content flow generation + return {} + + def generate_variation(self, content: Dict[str, Any], variation_type: str) -> Dict[str, Any]: + """Generate a variation of the given content. + + Args: + content: Original content to vary + variation_type: Type of variation to generate ('tone', 'length', 'style', etc.) + + Returns: + Dictionary containing the varied content + """ + try: + self.logger.info(f"Generating {variation_type} variation for content") + + # Generate variation based on type + variation = { + 'title': f"{content.get('title', '')} - {variation_type.title()} Variation", + 'content_flow': { + 'introduction': { + 'summary': f"Varied introduction for {content.get('title', '')}", + 'key_points': [ + f"Varied key point 1 for {variation_type}", + f"Varied key point 2 for {variation_type}", + f"Varied key point 3 for {variation_type}" + ] + }, + 'main_content': { + 'sections': [ + { + 'title': f"Varied Section 1: {variation_type.title()} Approach", + 'content': f"Varied content for {variation_type}", + 'subsections': [] + }, + { + 'title': f"Varied Section 2: {variation_type.title()} Details", + 'content': "Varied details and information", + 'subsections': [] + } + ] + }, + 'conclusion': { + 'summary': f"Varied conclusion for {variation_type}", + 'call_to_action': "Varied call to action" + } + }, + 'metadata': { + 'variation_type': variation_type, + 'original_content': content.get('title', ''), + 'platform': content.get('metadata', {}).get('platform', 'Unknown'), + 'content_type': content.get('metadata', {}).get('content_type', 'Unknown') + } + } + + return variation + + except Exception as e: + self.logger.error(f"Error generating variation: {str(e)}") + return {} \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/examples/calendar_usage.py b/lib/ai_seo_tools/content_calendar/examples/calendar_usage.py new file mode 100644 index 00000000..c3f77f33 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/examples/calendar_usage.py @@ -0,0 +1,80 @@ +from datetime import datetime +from typing import List, Dict, Any + +from ..core.calendar_manager import CalendarManager +from ..models.calendar import ContentType, Platform + +def create_content_calendar( + website_url: str, + start_date: datetime, + duration: str, + platforms: List[str] +) -> Dict[str, Any]: + """ + Example of creating a content calendar. + + Args: + website_url: URL of the website to analyze + start_date: When to start the calendar + duration: How long the calendar should span + platforms: List of platforms to create content for + + Returns: + Dictionary containing the calendar data + """ + # Initialize calendar manager + calendar_manager = CalendarManager() + + # Create calendar + calendar = calendar_manager.create_calendar( + start_date=start_date, + duration=duration, + platforms=platforms, + website_url=website_url + ) + + # Export calendar + calendar_data = calendar_manager.export_calendar() + + return calendar_data + +def main(): + """Example usage of the content calendar system.""" + # Example parameters + website_url = "https://example.com" + start_date = datetime.now() + duration = "monthly" + platforms = [ + Platform.WEBSITE.value, + Platform.FACEBOOK.value, + Platform.TWITTER.value, + Platform.LINKEDIN.value + ] + + try: + # Create calendar + calendar_data = create_content_calendar( + website_url=website_url, + start_date=start_date, + duration=duration, + platforms=platforms + ) + + # Print calendar summary + print("\nContent Calendar Summary:") + print(f"Duration: {calendar_data['duration']}") + print(f"Platforms: {', '.join(calendar_data['platforms'])}") + print("\nScheduled Content:") + + for date, items in calendar_data['schedule'].items(): + print(f"\n{date}:") + for item in items: + print(f"- {item['title']} ({item['content_type']})") + print(f" Platforms: {', '.join(item['platforms'])}") + print(f" Status: {item['status']}") + + except Exception as e: + print(f"Error creating calendar: {str(e)}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/examples/generate_content_brief.py b/lib/ai_seo_tools/content_calendar/examples/generate_content_brief.py new file mode 100644 index 00000000..8d2851f1 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/examples/generate_content_brief.py @@ -0,0 +1,138 @@ +from datetime import datetime +from typing import Dict, Any + +from ..models.calendar import ContentItem, ContentType, Platform, SEOData +from ..core.content_brief import ContentBriefGenerator + +def create_content_brief( + title: str, + content_type: ContentType, + platforms: list[Platform], + website_url: str, + target_audience: Dict[str, Any] +) -> Dict[str, Any]: + """ + Create a content brief for the given content. + + Args: + title: Content title + content_type: Type of content + platforms: List of platforms to publish on + website_url: Website URL for context + target_audience: Target audience information + + Returns: + Dictionary containing the content brief + """ + # Create content item + content_item = ContentItem( + id=f"content-{datetime.now().strftime('%Y%m%d%H%M%S')}", + title=title, + description=f"Content brief for {title}", + content_type=content_type, + platforms=platforms, + publish_date=datetime.now(), + seo_data=SEOData( + keywords=[], # Will be generated by SEO tools + meta_description="", # Will be generated by SEO tools + structured_data={} + ), + platform_specs={}, # Will be generated based on platforms + context={ + "website_url": website_url, + "target_audience": target_audience.get("demographics", {}).get("profession", ""), + "content_goals": ["educate", "generate leads"] + } + ) + + # Initialize content brief generator + generator = ContentBriefGenerator() + + # Generate brief + brief = generator.generate_brief( + content_item=content_item, + target_audience=target_audience + ) + + return brief + +def main(): + """Example usage of content brief generation.""" + # Example content details + title = "10 Ways to Improve Your SEO Strategy" + content_type = ContentType.BLOG_POST + platforms = [Platform.WEBSITE, Platform.LINKEDIN] + website_url = "https://example.com" + + # Example target audience + target_audience = { + "demographics": { + "age_range": "25-45", + "profession": "digital marketers", + "experience_level": "intermediate" + }, + "interests": [ + "SEO", + "content marketing", + "digital strategy", + "search engine optimization" + ], + "pain_points": [ + "low search rankings", + "poor content performance", + "lack of organic traffic", + "difficulty in keyword research" + ], + "goals": [ + "improve search rankings", + "increase organic traffic", + "generate more leads", + "build brand authority" + ] + } + + try: + # Generate content brief + brief = create_content_brief( + title=title, + content_type=content_type, + platforms=platforms, + website_url=website_url, + target_audience=target_audience + ) + + # Print brief summary + print("\nContent Brief Summary:") + print(f"Title: {brief['title']}") + print(f"Content Type: {brief['content_type']}") + + print("\nOutline:") + for heading in brief['outline']['main_headings']: + print(f"\n- {heading['title']}") + print(f" Keywords: {', '.join(heading['keywords'])}") + print(f" Summary: {heading['summary']}") + + # Print subheadings + subheadings = brief['outline']['subheadings'].get(heading['title'], []) + for subheading in subheadings: + print(f" - {subheading['title']}") + print(f" Keywords: {', '.join(subheading['keywords'])}") + + print("\nKey Points:") + for point in brief['key_points']: + print(f"\n- {point['point']}") + print(f" Importance: {point['importance']}") + print(f" Evidence: {', '.join(point['supporting_evidence'])}") + + print("\nContent Flow:") + flow = brief['content_flow'] + print(f"Introduction: {flow['introduction'].get('summary', '')}") + print(f"Main Sections: {len(flow['main_sections'])} sections") + print(f"Conclusion: {flow['conclusion'].get('summary', '')}") + print(f"Transitions: {len(flow['transitions'])} transition points") + + except Exception as e: + print(f"Error generating content brief: {str(e)}") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/examples/integration_example.py b/lib/ai_seo_tools/content_calendar/examples/integration_example.py new file mode 100644 index 00000000..e17f7ea7 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/examples/integration_example.py @@ -0,0 +1,196 @@ +import logging +from datetime import datetime, timedelta +from typing import Dict, Any, List + +from ..integrations.integration_manager import IntegrationManager + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def create_cross_platform_content( + title: str, + content: str, + platforms: List[str], + content_type: str, + target_audience: Dict[str, Any], + industry: str, + keywords: List[str] +) -> Dict[str, Any]: + """Create and optimize content for multiple platforms.""" + try: + # Initialize integration manager + integration_manager = IntegrationManager() + + # Prepare content item + content_item = { + 'title': title, + 'content': content, + 'content_type': content_type, + 'keywords': keywords, + 'target_audience': target_audience, + 'industry': industry + } + + # Get platform suggestions + suggestions = integration_manager.get_platform_suggestions( + content=content_item, + platforms=platforms + ) + + # Validate content for each platform + validation_results = {} + for platform in platforms: + validation = integration_manager.validate_platform_content( + content=content_item, + platform=platform + ) + validation_results[platform] = validation + + # Optimize content for each platform + optimized_content = integration_manager.optimize_cross_platform_content( + content=content_item, + platforms=platforms + ) + + return { + 'original_content': content_item, + 'platform_suggestions': suggestions, + 'validation_results': validation_results, + 'optimized_content': optimized_content + } + + except Exception as e: + logger.error(f"Error creating cross-platform content: {str(e)}") + return { + 'error': str(e) + } + +def create_content_calendar( + start_date: datetime, + end_date: datetime, + platforms: List[str], + content_types: List[str], + target_audience: Dict[str, Any], + industry: str, + keywords: List[str] +) -> Dict[str, Any]: + """Create a cross-platform content calendar.""" + try: + # Initialize integration manager + integration_manager = IntegrationManager() + + # Create calendar + calendar = integration_manager.create_cross_platform_calendar( + start_date=start_date, + end_date=end_date, + platforms=platforms, + content_types=content_types, + target_audience=target_audience, + industry=industry, + keywords=keywords + ) + + return calendar + + except Exception as e: + logger.error(f"Error creating content calendar: {str(e)}") + return { + 'error': str(e) + } + +def main(): + """Main function to demonstrate integration manager usage.""" + # Example content details + title = "The Future of AI in Content Marketing" + content = """ + Artificial Intelligence is revolutionizing the way we approach content marketing. + From automated content generation to personalized recommendations, AI tools are + helping marketers create more engaging and effective content strategies. + + Key points: + 1. AI-powered content generation + 2. Personalized content recommendations + 3. Automated content optimization + 4. Data-driven content strategy + 5. Future trends in AI marketing + """ + + # Platform and content settings + platforms = ['instagram', 'twitter', 'linkedin', 'blog', 'facebook'] + content_type = 'article' + target_audience = { + 'age_range': '25-34', + 'interests': ['technology', 'marketing', 'AI'], + 'location': 'global', + 'profession': 'marketing professionals' + } + industry = 'technology' + keywords = ['AI', 'content marketing', 'automation', 'personalization'] + + # Create cross-platform content + logger.info("Creating cross-platform content...") + content_result = create_cross_platform_content( + title=title, + content=content, + platforms=platforms, + content_type=content_type, + target_audience=target_audience, + industry=industry, + keywords=keywords + ) + + # Print content results + logger.info("\nCross-Platform Content Results:") + logger.info("===============================") + + # Print platform suggestions + logger.info("\nPlatform Suggestions:") + for platform, suggestions in content_result['platform_suggestions'].items(): + logger.info(f"\n{platform.upper()}:") + for key, value in suggestions.items(): + logger.info(f" {key}: {value}") + + # Print validation results + logger.info("\nValidation Results:") + for platform, validation in content_result['validation_results'].items(): + logger.info(f"\n{platform.upper()}:") + logger.info(f" Valid: {validation['is_valid']}") + if not validation['is_valid']: + logger.info(f" Error: {validation.get('error', 'N/A')}") + + # Print optimized content + logger.info("\nOptimized Content:") + for platform, optimized in content_result['optimized_content'].items(): + logger.info(f"\n{platform.upper()}:") + for key, value in optimized.items(): + logger.info(f" {key}: {value}") + + # Create content calendar + logger.info("\nCreating content calendar...") + start_date = datetime.now() + end_date = start_date + timedelta(days=30) + calendar_result = create_content_calendar( + start_date=start_date, + end_date=end_date, + platforms=platforms, + content_types=[content_type], + target_audience=target_audience, + industry=industry, + keywords=keywords + ) + + # Print calendar results + logger.info("\nContent Calendar Results:") + logger.info("========================") + + # Print platform calendars + logger.info("\nPlatform Calendars:") + for platform, calendar in calendar_result['platform_calendars'].items(): + logger.info(f"\n{platform.upper()}:") + logger.info(f" Content Items: {len(calendar['content_items'])}") + for item in calendar['content_items']: + logger.info(f" - {item['original_item']['title']}") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/examples/platform_adaptation_example.py b/lib/ai_seo_tools/content_calendar/examples/platform_adaptation_example.py new file mode 100644 index 00000000..de654b2f --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/examples/platform_adaptation_example.py @@ -0,0 +1,142 @@ +from typing import Dict, Any +from datetime import datetime + +from ..integrations.platform_adapters import UnifiedPlatformAdapter + +def create_platform_content( + title: str, + content: str, + platforms: list, + context: Dict[str, Any] = None +) -> Dict[str, Any]: + """ + Create platform-specific content using the UnifiedPlatformAdapter. + + Args: + title: The title of the content + content: The main content to be adapted + platforms: List of platforms to adapt content for + context: Additional context for content adaptation + + Returns: + Dict containing adapted content for each platform + """ + # Initialize the platform adapter + adapter = UnifiedPlatformAdapter() + + # Prepare base content + base_content = { + 'title': title, + 'content': content, + 'keywords': ['content', 'marketing', 'social media'], + 'tone': 'professional', + 'cta': 'Learn More', + 'audience': 'For All', + 'language': 'English', + 'industry': 'technology', + 'word_count': 1000 + } + + # Adapt content for each platform + adapted_content = {} + for platform in platforms: + try: + platform_content = adapter.adapt_content( + content=base_content, + platform=platform, + context=context + ) + adapted_content[platform] = platform_content + except Exception as e: + print(f"Error adapting content for {platform}: {str(e)}") + adapted_content[platform] = {'error': str(e)} + + return adapted_content + +def main(): + """Example usage of platform content adaptation.""" + # Example content + title = "The Future of AI in Content Marketing" + content = """ + Artificial Intelligence is revolutionizing content marketing in unprecedented ways. + From automated content generation to personalized user experiences, AI is becoming + an indispensable tool for marketers. This article explores the latest trends and + innovations in AI-powered content marketing. + """ + + # Example context + context = { + 'target_audience': 'marketing professionals', + 'campaign_goals': ['awareness', 'engagement', 'lead generation'], + 'brand_voice': 'authoritative yet approachable', + 'content_theme': 'technology and innovation' + } + + # Platforms to adapt content for + platforms = ['instagram', 'twitter', 'linkedin', 'blog', 'facebook'] + + # Generate platform-specific content + adapted_content = create_platform_content( + title=title, + content=content, + platforms=platforms, + context=context + ) + + # Print results + print("\nPlatform-Specific Content Adaptation Results:") + print("=" * 50) + + for platform, content in adapted_content.items(): + print(f"\n{platform.upper()} Content:") + print("-" * 30) + + if 'error' in content: + print(f"Error: {content['error']}") + continue + + # Print platform-specific content + if platform == 'instagram': + print("\nCaptions:") + for caption in content['captions']: + print(f"- {caption}") + print("\nHashtags:") + print(content['hashtags']) + + elif platform == 'twitter': + print("\nTweets:") + for tweet in content['tweets']: + print(f"- {tweet}") + print("\nThread Structure:") + print(content['thread_structure']) + + elif platform == 'linkedin': + print("\nPost:") + print(content['post']) + print("\nEngagement Optimization:") + print(content['engagement_optimization']) + + elif platform == 'blog': + print("\nPost:") + print(content['post']) + print("\nSEO Optimization:") + print(content['seo_optimization']) + + elif platform == 'facebook': + print("\nPost:") + print(content['post']) + print("\nEngagement Optimization:") + print(content['engagement_optimization']) + + # Print media suggestions + print("\nMedia Suggestions:") + for media in content['media_suggestions']: + print(f"- {media['type']}: {media['description']}") + + # Print platform-specific recommendations + print("\nPlatform-Specific Recommendations:") + for key, value in content['platform_specific'].items(): + print(f"- {key}: {value}") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/integrations/gap_analyzer.py b/lib/ai_seo_tools/content_calendar/integrations/gap_analyzer.py new file mode 100644 index 00000000..78dd0051 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/integrations/gap_analyzer.py @@ -0,0 +1,127 @@ +""" +Gap analyzer integration for content calendar. +""" + +import streamlit as st +from typing import Dict, Any, List, Optional +from loguru import logger +from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer +from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis +import asyncio +import sys +import os +import json +from datetime import datetime + +# Configure logger for content calendar debugging +logger.remove() # Remove default handler +logger.add( + sys.stdout, + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name} | {function} | {message}", + filter=lambda record: "content_calendar" in record["name"].lower() +) + +class GapAnalyzerIntegration: + """Integrates content gap analysis with content calendar.""" + + def __init__(self): + """Initialize the gap analyzer integration.""" + self.gap_analyzer = ContentGapAnalysis() + logger.debug("GapAnalyzerIntegration initialized for content calendar") + + def analyze_gaps(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Analyze content gaps. + + Args: + data: Dictionary containing content data + + Returns: + Dictionary containing gap analysis results + """ + try: + logger.debug(f"Starting gap analysis with data: {json.dumps(data, indent=2)}") + # Run gap analysis + results = self.gap_analyzer.analyze(data) + logger.debug(f"Gap analysis completed with results: {json.dumps(results, indent=2)}") + return results + + except Exception as e: + error_msg = f"Error analyzing content gaps: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + 'error': error_msg, + 'gaps': [], + 'recommendations': [] + } + + def get_topic_suggestions( + self, + gap_analysis: Dict[str, Any], + platform: str, + count: int = 5 + ) -> List[Dict[str, Any]]: + """ + Get topic suggestions for a specific platform based on gap analysis. + + Args: + gap_analysis: Results from gap analysis + platform: Target platform for content + count: Number of suggestions to generate + + Returns: + List of topic suggestions + """ + try: + logger.debug(f"Generating topic suggestions for platform: {platform}, count: {count}") + suggestions = [] + + for gap in gap_analysis.get('processed_gaps', []): + # Generate platform-specific topics + platform_topics = self.ai_processor.generate_platform_topics( + gap=gap, + platform=platform, + count=count + ) + logger.debug(f"Generated topics for gap: {json.dumps(platform_topics, indent=2)}") + suggestions.extend(platform_topics) + + logger.debug(f"Total suggestions generated: {len(suggestions)}") + return suggestions + + except Exception as e: + logger.error(f"Error generating topic suggestions: {str(e)}") + return [] + + def analyze_topic_relevance( + self, + topic: Dict[str, Any], + gap_analysis: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Analyze how well a topic addresses content gaps. + + Args: + topic: Topic to analyze + gap_analysis: Results from gap analysis + + Returns: + Dictionary containing relevance analysis + """ + try: + logger.debug(f"Analyzing topic relevance: {json.dumps(topic, indent=2)}") + relevance = self.ai_processor.analyze_topic_relevance( + topic=topic, + gaps=gap_analysis.get('gaps', []) + ) + + logger.debug(f"Topic relevance analysis completed: {json.dumps(relevance, indent=2)}") + return relevance + + except Exception as e: + logger.error(f"Error analyzing topic relevance: {str(e)}") + return { + 'error': str(e), + 'score': 0 + } \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/integrations/integration_manager.py b/lib/ai_seo_tools/content_calendar/integrations/integration_manager.py new file mode 100644 index 00000000..f8a5bec5 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/integrations/integration_manager.py @@ -0,0 +1,196 @@ +import logging +from typing import Dict, List, Any, Optional +from datetime import datetime, timedelta + +from ..core.calendar_manager import CalendarManager +from ..core.content_brief import ContentBriefGenerator +from .platform_adapters import UnifiedPlatformAdapter + +logger = logging.getLogger(__name__) + +class IntegrationManager: + """Manages integration between content calendar and platform adapters.""" + + def __init__(self): + """Initialize the integration manager.""" + self.calendar_manager = CalendarManager() + self.content_brief_generator = ContentBriefGenerator() + self.platform_adapter = UnifiedPlatformAdapter() + + def create_cross_platform_calendar( + self, + start_date: datetime, + end_date: datetime, + platforms: List[str], + content_types: List[str], + target_audience: Optional[Dict[str, Any]] = None, + industry: Optional[str] = None, + keywords: Optional[List[str]] = None + ) -> Dict[str, Any]: + """Create a cross-platform content calendar.""" + try: + # Generate base calendar + calendar = self.calendar_manager.create_calendar( + start_date=start_date, + end_date=end_date, + content_types=content_types, + target_audience=target_audience, + industry=industry, + keywords=keywords + ) + + # Adapt content for each platform + platform_calendars = {} + for platform in platforms: + platform_calendars[platform] = self._adapt_calendar_for_platform( + calendar=calendar, + platform=platform + ) + + return { + 'base_calendar': calendar, + 'platform_calendars': platform_calendars, + 'metadata': { + 'start_date': start_date, + 'end_date': end_date, + 'platforms': platforms, + 'content_types': content_types, + 'industry': industry, + 'keywords': keywords + } + } + + except Exception as e: + logger.error(f"Error creating cross-platform calendar: {str(e)}") + raise + + def _adapt_calendar_for_platform( + self, + calendar: Dict[str, Any], + platform: str + ) -> Dict[str, Any]: + """Adapt calendar content for a specific platform.""" + try: + adapted_calendar = { + 'platform': platform, + 'content_items': [], + 'metadata': calendar.get('metadata', {}) + } + + # Adapt each content item + for item in calendar.get('content_items', []): + adapted_item = self._adapt_content_item(item, platform) + if adapted_item: + adapted_calendar['content_items'].append(adapted_item) + + return adapted_calendar + + except Exception as e: + logger.error(f"Error adapting calendar for platform {platform}: {str(e)}") + return { + 'platform': platform, + 'content_items': [], + 'error': str(e) + } + + def _adapt_content_item( + self, + item: Dict[str, Any], + platform: str + ) -> Optional[Dict[str, Any]]: + """Adapt a content item for a specific platform.""" + try: + # Generate content brief if not exists + if 'brief' not in item: + item['brief'] = self.content_brief_generator.generate_brief(item) + + # Adapt content for platform + adapted_content = self.platform_adapter.adapt_content( + content=item, + platform=platform + ) + + if adapted_content: + return { + 'original_item': item, + 'adapted_content': adapted_content, + 'platform_specifics': self.platform_adapter.get_platform_specs(platform) + } + + return None + + except Exception as e: + logger.error(f"Error adapting content item for platform {platform}: {str(e)}") + return None + + def get_platform_suggestions( + self, + content: Dict[str, Any], + platforms: List[str] + ) -> Dict[str, Any]: + """Get platform-specific suggestions for content.""" + try: + suggestions = {} + + for platform in platforms: + platform_suggestions = self.platform_adapter.get_platform_suggestions( + content=content, + platform=platform + ) + if platform_suggestions: + suggestions[platform] = platform_suggestions + + return suggestions + + except Exception as e: + logger.error(f"Error getting platform suggestions: {str(e)}") + return {} + + def validate_platform_content( + self, + content: Dict[str, Any], + platform: str + ) -> Dict[str, Any]: + """Validate content for a specific platform.""" + try: + validation_result = self.platform_adapter.validate_content( + content=content, + platform=platform + ) + + return { + 'platform': platform, + 'is_valid': validation_result, + 'specifications': self.platform_adapter.get_platform_specs(platform) + } + + except Exception as e: + logger.error(f"Error validating platform content: {str(e)}") + return { + 'platform': platform, + 'is_valid': False, + 'error': str(e) + } + + def optimize_cross_platform_content( + self, + content: Dict[str, Any], + platforms: List[str] + ) -> Dict[str, Any]: + """Optimize content for multiple platforms.""" + try: + optimized_content = {} + + for platform in platforms: + platform_optimized = self.platform_adapter.optimize_content( + content=content, + platform=platform + ) + if platform_optimized: + optimized_content[platform] = platform_optimized + + return optimized_content + + except Exception as e: + logger.error(f"Error optimizing cross-platform content: {str(e)}") + return {} \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/integrations/platform_adapters.py b/lib/ai_seo_tools/content_calendar/integrations/platform_adapters.py new file mode 100644 index 00000000..641f87bb --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/integrations/platform_adapters.py @@ -0,0 +1,137 @@ +""" +Platform adapters for content calendar. +""" + +import streamlit as st +from typing import Dict, Any, List, Optional +from loguru import logger +from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer +from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis +from lib.ai_seo_tools.content_title_generator import ai_title_generator +from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main +from lib.ai_seo_tools.seo_structured_data import ai_structured_data +import asyncio +import sys +import os +import json + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/platform_adapters.log", + rotation="50 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}" +) + +# Ensure logs directory exists +os.makedirs("logs", exist_ok=True) + +class UnifiedPlatformAdapter: + """Unified adapter for different social media platforms.""" + + def __init__(self): + """Initialize the platform adapter.""" + self.platform_handlers = { + 'instagram': self._handle_instagram, + 'linkedin': self._handle_linkedin, + 'twitter': self._handle_twitter, + 'facebook': self._handle_facebook + } + logger.info("UnifiedPlatformAdapter initialized") + + def generate_content(self, platform: str, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate content for a specific platform. + + Args: + platform: Target platform + data: Content data + + Returns: + Dictionary containing generated content + """ + try: + handler = self.platform_handlers.get(platform.lower()) + if not handler: + raise ValueError(f"Unsupported platform: {platform}") + + return handler(data) + + except Exception as e: + error_msg = f"Error generating content for {platform}: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + 'error': error_msg, + 'content': None + } + + def _handle_instagram(self, data: Dict[str, Any]) -> Dict[str, Any]: + """Handle Instagram content generation.""" + try: + # Use content title generator for Instagram captions + caption = ai_title_generator(data) + return { + 'platform': 'instagram', + 'content': caption + } + except Exception as e: + logger.error(f"Error generating Instagram content: {str(e)}") + return { + 'platform': 'instagram', + 'error': str(e) + } + + def _handle_linkedin(self, data: Dict[str, Any]) -> Dict[str, Any]: + """Handle LinkedIn content generation.""" + try: + # Use meta description generator for LinkedIn posts + post = metadesc_generator_main(data) + return { + 'platform': 'linkedin', + 'content': post + } + except Exception as e: + logger.error(f"Error generating LinkedIn content: {str(e)}") + return { + 'platform': 'linkedin', + 'error': str(e) + } + + def _handle_twitter(self, data: Dict[str, Any]) -> Dict[str, Any]: + """Handle Twitter content generation.""" + try: + # Use content title generator for tweets + tweet = ai_title_generator(data) + return { + 'platform': 'twitter', + 'content': tweet + } + except Exception as e: + logger.error(f"Error generating Twitter content: {str(e)}") + return { + 'platform': 'twitter', + 'error': str(e) + } + + def _handle_facebook(self, data: Dict[str, Any]) -> Dict[str, Any]: + """Handle Facebook content generation.""" + try: + # Use meta description generator for Facebook posts + post = metadesc_generator_main(data) + return { + 'platform': 'facebook', + 'content': post + } + except Exception as e: + logger.error(f"Error generating Facebook content: {str(e)}") + return { + 'platform': 'facebook', + 'error': str(e) + } \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/integrations/seo_optimizer.py b/lib/ai_seo_tools/content_calendar/integrations/seo_optimizer.py new file mode 100644 index 00000000..877dba9b --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/integrations/seo_optimizer.py @@ -0,0 +1,219 @@ +import logging +from typing import Dict, Any, List, Optional +from datetime import datetime + +from ...meta_desc_generator import generate_blog_metadesc +from ...content_title_generator import generate_blog_titles +from ...seo_structured_data import generate_json_data + +logger = logging.getLogger(__name__) + +class SEOOptimizer: + """Integrates SEO tools with content calendar system.""" + + def __init__(self): + """Initialize the SEO optimizer.""" + self._setup_logging() + + def _setup_logging(self): + """Configure logging for SEO optimizer.""" + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + + def optimize_content( + self, + content: Dict[str, Any], + content_type: str = 'article', + language: str = 'English', + search_intent: str = 'Informational Intent' + ) -> Dict[str, Any]: + """ + Optimize content for SEO using existing tools. + + Args: + content: Content to optimize + content_type: Type of content (article, product, etc.) + language: Content language + search_intent: Search intent type + + Returns: + Optimized content with SEO elements + """ + try: + # Extract content details + title = content.get('title', '') + keywords = content.get('keywords', []) + content_text = content.get('content', '') + + # Generate SEO elements + optimized_title = self._optimize_title( + title=title, + keywords=keywords, + content_type=content_type, + language=language, + search_intent=search_intent + ) + + meta_description = self._generate_meta_description( + keywords=keywords, + content_type=content_type, + language=language, + search_intent=search_intent + ) + + structured_data = self._generate_structured_data( + content=content, + content_type=content_type + ) + + return { + 'original_content': content, + 'seo_optimized': { + 'title': optimized_title, + 'meta_description': meta_description, + 'structured_data': structured_data, + 'keywords': keywords, + 'content_type': content_type, + 'language': language, + 'search_intent': search_intent + } + } + + except Exception as e: + logger.error(f"Error optimizing content: {str(e)}") + return { + 'error': str(e) + } + + def _optimize_title( + self, + title: str, + keywords: List[str], + content_type: str, + language: str, + search_intent: str + ) -> List[str]: + """Generate SEO-optimized titles.""" + try: + # Convert keywords list to comma-separated string + keywords_str = ', '.join(keywords) + + # Generate titles using existing tool + titles = generate_blog_titles( + input_blog_keywords=keywords_str, + input_blog_content=title, + input_title_type=content_type, + input_title_intent=search_intent, + input_language=language + ) + + return titles.split('\n') if titles else [] + + except Exception as e: + logger.error(f"Error optimizing title: {str(e)}") + return [] + + def _generate_meta_description( + self, + keywords: List[str], + content_type: str, + language: str, + search_intent: str + ) -> List[str]: + """Generate SEO-optimized meta descriptions.""" + try: + # Convert keywords list to comma-separated string + keywords_str = ', '.join(keywords) + + # Generate meta descriptions using existing tool + descriptions = generate_blog_metadesc( + keywords=keywords_str, + tone='Informative', + search_type=search_intent, + language=language + ) + + return descriptions.split('\n') if descriptions else [] + + except Exception as e: + logger.error(f"Error generating meta description: {str(e)}") + return [] + + def _generate_structured_data( + self, + content: Dict[str, Any], + content_type: str + ) -> Optional[Dict[str, Any]]: + """Generate structured data for content.""" + try: + # Prepare content details for structured data + details = { + 'Headline': content.get('title', ''), + 'Author': content.get('author', ''), + 'Date Published': content.get('publish_date', datetime.now().isoformat()), + 'Keywords': ', '.join(content.get('keywords', [])), + 'Description': content.get('description', ''), + 'Image URL': content.get('image_url', '') + } + + # Generate structured data using existing tool + structured_data = generate_json_data( + content_type=content_type, + details=details, + url=content.get('url', '') + ) + + return structured_data + + except Exception as e: + logger.error(f"Error generating structured data: {str(e)}") + return None + + def optimize_calendar_content( + self, + calendar: Dict[str, Any], + content_type: str = 'article', + language: str = 'English', + search_intent: str = 'Informational Intent' + ) -> Dict[str, Any]: + """ + Optimize all content in calendar for SEO. + + Args: + calendar: Content calendar to optimize + content_type: Type of content + language: Content language + search_intent: Search intent type + + Returns: + Calendar with SEO-optimized content + """ + try: + optimized_calendar = { + 'metadata': calendar.get('metadata', {}), + 'content_items': [] + } + + # Optimize each content item + for item in calendar.get('content_items', []): + optimized_item = self.optimize_content( + content=item, + content_type=content_type, + language=language, + search_intent=search_intent + ) + if optimized_item: + optimized_calendar['content_items'].append(optimized_item) + + return optimized_calendar + + except Exception as e: + logger.error(f"Error optimizing calendar content: {str(e)}") + return { + 'error': str(e) + } \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/integrations/seo_tools.py b/lib/ai_seo_tools/content_calendar/integrations/seo_tools.py new file mode 100644 index 00000000..29298d16 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/integrations/seo_tools.py @@ -0,0 +1,143 @@ +"""SEO tools integration for content calendar.""" + +import streamlit as st +from loguru import logger +from typing import Dict, Any, List, Optional +import asyncio +import sys +import os +from lib.ai_seo_tools.content_title_generator import ai_title_generator +from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/seo_tools_integration.log", + rotation="50 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}" +) + +# Ensure logs directory exists +os.makedirs("logs", exist_ok=True) + +class SEOToolsIntegration: + """Integration with SEO tools for content calendar.""" + + def __init__(self): + """Initialize the SEO tools integration.""" + self.website_analyzer = WebsiteAnalyzer() + logger.info("SEOToolsIntegration initialized") + + def analyze_content(self, url: str) -> Dict[str, Any]: + """ + Analyze content for SEO optimization. + + Args: + url: The URL to analyze + + Returns: + Dictionary containing SEO analysis results + """ + try: + # Analyze website + analysis = self.website_analyzer.analyze_website(url) + if not analysis.get('success', False): + return { + 'error': analysis.get('error', 'Unknown error in analysis'), + 'seo_score': 0, + 'recommendations': [] + } + + # Extract SEO information + seo_info = analysis['data']['analysis']['seo_info'] + + return { + 'seo_score': seo_info.get('overall_score', 0), + 'meta_tags': seo_info.get('meta_tags', {}), + 'content': seo_info.get('content', {}), + 'recommendations': seo_info.get('recommendations', []) + } + + except Exception as e: + error_msg = f"Error analyzing content: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + 'error': error_msg, + 'seo_score': 0, + 'recommendations': [] + } + + def generate_title(self, url: str) -> Dict[str, Any]: + """ + Generate SEO-optimized title. + + Args: + url: The URL to analyze + + Returns: + Dictionary containing title suggestions + """ + return ai_title_generator(url) + + def optimize_content(self, content: str, keywords: List[str]) -> Dict[str, Any]: + """ + Optimize content for SEO. + + Args: + content: The content to optimize + keywords: List of target keywords + + Returns: + Dictionary containing optimization suggestions + """ + try: + # Prepare prompt for content optimization + prompt = f"""Optimize the following content for SEO: + + Content: {content} + Target Keywords: {', '.join(keywords)} + + Provide optimization suggestions for: + 1. Keyword usage and placement + 2. Content structure and readability + 3. Meta information + 4. Internal linking opportunities + 5. Content length and depth + + Format the response as JSON with 'suggestions' and 'score' keys.""" + + # Get AI optimization suggestions + suggestions = llm_text_gen( + prompt=prompt, + system_prompt="You are an SEO expert specializing in content optimization.", + response_format="json_object" + ) + + if not suggestions: + return { + 'error': 'Failed to generate optimization suggestions', + 'suggestions': [], + 'score': 0 + } + + return { + 'suggestions': suggestions.get('suggestions', []), + 'score': suggestions.get('score', 0) + } + + except Exception as e: + error_msg = f"Error optimizing content: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + 'error': error_msg, + 'suggestions': [], + 'score': 0 + } \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/models/calendar.py b/lib/ai_seo_tools/content_calendar/models/calendar.py new file mode 100644 index 00000000..8e1713e3 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/models/calendar.py @@ -0,0 +1,237 @@ +import logging +import sys + +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout), + logging.FileHandler('content_calendar_debug.log', mode='a') + ] +) +logger = logging.getLogger(__name__) + +from datetime import datetime +from typing import Dict, List, Any, Optional +from dataclasses import dataclass, field +from enum import Enum +import pandas as pd + +class ContentType(Enum): + """Types of content that can be scheduled.""" + BLOG_POST = "blog_post" + SOCIAL_MEDIA = "social_media" + VIDEO = "video" + PODCAST = "podcast" + NEWSLETTER = "newsletter" + LANDING_PAGE = "landing_page" + +class Platform(Enum): + """Supported content platforms.""" + WEBSITE = "website" + FACEBOOK = "facebook" + TWITTER = "twitter" + LINKEDIN = "linkedin" + INSTAGRAM = "instagram" + YOUTUBE = "youtube" + MEDIUM = "medium" + +@dataclass +class SEOData: + """SEO-related data for content.""" + title: str + meta_description: str + keywords: List[str] + structured_data: Dict[str, Any] + canonical_url: Optional[str] = None + og_tags: Optional[Dict[str, str]] = None + twitter_cards: Optional[Dict[str, str]] = None + + @staticmethod + def from_dict(data): + return SEOData( + title=data.get('title', ''), + meta_description=data.get('meta_description', ''), + keywords=data.get('keywords', []), + structured_data=data.get('structured_data', {}), + canonical_url=data.get('canonical_url'), + og_tags=data.get('og_tags'), + twitter_cards=data.get('twitter_cards') + ) + +@dataclass +class ContentItem: + """Represents a single content item in the calendar.""" + title: str + description: str + content_type: ContentType + platforms: List[Platform] + publish_date: datetime + seo_data: SEOData + status: str = "draft" + author: Optional[str] = None + tags: List[str] = field(default_factory=list) + notes: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + """Convert content item to dictionary.""" + return { + 'title': self.title, + 'description': self.description, + 'content_type': self.content_type.value, + 'platforms': [p.value for p in self.platforms], + 'publish_date': self.publish_date.isoformat(), + 'seo_data': { + 'title': self.seo_data.title, + 'meta_description': self.seo_data.meta_description, + 'keywords': self.seo_data.keywords, + 'structured_data': self.seo_data.structured_data, + 'canonical_url': self.seo_data.canonical_url, + 'og_tags': self.seo_data.og_tags, + 'twitter_cards': self.seo_data.twitter_cards + }, + 'status': self.status, + 'author': self.author, + 'tags': self.tags, + 'notes': self.notes + } + + @staticmethod + def from_dict(data): + from .calendar import ContentType, Platform, SEOData + return ContentItem( + title=data['title'], + description=data.get('description', ''), + content_type=ContentType(data['content_type']), + platforms=[Platform(p) for p in data['platforms']], + publish_date=pd.to_datetime(data['publish_date']), + seo_data=SEOData.from_dict(data.get('seo_data', {})), + status=data.get('status', 'draft'), + author=data.get('author'), + tags=data.get('tags', []), + notes=data.get('notes') + ) + +@dataclass +class Calendar: + """Represents a content calendar.""" + start_date: datetime + duration: str # 'weekly', 'monthly', 'quarterly' + platforms: List[Platform] + schedule: Dict[str, List[ContentItem]] + name: Optional[str] = None + description: Optional[str] = None + + def __init__(self, start_date: datetime, duration: str, platforms: List[Platform], + schedule: Dict[str, List[ContentItem]], name: Optional[str] = None, + description: Optional[str] = None): + """Initialize a new calendar. + + Args: + start_date: Start date of the calendar + duration: Duration of the calendar ('weekly', 'monthly', 'quarterly') + platforms: List of platforms to schedule content for + schedule: Dictionary mapping dates to content items + name: Optional name for the calendar + description: Optional description of the calendar + """ + self.start_date = start_date + self.duration = duration + self.platforms = platforms + self.schedule = schedule + self.name = name + self.description = description + self.content_items: List[ContentItem] = [] + self.logger = logging.getLogger('content_calendar.calendar') + + # Initialize content_items from schedule + for items in self.schedule.values(): + self.content_items.extend(items) + + def get_all_content(self) -> List[ContentItem]: + """Get all content items in the calendar. + + Returns: + List of all ContentItem objects in the calendar + """ + try: + self.logger.debug(f"Getting all content items. Count: {len(self.content_items)}") + return self.content_items + except Exception as e: + self.logger.error(f"Error getting all content: {str(e)}") + return [] + + def to_dict(self) -> Dict[str, Any]: + """Convert calendar to dictionary.""" + return { + 'name': self.name, + 'description': self.description, + 'start_date': self.start_date.isoformat(), + 'duration': self.duration, + 'platforms': [p.value for p in self.platforms], + 'schedule': { + date: [item.to_dict() for item in items] + for date, items in self.schedule.items() + } + } + + def export(self, format: str = 'json') -> Dict[str, Any]: + """ + Export calendar in specified format. + Currently only supports JSON format. + """ + if format.lower() != 'json': + raise ValueError(f"Unsupported export format: {format}") + + return self.to_dict() + + def get_content_for_date(self, date: datetime) -> List[ContentItem]: + """Get all content items scheduled for a specific date.""" + date_str = date.strftime('%Y-%m-%d') + return self.schedule.get(date_str, []) + + def get_content_for_platform( + self, + platform: Platform + ) -> List[ContentItem]: + """Get all content items for a specific platform.""" + all_content = [] + for items in self.schedule.values(): + platform_content = [ + item for item in items + if platform in item.platforms + ] + all_content.extend(platform_content) + return all_content + + def add_content(self, content: ContentItem) -> None: + """Add a new content item to the calendar.""" + date_str = content.publish_date.strftime('%Y-%m-%d') + if date_str not in self.schedule: + self.schedule[date_str] = [] + self.schedule[date_str].append(content) + + def remove_content(self, content: ContentItem) -> None: + """Remove a content item from the calendar.""" + date_str = content.publish_date.strftime('%Y-%m-%d') + if date_str in self.schedule: + self.schedule[date_str] = [ + item for item in self.schedule[date_str] + if item != content + ] + + @staticmethod + def from_dict(data): + from .calendar import ContentItem, Platform + schedule = { + date: [ContentItem.from_dict(item) for item in items] + for date, items in data.get('schedule', {}).items() + } + return Calendar( + start_date=pd.to_datetime(data['start_date']), + duration=data['duration'], + platforms=[Platform(p) for p in data['platforms']], + schedule=schedule, + name=data.get('name'), + description=data.get('description') + ) \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/tests/test_ai_generator.py b/lib/ai_seo_tools/content_calendar/tests/test_ai_generator.py new file mode 100644 index 00000000..7773e1dc --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/tests/test_ai_generator.py @@ -0,0 +1,185 @@ +import unittest +from typing import Dict, Any + +from ..models.calendar import ContentType +from ..core.ai_generator import AIContentGenerator + +class TestAIContentGenerator(unittest.TestCase): + """Test cases for AIContentGenerator.""" + + def setUp(self): + """Set up test cases.""" + self.generator = AIContentGenerator() + self.test_title = "10 Ways to Improve Your SEO Strategy" + self.test_content_type = ContentType.BLOG_POST + self.test_context = { + "website_url": "https://example.com", + "target_audience": "digital marketers", + "content_goals": ["educate", "generate leads"] + } + + def test_generate_headings(self): + """Test heading generation.""" + headings = self.generator.generate_headings( + title=self.test_title, + content_type=self.test_content_type, + context=self.test_context + ) + + self.assertIsInstance(headings, list) + for heading in headings: + self.assertIn('title', heading) + self.assertIn('level', heading) + self.assertIn('keywords', heading) + self.assertIn('summary', heading) + + # Verify heading level + self.assertEqual(heading['level'], 1) + + # Verify heading content + self.assertIsInstance(heading['title'], str) + self.assertIsInstance(heading['keywords'], list) + self.assertIsInstance(heading['summary'], str) + + def test_generate_subheadings(self): + """Test subheading generation.""" + main_heading = { + 'title': 'Understanding SEO Basics', + 'level': 1, + 'keywords': ['SEO', 'basics', 'fundamentals'], + 'summary': 'Introduction to core SEO concepts' + } + + subheadings = self.generator.generate_subheadings( + main_heading=main_heading, + content_type=self.test_content_type, + context=self.test_context + ) + + self.assertIsInstance(subheadings, list) + for subheading in subheadings: + self.assertIn('title', subheading) + self.assertIn('level', subheading) + self.assertIn('keywords', subheading) + self.assertIn('summary', subheading) + + # Verify subheading level + self.assertEqual(subheading['level'], 2) + + # Verify subheading content + self.assertIsInstance(subheading['title'], str) + self.assertIsInstance(subheading['keywords'], list) + self.assertIsInstance(subheading['summary'], str) + + def test_generate_key_points(self): + """Test key points generation.""" + key_points = self.generator.generate_key_points( + title=self.test_title, + content_type=self.test_content_type, + context=self.test_context + ) + + self.assertIsInstance(key_points, list) + for point in key_points: + self.assertIn('point', point) + self.assertIn('importance', point) + self.assertIn('supporting_evidence', point) + self.assertIn('related_keywords', point) + + # Verify point content + self.assertIsInstance(point['point'], str) + self.assertIn(point['importance'], ['high', 'medium', 'low']) + self.assertIsInstance(point['supporting_evidence'], list) + self.assertIsInstance(point['related_keywords'], list) + + def test_generate_content_flow(self): + """Test content flow generation.""" + outline = { + 'main_headings': [ + { + 'title': 'Introduction', + 'level': 1, + 'keywords': ['SEO', 'introduction'], + 'summary': 'Overview of SEO importance' + } + ], + 'subheadings': { + 'Introduction': [ + { + 'title': 'What is SEO?', + 'level': 2, + 'keywords': ['definition', 'basics'], + 'summary': 'Basic definition of SEO' + } + ] + } + } + + flow = self.generator.generate_content_flow( + title=self.test_title, + content_type=self.test_content_type, + outline=outline + ) + + self.assertIsInstance(flow, dict) + self.assertIn('introduction', flow) + self.assertIn('main_sections', flow) + self.assertIn('conclusion', flow) + self.assertIn('transitions', flow) + self.assertIn('content_pacing', flow) + + # Verify flow content + self.assertIsInstance(flow['introduction'], dict) + self.assertIsInstance(flow['main_sections'], list) + self.assertIsInstance(flow['conclusion'], dict) + self.assertIsInstance(flow['transitions'], list) + self.assertIsInstance(flow['content_pacing'], dict) + + def test_prompt_creation(self): + """Test prompt creation methods.""" + # Test heading prompt + heading_prompt = self.generator._create_heading_prompt( + title=self.test_title, + content_type=self.test_content_type, + gaps={'opportunities': ['keyword research', 'content optimization']} + ) + self.assertIsInstance(heading_prompt, str) + self.assertIn(self.test_title, heading_prompt) + self.assertIn(self.test_content_type.value, heading_prompt) + + # Test subheading prompt + main_heading = { + 'title': 'Understanding SEO Basics', + 'level': 1, + 'keywords': ['SEO', 'basics'], + 'summary': 'Introduction to SEO' + } + subheading_prompt = self.generator._create_subheading_prompt( + main_heading=main_heading, + content_type=self.test_content_type, + context=self.test_context + ) + self.assertIsInstance(subheading_prompt, str) + self.assertIn(main_heading['title'], subheading_prompt) + + # Test key points prompt + key_points_prompt = self.generator._create_key_points_prompt( + title=self.test_title, + content_type=self.test_content_type, + seo_data={'keywords': ['SEO', 'strategy']}, + context=self.test_context + ) + self.assertIsInstance(key_points_prompt, str) + self.assertIn(self.test_title, key_points_prompt) + + # Test flow prompt + flow_prompt = self.generator._create_flow_prompt( + title=self.test_title, + content_type=self.test_content_type, + outline={'main_headings': []} + ) + self.assertIsInstance(flow_prompt, str) + self.assertIn(self.test_title, flow_prompt) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/tests/test_content_brief.py b/lib/ai_seo_tools/content_calendar/tests/test_content_brief.py new file mode 100644 index 00000000..f5105da5 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/tests/test_content_brief.py @@ -0,0 +1,132 @@ +import unittest +from datetime import datetime +from typing import Dict, Any + +from ..models.calendar import ContentItem, ContentType, Platform, SEOData +from ..core.content_brief import ContentBriefGenerator + +class TestContentBriefGenerator(unittest.TestCase): + """Test cases for ContentBriefGenerator.""" + + def setUp(self): + """Set up test cases.""" + self.generator = ContentBriefGenerator() + self.test_content_item = self._create_test_content_item() + + def _create_test_content_item(self) -> ContentItem: + """Create a test content item.""" + return ContentItem( + id="test-001", + title="10 Ways to Improve Your SEO Strategy", + description="A comprehensive guide to enhancing your website's SEO performance", + content_type=ContentType.BLOG_POST, + platforms=[Platform.WEBSITE, Platform.LINKEDIN], + publish_date=datetime.now(), + seo_data=SEOData( + keywords=["SEO", "search engine optimization", "digital marketing"], + meta_description="Learn effective SEO strategies to boost your website's visibility", + structured_data={} + ), + platform_specs={ + "website": { + "format": "blog post", + "min_length": 1500 + }, + "linkedin": { + "format": "article", + "min_length": 800 + } + }, + context={ + "website_url": "https://example.com", + "target_audience": "digital marketers", + "content_goals": ["educate", "generate leads"] + } + ) + + def test_generate_brief(self): + """Test content brief generation.""" + # Generate brief + brief = self.generator.generate_brief( + content_item=self.test_content_item, + target_audience={ + "demographics": { + "age_range": "25-45", + "profession": "digital marketers" + }, + "interests": ["SEO", "content marketing", "digital strategy"], + "pain_points": [ + "low search rankings", + "poor content performance", + "lack of organic traffic" + ] + } + ) + + # Verify brief structure + self.assertIsInstance(brief, dict) + self.assertIn('title', brief) + self.assertIn('content_type', brief) + self.assertIn('outline', brief) + self.assertIn('key_points', brief) + self.assertIn('content_flow', brief) + self.assertIn('target_audience', brief) + self.assertIn('seo_data', brief) + self.assertIn('platform_specs', brief) + + # Verify outline structure + outline = brief['outline'] + self.assertIn('main_headings', outline) + self.assertIn('subheadings', outline) + + # Verify key points + self.assertIsInstance(brief['key_points'], list) + + # Verify content flow + flow = brief['content_flow'] + self.assertIn('introduction', flow) + self.assertIn('main_sections', flow) + self.assertIn('conclusion', flow) + self.assertIn('transitions', flow) + self.assertIn('content_pacing', flow) + + def test_generate_brief_without_audience(self): + """Test content brief generation without target audience data.""" + brief = self.generator.generate_brief( + content_item=self.test_content_item + ) + + self.assertIsInstance(brief, dict) + self.assertIn('target_audience', brief) + self.assertEqual(brief['target_audience'], {}) + + def test_generate_outline(self): + """Test outline generation.""" + outline = self.generator._generate_outline(self.test_content_item) + + self.assertIsInstance(outline, dict) + self.assertIn('main_headings', outline) + self.assertIn('subheadings', outline) + + # Verify main headings + main_headings = outline['main_headings'] + self.assertIsInstance(main_headings, list) + for heading in main_headings: + self.assertIn('title', heading) + self.assertIn('level', heading) + self.assertIn('keywords', heading) + self.assertIn('summary', heading) + + # Verify subheadings + subheadings = outline['subheadings'] + self.assertIsInstance(subheadings, dict) + for heading_title, heading_subheadings in subheadings.items(): + self.assertIsInstance(heading_subheadings, list) + for subheading in heading_subheadings: + self.assertIn('title', subheading) + self.assertIn('level', subheading) + self.assertIn('keywords', subheading) + self.assertIn('summary', subheading) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/tests/test_integration_manager.py b/lib/ai_seo_tools/content_calendar/tests/test_integration_manager.py new file mode 100644 index 00000000..869b5ad0 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/tests/test_integration_manager.py @@ -0,0 +1,171 @@ +import unittest +from datetime import datetime, timedelta +from typing import Dict, Any + +from ..integrations.integration_manager import IntegrationManager + +class TestIntegrationManager(unittest.TestCase): + """Test cases for the IntegrationManager class.""" + + def setUp(self): + """Set up test fixtures.""" + self.integration_manager = IntegrationManager() + self.start_date = datetime.now() + self.end_date = self.start_date + timedelta(days=30) + self.platforms = ['instagram', 'twitter', 'linkedin', 'blog', 'facebook'] + self.content_types = ['article', 'social', 'video'] + self.target_audience = { + 'age_range': '25-34', + 'interests': ['technology', 'marketing'], + 'location': 'global' + } + self.industry = 'technology' + self.keywords = ['AI', 'content marketing', 'social media'] + + # Sample content item + self.sample_content = { + 'title': 'The Future of AI in Content Marketing', + 'content': 'AI is revolutionizing content marketing...', + 'content_type': 'article', + 'keywords': ['AI', 'content marketing', 'automation'], + 'target_audience': self.target_audience, + 'industry': self.industry + } + + def test_create_cross_platform_calendar(self): + """Test creating a cross-platform content calendar.""" + calendar = self.integration_manager.create_cross_platform_calendar( + start_date=self.start_date, + end_date=self.end_date, + platforms=self.platforms, + content_types=self.content_types, + target_audience=self.target_audience, + industry=self.industry, + keywords=self.keywords + ) + + # Check basic structure + self.assertIn('base_calendar', calendar) + self.assertIn('platform_calendars', calendar) + self.assertIn('metadata', calendar) + + # Check platform calendars + platform_calendars = calendar['platform_calendars'] + self.assertEqual(len(platform_calendars), len(self.platforms)) + + for platform in self.platforms: + self.assertIn(platform, platform_calendars) + platform_calendar = platform_calendars[platform] + self.assertIn('content_items', platform_calendar) + self.assertIn('metadata', platform_calendar) + + def test_adapt_calendar_for_platform(self): + """Test adapting calendar for a specific platform.""" + # Create base calendar + calendar = self.integration_manager.create_cross_platform_calendar( + start_date=self.start_date, + end_date=self.end_date, + platforms=[self.platforms[0]], # Test with just Instagram + content_types=self.content_types, + target_audience=self.target_audience, + industry=self.industry, + keywords=self.keywords + ) + + # Get platform calendar + platform_calendar = calendar['platform_calendars'][self.platforms[0]] + + # Check structure + self.assertIn('content_items', platform_calendar) + self.assertIn('metadata', platform_calendar) + + # Check content items + for item in platform_calendar['content_items']: + self.assertIn('original_item', item) + self.assertIn('adapted_content', item) + self.assertIn('platform_specifics', item) + + def test_adapt_content_item(self): + """Test adapting a content item for a platform.""" + adapted_item = self.integration_manager._adapt_content_item( + item=self.sample_content, + platform='instagram' + ) + + # Check structure + self.assertIsNotNone(adapted_item) + self.assertIn('original_item', adapted_item) + self.assertIn('adapted_content', adapted_item) + self.assertIn('platform_specifics', adapted_item) + + # Check content adaptation + adapted_content = adapted_item['adapted_content'] + self.assertIn('captions', adapted_content) + self.assertIn('hashtags', adapted_content) + self.assertIn('media_suggestions', adapted_content) + + def test_get_platform_suggestions(self): + """Test getting platform-specific suggestions.""" + suggestions = self.integration_manager.get_platform_suggestions( + content=self.sample_content, + platforms=self.platforms + ) + + # Check structure + self.assertEqual(len(suggestions), len(self.platforms)) + + for platform in self.platforms: + self.assertIn(platform, suggestions) + platform_suggestions = suggestions[platform] + self.assertIsInstance(platform_suggestions, dict) + + def test_validate_platform_content(self): + """Test validating content for a platform.""" + validation = self.integration_manager.validate_platform_content( + content=self.sample_content, + platform='instagram' + ) + + # Check structure + self.assertIn('platform', validation) + self.assertIn('is_valid', validation) + self.assertIn('specifications', validation) + + # Check validation result + self.assertIsInstance(validation['is_valid'], bool) + + def test_optimize_cross_platform_content(self): + """Test optimizing content for multiple platforms.""" + optimized = self.integration_manager.optimize_cross_platform_content( + content=self.sample_content, + platforms=self.platforms + ) + + # Check structure + self.assertEqual(len(optimized), len(self.platforms)) + + for platform in self.platforms: + self.assertIn(platform, optimized) + platform_optimized = optimized[platform] + self.assertIsInstance(platform_optimized, dict) + + def test_error_handling(self): + """Test error handling with invalid inputs.""" + # Test with invalid platform + with self.assertRaises(Exception): + self.integration_manager.validate_platform_content( + content=self.sample_content, + platform='invalid_platform' + ) + + # Test with invalid content + invalid_content = {'title': 'Invalid Content'} + validation = self.integration_manager.validate_platform_content( + content=invalid_content, + platform='instagram' + ) + self.assertFalse(validation['is_valid']) + self.assertIn('error', validation) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/tests/test_platform_adapters.py b/lib/ai_seo_tools/content_calendar/tests/test_platform_adapters.py new file mode 100644 index 00000000..b06220bf --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/tests/test_platform_adapters.py @@ -0,0 +1,186 @@ +import unittest +from typing import Dict, Any +from datetime import datetime + +from ..integrations.platform_adapters import UnifiedPlatformAdapter + +class TestUnifiedPlatformAdapter(unittest.TestCase): + """Test cases for the UnifiedPlatformAdapter.""" + + def setUp(self): + """Set up test cases.""" + self.adapter = UnifiedPlatformAdapter() + self.test_content = { + 'title': 'Test Content', + 'content': 'This is a test content for platform adaptation.', + 'keywords': ['test', 'content', 'platform'], + 'tone': 'professional', + 'cta': 'Learn More', + 'audience': 'For All', + 'language': 'English', + 'industry': 'technology', + 'word_count': 1000 + } + + def test_adapt_instagram_content(self): + """Test Instagram content adaptation.""" + adapted_content = self.adapter.adapt_content( + content=self.test_content, + platform='instagram' + ) + + self.assertIsInstance(adapted_content, dict) + self.assertIn('captions', adapted_content) + self.assertIn('hashtags', adapted_content) + self.assertIn('media_suggestions', adapted_content) + self.assertIn('platform_specific', adapted_content) + + def test_adapt_twitter_content(self): + """Test Twitter content adaptation.""" + adapted_content = self.adapter.adapt_content( + content=self.test_content, + platform='twitter' + ) + + self.assertIsInstance(adapted_content, dict) + self.assertIn('tweets', adapted_content) + self.assertIn('thread_structure', adapted_content) + self.assertIn('media_suggestions', adapted_content) + self.assertIn('platform_specific', adapted_content) + + def test_adapt_linkedin_content(self): + """Test LinkedIn content adaptation.""" + adapted_content = self.adapter.adapt_content( + content=self.test_content, + platform='linkedin' + ) + + self.assertIsInstance(adapted_content, dict) + self.assertIn('post', adapted_content) + self.assertIn('engagement_optimization', adapted_content) + self.assertIn('media_suggestions', adapted_content) + self.assertIn('platform_specific', adapted_content) + + def test_adapt_blog_content(self): + """Test blog content adaptation.""" + adapted_content = self.adapter.adapt_content( + content=self.test_content, + platform='blog' + ) + + self.assertIsInstance(adapted_content, dict) + self.assertIn('post', adapted_content) + self.assertIn('seo_optimization', adapted_content) + self.assertIn('media_suggestions', adapted_content) + self.assertIn('platform_specific', adapted_content) + + def test_adapt_facebook_content(self): + """Test Facebook content adaptation.""" + adapted_content = self.adapter.adapt_content( + content=self.test_content, + platform='facebook' + ) + + self.assertIsInstance(adapted_content, dict) + self.assertIn('post', adapted_content) + self.assertIn('engagement_optimization', adapted_content) + self.assertIn('media_suggestions', adapted_content) + self.assertIn('platform_specific', adapted_content) + + def test_validate_content(self): + """Test content validation.""" + # Test valid content + self.assertTrue( + self.adapter.validate_content( + self.test_content, + 'instagram' + ) + ) + + # Test invalid content (missing required fields) + invalid_content = { + 'title': 'Test Content', + 'content': 'This is a test content.' + } + self.assertFalse( + self.adapter.validate_content( + invalid_content, + 'instagram' + ) + ) + + def test_unsupported_platform(self): + """Test handling of unsupported platform.""" + with self.assertRaises(ValueError): + self.adapter.adapt_content( + content=self.test_content, + platform='unsupported_platform' + ) + + def test_content_adaptation_with_context(self): + """Test content adaptation with additional context.""" + context = { + 'target_audience': 'professionals', + 'campaign_goals': ['awareness', 'engagement'], + 'brand_voice': 'authoritative' + } + + adapted_content = self.adapter.adapt_content( + content=self.test_content, + platform='linkedin', + context=context + ) + + self.assertIsInstance(adapted_content, dict) + self.assertIn('post', adapted_content) + self.assertIn('engagement_optimization', adapted_content) + + def test_error_handling(self): + """Test error handling in content adaptation.""" + # Test with invalid content structure + invalid_content = { + 'title': 123, # Invalid type + 'content': None # Missing required field + } + + adapted_content = self.adapter.adapt_content( + content=invalid_content, + platform='blog' + ) + + self.assertIn('error', adapted_content) + + def test_platform_specs(self): + """Test platform specifications.""" + specs = self.adapter.platform_specs + + # Check Instagram specs + self.assertIn('instagram', specs) + self.assertIn('max_caption_length', specs['instagram']) + self.assertIn('max_hashtags', specs['instagram']) + self.assertIn('required_fields', specs['instagram']) + + # Check Twitter specs + self.assertIn('twitter', specs) + self.assertIn('max_tweet_length', specs['twitter']) + self.assertIn('max_thread_length', specs['twitter']) + self.assertIn('required_fields', specs['twitter']) + + # Check LinkedIn specs + self.assertIn('linkedin', specs) + self.assertIn('max_post_length', specs['linkedin']) + self.assertIn('required_fields', specs['linkedin']) + + # Check blog specs + self.assertIn('blog', specs) + self.assertIn('min_word_count', specs['blog']) + self.assertIn('max_word_count', specs['blog']) + self.assertIn('required_fields', specs['blog']) + + # Check Facebook specs + self.assertIn('facebook', specs) + self.assertIn('max_post_length', specs['facebook']) + self.assertIn('required_fields', specs['facebook']) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/tests/test_seo_optimizer.py b/lib/ai_seo_tools/content_calendar/tests/test_seo_optimizer.py new file mode 100644 index 00000000..f36f1a26 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/tests/test_seo_optimizer.py @@ -0,0 +1,132 @@ +import unittest +from datetime import datetime +from typing import Dict, Any + +from ..integrations.seo_optimizer import SEOOptimizer + +class TestSEOOptimizer(unittest.TestCase): + """Test cases for the SEOOptimizer class.""" + + def setUp(self): + """Set up test fixtures.""" + self.seo_optimizer = SEOOptimizer() + + # Sample content for testing + self.sample_content = { + 'title': 'The Future of AI in Content Marketing', + 'content': 'AI is revolutionizing content marketing...', + 'keywords': ['AI', 'content marketing', 'automation'], + 'author': 'John Doe', + 'publish_date': datetime.now().isoformat(), + 'description': 'An in-depth look at AI in content marketing', + 'image_url': 'https://example.com/image.jpg', + 'url': 'https://example.com/article' + } + + # Sample calendar for testing + self.sample_calendar = { + 'metadata': { + 'start_date': datetime.now().isoformat(), + 'end_date': datetime.now().isoformat(), + 'platforms': ['blog', 'social'], + 'content_types': ['article'] + }, + 'content_items': [self.sample_content] + } + + def test_optimize_content(self): + """Test content optimization.""" + optimized = self.seo_optimizer.optimize_content( + content=self.sample_content, + content_type='article', + language='English', + search_intent='Informational Intent' + ) + + # Check structure + self.assertIn('original_content', optimized) + self.assertIn('seo_optimized', optimized) + + # Check SEO elements + seo_elements = optimized['seo_optimized'] + self.assertIn('title', seo_elements) + self.assertIn('meta_description', seo_elements) + self.assertIn('structured_data', seo_elements) + self.assertIn('keywords', seo_elements) + + def test_optimize_title(self): + """Test title optimization.""" + titles = self.seo_optimizer._optimize_title( + title=self.sample_content['title'], + keywords=self.sample_content['keywords'], + content_type='article', + language='English', + search_intent='Informational Intent' + ) + + # Check titles + self.assertIsInstance(titles, list) + self.assertTrue(len(titles) > 0) + + def test_generate_meta_description(self): + """Test meta description generation.""" + descriptions = self.seo_optimizer._generate_meta_description( + keywords=self.sample_content['keywords'], + content_type='article', + language='English', + search_intent='Informational Intent' + ) + + # Check descriptions + self.assertIsInstance(descriptions, list) + self.assertTrue(len(descriptions) > 0) + + def test_generate_structured_data(self): + """Test structured data generation.""" + structured_data = self.seo_optimizer._generate_structured_data( + content=self.sample_content, + content_type='article' + ) + + # Check structured data + self.assertIsNotNone(structured_data) + + def test_optimize_calendar_content(self): + """Test calendar content optimization.""" + optimized_calendar = self.seo_optimizer.optimize_calendar_content( + calendar=self.sample_calendar, + content_type='article', + language='English', + search_intent='Informational Intent' + ) + + # Check structure + self.assertIn('metadata', optimized_calendar) + self.assertIn('content_items', optimized_calendar) + + # Check content items + self.assertTrue(len(optimized_calendar['content_items']) > 0) + for item in optimized_calendar['content_items']: + self.assertIn('original_content', item) + self.assertIn('seo_optimized', item) + + def test_error_handling(self): + """Test error handling with invalid inputs.""" + # Test with invalid content + invalid_content = {'title': 'Invalid Content'} + optimized = self.seo_optimizer.optimize_content( + content=invalid_content, + content_type='article' + ) + self.assertIn('error', optimized) + + # Test with invalid calendar + invalid_calendar = {'metadata': {}} + optimized_calendar = self.seo_optimizer.optimize_calendar_content( + calendar=invalid_calendar, + content_type='article' + ) + self.assertIn('error', optimized_calendar) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/add_content_modal.py b/lib/ai_seo_tools/content_calendar/ui/add_content_modal.py new file mode 100644 index 00000000..c9b594b6 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/add_content_modal.py @@ -0,0 +1,21 @@ +import streamlit as st + +def render_add_content_modal(selected_date, on_add_content, on_generate_with_ai): + if st.button("+ Add Content", key="open_add_content_dialog_bottom"): + st.session_state['show_add_content_dialog'] = True + if st.session_state.get('show_add_content_dialog', False): + st.markdown("### Add Content") + with st.form("quick_add_form_dialog_bottom"): + title = st.text_input("Title") + platform = st.selectbox("Platform", ["Blog", "Instagram", "Twitter", "LinkedIn", "Facebook"]) + content_type = st.selectbox("Content Type", ["Article", "Social Post", "Video", "Newsletter"]) + publish_date = st.date_input("Publish Date", selected_date) + col_add, col_ai = st.columns([0.6, 0.4]) + with col_add: + if st.form_submit_button("Add Content"): + on_add_content(title, platform, content_type, publish_date) + with col_ai: + if st.form_submit_button("Generate with AI"): + on_generate_with_ai(title, platform, content_type) + if st.button("Close", key="close_add_content_dialog_bottom"): + st.session_state['show_add_content_dialog'] = False \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/ai_suggestions_modal.py b/lib/ai_seo_tools/content_calendar/ui/ai_suggestions_modal.py new file mode 100644 index 00000000..3d8cdd0b --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/ai_suggestions_modal.py @@ -0,0 +1,137 @@ +import streamlit as st + +def render_ai_suggestions_modal(generate_ai_suggestions, on_create_brief, on_schedule, on_refine, on_customize): + st.subheader("AI Content Suggestions") + default_type = st.session_state.get('ai_modal_type', "Blog Post") + default_topic = st.session_state.get('ai_modal_topic', "") + default_platform = st.session_state.get('ai_modal_platform', "Blog") + content_types = { + "Blog Post": "Long-form content for in-depth topics", + "Social Media Post": "Short, engaging content for social platforms", + "Video": "Visual content with script and storyboard", + "Newsletter": "Email content for subscriber engagement" + } + content_type = st.selectbox( + "Content Type", + list(content_types.keys()), + format_func=lambda x: f"{x} - {content_types[x]}", + key="modal_suggestion_type", + index=list(content_types.keys()).index(default_type) if default_type in content_types else 0 + ) + topic = st.text_input("Enter topic or keyword", value=default_topic, key="modal_suggestion_topic") + with st.expander("Advanced Options"): + audience = st.multiselect( + "Target Audience", + ["Professionals", "Students", "Entrepreneurs", "General Public", "Industry Experts"], + default=["Professionals"] + ) + goals = st.multiselect( + "Content Goals", + ["Increase Engagement", "Generate Leads", "Build Authority", "Drive Traffic", "Educate"], + default=["Increase Engagement"] + ) + tone = st.select_slider( + "Content Tone", + options=["Professional", "Casual", "Educational", "Entertaining", "Persuasive"], + value="Professional" + ) + length = st.radio( + "Content Length", + ["Short", "Medium", "Long"], + horizontal=True + ) + st.subheader("AI Model Settings") + model_settings = { + "Creativity Level": st.slider("Creativity Level", 0.0, 1.0, 0.7, 0.1), + "Formality Level": st.slider("Formality Level", 0.0, 1.0, 0.5, 0.1), + "Technical Depth": st.slider("Technical Depth", 0.0, 1.0, 0.5, 0.1) + } + st.subheader("Content Style Preferences") + style_preferences = { + "Use Examples": st.checkbox("Include Real-world Examples", True), + "Use Statistics": st.checkbox("Include Statistics and Data", True), + "Use Quotes": st.checkbox("Include Expert Quotes", False), + "Use Case Studies": st.checkbox("Include Case Studies", False) + } + st.subheader("SEO Preferences") + seo_preferences = { + "Keyword Density": st.slider("Keyword Density (%)", 1, 5, 2), + "Internal Linking": st.checkbox("Suggest Internal Links", True), + "External Linking": st.checkbox("Suggest External Links", True), + "Meta Description": st.checkbox("Generate Meta Description", True) + } + st.subheader("Platform-specific Settings") + platform_settings = { + "Hashtag Usage": st.checkbox("Suggest Hashtags", True), + "Image Suggestions": st.checkbox("Suggest Images", True), + "Video Suggestions": st.checkbox("Suggest Videos", False), + "Interactive Elements": st.checkbox("Suggest Interactive Elements", False) + } + if st.button("Generate Suggestions", type="primary", key="modal_generate_btn"): + with st.spinner("Generating suggestions..."): + suggestions = generate_ai_suggestions( + content_type, + topic, + audience, + goals, + tone, + length, + model_settings, + style_preferences, + seo_preferences, + platform_settings + ) + if suggestions: + suggestion_tabs = st.tabs([f"Suggestion {i+1}" for i in range(len(suggestions))]) + for i, (tab, suggestion) in enumerate(zip(suggestion_tabs, suggestions)): + with tab: + col1, col2 = st.columns([2, 1]) + with col1: + st.subheader(suggestion['title']) + st.write(f"**Type:** {suggestion['type']}") + st.write(f"**Platform:** {suggestion['platform']}") + st.write(f"**Target Audience:** {', '.join(suggestion['audience'])}") + st.write(f"**Estimated Impact:** {suggestion['impact']}") + with st.expander("Content Preview"): + st.write(suggestion.get('preview', 'Preview not available')) + if suggestion.get('style_elements'): + st.write("**Style Elements:**") + for element in suggestion['style_elements']: + st.write(f"- {element}") + if suggestion.get('seo_elements'): + st.write("**SEO Elements:**") + for element in suggestion['seo_elements']: + st.write(f"- {element}") + with col2: + st.subheader("Performance Metrics") + metrics = { + "Engagement Score": suggestion.get('engagement_score', '85%'), + "Reach Potential": suggestion.get('reach', 'High'), + "Conversion Rate": suggestion.get('conversion', '3.5%'), + "SEO Impact": suggestion.get('seo_impact', 'Strong') + } + for metric, value in metrics.items(): + st.metric(metric, value) + st.subheader("Actions") + if st.button("Create Brief", key=f"modal_brief_{i}"): + on_create_brief(suggestion) + if st.button("Schedule", key=f"modal_schedule_{i}"): + on_schedule(suggestion) + if st.button("Refine", key=f"modal_refine_{i}"): + on_refine(suggestion) + if st.button("Customize", key=f"modal_customize_{i}"): + on_customize(suggestion) + with st.expander("Additional Options"): + st.write("**Platform Optimizations**") + for platform in suggestion.get('platform_optimizations', []): + st.write(f"- {platform}") + st.write("**Content Variations**") + for variation in suggestion.get('variations', []): + st.write(f"- {variation}") + st.write("**SEO Recommendations**") + for seo in suggestion.get('seo_recommendations', []): + st.write(f"- {seo}") + if suggestion.get('media_suggestions'): + st.write("**Media Suggestions**") + for media in suggestion['media_suggestions']: + st.write(f"- {media}") \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/calendar_view.py b/lib/ai_seo_tools/content_calendar/ui/calendar_view.py new file mode 100644 index 00000000..a270664e --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/calendar_view.py @@ -0,0 +1,51 @@ +import streamlit as st +from .components.content_card import render_content_card +from .components.badge import render_badge + +def render_calendar_view(calendar_data, icon_map, status_color, on_edit, on_delete, on_generate, get_item_key): + if calendar_data is not None and not calendar_data.empty: + st.markdown("### All Scheduled Content") + calendar_data = calendar_data.sort_values(by="date") + grouped = list(calendar_data.groupby(calendar_data['date'].dt.date)) + for i, (date, group) in enumerate(grouped): + exp_open = (i == 0) + with st.expander(f"{date.strftime('%B %d, %Y')}", expanded=exp_open): + for idx, row in group.iterrows(): + item_key = get_item_key(row) + is_editing = st.session_state.get("editing_item_key") == item_key + platform = str(row['platform']) + if hasattr(platform, 'value'): + platform = platform.value + platform_map = { + 'blog': 'Blog', + 'website': 'Blog', + 'instagram': 'Instagram', + 'twitter': 'Twitter', + 'linkedin': 'LinkedIn', + 'facebook': 'Facebook', + } + platform_disp = platform_map.get(platform.lower(), 'Blog') + type_disp = str(row['type']) + if hasattr(type_disp, 'value'): + type_disp = type_disp.value + type_disp = type_disp.replace('_', ' ').title() + status_disp = row['status'].capitalize() + platform_icon = icon_map.get(platform_disp, '🌐') + type_icon = icon_map.get(type_disp, '📄') + render_content_card( + row=row, + is_editing=is_editing, + on_edit=lambda r=row: on_edit(r), + on_delete=lambda r=row: on_delete(r), + on_generate=lambda r=row: on_generate(r), + icon_map=icon_map, + status_color=status_color, + platform_disp=platform_disp, + type_disp=type_disp, + status_disp=status_disp, + platform_icon=platform_icon, + type_icon=type_icon, + item_key=item_key + ) + else: + st.info("No content scheduled yet. Add content to see it here.") \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/components/ab_testing.py b/lib/ai_seo_tools/content_calendar/ui/components/ab_testing.py new file mode 100644 index 00000000..d1d3a055 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/components/ab_testing.py @@ -0,0 +1,231 @@ +import streamlit as st +from typing import Dict, Any, List +from lib.ai_seo_tools.content_calendar.models.calendar import ContentItem +import logging + +logger = logging.getLogger(__name__) + +def render_ab_testing( + content_generator, + calendar_manager +) -> None: + """Render the A/B testing interface.""" + try: + st.header("A/B Testing") + + # Test Configuration + st.markdown("### Create A/B Test") + col1, col2 = st.columns([2, 1]) + + with col1: + test_content = st.selectbox( + "Select content for A/B testing", + options=[item.title for item in calendar_manager.get_calendar().get_all_content()], + key="ab_test_content_select" + ) + + with col2: + num_variants = st.slider( + "Number of variants", + min_value=2, + max_value=5, + value=2, + help="Number of different versions to test" + ) + + if test_content: + content_item = next( + item for item in calendar_manager.get_calendar().get_all_content() + if item.title == test_content + ) + + # Test Settings + with st.expander("Test Settings"): + col1, col2 = st.columns(2) + with col1: + test_duration = st.number_input( + "Test Duration (days)", + min_value=1, + max_value=30, + value=7 + ) + target_metric = st.selectbox( + "Primary Metric", + options=['Engagement', 'Conversion', 'Reach', 'Click-through'], + value='Engagement' + ) + with col2: + audience_size = st.select_slider( + "Audience Size", + options=['Small', 'Medium', 'Large'], + value='Medium' + ) + confidence_level = st.slider( + "Confidence Level", + min_value=90, + max_value=99, + value=95, + help="Statistical confidence level for test results" + ) + + # Generate Variants + if st.button("Generate Variants"): + with st.spinner("Generating variants..."): + variants = _generate_ab_test_variants(content_generator, content_item, num_variants) + if variants: + st.success(f"Generated {len(variants)} variants!") + + # Display variants in tabs + variant_tabs = st.tabs([f"Variant {i+1}" for i in range(len(variants))]) + for i, tab in enumerate(variant_tabs): + with tab: + st.markdown(f"### Variant {i+1}") + st.json(variants[i]['content']) + + # Variant metrics + col1, col2, col3 = st.columns(3) + with col1: + st.metric( + "Engagement Score", + f"{variants[i]['metrics']['engagement_score']:.1f}%" + ) + with col2: + st.metric( + "Conversion Rate", + f"{variants[i]['metrics']['conversion_rate']:.1f}%" + ) + with col3: + st.metric( + "Reach", + f"{variants[i]['metrics']['reach']:,}" + ) + + # Results Analysis + st.markdown("### Analyze Results") + if test_content in st.session_state.ab_test_results: + test_data = st.session_state.ab_test_results[test_content] + + # Test Status + st.info(f"Test Status: {test_data['status']}") + st.write(f"Started: {test_data['start_time']}") + + if test_data['status'] == 'running': + if st.button("End Test and Analyze"): + with st.spinner("Analyzing results..."): + results = _analyze_ab_test_results(content_item) + if results: + st.success("Analysis complete!") + _display_test_results(results) + + except Exception as e: + logger.error(f"Error in A/B testing interface: {str(e)}", exc_info=True) + st.error(f"Error in A/B testing: {str(e)}") + +def _generate_ab_test_variants( + content_generator, + content: ContentItem, + num_variants: int +) -> List[Dict[str, Any]]: + """Generate A/B test variants for content.""" + try: + logger.info(f"Generating {num_variants} variants for content: {content.title}") + + # Convert content to dictionary format + content_dict = { + 'title': content.title, + 'content': content.description, + 'metadata': { + 'platform': content.platforms[0].name if content.platforms else 'Unknown', + 'content_type': content.content_type.name + } + } + + variants = [] + for i in range(num_variants): + # Generate different variations + variant = content_generator.generate_variation( + content=content_dict, + variation_type=f"variant_{i+1}" + ) + if variant: + variants.append(variant) + + return variants + + except Exception as e: + logger.error(f"Error generating variants: {str(e)}") + return [] + +def _analyze_ab_test_results(content_item: ContentItem) -> Dict[str, Any]: + """Analyze results of A/B testing for content optimization.""" + try: + logger.info(f"Analyzing A/B test results for: {content_item.title}") + + if content_item.title not in st.session_state.ab_test_results: + raise ValueError("No A/B test results found for this content") + + test_data = st.session_state.ab_test_results[content_item.title] + variants = test_data['variants'] + + # Calculate performance metrics + results = { + 'total_engagement': sum(v['metrics']['engagement_score'] for v in variants), + 'total_conversions': sum(v['metrics']['conversion_rate'] for v in variants), + 'total_reach': sum(v['metrics']['reach'] for v in variants), + 'best_performing_variant': max(variants, key=lambda x: x['metrics']['engagement_score']), + 'recommendations': [] + } + + # Generate recommendations + for variant in variants: + if variant['metrics']['engagement_score'] > 0.7: # High engagement threshold + results['recommendations'].append({ + 'variant_id': variant['variant_id'], + 'reason': 'High engagement score', + 'suggested_actions': ['Scale this variant', 'Apply learnings to other content'] + }) + + # Update test status + test_data['status'] = 'completed' + test_data['results'] = results + + logger.info("A/B test results analyzed successfully") + return results + + except Exception as e: + logger.error(f"Error analyzing A/B test results: {str(e)}", exc_info=True) + st.error(f"Error analyzing A/B test results: {str(e)}") + return {} + +def _display_test_results(results: Dict[str, Any]) -> None: + """Display A/B test results in the UI.""" + with st.expander("Overall Performance", expanded=True): + col1, col2, col3 = st.columns(3) + with col1: + st.metric( + "Total Engagement", + f"{results['total_engagement']:.1f}%" + ) + with col2: + st.metric( + "Total Conversions", + f"{results['total_conversions']:.1f}%" + ) + with col3: + st.metric( + "Total Reach", + f"{results['total_reach']:,}" + ) + + with st.expander("Best Performing Variant", expanded=True): + best_variant = results['best_performing_variant'] + st.markdown(f"### {best_variant['variant_id']}") + st.json(best_variant['content']) + + with st.expander("Recommendations", expanded=True): + for rec in results['recommendations']: + st.markdown(f"#### {rec['variant_id']}") + st.write(f"Reason: {rec['reason']}") + st.write("Suggested Actions:") + for action in rec['suggested_actions']: + st.write(f"- {action}") \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/components/badge.py b/lib/ai_seo_tools/content_calendar/ui/components/badge.py new file mode 100644 index 00000000..645a0a4e --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/components/badge.py @@ -0,0 +1,2 @@ +def render_badge(platform_disp, platform_icon, type_disp, status_disp): + return f"{platform_icon} {platform_disp}  |  {type_disp}  |  {status_disp}" \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/components/content_card.py b/lib/ai_seo_tools/content_calendar/ui/components/content_card.py new file mode 100644 index 00000000..214e93d9 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/components/content_card.py @@ -0,0 +1,22 @@ +import streamlit as st + +def render_content_card(row, is_editing, on_edit, on_delete, on_generate, icon_map, status_color, platform_disp, type_disp, status_disp, platform_icon, type_icon, item_key): + st.markdown(f"
", unsafe_allow_html=True) + st.markdown(f"
", unsafe_allow_html=True) + st.markdown(f"
" + f"{type_icon}{row['title']}
", unsafe_allow_html=True) + st.markdown("
", unsafe_allow_html=True) + col1, col2, col3 = st.columns([1, 1, 1]) + with col1: + if st.button("⚡", key=f"generate_{item_key}", help="Generate with AI Blog Writer", use_container_width=True): + on_generate() + with col2: + if st.button("✏️", key=f"edit_{item_key}", help="Edit Content", use_container_width=True): + on_edit() + with col3: + if st.button("🗑️", key=f"delete_{item_key}", help="Delete Content", use_container_width=True): + on_delete() + st.markdown("
", unsafe_allow_html=True) + st.markdown("
", unsafe_allow_html=True) + st.markdown(f"
{platform_icon} {platform_disp}  |  {type_disp}  |  {status_disp}
", unsafe_allow_html=True) + st.markdown("
", unsafe_allow_html=True) \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/components/content_optimization.py b/lib/ai_seo_tools/content_calendar/ui/components/content_optimization.py new file mode 100644 index 00000000..fea72e10 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/components/content_optimization.py @@ -0,0 +1,467 @@ +import streamlit as st +from typing import Dict, Any, List +from datetime import datetime +import pandas as pd +from ...core.content_generator import ContentGenerator +from ...core.ai_generator import AIGenerator +from ...integrations.seo_optimizer import SEOOptimizer +from ...models.calendar import ContentItem, ContentType, Platform, SEOData +import logging + +logger = logging.getLogger('content_calendar.optimization') + +class OptimizationManager: + def __init__(self): + if 'optimization_history' not in st.session_state: + st.session_state.optimization_history = {} + if 'optimization_previews' not in st.session_state: + st.session_state.optimization_previews = {} + if 'optimization_metrics' not in st.session_state: + st.session_state.optimization_metrics = {} + + def track_optimization(self, content_id: str, optimization_data: Dict[str, Any]) -> bool: + """Track optimization changes for content with detailed metrics.""" + try: + if content_id not in st.session_state.optimization_history: + st.session_state.optimization_history[content_id] = [] + + optimization_data['timestamp'] = datetime.now() + optimization_data['metrics'] = self._calculate_optimization_metrics(optimization_data) + st.session_state.optimization_history[content_id].append(optimization_data) + + # Update metrics + if content_id not in st.session_state.optimization_metrics: + st.session_state.optimization_metrics[content_id] = [] + st.session_state.optimization_metrics[content_id].append(optimization_data['metrics']) + + return True + except Exception as e: + logger.error(f"Error tracking optimization: {str(e)}") + return False + + def _calculate_optimization_metrics(self, optimization_data: Dict[str, Any]) -> Dict[str, Any]: + """Calculate detailed optimization metrics.""" + try: + metrics = { + 'readability_score': 0, + 'seo_score': 0, + 'engagement_potential': 0, + 'keyword_density': 0, + 'content_quality': 0 + } + + # Calculate readability score + if 'content' in optimization_data: + content = optimization_data['content'] + metrics['readability_score'] = self._calculate_readability(content) + + # Calculate SEO score + if 'seo_data' in optimization_data: + seo_data = optimization_data['seo_data'] + metrics['seo_score'] = self._calculate_seo_score(seo_data) + metrics['keyword_density'] = self._calculate_keyword_density(seo_data) + + # Calculate engagement potential + if 'engagement_metrics' in optimization_data: + engagement = optimization_data['engagement_metrics'] + metrics['engagement_potential'] = self._calculate_engagement_potential(engagement) + + # Calculate overall content quality + metrics['content_quality'] = ( + metrics['readability_score'] * 0.3 + + metrics['seo_score'] * 0.3 + + metrics['engagement_potential'] * 0.4 + ) + + return metrics + except Exception as e: + logger.error(f"Error calculating optimization metrics: {str(e)}") + return {} + + def _calculate_readability(self, content: str) -> float: + """Calculate content readability score.""" + try: + # Implement readability calculation logic + # This is a placeholder implementation + return 0.8 + except Exception as e: + logger.error(f"Error calculating readability: {str(e)}") + return 0.0 + + def _calculate_seo_score(self, seo_data: SEOData) -> float: + """Calculate SEO optimization score.""" + try: + # Implement SEO score calculation logic + # This is a placeholder implementation + return 0.85 + except Exception as e: + logger.error(f"Error calculating SEO score: {str(e)}") + return 0.0 + + def _calculate_keyword_density(self, seo_data: SEOData) -> float: + """Calculate keyword density.""" + try: + # Implement keyword density calculation logic + # This is a placeholder implementation + return 2.5 + except Exception as e: + logger.error(f"Error calculating keyword density: {str(e)}") + return 0.0 + + def _calculate_engagement_potential(self, engagement: Dict[str, Any]) -> float: + """Calculate content engagement potential.""" + try: + # Implement engagement potential calculation logic + # This is a placeholder implementation + return 0.75 + except Exception as e: + logger.error(f"Error calculating engagement potential: {str(e)}") + return 0.0 + + def get_optimization_history(self, content_id: str) -> List[Dict[str, Any]]: + """Get detailed optimization history for content.""" + return st.session_state.optimization_history.get(content_id, []) + + def get_optimization_metrics(self, content_id: str) -> List[Dict[str, Any]]: + """Get optimization metrics history.""" + return st.session_state.optimization_metrics.get(content_id, []) + + def save_preview(self, content_id: str, preview_data: Dict[str, Any]) -> bool: + """Save optimization preview with versioning.""" + try: + if content_id not in st.session_state.optimization_previews: + st.session_state.optimization_previews[content_id] = [] + + preview_data['version'] = len(st.session_state.optimization_previews[content_id]) + 1 + preview_data['timestamp'] = datetime.now() + st.session_state.optimization_previews[content_id].append(preview_data) + return True + except Exception as e: + logger.error(f"Error saving preview: {str(e)}") + return False + + def get_preview(self, content_id: str, version: int = None) -> Dict[str, Any]: + """Get optimization preview with optional versioning.""" + try: + previews = st.session_state.optimization_previews.get(content_id, []) + if not previews: + return {} + + if version is None: + return previews[-1] + + for preview in previews: + if preview['version'] == version: + return preview + + return {} + except Exception as e: + logger.error(f"Error getting preview: {str(e)}") + return {} + +def render_content_optimization( + content_generator: ContentGenerator, + ai_generator: AIGenerator, + seo_optimizer: SEOOptimizer +): + """Render the content optimization interface with advanced features.""" + st.header("Content Optimization") + + # Initialize optimization manager + optimization_manager = OptimizationManager() + + # Check if calendar manager is available + if 'calendar_manager' not in st.session_state: + st.error("Calendar manager not initialized. Please refresh the page.") + return + + # Get available content + try: + available_content = st.session_state.calendar_manager.get_calendar().get_all_content() + content_options = [item.title for item in available_content] + except Exception as e: + logger.error(f"Error getting content options: {str(e)}") + st.error("Error loading content. Please try again.") + return + + if not content_options: + st.info("No content available for optimization. Please add some content first.") + return + + # Content Selection + selected_content = st.selectbox( + "Select content to optimize", + options=content_options, + key="optimize_content_select" + ) + + if selected_content: + try: + content_item = next( + item for item in available_content + if item.title == selected_content + ) + + # Create tabs for different optimization aspects + opt_tabs = st.tabs(["Content Optimization", "SEO Optimization", "Preview", "History", "Analytics"]) + + with opt_tabs[0]: + st.subheader("Content Optimization") + + # Advanced Optimization Settings + with st.expander("Advanced Settings", expanded=True): + col1, col2 = st.columns(2) + + with col1: + tone = st.select_slider( + "Content Tone", + options=['Professional', 'Casual', 'Friendly', 'Authoritative', 'Conversational'], + value='Professional' + ) + length = st.select_slider( + "Content Length", + options=['Short', 'Medium', 'Long', 'Comprehensive'], + value='Medium' + ) + + with col2: + engagement_goal = st.select_slider( + "Engagement Goal", + options=['Awareness', 'Consideration', 'Conversion', 'Retention'], + value='Consideration' + ) + creativity_level = st.slider( + "Creativity Level", + min_value=1, + max_value=10, + value=5 + ) + + # Platform-Specific Optimization + st.subheader("Platform-Specific Optimization") + platforms = st.multiselect( + "Target Platforms", + options=[p.name for p in content_item.platforms], + default=[p.name for p in content_item.platforms] + ) + + # Generate Optimization + if st.button("Generate Optimization"): + with st.spinner("Generating optimization..."): + try: + # Generate optimized content + optimized_content = content_generator.optimize_for_platform( + content=content_item, + platform=Platform[platforms[0]] if platforms else content_item.platforms[0], + requirements={ + 'tone': tone, + 'length': length, + 'engagement_goal': engagement_goal, + 'creativity_level': creativity_level + } + ) + + if optimized_content: + # Track optimization + optimization_manager.track_optimization( + content_item.title, + { + 'type': 'content', + 'changes': optimized_content.get('changes', []), + 'metrics': optimized_content.get('metrics', {}), + 'content': optimized_content.get('content', ''), + 'engagement_metrics': optimized_content.get('engagement_metrics', {}) + } + ) + + # Save preview + optimization_manager.save_preview( + content_item.title, + { + 'original': content_item.description, + 'optimized': optimized_content.get('content', ''), + 'changes': optimized_content.get('changes', []), + 'metrics': optimized_content.get('metrics', {}) + } + ) + + st.success("Content optimized successfully!") + except Exception as e: + logger.error(f"Error optimizing content: {str(e)}") + st.error(f"Error optimizing content: {str(e)}") + + with opt_tabs[1]: + st.subheader("SEO Optimization") + + # SEO Settings + with st.expander("SEO Settings", expanded=True): + col1, col2 = st.columns(2) + + with col1: + keyword_density = st.slider( + "Target Keyword Density", + min_value=1, + max_value=5, + value=2, + help="Target percentage of keywords in content" + ) + internal_linking = st.checkbox( + "Enable Internal Linking", + value=True, + help="Automatically add internal links to related content" + ) + + with col2: + external_linking = st.checkbox( + "Enable External Linking", + value=True, + help="Add relevant external links for credibility" + ) + structured_data = st.checkbox( + "Add Structured Data", + value=True, + help="Include schema.org structured data" + ) + + # Generate SEO Optimization + if st.button("Generate SEO Optimization"): + with st.spinner("Generating SEO optimization..."): + try: + # Generate SEO-optimized content + seo_optimized = seo_optimizer.optimize_content( + content=content_item, + content_type=content_item.content_type.name, + language='English', + search_intent='Informational Intent', + settings={ + 'keyword_density': keyword_density, + 'internal_linking': internal_linking, + 'external_linking': external_linking, + 'structured_data': structured_data + } + ) + + if seo_optimized: + # Track optimization + optimization_manager.track_optimization( + content_item.title, + { + 'type': 'seo', + 'changes': seo_optimized.get('changes', []), + 'metrics': seo_optimized.get('metrics', {}), + 'seo_data': seo_optimized + } + ) + + # Save preview + optimization_manager.save_preview( + content_item.title, + { + 'meta_description': seo_optimized.get('meta_description', ''), + 'keywords': seo_optimized.get('keywords', []), + 'structured_data': seo_optimized.get('structured_data', {}), + 'changes': seo_optimized.get('changes', []) + } + ) + + st.success("SEO optimization completed!") + except Exception as e: + logger.error(f"Error optimizing SEO: {str(e)}") + st.error(f"Error optimizing SEO: {str(e)}") + + with opt_tabs[2]: + st.subheader("Optimization Preview") + + preview_data = optimization_manager.get_preview(content_item.title) + if preview_data: + # Content Preview + if 'original' in preview_data: + st.markdown("### Content Changes") + col1, col2 = st.columns(2) + + with col1: + st.markdown("#### Original Content") + st.write(preview_data['original']) + + with col2: + st.markdown("#### Optimized Content") + st.write(preview_data['optimized']) + + st.markdown("#### Key Changes") + for change in preview_data.get('changes', []): + st.write(f"- {change}") + + # SEO Preview + if 'meta_description' in preview_data: + st.markdown("### SEO Changes") + st.markdown("#### Meta Description") + st.write(preview_data['meta_description']) + + st.markdown("#### Keywords") + st.write(", ".join(preview_data['keywords'])) + + st.markdown("#### Structured Data") + st.json(preview_data['structured_data']) + + # Metrics Preview + if 'metrics' in preview_data: + st.markdown("### Optimization Metrics") + metrics = preview_data['metrics'] + col1, col2, col3 = st.columns(3) + + with col1: + st.metric("Readability Score", f"{metrics.get('readability_score', 0):.1%}") + with col2: + st.metric("SEO Score", f"{metrics.get('seo_score', 0):.1%}") + with col3: + st.metric("Engagement Potential", f"{metrics.get('engagement_potential', 0):.1%}") + else: + st.info("No optimization preview available. Generate optimization first.") + + with opt_tabs[3]: + st.subheader("Optimization History") + + history = optimization_manager.get_optimization_history(content_item.title) + if history: + for entry in history: + with st.expander(f"Optimization at {entry['timestamp']}"): + st.write(f"Type: {entry['type']}") + st.write("Changes:") + for change in entry.get('changes', []): + st.write(f"- {change}") + + if 'metrics' in entry: + st.write("Metrics:") + st.json(entry['metrics']) + else: + st.info("No optimization history available.") + + with opt_tabs[4]: + st.subheader("Optimization Analytics") + + metrics_history = optimization_manager.get_optimization_metrics(content_item.title) + if metrics_history: + # Convert metrics history to DataFrame + df = pd.DataFrame(metrics_history) + + # Plot metrics over time + st.line_chart(df[['readability_score', 'seo_score', 'engagement_potential', 'content_quality']]) + + # Display current metrics + current_metrics = metrics_history[-1] + col1, col2, col3, col4 = st.columns(4) + + with col1: + st.metric("Readability", f"{current_metrics.get('readability_score', 0):.1%}") + with col2: + st.metric("SEO Score", f"{current_metrics.get('seo_score', 0):.1%}") + with col3: + st.metric("Engagement", f"{current_metrics.get('engagement_potential', 0):.1%}") + with col4: + st.metric("Overall Quality", f"{current_metrics.get('content_quality', 0):.1%}") + + # Display keyword density trend + st.subheader("Keyword Density Trend") + st.line_chart(df['keyword_density']) + else: + st.info("No optimization metrics available. Generate optimization first.") \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/components/content_series.py b/lib/ai_seo_tools/content_calendar/ui/components/content_series.py new file mode 100644 index 00000000..6c414165 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/components/content_series.py @@ -0,0 +1,392 @@ +import streamlit as st +from typing import Dict, Any, List +from datetime import datetime, timedelta +import pandas as pd +from ...core.content_generator import ContentGenerator +from ...core.ai_generator import AIGenerator +from ...integrations.seo_optimizer import SEOOptimizer +from ...models.calendar import ContentItem, ContentType, Platform, SEOData +import logging + +logger = logging.getLogger('content_calendar.series') + +class SeriesManager: + def __init__(self): + self.series_data = {} + if 'content_series' not in st.session_state: + st.session_state.content_series = {} + if 'series_relationships' not in st.session_state: + st.session_state.series_relationships = {} + if 'series_performance' not in st.session_state: + st.session_state.series_performance = {} + + def create_series(self, series_id: str, topic: str, num_pieces: int, content_type: ContentType, + platforms: List[Platform], schedule_strategy: str = 'linear') -> Dict[str, Any]: + """Create a new content series with tracking and scheduling.""" + try: + series = { + 'id': series_id, + 'topic': topic, + 'num_pieces': num_pieces, + 'content_type': content_type, + 'platforms': platforms, + 'schedule_strategy': schedule_strategy, + 'pieces': [], + 'performance': {}, + 'created_at': datetime.now(), + 'status': 'draft', + 'relationships': {}, + 'platform_distribution': {p.name: [] for p in platforms} + } + st.session_state.content_series[series_id] = series + return series + except Exception as e: + logger.error(f"Error creating series: {str(e)}") + return None + + def add_piece(self, series_id: str, piece: Dict[str, Any]) -> bool: + """Add a content piece to the series with relationship tracking.""" + try: + if series_id in st.session_state.content_series: + series = st.session_state.content_series[series_id] + piece_id = f"piece_{len(series['pieces'])}" + piece['id'] = piece_id + + # Track relationships + if series['pieces']: + previous_piece = series['pieces'][-1] + piece['relationships'] = { + 'previous': previous_piece['id'], + 'next': None + } + previous_piece['relationships']['next'] = piece_id + + # Add to platform distribution + for platform in piece.get('platforms', []): + if platform.name in series['platform_distribution']: + series['platform_distribution'][platform.name].append(piece_id) + + series['pieces'].append(piece) + return True + return False + except Exception as e: + logger.error(f"Error adding piece to series: {str(e)}") + return False + + def get_series_performance(self, series_id: str) -> Dict[str, Any]: + """Get comprehensive performance analytics for a series.""" + try: + if series_id in st.session_state.content_series: + series = st.session_state.content_series[series_id] + performance = { + 'overall': { + 'total_engagement': 0, + 'total_reach': 0, + 'conversion_rate': 0, + 'average_engagement': 0 + }, + 'platforms': {}, + 'pieces': {}, + 'trends': { + 'engagement': [], + 'reach': [], + 'conversions': [] + } + } + + # Calculate overall metrics + for piece in series['pieces']: + piece_performance = piece.get('performance', {}) + performance['overall']['total_engagement'] += piece_performance.get('engagement', 0) + performance['overall']['total_reach'] += piece_performance.get('reach', 0) + performance['overall']['conversion_rate'] += piece_performance.get('conversion_rate', 0) + + # Track piece-specific performance + performance['pieces'][piece['id']] = piece_performance + + # Track trends + performance['trends']['engagement'].append(piece_performance.get('engagement', 0)) + performance['trends']['reach'].append(piece_performance.get('reach', 0)) + performance['trends']['conversions'].append(piece_performance.get('conversion_rate', 0)) + + # Calculate averages + num_pieces = len(series['pieces']) + if num_pieces > 0: + performance['overall']['average_engagement'] = performance['overall']['total_engagement'] / num_pieces + performance['overall']['conversion_rate'] = performance['overall']['conversion_rate'] / num_pieces + + # Calculate platform-specific performance + for platform in series['platforms']: + platform_pieces = series['platform_distribution'].get(platform.name, []) + platform_performance = { + 'engagement': 0, + 'reach': 0, + 'conversion_rate': 0 + } + + for piece_id in platform_pieces: + piece_performance = performance['pieces'].get(piece_id, {}) + platform_performance['engagement'] += piece_performance.get('engagement', 0) + platform_performance['reach'] += piece_performance.get('reach', 0) + platform_performance['conversion_rate'] += piece_performance.get('conversion_rate', 0) + + if platform_pieces: + platform_performance['engagement'] /= len(platform_pieces) + platform_performance['conversion_rate'] /= len(platform_pieces) + + performance['platforms'][platform.name] = platform_performance + + return performance + return {} + except Exception as e: + logger.error(f"Error getting series performance: {str(e)}") + return {} + + def update_series_status(self, series_id: str, status: str) -> bool: + """Update the status of a series.""" + try: + if series_id in st.session_state.content_series: + st.session_state.content_series[series_id]['status'] = status + return True + return False + except Exception as e: + logger.error(f"Error updating series status: {str(e)}") + return False + + def schedule_series(self, series_id: str, start_date: datetime, interval: int = 7) -> bool: + """Schedule the series content with flexible scheduling strategies.""" + try: + if series_id in st.session_state.content_series: + series = st.session_state.content_series[series_id] + current_date = start_date + + for piece in series['pieces']: + piece['scheduled_date'] = current_date + if series['schedule_strategy'] == 'linear': + current_date += timedelta(days=interval) + elif series['schedule_strategy'] == 'burst': + current_date += timedelta(days=1) + elif series['schedule_strategy'] == 'custom': + # Custom scheduling is handled by the UI + pass + + return True + return False + except Exception as e: + logger.error(f"Error scheduling series: {str(e)}") + return False + +def render_content_series_generator(ai_generator: AIGenerator, content_generator: ContentGenerator, + seo_optimizer: SEOOptimizer): + """Render the content series generator interface with enhanced features.""" + st.header("Content Series Generator") + + # Initialize series manager + series_manager = SeriesManager() + + # Series Creation Form + with st.form("series_creation_form"): + st.subheader("Create New Series") + series_topic = st.text_input("Series Topic") + num_pieces = st.slider("Number of pieces", 2, 10, 3) + content_type = st.selectbox( + "Content Type", + options=[ct.name for ct in ContentType], + key="series_content_type" + ) + + # Multi-platform selection + platforms = st.multiselect( + "Target Platforms", + options=[p.name for p in Platform], + default=['WEBSITE'], + key="series_platforms" + ) + + # Schedule strategy + schedule_strategy = st.selectbox( + "Schedule Strategy", + options=['linear', 'burst', 'custom'], + help="Linear: Evenly spaced, Burst: Grouped together, Custom: Manual scheduling" + ) + + # Series metadata + with st.expander("Series Metadata"): + target_audience = st.text_area("Target Audience") + series_goals = st.multiselect( + "Series Goals", + options=['Awareness', 'Engagement', 'Conversion', 'Education'], + default=['Awareness'] + ) + series_tone = st.select_slider( + "Series Tone", + options=['Professional', 'Casual', 'Friendly', 'Authoritative', 'Conversational'], + value='Professional' + ) + + submitted = st.form_submit_button("Generate Series") + + if submitted and series_topic: + with st.spinner("Generating content series..."): + try: + # Create series + series_id = f"series_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + series = series_manager.create_series( + series_id=series_id, + topic=series_topic, + num_pieces=num_pieces, + content_type=ContentType[content_type], + platforms=[Platform[p] for p in platforms], + schedule_strategy=schedule_strategy + ) + + if series: + # Generate series content + for i in range(num_pieces): + content_item = ContentItem( + title=f"{series_topic} - Part {i+1}", + description="", + content_type=ContentType[content_type], + platforms=[Platform[p] for p in platforms], + publish_date=datetime.now() + timedelta(days=i*7), + seo_data=SEOData( + title=f"{series_topic} - Part {i+1}", + meta_description="", + keywords=[], + structured_data={} + ), + status='Draft' + ) + + # Generate content using AI + base_content = ai_generator.generate_series_content( + content_item=content_item, + series_info={ + 'topic': series_topic, + 'part_number': i+1, + 'total_parts': num_pieces, + 'content_type': content_type, + 'platforms': platforms, + 'audience': target_audience, + 'goals': series_goals, + 'tone': series_tone + } + ) + + if base_content: + # Enhance with Content Generator + enhanced_content = content_generator.enhance_series_content( + content=base_content, + series_info={ + 'topic': series_topic, + 'part_number': i+1, + 'total_parts': num_pieces + } + ) + + if enhanced_content: + base_content.update(enhanced_content) + + # Add to series + series_manager.add_piece(series_id, { + 'part_number': i+1, + 'content': base_content, + 'seo_data': seo_optimizer.optimize_content( + content=base_content, + content_type=content_type, + language='English', + search_intent='Informational Intent' + ) + }) + + st.success(f"Generated {num_pieces} content pieces for series!") + + # Display series preview + with st.expander("Series Preview", expanded=True): + for piece in series_manager.series_data[series_id]['pieces']: + st.markdown(f"### Part {piece['part_number']}") + st.json(piece['content']) + + # Platform-specific previews + st.markdown("#### Platform Previews") + for platform in platforms: + with st.expander(f"{platform} Preview"): + st.write(piece['content'].get('platform_previews', {}).get(platform, 'No preview available')) + + # Series scheduling + st.subheader("Series Scheduling") + if schedule_strategy == 'linear': + start_date = st.date_input("Start Date", datetime.now()) + interval = st.number_input("Days between pieces", min_value=1, value=7) + + if st.button("Schedule Series"): + series_manager.schedule_series(series_id, start_date, interval) + st.success("Series scheduled successfully!") + + elif schedule_strategy == 'burst': + start_date = st.date_input("Start Date", datetime.now()) + if st.button("Schedule Series"): + series_manager.schedule_series(series_id, start_date, interval=1) + st.success("Series scheduled successfully!") + + else: # custom + for i, piece in enumerate(series_manager.series_data[series_id]['pieces']): + piece['scheduled_date'] = st.date_input( + f"Publish Date for Part {i+1}", + datetime.now() + timedelta(days=i*7) + ) + + if st.button("Save Schedule"): + st.success("Series schedule saved!") + + # Series performance tracking + st.subheader("Series Performance") + performance_data = series_manager.get_series_performance(series_id) + if performance_data: + st.write("### Overall Performance") + col1, col2, col3 = st.columns(3) + with col1: + st.metric("Total Engagement", f"{performance_data['overall']['total_engagement']:.1f}%") + with col2: + st.metric("Total Reach", f"{performance_data['overall']['total_reach']:,}") + with col3: + st.metric("Conversion Rate", f"{performance_data['overall']['conversion_rate']:.1f}%") + + # Platform-specific performance + st.write("### Platform Performance") + for platform in platforms: + with st.expander(f"{platform} Performance"): + platform_data = performance_data['platforms'].get(platform, {}) + st.write(f"Engagement: {platform_data.get('engagement', 0):.1f}%") + st.write(f"Reach: {platform_data.get('reach', 0):,}") + st.write(f"Conversions: {platform_data.get('conversion_rate', 0):.1f}%") + + # Performance trends + st.write("### Performance Trends") + trend_data = performance_data['trends'] + st.line_chart(pd.DataFrame({ + 'Engagement': trend_data['engagement'], + 'Reach': trend_data['reach'], + 'Conversions': trend_data['conversions'] + })) + + except Exception as e: + logger.error(f"Error generating series: {str(e)}", exc_info=True) + st.error(f"Error generating series: {str(e)}") + + # Display existing series + if st.session_state.content_series: + st.subheader("Existing Series") + for series_id, series in st.session_state.content_series.items(): + with st.expander(f"Series: {series['topic']}"): + st.write(f"Status: {series['status']}") + st.write(f"Pieces: {len(series['pieces'])}") + st.write(f"Created: {series['created_at']}") + + # Series actions + if st.button(f"View Details", key=f"view_{series_id}"): + st.session_state.selected_series = series_id + + if st.button(f"Delete Series", key=f"delete_{series_id}"): + del st.session_state.content_series[series_id] + st.experimental_rerun() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/components/performance_insights.py b/lib/ai_seo_tools/content_calendar/ui/components/performance_insights.py new file mode 100644 index 00000000..78448a86 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/components/performance_insights.py @@ -0,0 +1,81 @@ +import streamlit as st +from typing import Dict, Any +from lib.ai_seo_tools.content_calendar.models.calendar import ContentItem +import logging + +logger = logging.getLogger(__name__) + +def render_performance_insights(content_item: ContentItem, platform_adapter) -> None: + """Render performance insights for a content item.""" + try: + logger.info(f"Rendering performance insights for: {content_item.title}") + + # Get performance data from platform adapter + performance_data = platform_adapter.get_content_performance(content_item) + + if not performance_data: + st.warning("No performance data available for this content") + return + + # Create metrics section + st.subheader("Performance Metrics") + col1, col2, col3 = st.columns(3) + + with col1: + st.metric( + "Engagement Rate", + f"{performance_data.get('engagement_rate', 0):.1f}%", + f"{performance_data.get('engagement_rate_change', 0):+.1f}%" + ) + + with col2: + st.metric( + "Reach", + f"{performance_data.get('reach', 0):,}", + f"{performance_data.get('reach_change', 0):+,}" + ) + + with col3: + st.metric( + "Conversion Rate", + f"{performance_data.get('conversion_rate', 0):.1f}%", + f"{performance_data.get('conversion_rate_change', 0):+.1f}%" + ) + + # Create audience insights section + st.subheader("Audience Insights") + audience_data = performance_data.get('audience_insights', {}) + + if audience_data: + col1, col2 = st.columns(2) + + with col1: + st.write("Demographics") + st.write(f"- Age: {audience_data.get('age_range', 'N/A')}") + st.write(f"- Gender: {audience_data.get('gender', 'N/A')}") + st.write(f"- Location: {audience_data.get('location', 'N/A')}") + + with col2: + st.write("Behavior") + st.write(f"- Peak Time: {audience_data.get('peak_time', 'N/A')}") + st.write(f"- Device: {audience_data.get('device', 'N/A')}") + st.write(f"- Platform: {audience_data.get('platform', 'N/A')}") + + # Create content insights section + st.subheader("Content Insights") + content_insights = performance_data.get('content_insights', {}) + + if content_insights: + st.write("Top Performing Elements") + for element, score in content_insights.get('top_elements', {}).items(): + st.write(f"- {element}: {score}") + + st.write("Improvement Suggestions") + for suggestion in content_insights.get('suggestions', []): + st.write(f"- {suggestion}") + + logger.info(f"Performance insights rendered successfully for: {content_item.title}") + + except Exception as e: + logger.error(f"Error rendering performance insights: {str(e)}", exc_info=True) + st.error(f"Error rendering performance insights: {str(e)}") \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/dashboard.py b/lib/ai_seo_tools/content_calendar/ui/dashboard.py new file mode 100644 index 00000000..963a77ae --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/dashboard.py @@ -0,0 +1,634 @@ +import streamlit as st +import pandas as pd +from datetime import datetime +import logging +import sys +import hashlib +from .calendar_view import render_calendar_view +from .filters import render_filters +from .add_content_modal import render_add_content_modal +from .ai_suggestions_modal import render_ai_suggestions_modal +from .components.performance_insights import render_performance_insights +from .components.content_series import render_content_series_generator +from .components.ab_testing import render_ab_testing +from .components.content_optimization import render_content_optimization +from ..core.calendar_manager import CalendarManager +from ..core.content_brief import ContentBriefGenerator +from ..core.content_generator import ContentGenerator +from ..core.ai_generator import AIGenerator +from ..integrations.platform_adapters import UnifiedPlatformAdapter +from ..integrations.seo_optimizer import SEOOptimizer +from lib.ai_seo_tools.content_calendar.models.calendar import ContentItem, Platform, ContentType, SEOData, Calendar +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen +from typing import Dict, Any, List, Tuple +import json + +class ContentCalendarDashboard: + """Interactive dashboard for content calendar management.""" + def __init__(self): + self.logger = logging.getLogger('content_calendar.dashboard') + self.logger.info("Initializing ContentCalendarDashboard") + + # Initialize calendar manager and store in session state + if 'calendar_manager' not in st.session_state: + st.session_state.calendar_manager = CalendarManager() + st.session_state.calendar_manager.load_calendar_from_json() + + self.calendar_manager = st.session_state.calendar_manager + self.content_brief_generator = ContentBriefGenerator() + self.content_generator = ContentGenerator() + self.ai_generator = AIGenerator() + self.platform_adapter = UnifiedPlatformAdapter() + self.seo_optimizer = SEOOptimizer() + + # Initialize A/B testing state + if 'ab_test_results' not in st.session_state: + st.session_state.ab_test_results = {} + + # Initialize content optimization state + if 'optimization_history' not in st.session_state: + st.session_state.optimization_history = {} + + # Ensure a calendar exists + if not self.calendar_manager.get_calendar(): + self.calendar_manager._calendar = Calendar( + start_date=datetime.now(), + duration='monthly', + platforms=[Platform.WEBSITE, Platform.INSTAGRAM, Platform.TWITTER, Platform.LINKEDIN, Platform.FACEBOOK], + schedule={} + ) + + # Initialize session state + if 'calendar_data' not in st.session_state: + st.session_state.calendar_data = None + if 'selected_content' not in st.session_state: + st.session_state.selected_content = None + if 'view_mode' not in st.session_state: + st.session_state.view_mode = 'day' + if 'selected_date' not in st.session_state: + st.session_state.selected_date = datetime.now() + + self.logger.info("ContentCalendarDashboard initialized successfully") + + def render(self): + self.logger.info("Starting dashboard render (tabbed UI)") + try: + self._inject_custom_css() + st.title("AI Content Planning") + st.markdown(""" + Plan, schedule, and manage your content strategy with AI-powered insights. Use the calendar to organize your content and leverage AI tools for optimization. + """) + tabs = st.tabs(["Content Planning", "Content Optimization", "A/B Testing", "Content Series", "Analytics"]) + + with tabs[0]: + icon_map = { + 'Blog': '📝', 'Website': '🌐', 'Instagram': '📸', 'Twitter': '🐦', 'LinkedIn': '💼', 'Facebook': '📘', + 'Article': '📄', 'Social Post': '💬', 'Video': '🎬', 'Newsletter': '✉️' + } + status_color = { + 'Draft': '#bdbdbd', 'Scheduled': '#1976d2', 'Published': '#43a047', 'Archived': '#757575' + } + calendar_data = self._get_calendar_data() + def on_edit(row): + st.session_state["editing_item_key"] = self._get_item_key(row) + st.experimental_rerun() + def on_delete(row): + self._delete_content(row) + st.experimental_rerun() + def on_generate(row): + st.session_state['show_ai_modal'] = True + st.session_state['ai_modal_topic'] = row['title'] + st.session_state['ai_modal_type'] = str(row['type']) + st.session_state['ai_modal_platform'] = str(row['platform']) + st.experimental_rerun() + render_calendar_view( + calendar_data=calendar_data, + icon_map=icon_map, + status_color=status_color, + on_edit=on_edit, + on_delete=on_delete, + on_generate=on_generate, + get_item_key=self._get_item_key + ) + st.markdown("---") + render_filters() + def handle_add_content(title, platform, content_type, publish_date): + self._add_content({ + 'title': title, + 'platform': platform, + 'type': content_type, + 'publish_date': publish_date + }) + st.session_state['show_add_content_dialog'] = False + st.success("Content added!") + st.experimental_rerun() + def handle_generate_with_ai(title, platform, content_type): + st.session_state['show_add_content_dialog'] = False + st.session_state['show_ai_modal'] = True + st.session_state['ai_modal_topic'] = title + st.session_state['ai_modal_type'] = content_type + st.session_state['ai_modal_platform'] = platform + render_add_content_modal( + selected_date=st.session_state.selected_date, + on_add_content=handle_add_content, + on_generate_with_ai=handle_generate_with_ai + ) + if st.session_state.get('show_ai_modal', False): + st.markdown("### AI Content Suggestions") + with st.container(): + render_ai_suggestions_modal( + generate_ai_suggestions=self._generate_ai_suggestions, + on_create_brief=self._create_content_brief, + on_schedule=self._schedule_content, + on_refine=self._refine_suggestion, + on_customize=self._customize_suggestion + ) + if st.button("Close"): + st.session_state['show_ai_modal'] = False + + with tabs[1]: + render_content_optimization( + content_generator=self.content_generator, + ai_generator=self.ai_generator, + seo_optimizer=self.seo_optimizer + ) + + with tabs[2]: + render_ab_testing(self.content_generator, self.calendar_manager) + + with tabs[3]: + render_content_series_generator( + self.ai_generator, + self.content_generator, + self.seo_optimizer + ) + + with tabs[4]: + st.header("Analytics") + st.markdown("### Performance Insights") + selected_content = st.selectbox( + "Select content to analyze", + options=[item.title for item in self.calendar_manager.get_calendar().get_all_content()], + key="analytics_content_select" + ) + if selected_content: + content_item = next( + item for item in self.calendar_manager.get_calendar().get_all_content() + if item.title == selected_content + ) + render_performance_insights(content_item, self.platform_adapter) + + st.markdown("### Optimization History") + if selected_content in st.session_state.optimization_history: + st.json(st.session_state.optimization_history[selected_content]) + + self.logger.info("Dashboard render completed successfully (tabbed UI)") + except Exception as e: + self.logger.error(f"Error rendering dashboard: {str(e)}", exc_info=True) + st.error(f"An error occurred: {str(e)}") + + def _inject_custom_css(self): + st.markdown(""" + + """, unsafe_allow_html=True) + + def _get_calendar_data(self): + self.logger.info("_get_calendar_data called") + try: + calendar_obj = self.calendar_manager.get_calendar() + if not calendar_obj: + self.logger.info("No calendar found in manager") + return None + data = [] + for date_str, items in calendar_obj.schedule.items(): + for item in items: + data.append({ + 'date': pd.to_datetime(date_str), + 'title': item.title, + 'platform': item.platforms[0] if item.platforms else 'Unknown', + 'type': item.content_type, + 'status': item.status + }) + df = pd.DataFrame(data) if data else None + return df + except Exception as e: + self.logger.error(f"Error loading calendar data: {str(e)}", exc_info=True) + st.error(f"Error loading calendar data: {str(e)}") + return None + + def _add_content(self, content): + calendar = self.calendar_manager.get_calendar() + if not calendar: + st.error("No calendar found. Please create a calendar first.") + return + platform_map = { + 'Blog': Platform.WEBSITE, + 'Instagram': Platform.INSTAGRAM, + 'Twitter': Platform.TWITTER, + 'LinkedIn': Platform.LINKEDIN, + 'Facebook': Platform.FACEBOOK, + } + platform_enum = platform_map.get(content['platform'], Platform.WEBSITE) + content_type_map = { + 'Article': ContentType.BLOG_POST, + 'Social Post': ContentType.SOCIAL_MEDIA, + 'Video': ContentType.VIDEO, + 'Newsletter': ContentType.NEWSLETTER, + } + content_type_enum = content_type_map.get(content['type'], ContentType.BLOG_POST) + seo_data = SEOData( + title=content['title'], + meta_description="", + keywords=[], + structured_data={}, + ) + new_item = ContentItem( + title=content['title'], + description="", + content_type=content_type_enum, + platforms=[platform_enum], + publish_date=pd.to_datetime(content['publish_date']), + seo_data=seo_data, + status=content.get('status', 'Draft') + ) + calendar.add_content(new_item) + self.calendar_manager.save_calendar_to_json() + + def _delete_content(self, row): + calendar = self.calendar_manager.get_calendar() + if not calendar: + return + for date_str, items in list(calendar.schedule.items()): + calendar.schedule[date_str] = [ + item for item in items + if not ( + item.title == row['title'] and + str(item.publish_date.date()) == str(row['date'].date()) and + item.platforms[0].name == str(row['platform']) and + item.content_type.name == str(row['type']) + ) + ] + if not calendar.schedule[date_str]: + del calendar.schedule[date_str] + self.calendar_manager.save_calendar_to_json() + + def _edit_content(self, row, new_title, new_platform, new_type, new_status): + self._delete_content(row) + self._add_content({ + 'title': new_title, + 'platform': new_platform, + 'type': new_type, + 'publish_date': row['date'], + 'status': new_status + }) + + def _get_item_key(self, row): + key_str = f"{row['title']}_{row['date']}_{row['platform']}_{row['type']}" + return hashlib.md5(key_str.encode()).hexdigest() + + def _generate_ai_suggestions(self, content_type, topic, audience, goals, tone, length, model_settings, style_preferences, seo_preferences, platform_settings): + """Generate AI content suggestions based on input parameters.""" + try: + self.logger.info(f"Generating AI suggestions for topic: {topic}") + + # Map content type string to ContentType enum + content_type_map = { + 'Blog Post': ContentType.BLOG_POST, + 'Social Media Post': ContentType.SOCIAL_MEDIA, + 'Video': ContentType.VIDEO, + 'Newsletter': ContentType.NEWSLETTER, + 'Article': ContentType.BLOG_POST, + 'Social Post': ContentType.SOCIAL_MEDIA + } + content_type_enum = content_type_map.get(content_type, ContentType.BLOG_POST) + + # Map platform string to Platform enum + platform_map = { + 'Blog': Platform.WEBSITE, + 'Instagram': Platform.INSTAGRAM, + 'Twitter': Platform.TWITTER, + 'LinkedIn': Platform.LINKEDIN, + 'Facebook': Platform.FACEBOOK, + 'Website': Platform.WEBSITE + } + platform = st.session_state.get('ai_modal_platform', 'Blog') + platform_enum = platform_map.get(platform, Platform.WEBSITE) + + # Create a content item for the suggestion + content_item = ContentItem( + title=topic, + description="", + content_type=content_type_enum, + platforms=[platform_enum], + publish_date=datetime.now(), + seo_data=SEOData( + title=topic, + meta_description="", + keywords=[], + structured_data={} + ), + status='Draft' + ) + + # Use AIGenerator to generate suggestions + suggestions = self.ai_generator.generate_ai_suggestions( + content_type=content_type_enum, + topic=topic, + audience=audience, + goals=goals, + tone=tone, + length=length, + model_settings=model_settings, + style_preferences=style_preferences, + seo_preferences=seo_preferences, + platform_settings=platform_settings, + platform=platform_enum + ) + + if not suggestions: + self.logger.warning("No suggestions generated") + return [] + + # Format suggestions + formatted_suggestions = [] + for suggestion in suggestions: + formatted_suggestion = { + 'title': suggestion.get('title', topic), + 'type': content_type, + 'platform': platform, + 'audience': audience, + 'impact': f"High impact for {', '.join(goals)}", + 'preview': suggestion.get('preview', ''), + 'style_elements': [ + f"Tone: {tone}", + f"Length: {length}", + f"Creativity: {model_settings.get('Creativity Level', 'balanced')}", + f"Formality: {model_settings.get('Formality Level', 'professional')}" + ], + 'seo_elements': [ + f"Keyword Density: {seo_preferences.get('Keyword Density', '2')}%", + "Internal Linking: Enabled" if seo_preferences.get('Internal Linking', True) else "Internal Linking: Disabled", + "External Linking: Enabled" if seo_preferences.get('External Linking', True) else "External Linking: Disabled" + ], + 'engagement_score': f"{85 + len(formatted_suggestions)*5}%", + 'reach': 'High', + 'conversion': f"{3.5 + len(formatted_suggestions)*0.5}%", + 'seo_impact': 'Strong', + 'platform_optimizations': suggestion.get('platform_optimizations', []), + 'variations': suggestion.get('variations', [ + "Alternative headline", + "Different content angle", + "Alternative format" + ]), + 'seo_recommendations': suggestion.get('seo_elements', []), + 'media_suggestions': suggestion.get('media_suggestions', [ + "Featured image", + "Supporting graphics", + "Social media visuals" + ]) + } + formatted_suggestions.append(formatted_suggestion) + + self.logger.info(f"Generated {len(formatted_suggestions)} suggestions successfully") + return formatted_suggestions + + except Exception as e: + self.logger.error(f"Error generating AI suggestions: {str(e)}", exc_info=True) + st.error(f"Error generating suggestions: {str(e)}") + return [] + + def _create_content_brief(self, content_item: ContentItem) -> Dict[str, Any]: + """Create a detailed content brief for the given content item.""" + try: + self.logger.info(f"Creating content brief for: {content_item.title}") + + # Generate content brief using the content brief generator + brief = self.content_brief_generator.generate_brief( + content_item=content_item, + target_audience={ + 'audience': content_item.description, + 'goals': ['engage', 'inform', 'convert'] + } + ) + + # Enhance brief with SEO data + if brief and 'content_flow' in brief: + brief['seo_optimization'] = { + 'meta_description': self.seo_optimizer.generate_meta_description( + brief['content_flow'].get('introduction', {}).get('summary', '') + ), + 'keywords': self.seo_optimizer.extract_keywords( + brief['content_flow'].get('introduction', {}).get('summary', '') + ), + 'structured_data': self.seo_optimizer.generate_structured_data( + content_item.content_type + ) + } + + self.logger.info(f"Content brief created successfully for: {content_item.title}") + return brief + + except Exception as e: + self.logger.error(f"Error creating content brief: {str(e)}", exc_info=True) + st.error(f"Error creating content brief: {str(e)}") + return {} + + def _schedule_content(self, content_item: ContentItem, publish_date: datetime) -> bool: + """Schedule content for publishing on the specified date.""" + try: + self.logger.info(f"Scheduling content: {content_item.title} for {publish_date}") + + # Get the calendar + calendar = self.calendar_manager.get_calendar() + if not calendar: + raise ValueError("No calendar found") + + # Update the publish date + content_item.publish_date = publish_date + + # Add to calendar + calendar.add_content(content_item) + + # Save changes + self.calendar_manager.save_calendar_to_json() + + self.logger.info(f"Content scheduled successfully: {content_item.title}") + return True + + except Exception as e: + self.logger.error(f"Error scheduling content: {str(e)}", exc_info=True) + st.error(f"Error scheduling content: {str(e)}") + return False + + def _refine_suggestion(self, suggestion: Dict[str, Any], feedback: Dict[str, Any]) -> Dict[str, Any]: + """Refine an AI-generated suggestion based on user feedback.""" + try: + self.logger.info("Refining AI suggestion based on feedback") + + # Update suggestion based on feedback + if 'tone' in feedback: + suggestion['style_elements'] = [ + f"Tone: {feedback['tone']}", + *[elem for elem in suggestion['style_elements'] if not elem.startswith('Tone:')] + ] + + if 'length' in feedback: + suggestion['style_elements'] = [ + f"Length: {feedback['length']}", + *[elem for elem in suggestion['style_elements'] if not elem.startswith('Length:')] + ] + + if 'keywords' in feedback: + suggestion['seo_elements'] = [ + f"Keywords: {', '.join(feedback['keywords'])}", + *[elem for elem in suggestion['seo_elements'] if not elem.startswith('Keywords:')] + ] + + # Regenerate content with refined parameters + refined_content = self.content_brief_generator.generate_brief( + content_item=ContentItem( + title=suggestion['title'], + description="", + content_type=ContentType[suggestion['type'].upper().replace(' ', '_')], + platforms=[Platform[suggestion['platform'].upper()]], + publish_date=datetime.now(), + seo_data=SEOData( + title=suggestion['title'], + meta_description="", + keywords=feedback.get('keywords', []), + structured_data={} + ), + status='Draft' + ), + target_audience={ + 'audience': suggestion['audience'], + 'goals': feedback.get('goals', ['engage', 'inform']), + 'preferences': { + 'tone': feedback.get('tone', 'professional'), + 'length': feedback.get('length', 'medium') + } + } + ) + + if refined_content: + suggestion['preview'] = refined_content.get('content_flow', {}).get('introduction', {}).get('summary', '') + + self.logger.info("Suggestion refined successfully") + return suggestion + + except Exception as e: + self.logger.error(f"Error refining suggestion: {str(e)}", exc_info=True) + st.error(f"Error refining suggestion: {str(e)}") + return suggestion + + def _customize_suggestion(self, suggestion: Dict[str, Any], customizations: Dict[str, Any]) -> Dict[str, Any]: + """Customize an AI-generated suggestion with specific requirements.""" + try: + self.logger.info("Customizing AI suggestion") + + # Apply customizations + if 'title' in customizations: + suggestion['title'] = customizations['title'] + + if 'platform' in customizations: + suggestion['platform'] = customizations['platform'] + + if 'style' in customizations: + suggestion['style_elements'] = [ + f"Tone: {customizations['style'].get('tone', 'professional')}", + f"Length: {customizations['style'].get('length', 'medium')}", + f"Creativity: {customizations['style'].get('creativity', 'balanced')}", + f"Formality: {customizations['style'].get('formality', 'professional')}" + ] + + if 'seo' in customizations: + suggestion['seo_elements'] = [ + f"Keyword Density: {customizations['seo'].get('keyword_density', '2')}%", + "Internal Linking: Enabled" if customizations['seo'].get('internal_linking', True) else "Internal Linking: Disabled", + "External Linking: Enabled" if customizations['seo'].get('external_linking', True) else "External Linking: Disabled" + ] + + # Regenerate content with customizations + customized_content = self.content_brief_generator.generate_brief( + content_item=ContentItem( + title=suggestion['title'], + description="", + content_type=ContentType[suggestion['type'].upper().replace(' ', '_')], + platforms=[Platform[suggestion['platform'].upper()]], + publish_date=datetime.now(), + seo_data=SEOData( + title=suggestion['title'], + meta_description="", + keywords=customizations.get('seo', {}).get('keywords', []), + structured_data={} + ), + status='Draft' + ), + target_audience={ + 'audience': suggestion['audience'], + 'goals': customizations.get('goals', ['engage', 'inform']), + 'preferences': customizations.get('style', {}) + } + ) + + if customized_content: + suggestion['preview'] = customized_content.get('content_flow', {}).get('introduction', {}).get('summary', '') + + self.logger.info("Suggestion customized successfully") + return suggestion + + except Exception as e: + self.logger.error(f"Error customizing suggestion: {str(e)}", exc_info=True) + st.error(f"Error customizing suggestion: {str(e)}") + return suggestion + + def _optimize_content_for_platform(self, content_item: ContentItem, platform: Platform) -> Dict[str, Any]: + """Optimize content specifically for a target platform.""" + try: + self.logger.info(f"Optimizing content for {platform.name}: {content_item.title}") + + # Get platform-specific requirements + platform_requirements = self.platform_adapter.get_platform_requirements(platform) + + # Generate platform-optimized content + optimized_content = self.content_generator.optimize_for_platform( + content=content_item, + platform=platform, + requirements=platform_requirements + ) + + if not optimized_content: + raise ValueError(f"Failed to optimize content for {platform.name}") + + # Enhance with AI + ai_enhanced = self.ai_generator.enhance_for_platform( + content=optimized_content, + platform=platform, + enhancement_type='platform_specific' + ) + + if ai_enhanced: + optimized_content.update(ai_enhanced) + + # Track optimization history + if content_item.title not in st.session_state.optimization_history: + st.session_state.optimization_history[content_item.title] = [] + st.session_state.optimization_history[content_item.title].append({ + 'platform': platform.name, + 'timestamp': datetime.now(), + 'changes': optimized_content.get('changes', []) + }) + + self.logger.info(f"Content optimized successfully for {platform.name}") + return optimized_content + + except Exception as e: + self.logger.error(f"Error optimizing content: {str(e)}", exc_info=True) + st.error(f"Error optimizing content: {str(e)}") + return {} + +if __name__ == "__main__": + dashboard = ContentCalendarDashboard() + dashboard.render() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/ui/filters.py b/lib/ai_seo_tools/content_calendar/ui/filters.py new file mode 100644 index 00000000..39d9a27d --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/ui/filters.py @@ -0,0 +1,30 @@ +import streamlit as st +from datetime import datetime, timedelta + +def render_filters(): + with st.expander("Filters", expanded=False): + col1, col2 = st.columns(2) + with col1: + start_date = st.date_input("Start Date", st.session_state.get('filter_start_date', datetime.now())) + end_date = st.date_input("End Date", st.session_state.get('filter_end_date', datetime.now() + timedelta(days=30))) + st.session_state['filter_start_date'] = start_date + st.session_state['filter_end_date'] = end_date + with col2: + platforms = st.multiselect( + "Platforms", + ["Blog", "Instagram", "Twitter", "LinkedIn", "Facebook"], + default=st.session_state.get('filter_platforms', ["Blog"]) + ) + st.session_state['filter_platforms'] = platforms + content_types = st.multiselect( + "Content Types", + ["Article", "Social Post", "Video", "Newsletter"], + default=st.session_state.get('filter_content_types', ["Article"]) + ) + st.session_state['filter_content_types'] = content_types + statuses = st.multiselect( + "Status", + ["Draft", "Scheduled", "Published", "Archived"], + default=st.session_state.get('filter_statuses', ["Draft", "Scheduled"]) + ) + st.session_state['filter_statuses'] = statuses \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/utils/date_utils.py b/lib/ai_seo_tools/content_calendar/utils/date_utils.py new file mode 100644 index 00000000..65da3712 --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/utils/date_utils.py @@ -0,0 +1,198 @@ +from datetime import datetime, timedelta +from typing import Dict, List, Any +import calendar +import random + +def calculate_publish_dates( + topics: List[Dict[str, Any]], + start_date: datetime, + duration: str +) -> Dict[str, List[Dict[str, Any]]]: + """ + Calculate optimal publish dates for content topics. + + Args: + topics: List of content topics to schedule + start_date: When to start publishing + duration: How long the calendar should span ('weekly', 'monthly', 'quarterly') + + Returns: + Dictionary mapping dates to scheduled content + """ + # Calculate end date based on duration + end_date = _calculate_end_date(start_date, duration) + + # Get all dates in range + dates = _get_dates_in_range(start_date, end_date) + + # Calculate optimal posting frequency + frequency = _calculate_posting_frequency(len(topics), len(dates)) + + # Schedule content + schedule = _schedule_content(topics, dates, frequency) + + return schedule + +def _calculate_end_date(start_date: datetime, duration: str) -> datetime: + """Calculate end date based on duration.""" + if duration == 'weekly': + return start_date + timedelta(days=7) + elif duration == 'monthly': + # Add one month + if start_date.month == 12: + return datetime(start_date.year + 1, 1, start_date.day) + return datetime(start_date.year, start_date.month + 1, start_date.day) + elif duration == 'quarterly': + # Add three months + new_month = start_date.month + 3 + new_year = start_date.year + if new_month > 12: + new_month -= 12 + new_year += 1 + return datetime(new_year, new_month, start_date.day) + else: + raise ValueError(f"Invalid duration: {duration}") + +def _get_dates_in_range( + start_date: datetime, + end_date: datetime +) -> List[datetime]: + """Get all dates in the given range.""" + dates = [] + current_date = start_date + + while current_date <= end_date: + # Skip weekends + if current_date.weekday() < 5: # 0-4 are weekdays + dates.append(current_date) + current_date += timedelta(days=1) + + return dates + +def _calculate_posting_frequency( + num_topics: int, + num_dates: int +) -> Dict[str, int]: + """ + Calculate optimal posting frequency based on number of topics and dates. + + Returns: + Dictionary with posting frequency for each content type + """ + # Calculate base frequency + base_frequency = num_dates / num_topics + + # Adjust for content types + return { + 'blog_post': max(1, int(base_frequency * 0.4)), # 40% of content + 'social_media': max(1, int(base_frequency * 0.3)), # 30% of content + 'video': max(1, int(base_frequency * 0.2)), # 20% of content + 'newsletter': max(1, int(base_frequency * 0.1)) # 10% of content + } + +def _schedule_content( + topics: List[Dict[str, Any]], + dates: List[datetime], + frequency: Dict[str, int] +) -> Dict[str, List[Dict[str, Any]]]: + """ + Schedule content topics across available dates. + + Args: + topics: List of content topics to schedule + dates: Available dates for scheduling + frequency: Posting frequency for each content type + + Returns: + Dictionary mapping dates to scheduled content + """ + schedule = {} + current_date_index = 0 + + # Group topics by content type + topics_by_type = _group_topics_by_type(topics) + + # Schedule each content type + for content_type, type_topics in topics_by_type.items(): + type_frequency = frequency.get(content_type, 1) + + for topic in type_topics: + # Find next available date + while current_date_index < len(dates): + date = dates[current_date_index] + date_str = date.strftime('%Y-%m-%d') + + # Check if date is available + if date_str not in schedule: + schedule[date_str] = [] + + # Add topic to schedule + schedule[date_str].append(topic) + + # Move to next date based on frequency + current_date_index += type_frequency + break + + # If we've used all dates, wrap around + if current_date_index >= len(dates): + current_date_index = 0 + + return schedule + +def _group_topics_by_type( + topics: List[Dict[str, Any]] +) -> Dict[str, List[Dict[str, Any]]]: + """Group topics by their content type.""" + grouped = {} + + for topic in topics: + content_type = topic.get('content_type', 'blog_post') + if content_type not in grouped: + grouped[content_type] = [] + grouped[content_type].append(topic) + + return grouped + +def get_optimal_posting_time( + content_type: str, + platform: str +) -> datetime.time: + """ + Get optimal posting time for content type and platform. + + Args: + content_type: Type of content + platform: Target platform + + Returns: + Optimal time to post + """ + # Default optimal times (can be customized based on platform analytics) + optimal_times = { + 'blog_post': { + 'website': datetime.time(9, 0), # 9 AM + 'medium': datetime.time(10, 0) # 10 AM + }, + 'social_media': { + 'facebook': datetime.time(15, 0), # 3 PM + 'twitter': datetime.time(12, 0), # 12 PM + 'linkedin': datetime.time(8, 0), # 8 AM + 'instagram': datetime.time(19, 0) # 7 PM + }, + 'video': { + 'youtube': datetime.time(14, 0) # 2 PM + }, + 'newsletter': { + 'email': datetime.time(6, 0) # 6 AM + } + } + + # Get optimal time for content type and platform + content_times = optimal_times.get(content_type, {}) + optimal_time = content_times.get(platform) + + if optimal_time is None: + # Default to 9 AM if no specific time is set + optimal_time = datetime.time(9, 0) + + return optimal_time \ No newline at end of file diff --git a/lib/ai_seo_tools/content_calendar/utils/error_handling.py b/lib/ai_seo_tools/content_calendar/utils/error_handling.py new file mode 100644 index 00000000..af47b98d --- /dev/null +++ b/lib/ai_seo_tools/content_calendar/utils/error_handling.py @@ -0,0 +1,154 @@ +import functools +import logging +from typing import Any, Callable, TypeVar, cast +from datetime import datetime + +logger = logging.getLogger(__name__) + +T = TypeVar('T') + +def handle_calendar_error(func: Callable[..., T]) -> Callable[..., T]: + """ + Decorator to handle errors in calendar operations. + + Args: + func: Function to decorate + + Returns: + Decorated function with error handling + """ + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + try: + return func(*args, **kwargs) + except ValueError as e: + logger.error(f"Invalid input in {func.__name__}: {str(e)}") + raise + except Exception as e: + logger.error(f"Error in {func.__name__}: {str(e)}") + raise CalendarError(f"Calendar operation failed: {str(e)}") + return cast(Callable[..., T], wrapper) + +class CalendarError(Exception): + """Base exception for calendar-related errors.""" + pass + +class ContentError(CalendarError): + """Exception for content-related errors.""" + pass + +class SchedulingError(CalendarError): + """Exception for scheduling-related errors.""" + pass + +class ValidationError(CalendarError): + """Exception for validation-related errors.""" + pass + +def validate_date_range( + start_date: datetime, + end_date: datetime +) -> None: + """ + Validate date range for calendar operations. + + Args: + start_date: Start date + end_date: End date + + Raises: + ValidationError: If date range is invalid + """ + if not isinstance(start_date, datetime): + raise ValidationError("Start date must be a datetime object") + + if not isinstance(end_date, datetime): + raise ValidationError("End date must be a datetime object") + + if start_date > end_date: + raise ValidationError("Start date must be before end date") + + if (end_date - start_date).days > 365: + raise ValidationError("Calendar duration cannot exceed one year") + +def validate_content_item(content: dict) -> None: + """ + Validate content item structure. + + Args: + content: Content item to validate + + Raises: + ValidationError: If content item is invalid + """ + required_fields = ['title', 'description', 'content_type', 'platforms'] + + for field in required_fields: + if field not in content: + raise ValidationError(f"Missing required field: {field}") + + if not isinstance(content['platforms'], list): + raise ValidationError("Platforms must be a list") + + if not content['platforms']: + raise ValidationError("At least one platform must be specified") + +def validate_calendar_duration(duration: str) -> None: + """ + Validate calendar duration. + + Args: + duration: Duration to validate ('weekly', 'monthly', 'quarterly') + + Raises: + ValidationError: If duration is invalid + """ + valid_durations = ['weekly', 'monthly', 'quarterly'] + + if duration not in valid_durations: + raise ValidationError( + f"Invalid duration: {duration}. " + f"Must be one of: {', '.join(valid_durations)}" + ) + +def log_calendar_operation( + operation: str, + details: dict +) -> None: + """ + Log calendar operation details. + + Args: + operation: Name of the operation + details: Operation details to log + """ + logger.info(f"Calendar operation: {operation}") + logger.debug(f"Operation details: {details}") + +def handle_api_error( + error: Exception, + operation: str +) -> None: + """ + Handle API-related errors. + + Args: + error: The error that occurred + operation: The operation that failed + """ + logger.error(f"API error in {operation}: {str(error)}") + raise CalendarError(f"API operation failed: {str(error)}") + +def handle_integration_error( + error: Exception, + integration: str +) -> None: + """ + Handle integration-related errors. + + Args: + error: The error that occurred + integration: The integration that failed + """ + logger.error(f"Integration error with {integration}: {str(error)}") + raise CalendarError(f"Integration failed: {str(error)}") \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/README.md b/lib/ai_seo_tools/content_gap_analysis/README.md new file mode 100644 index 00000000..670a94ca --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/README.md @@ -0,0 +1,182 @@ +# Content Gap Analysis Tool + +A comprehensive AI-powered tool for analyzing content gaps and generating strategic content recommendations. + +## Overview + +The Content Gap Analysis tool combines multiple SEO tools to provide a complete analysis of your content strategy, identify opportunities, and generate actionable recommendations. It leverages existing AI SEO tools and adds new capabilities for comprehensive content analysis. + +## Workflow Design + +### 1. Website Analysis +**Input:** Website URL +**Tools Integration:** +- `analyze_onpage_seo()`: Analyze content quality and structure +- `url_seo_checker()`: Check technical SEO aspects +- `google_pagespeed_insights()`: Assess page performance + +**Analysis Components:** +- Content structure mapping +- Topic categorization +- Content depth assessment +- Performance metrics + +### 2. Competitor Analysis +**Input:** Competitor URLs +**Tools Integration:** +- `url_seo_checker()`: Analyze competitor URLs +- `analyze_onpage_seo()`: Compare content quality +- `ai_title_generator()`: Analyze title patterns + +**Analysis Components:** +- Content strategy comparison +- Topic coverage gaps +- Content format analysis +- Title pattern analysis + +### 3. Keyword Research +**Input:** Industry/Niche +**Tools Integration:** +- `ai_title_generator()`: Generate keyword-based titles +- `metadesc_generator_main()`: Analyze meta descriptions for keyword usage +- `ai_structured_data()`: Check structured data implementation + +**Analysis Components:** +- Keyword opportunity identification +- Search intent analysis +- Content format suggestions +- Topic clustering + +### 4. AI-Powered Recommendations +**Tools Integration:** +- `ai_title_generator()`: Generate content titles +- `metadesc_generator_main()`: Create content summaries +- `ai_structured_data()`: Suggest structured data implementation + +**Output Components:** +- Content topic suggestions +- Format recommendations +- Priority scoring +- Implementation timeline + +## Implementation Plan + +### Phase 1: Core Infrastructure +1. Create base classes and interfaces +2. Implement data collection modules +3. Set up AI model integration +4. Develop data storage system + +### Phase 2: Tool Integration +1. Integrate existing SEO tools +2. Create unified API for tool interaction +3. Implement data sharing between tools +4. Develop result aggregation system + +### Phase 3: Analysis Engine +1. Implement content structure analysis +2. Develop competitor analysis algorithms +3. Create keyword research system +4. Build recommendation engine + +### Phase 4: UI/UX Development +1. Create step-by-step workflow interface +2. Implement progress tracking +3. Develop visualization components +4. Add export functionality + +## Technical Requirements + +### Dependencies +- Existing SEO tools from `lib/ai_seo_tools/` +- AI models for content analysis +- Web scraping capabilities +- Data storage system + +### File Structure +``` +content_gap_analysis/ +├── __init__.py +├── main.py +├── website_analyzer.py +├── competitor_analyzer.py +├── keyword_researcher.py +├── recommendation_engine.py +├── utils/ +│ ├── __init__.py +│ ├── data_collector.py +│ ├── content_parser.py +│ └── ai_processor.py +└── tests/ + ├── __init__.py + ├── test_website_analyzer.py + ├── test_competitor_analyzer.py + └── test_keyword_researcher.py +``` + +## Integration Points + +### Existing Tools +1. **On-Page SEO Analyzer** + - Function: `analyze_onpage_seo()` + - Purpose: Content quality assessment + - Integration: Content structure analysis + +2. **URL SEO Checker** + - Function: `url_seo_checker()` + - Purpose: Technical optimization + - Integration: URL structure analysis + +3. **Blog Title Generator** + - Function: `ai_title_generator()` + - Purpose: Content ideas + - Integration: Keyword analysis + +4. **Meta Description Generator** + - Function: `metadesc_generator_main()` + - Purpose: Content summaries + - Integration: Content optimization + +5. **Structured Data Generator** + - Function: `ai_structured_data()` + - Purpose: Rich snippets + - Integration: Content enhancement + +### New Components +1. **Content Structure Analyzer** + - Purpose: Map website content structure + - Output: Content hierarchy and relationships + +2. **Competitor Content Analyzer** + - Purpose: Analyze competitor content strategy + - Output: Content gaps and opportunities + +3. **Keyword Opportunity Finder** + - Purpose: Identify keyword gaps + - Output: Keyword recommendations + +4. **AI Recommendation Engine** + - Purpose: Generate content recommendations + - Output: Actionable content strategy + +## Future Enhancements + +1. **Advanced Analytics** + - Content performance tracking + - ROI analysis + - Trend prediction + +2. **Automation Features** + - Automated content planning + - Schedule generation + - Priority scoring + +3. **Integration Expansion** + - CMS integration + - Analytics platform connection + - Social media analysis + +4. **AI Improvements** + - Advanced topic modeling + - Sentiment analysis + - Content quality scoring \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/__init__.py b/lib/ai_seo_tools/content_gap_analysis/__init__.py new file mode 100644 index 00000000..a786dbf0 --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/__init__.py @@ -0,0 +1,36 @@ +""" +Content Gap Analysis Tool for Alwrity. +""" + +from .ui import ContentGapAnalysisUI +from .main import ContentGapAnalysis +from .keyword_researcher import KeywordResearcher +from .competitor_analyzer import CompetitorAnalyzer +from .website_analyzer import WebsiteAnalyzer +from .recommendation_engine import RecommendationEngine +from .utils.ai_processor import AIProcessor + +__all__ = [ + 'ContentGapAnalysisUI', + 'ContentGapAnalysis', + 'KeywordResearcher', + 'CompetitorAnalyzer', + 'WebsiteAnalyzer', + 'RecommendationEngine', + 'AIProcessor' +] + +def run_content_gap_analysis(): + """Run the Content Gap Analysis tool.""" + # Initialize the UI with proper configuration + ui = ContentGapAnalysisUI() + + # Set up the page configuration + st.set_page_config( + page_title="Content Gap Analysis", + page_icon="📊", + layout="wide" + ) + + # Run the UI + ui.run() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/competitor_analyzer.py b/lib/ai_seo_tools/content_gap_analysis/competitor_analyzer.py new file mode 100644 index 00000000..d5effd97 --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/competitor_analyzer.py @@ -0,0 +1,711 @@ +""" +Competitor analyzer for content gap analysis. +""" + +from typing import Dict, Any, List, Optional +import streamlit as st +from collections import Counter, defaultdict +from loguru import logger +from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer +from lib.ai_seo_tools.content_gap_analysis.utils.data_collector import DataCollector +from lib.ai_seo_tools.content_gap_analysis.utils.content_parser import ContentParser +from lib.ai_seo_tools.content_gap_analysis.utils.ai_processor import AIProcessor, ProgressTracker +import asyncio +import sys +import os +import json +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/competitor_analyzer.log", + rotation="50 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}" +) + +# Ensure logs directory exists +os.makedirs("logs", exist_ok=True) + +class CompetitorAnalyzer: + """Analyzes competitor content and market position.""" + + def __init__(self): + """Initialize the competitor analyzer.""" + self.website_analyzer = WebsiteAnalyzer() + self.ai_processor = AIProcessor() + self.progress = ProgressTracker() + + # Define analysis stages + self.stages = { + 'competitor_analysis': { + 'name': 'Competitor Analysis', + 'steps': [ + 'Initializing competitor analysis', + 'Analyzing competitor content', + 'Evaluating market position', + 'Identifying content gaps', + 'Generating competitive insights' + ] + } + } + + logger.info("CompetitorAnalyzer initialized") + + def analyze(self, competitor_urls: List[str], industry: str) -> Dict[str, Any]: + """ + Analyze competitor websites. + + Args: + competitor_urls: List of competitor URLs to analyze + industry: Industry category + + Returns: + Dictionary containing competitor analysis results + """ + try: + results = { + 'competitors': [], + 'market_position': {}, + 'content_gaps': [], + 'advantages': [] + } + + # Analyze each competitor + for url in competitor_urls: + competitor_analysis = self.website_analyzer.analyze_website(url) + if competitor_analysis.get('success', False): + results['competitors'].append({ + 'url': url, + 'analysis': competitor_analysis['data'] + }) + + # Generate market position analysis using AI + prompt = f"""Analyze the market position of competitors in the {industry} industry: + + Competitor Analyses: + {json.dumps(results['competitors'], indent=2)} + + Provide: + 1. Market position analysis + 2. Content gaps + 3. Competitive advantages + + Format the response as JSON with 'market_position', 'content_gaps', and 'advantages' keys.""" + + # Get AI analysis + analysis = llm_text_gen( + prompt=prompt, + system_prompt="You are an SEO expert specializing in competitive analysis.", + response_format="json_object" + ) + + if analysis: + results['market_position'] = analysis.get('market_position', {}) + results['content_gaps'] = analysis.get('content_gaps', []) + results['advantages'] = analysis.get('advantages', []) + + return results + + except Exception as e: + error_msg = f"Error analyzing competitors: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + 'error': error_msg, + 'competitors': [], + 'market_position': {}, + 'content_gaps': [], + 'advantages': [] + } + + def _analyze_competitor_content(self, competitor_urls: List[str]) -> Dict[str, Any]: + """Analyze competitor content.""" + try: + content_analysis = {} + + for url in competitor_urls: + # Get AI analysis for each competitor + analysis = self.ai_processor.analyze_content({ + 'url': url, + 'content': {} # Content will be fetched by AI processor + }) + + content_analysis[url] = { + 'content_metrics': analysis.get('content_metrics', {}), + 'content_evolution': analysis.get('content_evolution', {}), + 'topic_trends': analysis.get('topic_trends', {}), + 'performance_trends': analysis.get('performance_trends', {}) + } + + return content_analysis + except Exception as e: + st.error(f"Error analyzing competitor content: {str(e)}") + return {} + + def _evaluate_market_position(self, content_analysis: Dict[str, Any], industry: str) -> Dict[str, Any]: + """Evaluate market position.""" + try: + market_position = { + 'industry_rank': 0, + 'content_quality_rank': 0, + 'market_share': 0, + 'competitive_advantages': [], + 'competitive_disadvantages': [] + } + + # Calculate industry rank based on content quality + content_quality_scores = [ + analysis.get('content_metrics', {}).get('quality_score', 0) + for analysis in content_analysis.values() + ] + + if content_quality_scores: + market_position['content_quality_rank'] = sum(content_quality_scores) / len(content_quality_scores) + + # Identify competitive advantages and disadvantages + for url, analysis in content_analysis.items(): + quality_score = analysis.get('content_metrics', {}).get('quality_score', 0) + + if quality_score > market_position['content_quality_rank']: + market_position['competitive_advantages'].append({ + 'url': url, + 'advantage': 'Higher content quality', + 'score': quality_score + }) + elif quality_score < market_position['content_quality_rank']: + market_position['competitive_disadvantages'].append({ + 'url': url, + 'disadvantage': 'Lower content quality', + 'score': quality_score + }) + + return market_position + except Exception as e: + st.error(f"Error evaluating market position: {str(e)}") + return {} + + def _identify_content_gaps(self, content_analysis: Dict[str, Any]) -> List[Dict[str, Any]]: + """Identify content gaps.""" + try: + content_gaps = [] + + # Analyze content coverage + all_topics = set() + for analysis in content_analysis.values(): + topics = analysis.get('topic_trends', {}).get('topics', []) + all_topics.update(topics) + + # Identify missing topics for each competitor + for url, analysis in content_analysis.items(): + covered_topics = set(analysis.get('topic_trends', {}).get('topics', [])) + missing_topics = all_topics - covered_topics + + if missing_topics: + content_gaps.append({ + 'url': url, + 'missing_topics': list(missing_topics), + 'gap_type': 'topic_coverage' + }) + + return content_gaps + except Exception as e: + st.error(f"Error identifying content gaps: {str(e)}") + return [] + + def _generate_competitive_insights(self, content_analysis: Dict[str, Any], market_position: Dict[str, Any], content_gaps: List[Dict[str, Any]]) -> List[str]: + """Generate competitive insights.""" + try: + insights = [] + + # Market position insights + if market_position.get('content_quality_rank', 0) > 80: + insights.append("Strong market position with high content quality") + elif market_position.get('content_quality_rank', 0) > 60: + insights.append("Moderate market position with room for improvement") + else: + insights.append("Weak market position requiring significant improvement") + + # Content gap insights + if content_gaps: + insights.append(f"Identified {len(content_gaps)} content gaps across competitors") + + # Competitive advantage insights + if market_position.get('competitive_advantages'): + insights.append(f"Found {len(market_position['competitive_advantages'])} competitive advantages") + + return insights + except Exception as e: + st.error(f"Error generating competitive insights: {str(e)}") + return [] + + def _run_seo_analysis(self, url: str) -> dict: + """ + Run SEO analysis on competitor website. + + Args: + url (str): The URL to analyze + + Returns: + dict: SEO analysis results + """ + # Run website analysis using the new analyzer + analysis = self.website_analyzer.analyze_website(url) + + if not analysis.get('success', False): + return { + 'error': analysis.get('error', 'Unknown error in SEO analysis'), + 'onpage_seo': {}, + 'url_seo': {} + } + + # Extract SEO information from the analysis + seo_info = analysis['data']['analysis']['seo_info'] + basic_info = analysis['data']['analysis']['basic_info'] + + return { + 'onpage_seo': { + 'meta_tags': seo_info.get('meta_tags', {}), + 'content': seo_info.get('content', {}), + 'recommendations': seo_info.get('recommendations', []) + }, + 'url_seo': { + 'title': basic_info.get('title', ''), + 'meta_description': basic_info.get('meta_description', ''), + 'has_robots_txt': bool(basic_info.get('robots_txt')), + 'has_sitemap': bool(basic_info.get('sitemap')) + } + } + + def _analyze_title_patterns(self, url: str) -> dict: + """ + Analyze title patterns using the title generator. + + Args: + url (str): The URL to analyze + + Returns: + dict: Title pattern analysis results + """ + # Use title generator to analyze patterns + title_analysis = ai_title_generator(url) + + return { + 'patterns': title_analysis.get('patterns', {}), + 'suggestions': title_analysis.get('suggestions', []) + } + + def _compare_competitors(self, results: dict) -> dict: + """ + Compare results across all competitors. + + Args: + results (dict): Analysis results for all competitors + + Returns: + dict: Comparative analysis results + """ + comparison = { + 'content_comparison': self._compare_content(results), + 'seo_comparison': self._compare_seo(results), + 'title_comparison': self._compare_titles(results), + 'performance_metrics': self._compare_performance(results), + 'content_gaps': self._identify_content_gaps(results) + } + + # Add AI-enhanced insights + comparison['ai_insights'] = self.ai_processor.analyze_competitor_comparison(comparison) + + return comparison + + def _compare_content(self, results: dict) -> dict: + """Compare content structure across competitors.""" + content_comparison = { + 'topic_distribution': self._analyze_topic_distribution(results), + 'content_depth': self._analyze_content_depth(results), + 'content_formats': self._analyze_content_formats(results), + 'content_quality': self._analyze_content_quality(results) + } + + return content_comparison + + def _analyze_topic_distribution(self, results: dict) -> dict: + """Analyze topic distribution across competitors.""" + all_topics = [] + topic_frequency = Counter() + + for url, data in results.items(): + topics = data['content_structure'].get('topics', []) + all_topics.extend([t['topic'] for t in topics]) + topic_frequency.update([t['topic'] for t in topics]) + + return { + 'common_topics': [topic for topic, count in topic_frequency.most_common(10)], + 'unique_topics': list(set(all_topics)), + 'topic_frequency': dict(topic_frequency.most_common()), + 'topic_coverage': len(set(all_topics)) / len(all_topics) if all_topics else 0 + } + + def _analyze_content_depth(self, results: dict) -> dict: + """Analyze content depth across competitors.""" + depth_metrics = { + 'word_counts': {}, + 'section_counts': {}, + 'heading_distribution': defaultdict(list), + 'content_hierarchy': {} + } + + for url, data in results.items(): + content_structure = data['content_structure'] + + # Word count analysis + depth_metrics['word_counts'][url] = content_structure.get('text_statistics', {}).get('word_count', 0) + + # Section analysis + depth_metrics['section_counts'][url] = len(content_structure.get('sections', [])) + + # Heading distribution + for level, count in content_structure.get('hierarchy', {}).get('heading_distribution', {}).items(): + depth_metrics['heading_distribution'][level].append(count) + + # Content hierarchy + depth_metrics['content_hierarchy'][url] = content_structure.get('hierarchy', {}) + + return depth_metrics + + def _analyze_content_formats(self, results: dict) -> dict: + """Analyze content formats across competitors.""" + format_analysis = { + 'format_types': defaultdict(int), + 'format_distribution': defaultdict(list), + 'format_effectiveness': {} + } + + for url, data in results.items(): + sections = data['content_structure'].get('sections', []) + + for section in sections: + format_type = section.get('type', 'unknown') + format_analysis['format_types'][format_type] += 1 + format_analysis['format_distribution'][format_type].append({ + 'url': url, + 'heading': section.get('heading', ''), + 'word_count': section.get('word_count', 0) + }) + + return format_analysis + + def _analyze_content_quality(self, results: dict) -> dict: + """Analyze content quality across competitors.""" + quality_metrics = { + 'readability_scores': {}, + 'content_structure_scores': {}, + 'engagement_metrics': {}, + 'overall_quality': {} + } + + for url, data in results.items(): + content_structure = data['content_structure'] + + # Readability analysis + readability = content_structure.get('readability', {}) + quality_metrics['readability_scores'][url] = { + 'flesch_score': readability.get('flesch_score', 0), + 'avg_sentence_length': readability.get('avg_sentence_length', 0), + 'avg_word_length': readability.get('avg_word_length', 0) + } + + # Structure analysis + hierarchy = content_structure.get('hierarchy', {}) + quality_metrics['content_structure_scores'][url] = { + 'has_proper_hierarchy': hierarchy.get('has_proper_hierarchy', False), + 'heading_distribution': hierarchy.get('heading_distribution', {}), + 'max_depth': hierarchy.get('max_depth', 0) + } + + return quality_metrics + + def _compare_seo(self, results: dict) -> dict: + """Compare SEO metrics across competitors.""" + seo_comparison = { + 'onpage_metrics': defaultdict(list), + 'technical_metrics': defaultdict(list), + 'content_metrics': defaultdict(list), + 'overall_seo_score': {} + } + + for url, data in results.items(): + seo_info = data.get('website_analysis', {}).get('analysis', {}).get('seo_info', {}) + + # On-page SEO metrics + meta_tags = seo_info.get('meta_tags', {}) + seo_comparison['onpage_metrics']['title_score'].append( + 100 if meta_tags.get('title', {}).get('status') == 'good' else 50 + ) + seo_comparison['onpage_metrics']['description_score'].append( + 100 if meta_tags.get('description', {}).get('status') == 'good' else 50 + ) + seo_comparison['onpage_metrics']['keywords_score'].append( + 100 if meta_tags.get('keywords', {}).get('status') == 'good' else 50 + ) + + # Technical SEO metrics + technical = data.get('website_analysis', {}).get('analysis', {}).get('basic_info', {}) + seo_comparison['technical_metrics']['has_robots_txt'].append( + 100 if technical.get('robots_txt') else 0 + ) + seo_comparison['technical_metrics']['has_sitemap'].append( + 100 if technical.get('sitemap') else 0 + ) + + # Content SEO metrics + content = seo_info.get('content', {}) + seo_comparison['content_metrics']['readability_score'].append( + content.get('readability_score', 0) + ) + seo_comparison['content_metrics']['content_quality_score'].append( + content.get('content_quality_score', 0) + ) + + # Overall SEO score + seo_comparison['overall_seo_score'][url] = seo_info.get('overall_score', 0) + + return seo_comparison + + def _compare_titles(self, results: dict) -> dict: + """Compare title patterns across competitors.""" + title_comparison = { + 'pattern_distribution': defaultdict(int), + 'length_distribution': defaultdict(list), + 'keyword_usage': defaultdict(int), + 'format_preferences': defaultdict(int) + } + + for url, data in results.items(): + title_patterns = data['title_patterns'] + + # Pattern analysis + for pattern in title_patterns.get('patterns', {}): + title_comparison['pattern_distribution'][pattern] += 1 + + # Length analysis + for suggestion in title_patterns.get('suggestions', []): + title_comparison['length_distribution'][len(suggestion)].append(suggestion) + + # Keyword analysis + for suggestion in title_patterns.get('suggestions', []): + words = suggestion.lower().split() + for word in words: + if len(word) > 3: # Filter out short words + title_comparison['keyword_usage'][word] += 1 + + return title_comparison + + def _compare_performance(self, results: dict) -> dict: + """Compare performance metrics across competitors.""" + performance_metrics = { + 'content_effectiveness': {}, + 'engagement_metrics': {}, + 'technical_performance': {}, + 'overall_performance': {} + } + + for url, data in results.items(): + # Content effectiveness + content_structure = data['content_structure'] + performance_metrics['content_effectiveness'][url] = { + 'content_depth': content_structure.get('text_statistics', {}).get('word_count', 0), + 'content_quality': content_structure.get('readability', {}).get('flesch_score', 0), + 'content_structure': content_structure.get('hierarchy', {}).get('has_proper_hierarchy', False) + } + + # Technical performance + seo_analysis = data['seo_analysis'] + performance_metrics['technical_performance'][url] = { + 'onpage_score': sum(1 for v in seo_analysis.get('onpage_seo', {}).values() if v), + 'technical_score': sum(1 for v in seo_analysis.get('url_seo', {}).values() if v) + } + + return performance_metrics + + def _find_missing_topics(self, results: dict) -> List[Dict[str, Any]]: + """Find topics that are missing or underrepresented.""" + all_topics = set() + topic_coverage = defaultdict(int) + + # Collect all topics and their coverage + for url, data in results.items(): + topics = data['content_structure'].get('topics', []) + for topic in topics: + all_topics.add(topic['topic']) + topic_coverage[topic['topic']] += 1 + + # Identify missing or underrepresented topics + missing_topics = [] + total_competitors = len(results) + + for topic in all_topics: + coverage = topic_coverage[topic] / total_competitors + if coverage < 0.5: # Topic covered by less than 50% of competitors + missing_topics.append({ + 'topic': topic, + 'coverage': coverage, + 'opportunity_score': 1 - coverage + }) + + return sorted(missing_topics, key=lambda x: x['opportunity_score'], reverse=True) + + def _identify_opportunities(self, results: dict) -> List[Dict[str, Any]]: + """Identify content opportunities based on analysis.""" + opportunities = [] + + # Analyze content depth opportunities + depth_metrics = self._analyze_content_depth(results) + avg_word_count = sum(depth_metrics['word_counts'].values()) / len(depth_metrics['word_counts']) + + for url, word_count in depth_metrics['word_counts'].items(): + if word_count < avg_word_count * 0.7: # Content depth significantly below average + opportunities.append({ + 'type': 'content_depth', + 'url': url, + 'current_value': word_count, + 'target_value': avg_word_count, + 'opportunity_score': (avg_word_count - word_count) / avg_word_count + }) + + # Analyze format opportunities + format_analysis = self._analyze_content_formats(results) + for format_type, distribution in format_analysis['format_distribution'].items(): + if len(distribution) < len(results) * 0.3: # Format used by less than 30% of competitors + opportunities.append({ + 'type': 'content_format', + 'format': format_type, + 'current_coverage': len(distribution) / len(results), + 'opportunity_score': 1 - (len(distribution) / len(results)) + }) + + return sorted(opportunities, key=lambda x: x['opportunity_score'], reverse=True) + + def _analyze_format_gaps(self, results: dict) -> List[Dict[str, Any]]: + """Analyze gaps in content formats.""" + format_gaps = [] + format_analysis = self._analyze_content_formats(results) + + # Identify underutilized formats + for format_type, count in format_analysis['format_types'].items(): + if count < len(results) * 0.3: # Format used by less than 30% of competitors + format_gaps.append({ + 'format': format_type, + 'current_usage': count, + 'potential_impact': 'high' if count < len(results) * 0.2 else 'medium', + 'suggested_implementation': self._generate_format_suggestions(format_type) + }) + + return format_gaps + + def _analyze_quality_gaps(self, results: dict) -> List[Dict[str, Any]]: + """Analyze gaps in content quality.""" + quality_gaps = [] + quality_metrics = self._analyze_content_quality(results) + + # Analyze readability gaps + readability_scores = quality_metrics['readability_scores'] + avg_flesch = sum(score['flesch_score'] for score in readability_scores.values()) / len(readability_scores) + + for url, scores in readability_scores.items(): + if scores['flesch_score'] < avg_flesch * 0.8: # Readability significantly below average + quality_gaps.append({ + 'type': 'readability', + 'url': url, + 'current_score': scores['flesch_score'], + 'target_score': avg_flesch, + 'improvement_needed': avg_flesch - scores['flesch_score'] + }) + + return quality_gaps + + def _analyze_seo_gaps(self, results: dict) -> List[Dict[str, Any]]: + """Analyze gaps in SEO implementation.""" + seo_gaps = [] + seo_comparison = self._compare_seo(results) + + # Analyze on-page SEO gaps + for metric, values in seo_comparison['onpage_metrics'].items(): + avg_value = sum(values) / len(values) + for url, value in zip(results.keys(), values): + if value < avg_value * 0.7: # Significantly below average + seo_gaps.append({ + 'type': 'onpage_seo', + 'metric': metric, + 'url': url, + 'current_value': value, + 'target_value': avg_value, + 'improvement_needed': avg_value - value + }) + + # Analyze technical SEO gaps + for metric, values in seo_comparison['technical_metrics'].items(): + avg_value = sum(values) / len(values) + for url, value in zip(results.keys(), values): + if value < avg_value * 0.7: # Significantly below average + seo_gaps.append({ + 'type': 'technical_seo', + 'metric': metric, + 'url': url, + 'current_value': value, + 'target_value': avg_value, + 'improvement_needed': avg_value - value + }) + + # Analyze content SEO gaps + for metric, values in seo_comparison['content_metrics'].items(): + avg_value = sum(values) / len(values) + for url, value in zip(results.keys(), values): + if value < avg_value * 0.7: # Significantly below average + seo_gaps.append({ + 'type': 'content_seo', + 'metric': metric, + 'url': url, + 'current_value': value, + 'target_value': avg_value, + 'improvement_needed': avg_value - value + }) + + return seo_gaps + + def _generate_format_suggestions(self, format_type: str) -> List[str]: + """Generate suggestions for implementing specific content formats.""" + format_suggestions = { + 'article': [ + 'Create in-depth articles with comprehensive coverage', + 'Include expert quotes and statistics', + 'Add visual elements and infographics' + ], + 'blog_post': [ + 'Write engaging blog posts with personal insights', + 'Include call-to-actions', + 'Add social sharing buttons' + ], + 'how-to': [ + 'Create step-by-step guides', + 'Include screenshots or videos', + 'Add troubleshooting sections' + ], + 'case_study': [ + 'Present real-world examples', + 'Include metrics and results', + 'Add client testimonials' + ] + } + + return format_suggestions.get(format_type, [ + 'Research successful examples', + 'Analyze competitor implementation', + 'Create unique value proposition' + ]) \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/keyword_researcher.py b/lib/ai_seo_tools/content_gap_analysis/keyword_researcher.py new file mode 100644 index 00000000..8c8f8415 --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/keyword_researcher.py @@ -0,0 +1,649 @@ +""" +Keyword researcher for content gap analysis. +""" + +from typing import Dict, Any, List, Optional +import streamlit as st +from loguru import logger +from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer +from lib.ai_seo_tools.content_gap_analysis.utils.data_collector import DataCollector +from lib.ai_seo_tools.content_gap_analysis.utils.content_parser import ContentParser +from lib.ai_seo_tools.content_gap_analysis.utils.ai_processor import AIProcessor, ProgressTracker +import asyncio +import sys +import os +import json +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen +from lib.ai_seo_tools.content_title_generator import ai_title_generator +from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main +from lib.ai_seo_tools.seo_structured_data import ai_structured_data + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/keyword_researcher.log", + rotation="50 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}" +) + +# Ensure logs directory exists +os.makedirs("logs", exist_ok=True) + +class KeywordResearcher: + """Researches and analyzes keywords for content strategy.""" + + def __init__(self): + """Initialize the keyword researcher.""" + self.ai_processor = AIProcessor() + self.progress = ProgressTracker() + + # Define analysis stages + self.stages = { + 'keyword_analysis': { + 'name': 'Keyword Analysis', + 'steps': [ + 'Initializing keyword research', + 'Analyzing keyword trends', + 'Evaluating search intent', + 'Identifying opportunities', + 'Generating keyword insights' + ] + } + } + + def analyze(self, industry: str, url: str) -> Dict[str, Any]: + """ + Analyze keywords for content strategy. + + Args: + industry: Industry category + url: Target website URL + + Returns: + Dictionary containing analysis results + """ + try: + self.progress.start_stage('keyword_analysis') + self.progress.next_step() + + # Analyze keyword trends + trend_analysis = self._analyze_keyword_trends(industry) + self.progress.next_step() + + # Evaluate search intent + intent_analysis = self._evaluate_search_intent(trend_analysis) + self.progress.next_step() + + # Identify opportunities + opportunities = self._identify_opportunities(trend_analysis, intent_analysis) + self.progress.next_step() + + # Generate insights + insights = self._generate_keyword_insights(trend_analysis, intent_analysis, opportunities) + self.progress.next_step() + + self.progress.complete_stage() + + return { + 'trend_analysis': trend_analysis, + 'intent_analysis': intent_analysis, + 'opportunities': opportunities, + 'insights': insights + } + + except Exception as e: + if self.progress.current_stage: + self.progress.update_progress(0, f"Error in {self.progress.stages[self.progress.current_stage]['name']}: {str(e)}") + st.error(f"Error analyzing keywords: {str(e)}") + return { + 'error': str(e), + 'trend_analysis': {}, + 'intent_analysis': {}, + 'opportunities': [], + 'insights': [] + } + + def _analyze_keyword_trends(self, industry: str) -> Dict[str, Any]: + """Analyze keyword trends.""" + try: + # Get AI analysis for keyword trends + analysis = self.ai_processor.analyze_keywords({ + 'industry': industry, + 'keywords': {} # Keywords will be fetched by AI processor + }) + + return { + 'trends': analysis.get('keyword_trends', {}), + 'search_intent': analysis.get('search_intent', {}), + 'keyword_insights': analysis.get('keyword_insights', {}) + } + except Exception as e: + st.error(f"Error analyzing keyword trends: {str(e)}") + return {} + + def _evaluate_search_intent(self, trend_analysis: Dict[str, Any]) -> Dict[str, Any]: + """Evaluate search intent.""" + try: + intent_analysis = { + 'informational': [], + 'transactional': [], + 'navigational': [], + 'commercial': [] + } + + # Categorize keywords by intent + for keyword, data in trend_analysis.get('trends', {}).items(): + intent = data.get('intent', 'informational') + if intent in intent_analysis: + intent_analysis[intent].append({ + 'keyword': keyword, + 'volume': data.get('volume', 0), + 'difficulty': data.get('difficulty', 0) + }) + + return intent_analysis + except Exception as e: + st.error(f"Error evaluating search intent: {str(e)}") + return {} + + def _identify_opportunities(self, trend_analysis: Dict[str, Any], intent_analysis: Dict[str, Any]) -> List[Dict[str, Any]]: + """Identify keyword opportunities.""" + try: + opportunities = [] + + # Analyze each intent category + for intent, keywords in intent_analysis.items(): + for keyword_data in keywords: + # Calculate opportunity score + volume = keyword_data.get('volume', 0) + difficulty = keyword_data.get('difficulty', 0) + opportunity_score = volume * (1 - difficulty/100) + + if opportunity_score > 50: # Threshold for good opportunities + opportunities.append({ + 'keyword': keyword_data['keyword'], + 'intent': intent, + 'volume': volume, + 'difficulty': difficulty, + 'opportunity_score': opportunity_score + }) + + # Sort by opportunity score + opportunities.sort(key=lambda x: x['opportunity_score'], reverse=True) + + return opportunities + except Exception as e: + st.error(f"Error identifying opportunities: {str(e)}") + return [] + + def _generate_keyword_insights(self, trend_analysis: Dict[str, Any], intent_analysis: Dict[str, Any], opportunities: List[Dict[str, Any]]) -> List[str]: + """Generate keyword insights.""" + try: + insights = [] + + # Trend insights + if trend_analysis.get('trends'): + insights.append(f"Analyzed {len(trend_analysis['trends'])} keywords for trends") + + # Intent insights + for intent, keywords in intent_analysis.items(): + if keywords: + insights.append(f"Found {len(keywords)} {intent} keywords") + + # Opportunity insights + if opportunities: + insights.append(f"Identified {len(opportunities)} high-potential keyword opportunities") + + return insights + except Exception as e: + st.error(f"Error generating keyword insights: {str(e)}") + return [] + + def _generate_titles(self, industry: str) -> dict: + """ + Generate keyword-based titles using the title generator. + + Args: + industry (str): The industry to generate titles for + + Returns: + dict: Generated titles and patterns + """ + return ai_title_generator(industry) + + def _analyze_meta_descriptions(self, industry: str) -> dict: + """ + Analyze meta descriptions for keyword usage. + + Args: + industry (str): The industry to analyze + + Returns: + dict: Meta description analysis results + """ + return metadesc_generator_main(industry) + + def _analyze_structured_data(self, industry: str) -> dict: + """ + Analyze structured data implementation. + + Args: + industry (str): The industry to analyze + + Returns: + dict: Structured data analysis results + """ + return ai_structured_data(industry) + + def _extract_keywords(self, titles: dict, meta_analysis: dict) -> list: + """ + Extract keywords from titles and meta descriptions. + + Args: + titles (dict): Generated titles + meta_analysis (dict): Meta description analysis + + Returns: + list: Extracted keywords with metrics + """ + prompt = f""" + As an SEO expert, analyze the following content and extract relevant keywords with their metrics: + + Titles: {titles} + Meta Descriptions: {meta_analysis} + + Please provide a JSON response with the following structure: + {{ + "keywords": [ + {{ + "keyword": "string", + "search_volume": "number", + "difficulty": "number", + "relevance_score": "number", + "content_type": "string" + }} + ], + "summary": {{ + "total_keywords": "number", + "high_opportunity_keywords": "number", + "recommended_focus_areas": ["string"] + }} + }} + + Focus on: + 1. Primary keywords and their variations + 2. Long-tail keywords + 3. Industry-specific terminology + 4. Search volume and difficulty metrics + 5. Content type recommendations + """ + + try: + response = llm_text_gen(prompt, json_struct={ + "type": "object", + "properties": { + "keywords": { + "type": "array", + "items": { + "type": "object", + "properties": { + "keyword": {"type": "string"}, + "search_volume": {"type": "number"}, + "difficulty": {"type": "number"}, + "relevance_score": {"type": "number"}, + "content_type": {"type": "string"} + } + } + }, + "summary": { + "type": "object", + "properties": { + "total_keywords": {"type": "number"}, + "high_opportunity_keywords": {"type": "number"}, + "recommended_focus_areas": { + "type": "array", + "items": {"type": "string"} + } + } + } + } + }) + return response + except Exception as e: + st.error(f"Error extracting keywords: {e}") + return [] + + def _analyze_search_intent(self, ai_insights: dict) -> dict: + """ + Analyze search intent from AI insights. + + Args: + ai_insights (dict): AI-processed insights + + Returns: + dict: Search intent analysis + """ + prompt = f""" + As an SEO expert, analyze the following content insights and determine the search intent: + + Content Insights: {ai_insights} + + Please provide a JSON response with the following structure: + {{ + "informational": [ + {{ + "keyword": "string", + "intent_type": "string", + "content_suggestions": ["string"] + }} + ], + "transactional": [ + {{ + "keyword": "string", + "intent_type": "string", + "content_suggestions": ["string"] + }} + ], + "navigational": [ + {{ + "keyword": "string", + "intent_type": "string", + "content_suggestions": ["string"] + }} + ], + "summary": {{ + "dominant_intent": "string", + "content_strategy_recommendations": ["string"] + }} + }} + + Focus on: + 1. Identifying primary search intent for each keyword + 2. Suggesting appropriate content types + 3. Providing content strategy recommendations + 4. Analyzing user behavior patterns + """ + + try: + response = llm_text_gen(prompt, json_struct={ + "type": "object", + "properties": { + "informational": { + "type": "array", + "items": { + "type": "object", + "properties": { + "keyword": {"type": "string"}, + "intent_type": {"type": "string"}, + "content_suggestions": { + "type": "array", + "items": {"type": "string"} + } + } + } + }, + "transactional": { + "type": "array", + "items": { + "type": "object", + "properties": { + "keyword": {"type": "string"}, + "intent_type": {"type": "string"}, + "content_suggestions": { + "type": "array", + "items": {"type": "string"} + } + } + } + }, + "navigational": { + "type": "array", + "items": { + "type": "object", + "properties": { + "keyword": {"type": "string"}, + "intent_type": {"type": "string"}, + "content_suggestions": { + "type": "array", + "items": {"type": "string"} + } + } + } + }, + "summary": { + "type": "object", + "properties": { + "dominant_intent": {"type": "string"}, + "content_strategy_recommendations": { + "type": "array", + "items": {"type": "string"} + } + } + } + } + }) + return response + except Exception as e: + st.error(f"Error analyzing search intent: {e}") + return { + 'informational': [], + 'transactional': [], + 'navigational': [] + } + + def _suggest_content_formats(self, ai_insights: dict) -> list: + """ + Suggest content formats based on AI insights. + + Args: + ai_insights (dict): AI-processed insights + + Returns: + list: Suggested content formats + """ + prompt = f""" + As a content strategy expert, analyze the following insights and suggest appropriate content formats: + + AI Insights: {ai_insights} + + Please provide a JSON response with the following structure: + {{ + "content_formats": [ + {{ + "format": "string", + "description": "string", + "use_cases": ["string"], + "recommended_topics": ["string"], + "estimated_impact": "string" + }} + ], + "format_strategy": {{ + "primary_formats": ["string"], + "secondary_formats": ["string"], + "implementation_priority": ["string"] + }} + }} + + Focus on: + 1. Identifying the most effective content formats + 2. Matching formats to user intent + 3. Suggesting specific use cases + 4. Providing implementation guidance + """ + + try: + response = llm_text_gen(prompt, json_struct={ + "type": "object", + "properties": { + "content_formats": { + "type": "array", + "items": { + "type": "object", + "properties": { + "format": {"type": "string"}, + "description": {"type": "string"}, + "use_cases": { + "type": "array", + "items": {"type": "string"} + }, + "recommended_topics": { + "type": "array", + "items": {"type": "string"} + }, + "estimated_impact": {"type": "string"} + } + } + }, + "format_strategy": { + "type": "object", + "properties": { + "primary_formats": { + "type": "array", + "items": {"type": "string"} + }, + "secondary_formats": { + "type": "array", + "items": {"type": "string"} + }, + "implementation_priority": { + "type": "array", + "items": {"type": "string"} + } + } + } + } + }) + return response + except Exception as e: + st.error(f"Error suggesting content formats: {e}") + return [] + + def _create_topic_clusters(self, ai_insights: dict) -> dict: + """ + Create topic clusters from AI insights. + + Args: + ai_insights (dict): AI-processed insights + + Returns: + dict: Topic clusters and relationships + """ + prompt = f""" + As a content organization expert, analyze the following insights and create topic clusters: + + AI Insights: {ai_insights} + + Please provide a JSON response with the following structure: + {{ + "clusters": [ + {{ + "cluster_name": "string", + "main_topics": ["string"], + "subtopics": ["string"], + "related_keywords": ["string"], + "content_opportunities": ["string"] + }} + ], + "relationships": {{ + "cluster_connections": [ + {{ + "source": "string", + "target": "string", + "relationship_type": "string", + "strength": "number" + }} + ], + "content_hierarchy": {{ + "primary_topics": ["string"], + "secondary_topics": ["string"], + "tertiary_topics": ["string"] + }} + }} + }} + + Focus on: + 1. Identifying main topic clusters + 2. Organizing subtopics and related keywords + 3. Mapping relationships between clusters + 4. Suggesting content opportunities + """ + + try: + response = llm_text_gen(prompt, json_struct={ + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "type": "object", + "properties": { + "cluster_name": {"type": "string"}, + "main_topics": { + "type": "array", + "items": {"type": "string"} + }, + "subtopics": { + "type": "array", + "items": {"type": "string"} + }, + "related_keywords": { + "type": "array", + "items": {"type": "string"} + }, + "content_opportunities": { + "type": "array", + "items": {"type": "string"} + } + } + } + }, + "relationships": { + "type": "object", + "properties": { + "cluster_connections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "source": {"type": "string"}, + "target": {"type": "string"}, + "relationship_type": {"type": "string"}, + "strength": {"type": "number"} + } + } + }, + "content_hierarchy": { + "type": "object", + "properties": { + "primary_topics": { + "type": "array", + "items": {"type": "string"} + }, + "secondary_topics": { + "type": "array", + "items": {"type": "string"} + }, + "tertiary_topics": { + "type": "array", + "items": {"type": "string"} + } + } + } + } + } + } + }) + return response + except Exception as e: + st.error(f"Error creating topic clusters: {e}") + return { + 'clusters': [], + 'relationships': {} + } \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/main.py b/lib/ai_seo_tools/content_gap_analysis/main.py new file mode 100644 index 00000000..08c4f6f8 --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/main.py @@ -0,0 +1,361 @@ +""" +Main module for content gap analysis. +""" + +from typing import Dict, Any, List, Optional +import streamlit as st +from loguru import logger +from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer +from .competitor_analyzer import CompetitorAnalyzer +from .keyword_researcher import KeywordResearcher +from .recommendation_engine import RecommendationEngine +from .utils.ai_processor import AIProcessor, ProgressTracker +from .utils.storage import ContentGapAnalysisStorage +from datetime import datetime +import asyncio +import sys +import os +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen +from .utils.content_parser import ContentParser + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/content_gap_analysis.log", + rotation="50 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}" +) + +# Ensure logs directory exists +os.makedirs("logs", exist_ok=True) + +class ContentGapAnalysis: + """Main class for content gap analysis.""" + + def __init__(self, db_session=None): + """Initialize the content gap analysis components.""" + self.website_analyzer = WebsiteAnalyzer() + self.competitor_analyzer = CompetitorAnalyzer() + self.keyword_researcher = KeywordResearcher() + self.recommendation_engine = RecommendationEngine() + self.ai_processor = AIProcessor() + self.progress = ProgressTracker() + self.storage = ContentGapAnalysisStorage(db_session) if db_session else None + + # Define analysis phases + self.phases = { + 'website_analysis': { + 'name': 'Website Analysis', + 'steps': [ + 'Initializing website analysis', + 'Analyzing website content', + 'Evaluating SEO elements', + 'Generating website insights' + ] + }, + 'competitor_analysis': { + 'name': 'Competitor Analysis', + 'steps': [ + 'Initializing competitor analysis', + 'Analyzing competitor content', + 'Comparing market position', + 'Generating competitive insights' + ] + }, + 'keyword_analysis': { + 'name': 'Keyword Analysis', + 'steps': [ + 'Initializing keyword research', + 'Analyzing keyword trends', + 'Evaluating search intent', + 'Generating keyword insights' + ] + }, + 'recommendation_generation': { + 'name': 'Recommendation Generation', + 'steps': [ + 'Initializing recommendation engine', + 'Analyzing content gaps', + 'Generating recommendations', + 'Creating implementation plan' + ] + } + } + + logger.info("ContentGapAnalysis initialized") + + def analyze(self, url: str, industry: str, competitor_urls: Optional[List[str]] = None, user_id: Optional[int] = None) -> Dict[str, Any]: + """ + Run the complete content gap analysis workflow. + + Args: + url: Target website URL + industry: Industry category + competitor_urls: Optional list of competitor URLs + user_id: Optional user ID for storing results + + Returns: + Dictionary containing analysis results + """ + try: + results = {} + start_time = datetime.utcnow() + + # Phase 1: Website Analysis + self.progress.start_stage('website_analysis') + self.progress.next_step() + + website_analysis = self.website_analyzer.analyze(url) + results['website'] = website_analysis + + self.progress.next_step() + self.progress.complete_stage() + + # Phase 2: Competitor Analysis + if competitor_urls: + self.progress.start_stage('competitor_analysis') + self.progress.next_step() + + competitor_analysis = self.competitor_analyzer.analyze(competitor_urls, industry) + results['competitors'] = competitor_analysis + + self.progress.next_step() + self.progress.complete_stage() + + # Phase 3: Keyword Analysis + self.progress.start_stage('keyword_analysis') + self.progress.next_step() + + keyword_analysis = self.keyword_researcher.analyze(industry, url) + results['keywords'] = keyword_analysis + + self.progress.next_step() + self.progress.complete_stage() + + # Phase 4: Recommendation Generation + self.progress.start_stage('recommendation_generation') + self.progress.next_step() + + recommendations = self.recommendation_engine.generate_recommendations( + website_analysis, + competitor_analysis if competitor_urls else None, + keyword_analysis + ) + results['recommendations'] = recommendations + + self.progress.next_step() + self.progress.complete_stage() + + # Calculate analysis duration + end_time = datetime.utcnow() + results['duration'] = (end_time - start_time).total_seconds() + + # Store results if user_id is provided and storage is available + if user_id and self.storage: + analysis_id = self.storage.save_analysis(user_id, url, industry, results) + if analysis_id: + results['analysis_id'] = analysis_id + + return results + + except Exception as e: + if self.progress.current_stage: + self.progress.update_progress(0, f"Error in {self.progress.stages[self.progress.current_stage]['name']}: {str(e)}") + st.error(f"Error in content gap analysis: {str(e)}") + return { + 'error': str(e), + 'website': {}, + 'competitors': [], + 'keywords': {}, + 'recommendations': [] + } + + def get_analysis(self, analysis_id: int) -> Optional[Dict[str, Any]]: + """ + Retrieve stored analysis results. + + Args: + analysis_id: Analysis ID + + Returns: + Dictionary containing analysis results if found, None otherwise + """ + if not self.storage: + st.error("Storage not initialized") + return None + return self.storage.get_analysis(analysis_id) + + def get_user_analyses(self, user_id: int) -> List[Dict[str, Any]]: + """ + Get all analyses for a user. + + Args: + user_id: User ID + + Returns: + List of analysis summaries + """ + if not self.storage: + st.error("Storage not initialized") + return [] + return self.storage.get_user_analyses(user_id) + + def update_recommendation_status(self, recommendation_id: int, status: str) -> bool: + """ + Update the status of a recommendation. + + Args: + recommendation_id: Recommendation ID + status: New status + + Returns: + True if successful, False otherwise + """ + if not self.storage: + st.error("Storage not initialized") + return False + return self.storage.update_recommendation_status(recommendation_id, status) + + def delete_analysis(self, analysis_id: int) -> bool: + """ + Delete an analysis and all related data. + + Args: + analysis_id: Analysis ID + + Returns: + True if successful, False otherwise + """ + if not self.storage: + st.error("Storage not initialized") + return False + return self.storage.delete_analysis(analysis_id) + + def get_analysis_summary(self, results: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate a summary of the analysis results. + + Args: + results: Dictionary containing analysis results + + Returns: + Dictionary containing summary metrics and insights + """ + try: + self.progress.start_stage('summary_generation') + self.progress.next_step() + + summary = { + 'website_metrics': self._summarize_website_metrics(results.get('website', {})), + 'competitor_insights': self._summarize_competitor_insights(results.get('competitors', {})), + 'keyword_opportunities': self._summarize_keyword_opportunities(results.get('keywords', {})), + 'recommendation_highlights': self._summarize_recommendations(results.get('recommendations', {})), + 'ai_insights': results.get('ai_insights', {}) + } + + self.progress.complete_stage() + return summary + + except Exception as e: + if self.progress.current_stage: + self.progress.update_progress(0, f"Error generating summary: {str(e)}") + st.error(f"Error generating analysis summary: {str(e)}") + return { + 'error': str(e), + 'website_metrics': {}, + 'competitor_insights': {}, + 'keyword_opportunities': {}, + 'recommendation_highlights': {}, + 'ai_insights': {} + } + + def export_results(self, results: Dict[str, Any], format: str = 'json') -> str: + """ + Export analysis results in the specified format. + + Args: + results: Dictionary containing analysis results + format: Export format ('json' or 'csv') + + Returns: + String containing exported results + """ + try: + self.progress.start_stage('export') + self.progress.next_step() + + if format.lower() == 'json': + import json + exported = json.dumps(results, indent=2) + elif format.lower() == 'csv': + import pandas as pd + # Convert results to DataFrame and then to CSV + df = pd.DataFrame(results) + exported = df.to_csv(index=False) + else: + raise ValueError(f"Unsupported export format: {format}") + + self.progress.complete_stage() + return exported + + except Exception as e: + if self.progress.current_stage: + self.progress.update_progress(0, f"Error exporting results: {str(e)}") + st.error(f"Error exporting results: {str(e)}") + return str(e) + + def _summarize_website_metrics(self, website_data: Dict[str, Any]) -> Dict[str, Any]: + """Generate summary of website metrics.""" + try: + return { + 'content_score': website_data.get('content_score', 0), + 'seo_score': website_data.get('seo_score', 0), + 'structure_score': website_data.get('structure_score', 0), + 'key_insights': website_data.get('insights', [])[:5] # Top 5 insights + } + except Exception as e: + st.error(f"Error summarizing website metrics: {str(e)}") + return {} + + def _summarize_competitor_insights(self, competitor_data: Dict[str, Any]) -> Dict[str, Any]: + """Generate summary of competitor insights.""" + try: + return { + 'market_position': competitor_data.get('market_position', {}), + 'content_gaps': competitor_data.get('content_gaps', [])[:5], # Top 5 gaps + 'competitive_advantages': competitor_data.get('advantages', [])[:5] # Top 5 advantages + } + except Exception as e: + st.error(f"Error summarizing competitor insights: {str(e)}") + return {} + + def _summarize_keyword_opportunities(self, keyword_data: Dict[str, Any]) -> Dict[str, Any]: + """Generate summary of keyword opportunities.""" + try: + return { + 'top_keywords': keyword_data.get('top_keywords', [])[:10], # Top 10 keywords + 'search_intent': keyword_data.get('search_intent', {}), + 'opportunities': keyword_data.get('opportunities', [])[:5] # Top 5 opportunities + } + except Exception as e: + st.error(f"Error summarizing keyword opportunities: {str(e)}") + return {} + + def _summarize_recommendations(self, recommendation_data: Dict[str, Any]) -> Dict[str, Any]: + """Generate summary of recommendations.""" + try: + return { + 'priority_recommendations': recommendation_data.get('priority_recommendations', [])[:5], # Top 5 recommendations + 'implementation_timeline': recommendation_data.get('timeline', {}), + 'expected_impact': recommendation_data.get('impact', {}) + } + except Exception as e: + st.error(f"Error summarizing recommendations: {str(e)}") + return {} \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/navigation.py b/lib/ai_seo_tools/content_gap_analysis/navigation.py new file mode 100644 index 00000000..6e99bbf7 --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/navigation.py @@ -0,0 +1,41 @@ +""" +Navigation component for Content Gap Analysis tool. +""" + +import streamlit as st + +def show_content_gap_analysis_nav(): + """Show navigation for Content Gap Analysis tool.""" + st.sidebar.title("Content Gap Analysis") + st.sidebar.markdown(""" + Analyze your content strategy, identify gaps, and get AI-powered recommendations. + """) + + # Navigation options + nav_option = st.sidebar.radio( + "Select Analysis Type", + ["Website Analysis", "Competitor Analysis", "Keyword Research", "Recommendations"] + ) + + # Tool description + st.sidebar.markdown(""" + ### Features + - Website content analysis + - Competitor content comparison + - Keyword research and trends + - AI-powered recommendations + - Content gap identification + - Implementation timeline + """) + + # Help section + with st.sidebar.expander("How to Use"): + st.markdown(""" + 1. Start with Website Analysis + 2. Add competitor URLs + 3. Research keywords + 4. Get recommendations + 5. Export results + """) + + return nav_option \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/recommendation_engine.py b/lib/ai_seo_tools/content_gap_analysis/recommendation_engine.py new file mode 100644 index 00000000..9f7f9574 --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/recommendation_engine.py @@ -0,0 +1,440 @@ +""" +Recommendation engine for content gap analysis. +""" + +import streamlit as st +from typing import Dict, Any, List, Optional +from loguru import logger +from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer +from lib.ai_seo_tools.content_gap_analysis.utils.data_collector import DataCollector +from lib.ai_seo_tools.content_gap_analysis.utils.content_parser import ContentParser +from lib.ai_seo_tools.content_gap_analysis.utils.ai_processor import AIProcessor, ProgressTracker +from lib.ai_seo_tools.content_title_generator import ai_title_generator +import asyncio +import sys +import os +import json +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/recommendation_engine.log", + rotation="50 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}" +) + +# Ensure logs directory exists +os.makedirs("logs", exist_ok=True) + +class RecommendationEngine: + """ + Generates content recommendations based on analysis results. + """ + + def __init__(self): + """Initialize the recommendation engine with required components.""" + self.ai_processor = AIProcessor() + self.progress = ProgressTracker() + + # Define analysis stages + self.stages = { + 'recommendation_generation': { + 'name': 'Recommendation Generation', + 'steps': [ + 'Initializing recommendation engine', + 'Analyzing content gaps', + 'Evaluating opportunities', + 'Generating recommendations', + 'Creating implementation plan' + ] + } + } + + def generate_recommendations(self, website_analysis: Dict[str, Any], competitor_analysis: Optional[Dict[str, Any]], keyword_analysis: Dict[str, Any]) -> Dict[str, Any]: + """ + Generate content recommendations. + + Args: + website_analysis: Website analysis results + competitor_analysis: Optional competitor analysis results + keyword_analysis: Keyword analysis results + + Returns: + Dictionary containing recommendations + """ + try: + self.progress.start_stage('recommendation_generation') + self.progress.next_step() + + # Analyze content gaps + content_gaps = self._analyze_content_gaps(website_analysis, competitor_analysis, keyword_analysis) + self.progress.next_step() + + # Evaluate opportunities + opportunities = self._evaluate_opportunities(content_gaps, keyword_analysis) + self.progress.next_step() + + # Generate recommendations + recommendations = self._generate_recommendations(content_gaps, opportunities) + self.progress.next_step() + + # Create implementation plan + implementation_plan = self._create_implementation_plan(recommendations) + self.progress.next_step() + + self.progress.complete_stage() + + return { + 'content_gaps': content_gaps, + 'opportunities': opportunities, + 'recommendations': recommendations, + 'implementation_plan': implementation_plan + } + + except Exception as e: + if self.progress.current_stage: + self.progress.update_progress(0, f"Error in {self.progress.stages[self.progress.current_stage]['name']}: {str(e)}") + st.error(f"Error generating recommendations: {str(e)}") + return { + 'error': str(e), + 'content_gaps': [], + 'opportunities': [], + 'recommendations': [], + 'implementation_plan': {} + } + + def _analyze_content_gaps(self, website_analysis: Dict[str, Any], competitor_analysis: Optional[Dict[str, Any]], keyword_analysis: Dict[str, Any]) -> List[Dict[str, Any]]: + """Analyze content gaps.""" + try: + content_gaps = [] + + # Analyze website content gaps + website_gaps = self._analyze_website_gaps(website_analysis) + content_gaps.extend(website_gaps) + + # Analyze competitor gaps if available + if competitor_analysis: + competitor_gaps = self._analyze_competitor_gaps(competitor_analysis) + content_gaps.extend(competitor_gaps) + + # Analyze keyword gaps + keyword_gaps = self._analyze_keyword_gaps(keyword_analysis) + content_gaps.extend(keyword_gaps) + + return content_gaps + except Exception as e: + st.error(f"Error analyzing content gaps: {str(e)}") + return [] + + def _analyze_website_gaps(self, website_analysis: Dict[str, Any]) -> List[Dict[str, Any]]: + """Analyze website content gaps.""" + try: + gaps = [] + + # Check content quality + quality_metrics = website_analysis.get('quality_metrics', {}) + if quality_metrics.get('readability_score', 0) < 70: + gaps.append({ + 'type': 'content_quality', + 'issue': 'Low readability score', + 'score': quality_metrics.get('readability_score', 0), + 'recommendation': 'Improve content readability' + }) + + # Check SEO elements + seo_metrics = website_analysis.get('seo_metrics', {}) + if seo_metrics.get('seo_score', 0) < 70: + gaps.append({ + 'type': 'seo', + 'issue': 'Low SEO score', + 'score': seo_metrics.get('seo_score', 0), + 'recommendation': 'Enhance SEO optimization' + }) + + return gaps + except Exception as e: + st.error(f"Error analyzing website gaps: {str(e)}") + return [] + + def _analyze_competitor_gaps(self, competitor_analysis: Dict[str, Any]) -> List[Dict[str, Any]]: + """Analyze competitor content gaps.""" + try: + gaps = [] + + # Check content gaps + content_gaps = competitor_analysis.get('content_gaps', []) + for gap in content_gaps: + gaps.append({ + 'type': 'competitor', + 'issue': f"Missing topic: {', '.join(gap.get('missing_topics', []))}", + 'recommendation': 'Create content for missing topics' + }) + + return gaps + except Exception as e: + st.error(f"Error analyzing competitor gaps: {str(e)}") + return [] + + def _analyze_keyword_gaps(self, keyword_analysis: Dict[str, Any]) -> List[Dict[str, Any]]: + """Analyze keyword gaps.""" + try: + gaps = [] + + # Check keyword opportunities + opportunities = keyword_analysis.get('opportunities', []) + for opportunity in opportunities: + gaps.append({ + 'type': 'keyword', + 'issue': f"Keyword opportunity: {opportunity.get('keyword')}", + 'volume': opportunity.get('volume', 0), + 'difficulty': opportunity.get('difficulty', 0), + 'recommendation': f"Target keyword: {opportunity.get('keyword')}" + }) + + return gaps + except Exception as e: + st.error(f"Error analyzing keyword gaps: {str(e)}") + return [] + + def _evaluate_opportunities(self, content_gaps: List[Dict[str, Any]], keyword_analysis: Dict[str, Any]) -> List[Dict[str, Any]]: + """Evaluate content opportunities.""" + try: + opportunities = [] + + # Evaluate each gap + for gap in content_gaps: + # Calculate priority score + priority_score = self._calculate_priority_score(gap, keyword_analysis) + + if priority_score > 50: # Threshold for good opportunities + opportunities.append({ + 'type': gap.get('type'), + 'issue': gap.get('issue'), + 'recommendation': gap.get('recommendation'), + 'priority_score': priority_score + }) + + # Sort by priority score + opportunities.sort(key=lambda x: x['priority_score'], reverse=True) + + return opportunities + except Exception as e: + st.error(f"Error evaluating opportunities: {str(e)}") + return [] + + def _calculate_priority_score(self, gap: Dict[str, Any], keyword_analysis: Dict[str, Any]) -> float: + """Calculate priority score for a gap.""" + try: + base_score = 0 + + # Base score based on gap type + if gap.get('type') == 'content_quality': + base_score = 70 + elif gap.get('type') == 'seo': + base_score = 80 + elif gap.get('type') == 'competitor': + base_score = 60 + elif gap.get('type') == 'keyword': + base_score = 50 + + # Adjust score based on keyword data + if gap.get('type') == 'keyword': + keyword = gap.get('issue', '').split(': ')[-1] + keyword_data = keyword_analysis.get('trend_analysis', {}).get('trends', {}).get(keyword, {}) + if keyword_data: + base_score += keyword_data.get('volume', 0) * 0.1 + base_score -= keyword_data.get('difficulty', 0) * 0.2 + + return min(100, max(0, base_score)) + except Exception as e: + st.error(f"Error calculating priority score: {str(e)}") + return 0 + + def _generate_recommendations(self, content_gaps: List[Dict[str, Any]], opportunities: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Generate content recommendations.""" + try: + recommendations = [] + + # Generate recommendations for each opportunity + for opportunity in opportunities: + recommendations.append({ + 'type': opportunity.get('type'), + 'issue': opportunity.get('issue'), + 'recommendation': opportunity.get('recommendation'), + 'priority': opportunity.get('priority_score', 0), + 'implementation_steps': self._generate_implementation_steps(opportunity) + }) + + return recommendations + except Exception as e: + st.error(f"Error generating recommendations: {str(e)}") + return [] + + def _generate_implementation_steps(self, opportunity: Dict[str, Any]) -> List[str]: + """Generate implementation steps for a recommendation.""" + try: + steps = [] + + if opportunity.get('type') == 'content_quality': + steps = [ + 'Review current content structure', + 'Improve readability and formatting', + 'Enhance content organization', + 'Update content based on best practices' + ] + elif opportunity.get('type') == 'seo': + steps = [ + 'Audit current SEO implementation', + 'Optimize meta tags and descriptions', + 'Improve content structure for SEO', + 'Implement technical SEO improvements' + ] + elif opportunity.get('type') == 'competitor': + steps = [ + 'Research competitor content', + 'Identify unique value proposition', + 'Create content for missing topics', + 'Optimize content for target keywords' + ] + elif opportunity.get('type') == 'keyword': + steps = [ + 'Research keyword intent', + 'Create content strategy', + 'Develop content for target keyword', + 'Optimize content for search' + ] + + return steps + except Exception as e: + st.error(f"Error generating implementation steps: {str(e)}") + return [] + + def _create_implementation_plan(self, recommendations: List[Dict[str, Any]]) -> Dict[str, Any]: + """Create implementation plan.""" + try: + plan = { + 'phases': [], + 'timeline': {}, + 'resources': {}, + 'success_metrics': {} + } + + # Create phases based on recommendation types + phases = { + 'content_quality': 'Content Enhancement', + 'seo': 'SEO Optimization', + 'competitor': 'Competitive Content', + 'keyword': 'Keyword Targeting' + } + + # Group recommendations by phase + for phase_name in phases.values(): + phase_recommendations = [ + rec for rec in recommendations + if phases.get(rec.get('type')) == phase_name + ] + + if phase_recommendations: + plan['phases'].append({ + 'name': phase_name, + 'recommendations': phase_recommendations, + 'duration': '2-4 weeks', + 'resources': ['Content team', 'SEO team'], + 'success_metrics': [ + 'Content quality score', + 'SEO performance', + 'User engagement' + ] + }) + + return plan + except Exception as e: + st.error(f"Error creating implementation plan: {str(e)}") + return {} + + def _generate_content_topics(self, ai_insights: dict) -> list: + """ + Generate content topic suggestions. + + Args: + ai_insights (dict): AI-processed insights + + Returns: + list: Content topic suggestions + """ + # TODO: Implement content topic generation + return [] + + def _suggest_content_formats(self, ai_insights: dict) -> list: + """ + Suggest content formats based on analysis. + + Args: + ai_insights (dict): AI-processed insights + + Returns: + list: Content format suggestions + """ + # TODO: Implement content format suggestions + return [] + + def _calculate_priority_scores(self, ai_insights: dict) -> dict: + """ + Calculate priority scores for recommendations. + + Args: + ai_insights (dict): AI-processed insights + + Returns: + dict: Priority scores for each recommendation + """ + # TODO: Implement priority scoring + return {} + + def _create_timeline(self, ai_insights: dict) -> dict: + """ + Create implementation timeline for recommendations. + + Args: + ai_insights (dict): AI-processed insights + + Returns: + dict: Implementation timeline + """ + # TODO: Implement timeline creation + return { + 'short_term': [], + 'medium_term': [], + 'long_term': [] + } + + def _generate_specific_suggestions(self, recommendations: dict, analysis_results: dict) -> dict: + """ + Generate specific content suggestions using existing tools. + + Args: + recommendations (dict): General recommendations + analysis_results (dict): Analysis results + + Returns: + dict: Specific content suggestions + """ + suggestions = {} + + # Generate titles for suggested topics + for topic in recommendations['content_topics']: + suggestions[topic] = { + 'titles': ai_title_generator(topic), + 'meta_descriptions': metadesc_generator_main(topic), + 'structured_data': ai_structured_data(topic) + } + + return suggestions \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/ui.py b/lib/ai_seo_tools/content_gap_analysis/ui.py new file mode 100644 index 00000000..6832597f --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/ui.py @@ -0,0 +1,769 @@ +""" +Streamlit UI for Content Gap Analysis workflow. +""" + +import streamlit as st +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +import json +from datetime import datetime +from .main import ContentGapAnalysis +from .keyword_researcher import KeywordResearcher +from .competitor_analyzer import CompetitorAnalyzer +from .website_analyzer import WebsiteAnalyzer +from .recommendation_engine import RecommendationEngine +from .utils.ai_processor import AIProcessor +from .navigation import show_content_gap_analysis_nav +from typing import Dict, Any +import logging + +# Configure logging +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +class ContentGapAnalysisUI: + """Streamlit UI for Content Gap Analysis workflow.""" + + def __init__(self): + """Initialize the UI components.""" + # Initialize session state for progress tracking + if 'current_step' not in st.session_state: + st.session_state.current_step = 1 + if 'analysis_results' not in st.session_state: + st.session_state.analysis_results = {} + + # Initialize analysis components + self.analyzer = ContentGapAnalysis() + self.keyword_researcher = KeywordResearcher() + self.competitor_analyzer = CompetitorAnalyzer() + self.website_analyzer = WebsiteAnalyzer() + self.recommendation_engine = RecommendationEngine() + self.ai_processor = AIProcessor() + + def run(self): + """Run the Streamlit interface.""" + try: + # Show navigation + nav_option = show_content_gap_analysis_nav() + + # Main content area + st.title("Content Gap Analysis") + st.markdown(""" + This tool helps you identify content gaps and opportunities by analyzing your website, + competitors, and market trends. Follow the steps below to get started. + """) + + # Progress tracking + self._show_progress() + + # Main workflow steps + if nav_option == "Website Analysis" or st.session_state.current_step == 1: + self._website_analysis_step() + elif nav_option == "Competitor Analysis" or st.session_state.current_step == 2: + self._competitor_analysis_step() + elif nav_option == "Keyword Research" or st.session_state.current_step == 3: + self._keyword_research_step() + elif nav_option == "Recommendations" or st.session_state.current_step == 4: + self._recommendations_step() + else: + self._export_results() + except Exception as e: + logger.error(f"Error in run method: {str(e)}", exc_info=True) + st.error(f"An error occurred: {str(e)}") + + def _show_progress(self): + """Display progress tracking.""" + steps = [ + "Website Analysis", + "Competitor Analysis", + "Keyword Research", + "Recommendations", + "Export Results" + ] + + progress = st.session_state.current_step / len(steps) + st.progress(progress) + + cols = st.columns(len(steps)) + for i, col in enumerate(cols): + with col: + if i + 1 < st.session_state.current_step: + st.success(f"✓ {steps[i]}") + elif i + 1 == st.session_state.current_step: + st.info(f"→ {steps[i]}") + else: + st.text(f"○ {steps[i]}") + + def _website_analysis_step(self): + """Website analysis step UI.""" + try: + st.header("Step 1: Website Analysis") + + # Display previous results if they exist + if 'website' in st.session_state.analysis_results: + st.info("Previous analysis results found. You can analyze a new website or proceed to the next step.") + self._display_website_analysis(st.session_state.analysis_results['website']) + + col1, col2 = st.columns(2) + with col1: + if st.button("Analyze New Website"): + st.session_state.analysis_results.pop('website', None) + st.rerun() + with col2: + if st.button("Proceed to Competitor Analysis"): + st.session_state.current_step = 2 + st.rerun() + return + + # Create form for new analysis + with st.form("website_analysis_form"): + website_url = st.text_input("Enter your website URL") + industry = st.text_input("Enter your industry/niche") + + submitted = st.form_submit_button("Analyze Website") + + # Handle form submission outside the form + if submitted and website_url and industry: + # Initialize progress tracking + if 'analysis_progress' not in st.session_state: + st.session_state.analysis_progress = { + 'status': 'initializing', + 'current_step': 'Starting Analysis', + 'progress': 0, + 'details': 'Initializing analysis...' + } + + # Create progress container + progress_container = st.empty() + status_container = st.empty() + details_container = st.empty() + + # Update progress display + def update_progress_display(): + progress = st.session_state.analysis_progress + + # Update progress bar + with progress_container: + st.progress(progress['progress'] / 100) + + # Update status + with status_container: + if progress['status'] == 'error': + st.error(f"Error: {progress['current_step']}") + elif progress['status'] == 'completed': + st.success(f"✓ {progress['current_step']}") + else: + st.info(f"→ {progress['current_step']}") + + # Update details + with details_container: + st.write(progress['details']) + + # Initial progress display + update_progress_display() + + try: + # Get basic analysis + results = self.website_analyzer.analyze(website_url) + + # Update progress from analyzer + st.session_state.analysis_progress = self.website_analyzer.progress.get_progress() + update_progress_display() + + if isinstance(results, dict) and 'error' in results: + st.error(f"Error in website analysis: {results['error']}") + return + + # Get AI-enhanced analysis + st.session_state.analysis_progress.update({ + 'current_step': 'AI Analysis', + 'progress': 95, + 'details': 'Performing AI-enhanced analysis...' + }) + update_progress_display() + + ai_analysis = self.ai_processor.analyze_content({ + 'url': website_url, + 'industry': industry, + 'content': results + }) + + # Combine results + if isinstance(results, dict): + results.update(ai_analysis) + else: + results = {'error': 'Invalid analysis results format'} + + # Store results in session state + st.session_state.analysis_results['website'] = results + + # Update final progress + st.session_state.analysis_progress.update({ + 'status': 'completed', + 'current_step': 'Analysis Complete', + 'progress': 100, + 'details': 'Analysis completed successfully!' + }) + update_progress_display() + + # Display results + self._display_website_analysis(results) + + except Exception as e: + logger.error(f"Error during website analysis: {str(e)}", exc_info=True) + st.session_state.analysis_progress.update({ + 'status': 'error', + 'current_step': 'Analysis Failed', + 'details': f"Error during website analysis: {str(e)}" + }) + update_progress_display() + st.error(f"Error during website analysis: {str(e)}") + return + + except Exception as e: + logger.error(f"Error in website analysis step: {str(e)}", exc_info=True) + st.error(f"Error in website analysis: {str(e)}") + + def _display_website_analysis(self, results: Dict[str, Any]): + """Display website analysis results.""" + try: + if not isinstance(results, dict): + st.error("Invalid analysis results format") + return + + if 'error' in results: + st.error(f"Error in analysis: {results['error']}") + return + + # Content Metrics + st.subheader("Content Metrics") + content_metrics = results.get('content_metrics', {}) + + if content_metrics: + # Basic metrics in columns + col1, col2, col3, col4 = st.columns(4) + with col1: + st.metric("Word Count", f"{content_metrics.get('word_count', 0):,}") + with col2: + st.metric("Headings", f"{content_metrics.get('heading_count', 0):,}") + with col3: + st.metric("Images", f"{content_metrics.get('image_count', 0):,}") + with col4: + st.metric("Links", f"{content_metrics.get('link_count', 0):,}") + + # Content Structure Visualization + st.write("Content Structure") + heading_data = { + 'Type': ['H1', 'H2', 'H3', 'Paragraphs'], + 'Count': [ + content_metrics.get('h1_count', 0), + content_metrics.get('h2_count', 0), + content_metrics.get('h3_count', 0), + content_metrics.get('paragraph_count', 0) + ] + } + fig = px.bar( + heading_data, + x='Type', + y='Count', + title="Content Structure Distribution", + color='Type', + color_discrete_sequence=px.colors.qualitative.Set3 + ) + st.plotly_chart(fig, use_container_width=True) + + # Content Features + st.write("Content Features") + features = { + 'Feature': ['Meta Description', 'Robots.txt', 'Sitemap'], + 'Status': [ + content_metrics.get('has_meta_description', False), + content_metrics.get('has_robots_txt', False), + content_metrics.get('has_sitemap', False) + ] + } + fig = px.bar( + features, + x='Feature', + y='Status', + title="Content Features Status", + color='Status', + color_discrete_sequence=['red', 'green'] + ) + st.plotly_chart(fig, use_container_width=True) + + # SEO Metrics + st.subheader("SEO Metrics") + seo_metrics = results.get('seo_metrics', {}) + + if seo_metrics: + # Basic metrics in columns + col1, col2, col3, col4 = st.columns(4) + with col1: + st.metric("Overall Score", f"{seo_metrics.get('overall_score', 0):.1f}%") + with col2: + content_quality = seo_metrics.get('content', {}).get('content_quality_score', 0) + st.metric("Content Quality", f"{content_quality:.1f}%") + with col3: + readability = seo_metrics.get('content', {}).get('readability_score', 0) + st.metric("Readability", f"{readability:.1f}%") + with col4: + keyword_density = seo_metrics.get('content', {}).get('keyword_density', 0) + st.metric("Keyword Density", f"{keyword_density:.1f}%") + + # SEO Scores Radar Chart + seo_scores = { + 'Metric': ['Overall', 'Content Quality', 'Readability', 'Keyword Density'], + 'Score': [ + seo_metrics.get('overall_score', 0), + content_quality, + readability, + keyword_density + ] + } + fig = px.line_polar( + seo_scores, + r='Score', + theta='Metric', + line_close=True, + title="SEO Performance Overview" + ) + fig.update_traces(fill='toself') + st.plotly_chart(fig, use_container_width=True) + + # Meta Tags Analysis + st.write("Meta Tags Analysis") + meta_tags = seo_metrics.get('meta_tags', {}) + if meta_tags: + # Title Analysis + title = meta_tags.get('title', {}) + st.write("Title Tag") + st.write(f"Status: {'✅' if title.get('status') == 'good' else '❌'}") + st.write(f"Value: {title.get('value', 'N/A')}") + st.write(f"Length: {title.get('length', 0)} characters") + st.write(f"Score: {title.get('score', 0)}%") + if title.get('recommendation'): + st.warning(title.get('recommendation')) + + # Description Analysis + desc = meta_tags.get('description', {}) + st.write("Meta Description") + st.write(f"Status: {'✅' if desc.get('status') == 'good' else '❌'}") + st.write(f"Value: {desc.get('value', 'N/A')}") + st.write(f"Length: {desc.get('length', 0)} characters") + st.write(f"Score: {desc.get('score', 0)}%") + if desc.get('recommendation'): + st.warning(desc.get('recommendation')) + + # Keywords Analysis + keywords = meta_tags.get('keywords', {}) + st.write("Meta Keywords") + st.write(f"Status: {'✅' if keywords.get('status') == 'good' else '❌'}") + st.write(f"Value: {keywords.get('value', 'N/A')}") + if keywords.get('recommendation'): + st.warning(keywords.get('recommendation')) + + # Technical Metrics + st.subheader("Technical Metrics") + technical_info = results.get('technical_info', {}) + + if technical_info: + col1, col2 = st.columns(2) + with col1: + st.write("Basic Information") + st.metric("Status Code", technical_info.get('status_code', 'N/A')) + st.metric("Server", technical_info.get('server_info', {}).get('server', 'N/A')) + st.metric("Content Type", technical_info.get('server_info', {}).get('content_type', 'N/A')) + with col2: + st.write("Security Information") + security_info = technical_info.get('security_info', {}) + security_data = { + 'Feature': ['SSL', 'HSTS', 'XSS Protection'], + 'Status': [ + security_info.get('ssl', False), + security_info.get('hsts', False), + security_info.get('xss_protection', False) + ] + } + fig = px.bar( + security_data, + x='Feature', + y='Status', + title="Security Features Status", + color='Status', + color_discrete_sequence=['red', 'green'] + ) + st.plotly_chart(fig, use_container_width=True) + + # Performance Metrics + st.subheader("Performance Metrics") + performance = results.get('performance', {}) + + if performance: + # Basic metrics in columns + col1, col2, col3, col4 = st.columns(4) + with col1: + st.metric("Load Time", f"{performance.get('load_time', 0):.2f}s") + with col2: + st.metric("Page Size", f"{performance.get('page_size', 0):.1f} KB") + with col3: + st.metric("Status Code", performance.get('status_code', 'N/A')) + with col4: + st.metric("Response Time", f"{performance.get('response_time', 0):.2f}s") + + # Insights and Recommendations + st.subheader("Insights and Recommendations") + insights = results.get('insights', []) + if insights: + for insight in insights: + st.info(f"• {insight}") + else: + st.info("No specific insights available") + + except Exception as e: + logger.error(f"Error displaying website analysis: {str(e)}", exc_info=True) + st.error(f"Error displaying website analysis: {str(e)}") + + def _competitor_analysis_step(self): + """Competitor analysis step UI.""" + try: + st.header("Step 2: Competitor Analysis") + + with st.form("competitor_analysis_form"): + competitors = st.text_area( + "Enter competitor URLs (one per line)", + help="Enter the URLs of your main competitors" + ) + + submitted = st.form_submit_button("Analyze Competitors") + + if submitted and competitors: + with st.spinner("Analyzing competitors..."): + competitor_urls = [url.strip() for url in competitors.split('\n') if url.strip()] + results = self.competitor_analyzer.analyze(competitor_urls) + + # Get AI-enhanced competitor analysis + ai_analysis = self.ai_processor.analyze_competitors({ + 'competitors': competitor_urls, + 'analysis': results + }) + + # Combine results + results.update(ai_analysis) + st.session_state.analysis_results['competitors'] = results + + # Display results + self._display_competitor_analysis(results) + + # Move to next step + st.session_state.current_step = 3 + st.rerun() + except Exception as e: + logger.error(f"Error in competitor analysis step: {str(e)}", exc_info=True) + st.error(f"Error in competitor analysis: {str(e)}") + + def _display_competitor_analysis(self, results: dict): + """Display competitor analysis results.""" + st.subheader("Competitor Analysis Results") + + # Competitor comparison + st.subheader("Competitor Comparison") + comp_data = pd.DataFrame(results.get('comparison', [])) + if not comp_data.empty: + fig = px.bar( + comp_data, + x='competitor', + y='score', + color='metric', + title="Competitor Comparison" + ) + st.plotly_chart(fig) + + # AI-Enhanced Competitor Analysis + st.subheader("AI-Enhanced Competitor Analysis") + + # Competitor Trend Analysis + trend_data = results.get('competitor_trends', {}) + if trend_data: + fig = go.Figure() + for competitor, trends in trend_data.items(): + fig.add_trace(go.Scatter( + x=trends.get('timeline', []), + y=trends.get('scores', []), + name=competitor, + mode='lines+markers' + )) + fig.update_layout( + title="Competitor Performance Trends", + xaxis_title="Timeline", + yaxis_title="Score" + ) + st.plotly_chart(fig) + + # Content gaps + st.subheader("Content Gaps") + gaps = results.get('content_gaps', []) + for gap in gaps: + st.info(f"• {gap}") + + # AI-Generated Competitive Insights + st.subheader("Competitive Insights") + insights = results.get('competitive_insights', {}) + if insights: + for category, points in insights.items(): + with st.expander(f"{category.title()} Analysis"): + for point in points: + st.success(f"• {point}") + + def _keyword_research_step(self): + """Keyword research step UI.""" + try: + st.header("Step 3: Keyword Research") + + with st.form("keyword_research_form"): + industry = st.text_input( + "Enter your industry/niche", + value=st.session_state.analysis_results.get('website', {}).get('industry', '') + ) + + submitted = st.form_submit_button("Research Keywords") + + if submitted and industry: + with st.spinner("Researching keywords..."): + results = self.keyword_researcher.research(industry) + + # Get AI-enhanced keyword analysis + ai_analysis = self.ai_processor.analyze_keywords({ + 'industry': industry, + 'keywords': results + }) + + # Combine results + results.update(ai_analysis) + st.session_state.analysis_results['keywords'] = results + + # Display results + self._display_keyword_research(results) + + # Move to next step + st.session_state.current_step = 4 + st.rerun() + except Exception as e: + logger.error(f"Error in keyword research step: {str(e)}", exc_info=True) + st.error(f"Error in keyword research: {str(e)}") + + def _display_keyword_research(self, results: dict): + """Display keyword research results.""" + st.subheader("Keyword Research Results") + + # Keyword metrics + st.subheader("Keyword Metrics") + keyword_data = pd.DataFrame(results.get('keywords', [])) + if not keyword_data.empty: + fig = px.scatter( + keyword_data, + x='search_volume', + y='difficulty', + size='relevance_score', + hover_data=['keyword'], + title="Keyword Opportunities" + ) + st.plotly_chart(fig) + + # AI-Enhanced Keyword Analysis + st.subheader("AI-Enhanced Keyword Analysis") + + # Keyword Trend Analysis + trend_data = results.get('keyword_trends', {}) + if trend_data: + fig = go.Figure() + for keyword, trends in trend_data.items(): + fig.add_trace(go.Scatter( + x=trends.get('timeline', []), + y=trends.get('scores', []), + name=keyword, + mode='lines+markers' + )) + fig.update_layout( + title="Keyword Trend Analysis", + xaxis_title="Timeline", + yaxis_title="Trend Score" + ) + st.plotly_chart(fig) + + # Search intent distribution + st.subheader("Search Intent Distribution") + intent_data = pd.DataFrame(results.get('search_intent', {}).get('summary', {})) + if not intent_data.empty: + fig = px.pie( + intent_data, + values='count', + names='intent', + title="Search Intent Distribution" + ) + st.plotly_chart(fig) + + # Content format suggestions + st.subheader("Content Format Suggestions") + formats = results.get('content_formats', []) + for format in formats: + st.info(f"• {format}") + + # AI-Generated Keyword Insights + st.subheader("Keyword Insights") + insights = results.get('keyword_insights', {}) + if insights: + for category, points in insights.items(): + with st.expander(f"{category.title()} Insights"): + for point in points: + st.success(f"• {point}") + + def _recommendations_step(self): + """Recommendations step UI.""" + try: + st.header("Step 4: Content Recommendations") + + with st.spinner("Generating recommendations..."): + results = self.recommendation_engine.generate_recommendations( + st.session_state.analysis_results + ) + + # Get AI-enhanced recommendations + ai_recommendations = self.ai_processor.analyze_recommendations({ + 'recommendations': results, + 'analysis': st.session_state.analysis_results + }) + + # Combine results + results.update(ai_recommendations) + st.session_state.analysis_results['recommendations'] = results + + # Display results + self._display_recommendations(results) + + # Move to next step + st.session_state.current_step = 5 + st.rerun() + except Exception as e: + logger.error(f"Error in recommendations step: {str(e)}", exc_info=True) + st.error(f"Error in recommendations: {str(e)}") + + def _display_recommendations(self, results: dict): + """Display content recommendations.""" + st.subheader("Content Recommendations") + + # Priority recommendations + st.subheader("Priority Recommendations") + priorities = results.get('priorities', []) + for priority in priorities: + st.success(f"• {priority}") + + # AI-Enhanced Recommendations + st.subheader("AI-Enhanced Recommendations") + + # Recommendation Impact Analysis + impact_data = results.get('impact_analysis', {}) + if impact_data: + fig = go.Figure() + for metric, values in impact_data.items(): + fig.add_trace(go.Bar( + name=metric, + x=values.get('categories', []), + y=values.get('scores', []) + )) + fig.update_layout( + title="Recommendation Impact Analysis", + xaxis_title="Categories", + yaxis_title="Impact Score", + barmode='group' + ) + st.plotly_chart(fig) + + # Implementation timeline + st.subheader("Implementation Timeline") + timeline = results.get('timeline', []) + for item in timeline: + st.info(f"• {item}") + + # Expected impact + st.subheader("Expected Impact") + impact = results.get('impact', {}) + for metric, value in impact.items(): + st.metric(metric, value) + + # AI-Generated Strategic Insights + st.subheader("Strategic Insights") + insights = results.get('strategic_insights', {}) + if insights: + for category, points in insights.items(): + with st.expander(f"{category.title()} Strategy"): + for point in points: + st.success(f"• {point}") + + def _export_results(self): + """Export results step UI.""" + st.header("Step 5: Export Results") + + # Export options + export_format = st.radio( + "Choose export format", + ["JSON", "CSV", "PDF"] + ) + + if st.button("Export Results"): + if export_format == "JSON": + self._export_json() + elif export_format == "CSV": + self._export_csv() + else: + st.info("PDF export coming soon!") + + def _export_json(self): + """Export results as JSON.""" + results = st.session_state.analysis_results + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"content_gap_analysis_{timestamp}.json" + + st.download_button( + "Download JSON", + data=json.dumps(results, indent=2), + file_name=filename, + mime="application/json" + ) + + def _export_csv(self): + """Export results as CSV.""" + results = st.session_state.analysis_results + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Convert results to CSV format + csv_data = [] + for section, data in results.items(): + if isinstance(data, list): + for item in data: + if isinstance(item, dict): + item['section'] = section + csv_data.append(item) + elif isinstance(data, dict): + data['section'] = section + csv_data.append(data) + + if csv_data: + df = pd.DataFrame(csv_data) + filename = f"content_gap_analysis_{timestamp}.csv" + + st.download_button( + "Download CSV", + data=df.to_csv(index=False), + file_name=filename, + mime="text/csv" + ) + +def main(): + """Main entry point for the Streamlit app.""" + ui = ContentGapAnalysisUI() + ui.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/utils/README.md b/lib/ai_seo_tools/content_gap_analysis/utils/README.md new file mode 100644 index 00000000..86bc5ffb --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/utils/README.md @@ -0,0 +1,249 @@ +# Content Gap Analysis Utils + +This directory contains utility modules that power the Content Gap Analysis tool. These modules provide core functionality for data collection, processing, analysis, and storage. + +## Directory Structure + +``` +utils/ +├── README.md +├── ai_processor.py # AI-powered content analysis and processing +├── content_parser.py # Content structure parsing and analysis +├── data_collector.py # Website data collection and processing +└── storage.py # Analysis results storage and retrieval +``` + +## Module Descriptions + +### 1. AI Processor (`ai_processor.py`) + +The AI Processor module enhances content analysis using AI techniques. It provides intelligent analysis of website content, competitor data, and keyword research. + +#### Key Features: +- Content quality assessment +- Topic analysis and clustering +- Performance metrics analysis +- Strategic recommendations generation +- Progress tracking for analysis tasks + +#### Main Components: +- `AIProcessor`: Main class for AI-powered analysis +- `ProgressTracker`: Tracks analysis progress and status + +#### Usage Example: +```python +from utils.ai_processor import AIProcessor + +processor = AIProcessor() +analysis = processor.analyze_content({ + 'url': 'https://example.com', + 'industry': 'technology', + 'content': content_data +}) +``` + +### 2. Content Parser (`content_parser.py`) + +The Content Parser module handles the parsing and analysis of website content structure. It provides detailed insights into content organization and quality. + +#### Key Features: +- Content structure analysis +- Text statistics calculation +- Topic extraction +- Readability analysis +- Content hierarchy analysis + +#### Main Components: +- `ContentParser`: Main class for content parsing and analysis + +#### Usage Example: +```python +from utils.content_parser import ContentParser + +parser = ContentParser() +structure = parser.parse_structure({ + 'main_content': content, + 'html': html_content, + 'headings': headings_data +}) +``` + +### 3. Data Collector (`data_collector.py`) + +The Data Collector module is responsible for gathering website data for analysis. It handles web scraping and data extraction. + +#### Key Features: +- Website content collection +- Meta data extraction +- Heading structure analysis +- Link and image extraction +- Error handling and retry logic + +#### Main Components: +- `DataCollector`: Main class for data collection + +#### Usage Example: +```python +from utils.data_collector import DataCollector + +collector = DataCollector() +data = collector.collect('https://example.com') +``` + +### 4. Storage (`storage.py`) + +The Storage module manages the persistence and retrieval of analysis results. It provides a robust database interface for storing and accessing analysis data. + +#### Key Features: +- Analysis results storage +- Historical data management +- Recommendation tracking +- User-specific analysis storage +- Error handling and rollback support + +#### Main Components: +- `ContentGapAnalysisStorage`: Main class for storage operations + +#### Usage Example: +```python +from utils.storage import ContentGapAnalysisStorage + +storage = ContentGapAnalysisStorage(db_session) +analysis_id = storage.save_analysis( + user_id=1, + website_url='https://example.com', + industry='technology', + results=analysis_results +) +``` + +## Integration Points + +### 1. Website Analysis Integration +```python +from utils.data_collector import DataCollector +from utils.content_parser import ContentParser +from utils.ai_processor import AIProcessor + +# Collect data +collector = DataCollector() +data = collector.collect(url) + +# Parse content +parser = ContentParser() +structure = parser.parse_structure(data) + +# Process with AI +processor = AIProcessor() +analysis = processor.analyze_content({ + 'url': url, + 'content': structure +}) +``` + +### 2. Storage Integration +```python +from utils.storage import ContentGapAnalysisStorage + +# Store analysis results +storage = ContentGapAnalysisStorage(db_session) +analysis_id = storage.save_analysis( + user_id=user_id, + website_url=url, + industry=industry, + results=analysis_results +) + +# Retrieve analysis +results = storage.get_analysis(analysis_id) +``` + +## Error Handling + +All modules implement comprehensive error handling: + +1. **Data Collection Errors** + - Network timeouts + - Invalid URLs + - Access restrictions + - Parsing errors + +2. **Processing Errors** + - Invalid data formats + - AI processing failures + - Resource limitations + - Analysis timeouts + +3. **Storage Errors** + - Database connection issues + - Transaction failures + - Data validation errors + - Concurrent access conflicts + +## Best Practices + +1. **Data Collection** + - Implement rate limiting + - Use proper user agents + - Handle redirects + - Validate input data + +2. **Content Processing** + - Clean and normalize data + - Handle encoding issues + - Implement fallback strategies + - Cache processed results + +3. **Storage Management** + - Use transactions + - Implement data validation + - Handle concurrent access + - Maintain data integrity + +## Future Enhancements + +1. **Performance Optimizations** + - Implement parallel processing + - Add caching layer + - Optimize database queries + - Enhance error recovery + +2. **Feature Additions** + - Content performance tracking + - Automated content planning + - Enhanced competitive intelligence + - Advanced topic clustering + +3. **Integration Improvements** + - API endpoints + - Export capabilities + - Data visualization + - Progress tracking + +4. **UI/UX Enhancements** + - Interactive visualizations + - Real-time progress updates + - Export interfaces + - Customization options + +## Contributing + +When contributing to these utility modules: + +1. Follow the existing code structure +2. Add comprehensive error handling +3. Include unit tests +4. Update documentation +5. Follow PEP 8 style guide + +## Dependencies + +- BeautifulSoup4: HTML parsing +- NLTK: Natural language processing +- SQLAlchemy: Database operations +- Streamlit: UI components +- Requests: HTTP requests + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/utils/__init__.py b/lib/ai_seo_tools/content_gap_analysis/utils/__init__.py new file mode 100644 index 00000000..04b0726f --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/utils/__init__.py @@ -0,0 +1,13 @@ +""" +Utility modules for content gap analysis. +""" + +from .data_collector import DataCollector +from .content_parser import ContentParser +from .ai_processor import AIProcessor + +__all__ = [ + 'DataCollector', + 'ContentParser', + 'AIProcessor' +] \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/utils/ai_processor.py b/lib/ai_seo_tools/content_gap_analysis/utils/ai_processor.py new file mode 100644 index 00000000..eddbead0 --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/utils/ai_processor.py @@ -0,0 +1,1134 @@ +""" +AI processor module for content gap analysis. +""" + +import streamlit as st +from typing import Dict, Any, List, Optional +from loguru import logger +from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer +from lib.ai_seo_tools.content_gap_analysis.utils.data_collector import DataCollector +from lib.ai_seo_tools.content_gap_analysis.utils.content_parser import ContentParser +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen +from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image +import asyncio +import sys +import os +import json +import re +from collections import Counter + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/ai_processor.log", + rotation="50 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}" +) + +# Ensure logs directory exists +os.makedirs("logs", exist_ok=True) + +class ProgressTracker: + """Tracks progress of AI processing tasks.""" + + def __init__(self): + """Initialize the progress tracker.""" + self._progress = { + 'status': 'initializing', + 'current_step': 'Starting', + 'progress': 0, + 'details': 'Initializing...' + } + + def update(self, progress_data: Dict[str, Any]) -> None: + """ + Update progress information. + + Args: + progress_data: Dictionary containing progress information + - status: Current status ('initializing', 'in_progress', 'completed', 'error') + - current_step: Description of current step + - progress: Progress percentage (0-100) + - details: Additional details about current step + """ + if not isinstance(progress_data, dict): + raise ValueError("Progress data must be a dictionary") + + required_fields = ['status', 'current_step', 'progress', 'details'] + for field in required_fields: + if field not in progress_data: + raise ValueError(f"Missing required field: {field}") + + # Validate progress value + progress = progress_data.get('progress', 0) + if not isinstance(progress, (int, float)) or progress < 0 or progress > 100: + raise ValueError("Progress must be a number between 0 and 100") + + # Validate status + valid_statuses = ['initializing', 'in_progress', 'completed', 'error'] + status = progress_data.get('status') + if status not in valid_statuses: + raise ValueError(f"Invalid status. Must be one of: {', '.join(valid_statuses)}") + + # Update progress + self._progress.update(progress_data) + + def get_progress(self) -> Dict[str, Any]: + """ + Get current progress information. + + Returns: + Dictionary containing current progress information + """ + return self._progress.copy() + + def reset(self) -> None: + """Reset progress to initial state.""" + self._progress = { + 'status': 'initializing', + 'current_step': 'Starting', + 'progress': 0, + 'details': 'Initializing...' + } + +class AIProcessor: + """Processes and enhances content analysis using AI techniques.""" + + def __init__(self): + """Initialize the AI processor.""" + self.cache = {} + self.progress = ProgressTracker() + self.system_prompts = { + 'content_analysis': """You are an expert SEO and content analyst. Your task is to analyze content and provide detailed insights. + Focus on: + 1. Content quality and structure + 2. Topic coverage and depth + 3. SEO optimization + 4. User engagement potential + 5. Content gaps and opportunities + + Provide your analysis in a structured format with specific, actionable recommendations. + Use clear metrics and examples to support your insights.""", + + 'competitor_analysis': """You are an expert competitive analyst. Your task is to analyze competitor data and provide strategic insights. + Focus on: + 1. Content strategy differences + 2. Competitive advantages + 3. Content gaps and opportunities + 4. Market positioning + 5. Strategic recommendations + + Provide your analysis in a structured format with specific, actionable recommendations. + Include competitive benchmarks and improvement opportunities.""", + + 'keyword_analysis': """You are an expert keyword analyst. Your task is to analyze keyword data and provide strategic insights. + Focus on: + 1. Keyword opportunities + 2. Search intent patterns + 3. Content format suggestions + 4. Topic clusters + 5. Strategic recommendations + + Provide your analysis in a structured format with specific, actionable recommendations. + Include keyword metrics and content strategy suggestions.""", + + 'recommendation_analysis': """You are an expert content strategist. Your task is to analyze recommendations and provide implementation insights. + Focus on: + 1. Implementation feasibility + 2. Expected impact + 3. Resource requirements + 4. Timeline suggestions + 5. Risk assessment + + Provide your analysis in a structured format with specific, actionable guidance. + Include implementation steps and success metrics.""" + } + + def analyze_content(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Analyze website content using AI techniques. + + Args: + data: Dictionary containing website content and metadata + + Returns: + Dictionary containing AI-enhanced analysis + """ + try: + self.progress.update({'status': 'in_progress', 'current_step': 'Analyzing content structure'}) + + url = data.get('url', '') + industry = data.get('industry', '') + content = data.get('content', {}) + + # Generate AI prompt for content analysis + prompt = f""" + Analyze the following website content and provide detailed insights: + + URL: {url} + Industry: {industry} + Content: {content} + + Please provide a comprehensive analysis covering: + 1. Content Quality Assessment + - Structure and organization + - Readability and engagement + - Technical elements + - SEO optimization + + 2. Topic Analysis + - Coverage and depth + - Relevance to industry + - Content gaps + - Opportunities + + 3. Performance Metrics + - User engagement potential + - SEO effectiveness + - Content value + + 4. Strategic Recommendations + - Content improvements + - SEO enhancements + - Engagement strategies + - Implementation steps + + Format your response in a clear, structured manner with specific examples and actionable recommendations. + """ + + self.progress.update({'current_step': 'Analyzing content structure'}) + + # Get AI analysis with custom system prompt + ai_analysis = llm_text_gen(prompt, system_prompt=self.system_prompts['content_analysis']) + + self.progress.update({'current_step': 'Evaluating content quality'}) + + # Extract content metrics + content_metrics = self._analyze_content_metrics(content) + + self.progress.update({'current_step': 'Assessing SEO elements'}) + + # Analyze content evolution + evolution_analysis = self._analyze_content_evolution(content) + + self.progress.update({'current_step': 'Analyzing topic trends'}) + + # Analyze topic trends + topic_trends = self._analyze_topic_trends(content, industry) + + self.progress.update({'current_step': 'Analyzing performance trends'}) + + # Analyze performance trends + performance_trends = self._analyze_performance_trends(content) + + self.progress.update({'current_step': 'Generating content insights'}) + + # Generate AI insights + insights = self._generate_content_insights(content_metrics, evolution_analysis, topic_trends, performance_trends) + + self.progress.update({'current_step': 'Creating visual representation'}) + + # Generate visual representation + visualization = self._generate_content_visualization(content, insights) + + self.progress.update({'status': 'completed', 'current_step': 'Completed content analysis'}) + + return { + 'content_metrics': content_metrics, + 'content_evolution': evolution_analysis, + 'topic_trends': topic_trends, + 'performance_trends': performance_trends, + 'ai_insights': insights, + 'ai_analysis': ai_analysis, + 'visualization': visualization + } + + except Exception as e: + if self.progress.get_progress().get('status') == 'in_progress': + self.progress.update({'status': 'error', 'current_step': 'Error in content analysis', 'details': str(e)}) + st.error(f"Error in AI content analysis: {str(e)}") + return { + 'error': str(e), + 'content_metrics': {}, + 'content_evolution': {}, + 'topic_trends': {}, + 'performance_trends': {}, + 'ai_insights': {}, + 'ai_analysis': {}, + 'visualization': None + } + + def analyze_competitors(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Analyze competitor content using AI techniques. + + Args: + data: Dictionary containing competitor data and analysis + + Returns: + Dictionary containing AI-enhanced competitor analysis + """ + try: + self.progress.update({'status': 'in_progress', 'current_step': 'Analyzing competitor content'}) + + competitors = data.get('competitors', []) + analysis = data.get('analysis', {}) + + # Generate AI prompt for competitor analysis + prompt = f""" + Analyze the following competitor data and provide strategic insights: + + Competitors: {competitors} + Analysis: {analysis} + + Please provide a comprehensive analysis covering: + 1. Content Strategy Analysis + - Content types and formats + - Topic coverage + - Content quality + - Engagement strategies + + 2. Competitive Position + - Market positioning + - Unique advantages + - Content gaps + - Opportunities + + 3. Performance Metrics + - Content effectiveness + - User engagement + - SEO performance + - Market share + + 4. Strategic Recommendations + - Content improvements + - Competitive advantages + - Market positioning + - Implementation steps + + Format your response in a clear, structured manner with specific examples and actionable recommendations. + """ + + self.progress.update({'current_step': 'Evaluating market position'}) + + # Get AI analysis with custom system prompt + ai_analysis = llm_text_gen(prompt, system_prompt=self.system_prompts['competitor_analysis']) + + self.progress.update({'current_step': 'Identifying content gaps'}) + + # Analyze competitor trends + trend_analysis = self._analyze_competitor_trends(competitors, analysis) + + self.progress.update({'current_step': 'Generating competitive insights'}) + + # Generate competitive insights + insights = self._generate_competitive_insights(competitors, analysis, trend_analysis) + + self.progress.update({'current_step': 'Creating visual representation'}) + + # Generate visual representation + visualization = self._generate_competitor_visualization(competitors, insights) + + self.progress.update({'status': 'completed', 'current_step': 'Completed competitor analysis'}) + + return { + 'competitor_trends': trend_analysis, + 'competitive_insights': insights, + 'ai_analysis': ai_analysis, + 'visualization': visualization + } + + except Exception as e: + if self.progress.get_progress().get('status') == 'in_progress': + self.progress.update({'status': 'error', 'current_step': 'Error in competitor analysis', 'details': str(e)}) + st.error(f"Error in AI competitor analysis: {str(e)}") + return { + 'error': str(e), + 'competitor_trends': {}, + 'competitive_insights': {}, + 'ai_analysis': {}, + 'visualization': None + } + + def analyze_keywords(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Analyze keywords using AI techniques. + + Args: + data: Dictionary containing keyword data + + Returns: + Dictionary containing AI-enhanced keyword analysis + """ + try: + self.progress.update({'status': 'in_progress', 'current_step': 'Analyzing keyword trends'}) + + industry = data.get('industry', '') + keywords = data.get('keywords', {}) + + # Generate AI prompt for keyword analysis + prompt = f""" + Analyze the following keyword data and provide strategic insights: + + Industry: {industry} + Keywords: {keywords} + + Please provide a comprehensive analysis covering: + 1. Keyword Opportunity Analysis + - Search volume trends + - Competition levels + - Difficulty scores + - Potential impact + + 2. Search Intent Analysis + - User intent patterns + - Content type alignment + - Topic clusters + - Content gaps + + 3. Content Strategy + - Format recommendations + - Topic suggestions + - Implementation approach + - Success metrics + + 4. Strategic Recommendations + - Keyword targeting + - Content development + - Implementation steps + - Performance tracking + + Format your response in a clear, structured manner with specific examples and actionable recommendations. + """ + + self.progress.update({'current_step': 'Evaluating search intent'}) + + # Get AI analysis with custom system prompt + ai_analysis = llm_text_gen(prompt, system_prompt=self.system_prompts['keyword_analysis']) + + self.progress.update({'current_step': 'Identifying opportunities'}) + + # Analyze keyword trends + trend_analysis = self._analyze_keyword_trends(keywords) + + self.progress.update({'current_step': 'Analyzing search intent'}) + + # Analyze search intent + intent_analysis = self._analyze_search_intent(keywords) + + self.progress.update({'current_step': 'Generating keyword insights'}) + + # Generate keyword insights + insights = self._generate_keyword_insights(keywords, trend_analysis, intent_analysis) + + self.progress.update({'current_step': 'Creating visual representation'}) + + # Generate visual representation + visualization = self._generate_keyword_visualization(keywords, insights) + + self.progress.update({'status': 'completed', 'current_step': 'Completed keyword analysis'}) + + return { + 'keyword_trends': trend_analysis, + 'search_intent': intent_analysis, + 'keyword_insights': insights, + 'ai_analysis': ai_analysis, + 'visualization': visualization + } + + except Exception as e: + if self.progress.get_progress().get('status') == 'in_progress': + self.progress.update({'status': 'error', 'current_step': 'Error in keyword analysis', 'details': str(e)}) + st.error(f"Error in AI keyword analysis: {str(e)}") + return { + 'error': str(e), + 'keyword_trends': {}, + 'search_intent': {}, + 'keyword_insights': {}, + 'ai_analysis': {}, + 'visualization': None + } + + def analyze_recommendations(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + Analyze recommendations using AI techniques. + + Args: + data: Dictionary containing recommendations and analysis + + Returns: + Dictionary containing AI-enhanced recommendation analysis + """ + try: + self.progress.update({'status': 'in_progress', 'current_step': 'Analyzing recommendation impact'}) + + recommendations = data.get('recommendations', {}) + analysis = data.get('analysis', {}) + + # Generate AI prompt for recommendation analysis + prompt = f""" + Analyze the following recommendations and provide implementation insights: + + Recommendations: {recommendations} + Analysis: {analysis} + + Please provide a comprehensive analysis covering: + 1. Implementation Feasibility + - Resource requirements + - Technical complexity + - Timeline estimates + - Risk assessment + + 2. Expected Impact + - SEO improvements + - User engagement + - Content quality + - Business value + + 3. Implementation Strategy + - Priority order + - Dependencies + - Success metrics + - Monitoring plan + + 4. Risk Management + - Potential challenges + - Mitigation strategies + - Contingency plans + - Success criteria + + Format your response in a clear, structured manner with specific examples and actionable guidance. + """ + + self.progress.update({'current_step': 'Assessing expected impact'}) + + # Get AI analysis with custom system prompt + ai_analysis = llm_text_gen(prompt, system_prompt=self.system_prompts['recommendation_analysis']) + + self.progress.update({'current_step': 'Analyzing resource requirements'}) + + # Analyze recommendation impact + impact_analysis = self._analyze_recommendation_impact(recommendations, analysis) + + self.progress.update({'current_step': 'Generating strategic insights'}) + + # Generate strategic insights + insights = self._generate_strategic_insights(recommendations, analysis, impact_analysis) + + self.progress.update({'current_step': 'Creating visual representation'}) + + # Generate visual representation + visualization = self._generate_recommendation_visualization(recommendations, insights) + + self.progress.update({'status': 'completed', 'current_step': 'Completed recommendation analysis'}) + + return { + 'impact_analysis': impact_analysis, + 'strategic_insights': insights, + 'ai_analysis': ai_analysis, + 'visualization': visualization + } + + except Exception as e: + if self.progress.get_progress().get('status') == 'in_progress': + self.progress.update({'status': 'error', 'current_step': 'Error in recommendation analysis', 'details': str(e)}) + st.error(f"Error in AI recommendation analysis: {str(e)}") + return { + 'error': str(e), + 'impact_analysis': {}, + 'strategic_insights': {}, + 'ai_analysis': {}, + 'visualization': None + } + + def _analyze_content_metrics(self, content: Dict[str, Any]) -> Dict[str, Any]: + """Analyze content metrics using AI.""" + prompt = f""" + Analyze the following content and provide detailed metrics: + + Content: {content} + + Please provide: + 1. Content Quality Metrics + - Structure score + - Readability score + - Engagement score + - SEO score + + 2. Technical Metrics + - Content length + - Internal linking + - Meta tags + - Technical elements + + 3. Performance Indicators + - User engagement potential + - SEO effectiveness + - Content value + - Improvement areas + + Format your response in a clear, structured manner with specific metrics and scores. + """ + + try: + metrics = llm_text_gen(prompt, system_prompt=self.system_prompts['content_analysis']) + return metrics + except Exception as e: + st.error(f"Error analyzing content metrics: {str(e)}") + return { + 'quality_score': 0, + 'readability_score': 0, + 'engagement_score': 0, + 'seo_score': 0 + } + + def _analyze_content_evolution(self, content: Dict[str, Any]) -> Dict[str, Any]: + """Analyze content evolution using AI.""" + prompt = f""" + Analyze the following content evolution: + + Content: {content} + + Please provide: + 1. Timeline Analysis + - Content development stages + - Key milestones + - Evolution patterns + - Growth indicators + + 2. Performance Metrics + - Depth scores over time + - Coverage scores over time + - Engagement trends + - SEO performance + + 3. Evolution Insights + - Content improvements + - Strategy changes + - Success factors + - Future opportunities + + Format your response in a clear, structured manner with specific metrics and insights. + """ + + try: + evolution = llm_text_gen(prompt, system_prompt=self.system_prompts['content_analysis']) + return evolution + except Exception as e: + st.error(f"Error analyzing content evolution: {str(e)}") + return { + 'timeline': [], + 'depth_scores': [], + 'coverage_scores': [] + } + + def _analyze_topic_trends(self, content: Dict[str, Any], industry: str) -> Dict[str, Any]: + """Analyze topic trends using AI.""" + prompt = f""" + Analyze topic trends for the following content and industry: + + Content: {content} + Industry: {industry} + + Please provide: + 1. Topic Analysis + - Current topic coverage + - Emerging topics + - Declining topics + - Industry alignment + + 2. Trend Matrix + - Topic popularity + - Growth potential + - Competition level + - Implementation priority + + 3. Strategic Insights + - Content opportunities + - Topic gaps + - Industry trends + - Action items + + Format your response in a clear, structured manner with specific metrics and recommendations. + """ + + try: + trends = llm_text_gen(prompt, system_prompt=self.system_prompts['content_analysis']) + return trends + except Exception as e: + st.error(f"Error analyzing topic trends: {str(e)}") + return { + 'trend_matrix': [] + } + + def _analyze_performance_trends(self, content: Dict[str, Any]) -> Dict[str, Any]: + """Analyze performance trends using AI.""" + prompt = f""" + Analyze performance trends for the following content: + + Content: {content} + + Please provide: + 1. Engagement Metrics + - User interaction + - Time on page + - Bounce rate + - Conversion rate + + 2. Content Performance + - Traffic trends + - Engagement patterns + - Conversion metrics + - ROI indicators + + 3. Improvement Areas + - Performance gaps + - Optimization opportunities + - Success factors + - Action items + + Format your response in a clear, structured manner with specific metrics and recommendations. + """ + + try: + trends = llm_text_gen(prompt, system_prompt=self.system_prompts['content_analysis']) + return trends + except Exception as e: + st.error(f"Error analyzing performance trends: {str(e)}") + return { + 'engagement': {}, + 'conversion': {} + } + + def _analyze_competitor_trends(self, competitors: List[str], analysis: Dict[str, Any]) -> Dict[str, Any]: + """Analyze competitor trends using AI.""" + prompt = f""" + Analyze competitor trends: + + Competitors: {competitors} + Analysis: {analysis} + + Please provide: + 1. Market Position + - Market share + - Growth trends + - Competitive advantages + - Market dynamics + + 2. Strategy Analysis + - Content approach + - Marketing tactics + - User engagement + - Success factors + + 3. Competitive Insights + - Market opportunities + - Threat assessment + - Strategic gaps + - Action items + + Format your response in a clear, structured manner with specific metrics and recommendations. + """ + + try: + trends = llm_text_gen(prompt, system_prompt=self.system_prompts['competitor_analysis']) + return trends + except Exception as e: + st.error(f"Error analyzing competitor trends: {str(e)}") + return {} + + def _analyze_keyword_trends(self, keywords: Dict[str, Any]) -> Dict[str, Any]: + """Analyze keyword trends using AI.""" + prompt = f""" + Analyze keyword trends: + + Keywords: {keywords} + + Please provide: + 1. Performance Metrics + - Search volume + - Competition level + - Difficulty score + - Trend direction + + 2. Opportunity Analysis + - Growth potential + - Competition gaps + - Content needs + - Implementation priority + + 3. Strategic Insights + - Keyword opportunities + - Content strategy + - Implementation steps + - Success metrics + + Format your response in a clear, structured manner with specific metrics and recommendations. + """ + + try: + trends = llm_text_gen(prompt, system_prompt=self.system_prompts['keyword_analysis']) + return trends + except Exception as e: + st.error(f"Error analyzing keyword trends: {str(e)}") + return {} + + def _analyze_search_intent(self, keywords: Dict[str, Any]) -> Dict[str, Any]: + """Analyze search intent using AI.""" + prompt = f""" + Analyze search intent: + + Keywords: {keywords} + + Please provide: + 1. Intent Analysis + - User intent types + - Content needs + - User journey + - Conversion potential + + 2. Content Strategy + - Format recommendations + - Topic suggestions + - Implementation approach + - Success metrics + + 3. Strategic Insights + - Content opportunities + - User engagement + - Conversion optimization + - Action items + + Format your response in a clear, structured manner with specific metrics and recommendations. + """ + + try: + intent = llm_text_gen(prompt, system_prompt=self.system_prompts['keyword_analysis']) + return intent + except Exception as e: + st.error(f"Error analyzing search intent: {str(e)}") + return { + 'summary': [] + } + + def _analyze_recommendation_impact(self, recommendations: Dict[str, Any], analysis: Dict[str, Any]) -> Dict[str, Any]: + """Analyze recommendation impact using AI.""" + prompt = f""" + Analyze recommendation impact: + + Recommendations: {recommendations} + Analysis: {analysis} + + Please provide: + 1. Impact Assessment + - SEO improvements + - User engagement + - Content quality + - Business value + + 2. Implementation Analysis + - Resource requirements + - Technical complexity + - Timeline estimates + - Risk assessment + + 3. Strategic Insights + - Priority order + - Dependencies + - Success metrics + - Monitoring plan + + Format your response in a clear, structured manner with specific metrics and recommendations. + """ + + try: + impact = llm_text_gen(prompt, system_prompt=self.system_prompts['recommendation_analysis']) + return impact + except Exception as e: + st.error(f"Error analyzing recommendation impact: {str(e)}") + return { + 'seo': {}, + 'engagement': {} + } + + def _generate_content_insights(self, metrics: Dict[str, Any], evolution: Dict[str, Any], trends: Dict[str, Any], performance: Dict[str, Any]) -> Dict[str, Any]: + """Generate content insights using AI.""" + prompt = f""" + Generate content insights: + + Metrics: {metrics} + Evolution: {evolution} + Trends: {trends} + Performance: {performance} + + Please provide: + 1. Quality Insights + - Content strengths + - Improvement areas + - Best practices + - Action items + + 2. Engagement Insights + - User behavior + - Interaction patterns + - Conversion factors + - Optimization opportunities + + 3. SEO Insights + - Technical optimization + - Content optimization + - Performance factors + - Implementation steps + + Format your response in a clear, structured manner with specific insights and recommendations. + """ + + try: + insights = llm_text_gen(prompt, system_prompt=self.system_prompts['content_analysis']) + return insights + except Exception as e: + st.error(f"Error generating content insights: {str(e)}") + return { + 'quality': [], + 'engagement': [], + 'seo': [] + } + + def _generate_competitive_insights(self, competitors: List[str], analysis: Dict[str, Any], trends: Dict[str, Any]) -> Dict[str, Any]: + """Generate competitive insights using AI.""" + prompt = f""" + Generate competitive insights: + + Competitors: {competitors} + Analysis: {analysis} + Trends: {trends} + + Please provide: + 1. Content Strategy + - Market position + - Competitive advantages + - Content gaps + - Opportunities + + 2. Market Insights + - Industry trends + - User preferences + - Market dynamics + - Growth potential + + 3. Strategic Recommendations + - Content improvements + - Market positioning + - Implementation steps + - Success metrics + + Format your response in a clear, structured manner with specific insights and recommendations. + """ + + try: + insights = llm_text_gen(prompt, system_prompt=self.system_prompts['competitor_analysis']) + return insights + except Exception as e: + st.error(f"Error generating competitive insights: {str(e)}") + return { + 'content': [], + 'strategy': [] + } + + def _generate_keyword_insights(self, keywords: Dict[str, Any], trends: Dict[str, Any], intent: Dict[str, Any]) -> Dict[str, Any]: + """Generate keyword insights using AI.""" + prompt = f""" + Generate keyword insights: + + Keywords: {keywords} + Trends: {trends} + Intent: {intent} + + Please provide: + 1. Opportunity Analysis + - Keyword potential + - Content needs + - Implementation priority + - Success metrics + + 2. Content Strategy + - Topic clusters + - Format recommendations + - Implementation approach + - Performance tracking + + 3. Strategic Recommendations + - Keyword targeting + - Content development + - Implementation steps + - Success metrics + + Format your response in a clear, structured manner with specific insights and recommendations. + """ + + try: + insights = llm_text_gen(prompt, system_prompt=self.system_prompts['keyword_analysis']) + return insights + except Exception as e: + st.error(f"Error generating keyword insights: {str(e)}") + return { + 'opportunities': [], + 'strategy': [] + } + + def _generate_strategic_insights(self, recommendations: Dict[str, Any], analysis: Dict[str, Any], impact: Dict[str, Any]) -> Dict[str, Any]: + """Generate strategic insights using AI.""" + prompt = f""" + Generate strategic insights: + + Recommendations: {recommendations} + Analysis: {analysis} + Impact: {impact} + + Please provide: + 1. Content Strategy + - Implementation approach + - Resource requirements + - Timeline estimates + - Success metrics + + 2. Technical Strategy + - Technical requirements + - Implementation steps + - Risk assessment + - Monitoring plan + + 3. Strategic Recommendations + - Priority order + - Dependencies + - Success criteria + - Action items + + Format your response in a clear, structured manner with specific insights and recommendations. + """ + + try: + insights = llm_text_gen(prompt, system_prompt=self.system_prompts['recommendation_analysis']) + return insights + except Exception as e: + st.error(f"Error generating strategic insights: {str(e)}") + return { + 'content': [], + 'technical': [] + } + + def _generate_content_visualization(self, content: Dict[str, Any], insights: Dict[str, Any]) -> str: + """Generate visual representation of content analysis.""" + try: + # Create a prompt for image generation + prompt = f""" + Create a visual representation of content analysis with the following insights: + + Content Quality: {insights.get('quality', [])} + Engagement Metrics: {insights.get('engagement', [])} + SEO Performance: {insights.get('seo', [])} + + The image should be professional, data-driven, and easy to understand. + """ + + # Generate image + image_path = generate_image( + user_prompt=prompt, + title="Content Analysis Visualization", + description="Visual representation of content analysis insights", + aspect_ratio="16:9" + ) + + return image_path + + except Exception as e: + st.error(f"Error generating content visualization: {str(e)}") + return None + + def _generate_competitor_visualization(self, competitors: List[str], insights: Dict[str, Any]) -> str: + """Generate visual representation of competitor analysis.""" + try: + # Create a prompt for image generation + prompt = f""" + Create a visual representation of competitor analysis with the following insights: + + Market Position: {insights.get('strategy', [])} + Competitive Advantages: {insights.get('content', [])} + + The image should be professional, data-driven, and easy to understand. + """ + + # Generate image + image_path = generate_image( + user_prompt=prompt, + title="Competitor Analysis Visualization", + description="Visual representation of competitor analysis insights", + aspect_ratio="16:9" + ) + + return image_path + + except Exception as e: + st.error(f"Error generating competitor visualization: {str(e)}") + return None + + def _generate_keyword_visualization(self, keywords: Dict[str, Any], insights: Dict[str, Any]) -> str: + """Generate visual representation of keyword analysis.""" + try: + # Create a prompt for image generation + prompt = f""" + Create a visual representation of keyword analysis with the following insights: + + Keyword Opportunities: {insights.get('opportunities', [])} + Content Strategy: {insights.get('strategy', [])} + + The image should be professional, data-driven, and easy to understand. + """ + + # Generate image + image_path = generate_image( + user_prompt=prompt, + title="Keyword Analysis Visualization", + description="Visual representation of keyword analysis insights", + aspect_ratio="16:9" + ) + + return image_path + + except Exception as e: + st.error(f"Error generating keyword visualization: {str(e)}") + return None + + def _generate_recommendation_visualization(self, recommendations: Dict[str, Any], insights: Dict[str, Any]) -> str: + """Generate visual representation of recommendation analysis.""" + try: + # Create a prompt for image generation + prompt = f""" + Create a visual representation of recommendation analysis with the following insights: + + Content Strategy: {insights.get('content', [])} + Technical Strategy: {insights.get('technical', [])} + + The image should be professional, data-driven, and easy to understand. + """ + + # Generate image + image_path = generate_image( + user_prompt=prompt, + title="Recommendation Analysis Visualization", + description="Visual representation of recommendation analysis insights", + aspect_ratio="16:9" + ) + + return image_path + + except Exception as e: + st.error(f"Error generating recommendation visualization: {str(e)}") + return None \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/utils/content_parser.py b/lib/ai_seo_tools/content_gap_analysis/utils/content_parser.py new file mode 100644 index 00000000..0a0af660 --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/utils/content_parser.py @@ -0,0 +1,236 @@ +""" +Content parser utility for analyzing website content structure. +""" + +from typing import Dict, Any, List +import re +from bs4 import BeautifulSoup +import nltk +from nltk.tokenize import sent_tokenize, word_tokenize +from nltk.corpus import stopwords +from collections import Counter + +class ContentParser: + """Parser for analyzing website content structure.""" + + def __init__(self): + """Initialize the content parser.""" + try: + nltk.data.find('tokenizers/punkt') + except LookupError: + nltk.download('punkt') + try: + nltk.data.find('corpora/stopwords') + except LookupError: + nltk.download('stopwords') + + self.stop_words = set(stopwords.words('english')) + + def parse_structure(self, content: Dict[str, Any]) -> Dict[str, Any]: + """ + Parse and analyze the structure of website content. + + Args: + content: Dictionary containing website content + + Returns: + Dictionary containing parsed content structure + """ + try: + # Parse main content + main_content = content.get('main_content', '') + soup = BeautifulSoup(content.get('html', ''), 'html.parser') + + # Extract text statistics + text_stats = self._analyze_text(main_content) + + # Extract content sections + sections = self._extract_sections(soup) + + # Extract topics + topics = self._extract_topics(main_content) + + # Analyze readability + readability = self._analyze_readability(main_content) + + # Analyze content hierarchy + hierarchy = self._analyze_hierarchy(content.get('headings', [])) + + return { + 'text_statistics': text_stats, + 'sections': sections, + 'topics': topics, + 'readability': readability, + 'hierarchy': hierarchy, + 'metadata': content.get('metadata', {}) + } + + except Exception as e: + return { + 'error': str(e), + 'text_statistics': {}, + 'sections': [], + 'topics': [], + 'readability': {}, + 'hierarchy': {}, + 'metadata': {} + } + + def _analyze_text(self, text: str) -> Dict[str, Any]: + """Analyze text statistics.""" + sentences = sent_tokenize(text) + words = word_tokenize(text.lower()) + words = [w for w in words if w.isalnum() and w not in self.stop_words] + + return { + 'word_count': len(words), + 'sentence_count': len(sentences), + 'average_sentence_length': len(words) / max(len(sentences), 1), + 'unique_words': len(set(words)), + 'stop_words': len([w for w in word_tokenize(text.lower()) if w in self.stop_words]), + 'characters': len(text), + 'paragraphs': len(text.split('\n\n')), + 'sentences': sentences + } + + def _extract_sections(self, soup: BeautifulSoup) -> List[Dict[str, Any]]: + """Extract content sections.""" + sections = [] + + # Find main content containers + containers = soup.find_all(['article', 'section', 'div'], class_=re.compile(r'content|main|article|section')) + + for container in containers: + # Get section heading + heading = container.find(['h1', 'h2', 'h3']) + heading_text = heading.get_text().strip() if heading else 'Untitled Section' + + # Get section content + content = container.get_text().strip() + + # Get section type + section_type = container.name + if container.get('class'): + section_type = ' '.join(container.get('class')) + + sections.append({ + 'heading': heading_text, + 'content': content, + 'type': section_type, + 'word_count': len(word_tokenize(content)), + 'position': self._get_element_position(container) + }) + + return sections + + def _extract_topics(self, text: str) -> List[Dict[str, Any]]: + """Extract main topics from content.""" + # Tokenize and clean text + words = word_tokenize(text.lower()) + words = [w for w in words if w.isalnum() and w not in self.stop_words] + + # Get word frequencies + word_freq = Counter(words) + + # Get top topics + topics = [] + for word, freq in word_freq.most_common(10): + topics.append({ + 'topic': word, + 'frequency': freq, + 'percentage': freq / len(words) * 100 + }) + + return topics + + def _analyze_readability(self, text: str) -> Dict[str, float]: + """Analyze text readability.""" + sentences = sent_tokenize(text) + words = word_tokenize(text.lower()) + words = [w for w in words if w.isalnum()] + + # Calculate average sentence length + avg_sentence_length = len(words) / max(len(sentences), 1) + + # Calculate average word length + avg_word_length = sum(len(w) for w in words) / max(len(words), 1) + + # Calculate Flesch Reading Ease score + # Formula: 206.835 - 1.015(total words/total sentences) - 84.6(total syllables/total words) + syllables = sum(self._count_syllables(w) for w in words) + flesch_score = 206.835 - 1.015 * avg_sentence_length - 84.6 * (syllables / max(len(words), 1)) + + return { + 'flesch_score': max(0, min(100, flesch_score)), + 'avg_sentence_length': avg_sentence_length, + 'avg_word_length': avg_word_length, + 'syllables_per_word': syllables / max(len(words), 1) + } + + def _analyze_hierarchy(self, headings: List[Dict[str, Any]]) -> Dict[str, Any]: + """Analyze content hierarchy.""" + # Group headings by level + heading_levels = {} + for heading in headings: + level = heading['level'] + if level not in heading_levels: + heading_levels[level] = [] + heading_levels[level].append(heading) + + # Calculate hierarchy metrics + total_headings = len(headings) + max_depth = max(int(level[1]) for level in heading_levels.keys()) if heading_levels else 0 + + return { + 'total_headings': total_headings, + 'max_depth': max_depth, + 'heading_distribution': {level: len(headings) for level, headings in heading_levels.items()}, + 'has_proper_hierarchy': self._check_proper_hierarchy(heading_levels) + } + + def _check_proper_hierarchy(self, heading_levels: Dict[str, List[Dict[str, Any]]]) -> bool: + """Check if headings follow proper hierarchy.""" + if not heading_levels: + return False + + # Check if h1 exists + if 'h1' not in heading_levels: + return False + + # Check if h1 is unique + if len(heading_levels['h1']) > 1: + return False + + # Check if levels are sequential + levels = sorted(int(level[1]) for level in heading_levels.keys()) + return all(levels[i] - levels[i-1] <= 1 for i in range(1, len(levels))) + + def _count_syllables(self, word: str) -> int: + """Count syllables in a word.""" + word = word.lower() + count = 0 + vowels = 'aeiouy' + word = word.lower() + if word[0] in vowels: + count += 1 + for index in range(1, len(word)): + if word[index] in vowels and word[index - 1] not in vowels: + count += 1 + if word.endswith('e'): + count -= 1 + if count == 0: + count += 1 + return count + + def _get_element_position(self, element) -> Dict[str, int]: + """Get element position in the document.""" + try: + return { + 'top': element.sourceline, + 'left': element.sourcepos + } + except: + return { + 'top': 0, + 'left': 0 + } \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/utils/data_collector.py b/lib/ai_seo_tools/content_gap_analysis/utils/data_collector.py new file mode 100644 index 00000000..5d302cdb --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/utils/data_collector.py @@ -0,0 +1,112 @@ +""" +Data collector utility for content gap analysis. +""" + +import requests +from bs4 import BeautifulSoup +from typing import Dict, Any + +class DataCollector: + """ + Collects and processes website data for analysis. + """ + + def __init__(self): + """Initialize the data collector.""" + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + + def collect(self, url: str) -> Dict[str, Any]: + """ + Collect website data for analysis. + + Args: + url (str): The URL to collect data from + + Returns: + dict: Collected website data + """ + try: + # Fetch webpage content + response = requests.get(url, headers=self.headers) + response.raise_for_status() + + # Parse HTML content + soup = BeautifulSoup(response.text, 'html.parser') + + # Extract relevant data + data = { + 'url': url, + 'title': self._extract_title(soup), + 'meta_description': self._extract_meta_description(soup), + 'headings': self._extract_headings(soup), + 'content': self._extract_content(soup), + 'links': self._extract_links(soup), + 'images': self._extract_images(soup) + } + + return data + + except Exception as e: + return { + 'error': str(e), + 'url': url + } + + def _extract_title(self, soup: BeautifulSoup) -> str: + """Extract page title.""" + title = soup.find('title') + return title.text if title else '' + + def _extract_meta_description(self, soup: BeautifulSoup) -> str: + """Extract meta description.""" + meta = soup.find('meta', attrs={'name': 'description'}) + return meta.get('content', '') if meta else '' + + def _extract_headings(self, soup: BeautifulSoup) -> Dict[str, list]: + """Extract all headings.""" + headings = {} + for i in range(1, 7): + tags = soup.find_all(f'h{i}') + headings[f'h{i}'] = [tag.text.strip() for tag in tags] + return headings + + def _extract_content(self, soup: BeautifulSoup) -> str: + """Extract main content.""" + # Remove script and style elements + for script in soup(['script', 'style']): + script.decompose() + + # Get text content + text = soup.get_text() + + # Clean up text + lines = (line.strip() for line in text.splitlines()) + chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) + text = ' '.join(chunk for chunk in chunks if chunk) + + return text + + def _extract_links(self, soup: BeautifulSoup) -> list: + """Extract all links.""" + links = [] + for link in soup.find_all('a'): + href = link.get('href') + if href: + links.append({ + 'url': href, + 'text': link.text.strip() + }) + return links + + def _extract_images(self, soup: BeautifulSoup) -> list: + """Extract all images.""" + images = [] + for img in soup.find_all('img'): + images.append({ + 'src': img.get('src', ''), + 'alt': img.get('alt', ''), + 'title': img.get('title', '') + }) + return images \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/utils/seo_analyzer.py b/lib/ai_seo_tools/content_gap_analysis/utils/seo_analyzer.py new file mode 100644 index 00000000..df4e8e6b --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/utils/seo_analyzer.py @@ -0,0 +1,237 @@ +""" +SEO analyzer utility for content gap analysis. +""" + +import requests +from bs4 import BeautifulSoup +from urllib.parse import urlparse, urljoin +import re +from typing import Dict, Any, List, Optional +from ....utils.website_analyzer.analyzer import WebsiteAnalyzer + +def analyze_onpage_seo(url: str) -> Dict[str, Any]: + """ + Analyze on-page SEO elements of a website. + + Args: + url: The URL to analyze + + Returns: + Dictionary containing SEO analysis results + """ + try: + # Use the combined website analyzer + analyzer = WebsiteAnalyzer() + analysis = analyzer.analyze_website(url) + + if not analysis.get('success', False): + return { + 'error': analysis.get('error', 'Unknown error in SEO analysis'), + 'meta_title': '', + 'meta_description': '', + 'has_robots_txt': False, + 'has_sitemap': False, + 'mobile_friendly': False, + 'load_time': 0 + } + + # Extract relevant information from the analysis + seo_info = analysis['data']['analysis']['seo_info'] + basic_info = analysis['data']['analysis']['basic_info'] + performance = analysis['data']['analysis']['performance'] + + return { + 'meta_tags': seo_info.get('meta_tags', {}), + 'content': seo_info.get('content', {}), + 'meta_title': basic_info.get('title', ''), + 'meta_description': basic_info.get('meta_description', ''), + 'has_robots_txt': bool(basic_info.get('robots_txt')), + 'has_sitemap': bool(basic_info.get('sitemap')), + 'mobile_friendly': True, # This would need to be implemented separately + 'load_time': performance.get('load_time', 0) + } + except Exception as e: + return { + 'error': str(e), + 'meta_title': '', + 'meta_description': '', + 'has_robots_txt': False, + 'has_sitemap': False, + 'mobile_friendly': False, + 'load_time': 0 + } + +def _analyze_meta_tags(soup: BeautifulSoup) -> Dict[str, Any]: + """Analyze meta tags of the webpage.""" + meta_tags = {} + + # Title tag + title_tag = soup.find('title') + if title_tag: + meta_tags['title'] = title_tag.string.strip() + + # Meta description + meta_desc = soup.find('meta', {'name': 'description'}) + if meta_desc: + meta_tags['description'] = meta_desc.get('content', '').strip() + + # Meta keywords + meta_keywords = soup.find('meta', {'name': 'keywords'}) + if meta_keywords: + meta_tags['keywords'] = meta_keywords.get('content', '').strip() + + # Open Graph tags + og_tags = {} + for tag in soup.find_all('meta', property=re.compile(r'^og:')): + og_tags[tag['property']] = tag.get('content', '') + meta_tags['og_tags'] = og_tags + + # Twitter Card tags + twitter_tags = {} + for tag in soup.find_all('meta', name=re.compile(r'^twitter:')): + twitter_tags[tag['name']] = tag.get('content', '') + meta_tags['twitter_tags'] = twitter_tags + + return meta_tags + +def _analyze_headings(soup: BeautifulSoup) -> Dict[str, Any]: + """Analyze heading structure of the webpage.""" + headings = { + 'h1': [], + 'h2': [], + 'h3': [], + 'h4': [], + 'h5': [], + 'h6': [] + } + + for tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']: + for heading in soup.find_all(tag): + headings[tag].append(heading.get_text().strip()) + + return headings + +def _analyze_content(soup: BeautifulSoup) -> Dict[str, Any]: + """Analyze main content of the webpage.""" + # Find main content + main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=re.compile(r'content|main|article')) + + if not main_content: + return { + 'word_count': 0, + 'paragraph_count': 0, + 'content': '' + } + + # Get text content + content = main_content.get_text() + + # Count words and paragraphs + words = content.split() + paragraphs = main_content.find_all('p') + + return { + 'word_count': len(words), + 'paragraph_count': len(paragraphs), + 'content': content + } + +def _analyze_links(soup: BeautifulSoup, base_url: str) -> Dict[str, Any]: + """Analyze links on the webpage.""" + links = { + 'internal': [], + 'external': [], + 'broken': [] + } + + base_domain = urlparse(base_url).netloc + + for link in soup.find_all('a', href=True): + href = link['href'] + + # Handle relative URLs + if not href.startswith(('http://', 'https://')): + href = urljoin(base_url, href) + + # Categorize link + if urlparse(href).netloc == base_domain: + links['internal'].append({ + 'url': href, + 'text': link.get_text().strip(), + 'title': link.get('title', '') + }) + else: + links['external'].append({ + 'url': href, + 'text': link.get_text().strip(), + 'title': link.get('title', '') + }) + + return links + +def _analyze_images(soup: BeautifulSoup) -> Dict[str, Any]: + """Analyze images on the webpage.""" + images = [] + + for img in soup.find_all('img'): + image_data = { + 'src': img.get('src', ''), + 'alt': img.get('alt', ''), + 'title': img.get('title', ''), + 'width': img.get('width', ''), + 'height': img.get('height', ''), + 'has_alt': bool(img.get('alt')), + 'has_title': bool(img.get('title')), + 'has_dimensions': bool(img.get('width') and img.get('height')) + } + images.append(image_data) + + return { + 'total': len(images), + 'with_alt': sum(1 for img in images if img['has_alt']), + 'with_title': sum(1 for img in images if img['has_title']), + 'with_dimensions': sum(1 for img in images if img['has_dimensions']), + 'images': images + } + +def _check_technical_elements(soup: BeautifulSoup, url: str) -> Dict[str, Any]: + """Check technical SEO elements.""" + base_url = urlparse(url) + domain = base_url.netloc + + # Check robots.txt + robots_url = f"{base_url.scheme}://{domain}/robots.txt" + try: + robots_response = requests.get(robots_url, timeout=5) + has_robots_txt = robots_response.status_code == 200 + except: + has_robots_txt = False + + # Check sitemap + sitemap_url = f"{base_url.scheme}://{domain}/sitemap.xml" + try: + sitemap_response = requests.get(sitemap_url, timeout=5) + has_sitemap = sitemap_response.status_code == 200 + except: + has_sitemap = False + + # Check mobile friendliness + viewport = soup.find('meta', {'name': 'viewport'}) + has_viewport = bool(viewport) + + # Check canonical URL + canonical = soup.find('link', {'rel': 'canonical'}) + has_canonical = bool(canonical) + + # Check language + html_lang = soup.find('html').get('lang', '') + has_language = bool(html_lang) + + return { + 'has_robots_txt': has_robots_txt, + 'has_sitemap': has_sitemap, + 'mobile_friendly': has_viewport, + 'has_canonical': has_canonical, + 'has_language': has_language, + 'language': html_lang + } \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/utils/storage.py b/lib/ai_seo_tools/content_gap_analysis/utils/storage.py new file mode 100644 index 00000000..bc05cc8c --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/utils/storage.py @@ -0,0 +1,270 @@ +""" +Storage module for content gap analysis results. +""" + +from typing import Dict, Any, List, Optional +from datetime import datetime +from sqlalchemy.orm import Session +from sqlalchemy.exc import SQLAlchemyError +import streamlit as st + +class ContentGapAnalysisStorage: + """Handles storage and retrieval of content gap analysis results.""" + + def __init__(self, db_session: Session): + """Initialize the storage handler.""" + self.db = db_session + + def save_analysis(self, user_id: int, website_url: str, industry: str, results: Dict[str, Any]) -> Optional[int]: + """ + Save content gap analysis results. + + Args: + user_id: User ID + website_url: Target website URL + industry: Industry category + results: Analysis results dictionary + + Returns: + Analysis ID if successful, None otherwise + """ + try: + # Create main analysis record + analysis = ContentGapAnalysis( + user_id=user_id, + website_url=website_url, + industry=industry, + status='completed', + metadata={'version': '1.0'} + ) + self.db.add(analysis) + self.db.flush() # Get the ID without committing + + # Save website analysis + website_analysis = WebsiteAnalysis( + content_gap_analysis_id=analysis.id, + content_score=results.get('website', {}).get('content_score', 0), + seo_score=results.get('website', {}).get('seo_score', 0), + structure_score=results.get('website', {}).get('structure_score', 0), + content_metrics=results.get('website', {}).get('content_metrics', {}), + seo_metrics=results.get('website', {}).get('seo_metrics', {}), + technical_metrics=results.get('website', {}).get('technical_metrics', {}), + ai_insights=results.get('website', {}).get('ai_insights', {}) + ) + self.db.add(website_analysis) + + # Save competitor analysis if available + if 'competitors' in results: + for competitor in results['competitors']: + competitor_analysis = CompetitorAnalysis( + content_gap_analysis_id=analysis.id, + competitor_url=competitor.get('url'), + market_position=competitor.get('market_position', {}), + content_gaps=competitor.get('content_gaps', []), + competitive_advantages=competitor.get('competitive_advantages', []), + trend_analysis=competitor.get('trend_analysis', {}) + ) + self.db.add(competitor_analysis) + + # Save keyword analysis + keyword_analysis = KeywordAnalysis( + content_gap_analysis_id=analysis.id, + top_keywords=results.get('keywords', {}).get('top_keywords', []), + search_intent=results.get('keywords', {}).get('search_intent', {}), + opportunities=results.get('keywords', {}).get('opportunities', []), + trend_analysis=results.get('keywords', {}).get('trend_analysis', {}) + ) + self.db.add(keyword_analysis) + + # Save recommendations + for recommendation in results.get('recommendations', []): + content_recommendation = ContentRecommendation( + content_gap_analysis_id=analysis.id, + recommendation_type=recommendation.get('type'), + priority_score=recommendation.get('priority_score', 0), + recommendation=recommendation.get('recommendation', ''), + implementation_steps=recommendation.get('implementation_steps', []), + expected_impact=recommendation.get('expected_impact', {}), + status='pending' + ) + self.db.add(content_recommendation) + + # Save analysis history + history = AnalysisHistory( + content_gap_analysis_id=analysis.id, + status='completed', + metrics={'duration': results.get('duration', 0)} + ) + self.db.add(history) + + # Commit all changes + self.db.commit() + return analysis.id + + except SQLAlchemyError as e: + self.db.rollback() + st.error(f"Error saving analysis results: {str(e)}") + return None + + def get_analysis(self, analysis_id: int) -> Optional[Dict[str, Any]]: + """ + Retrieve content gap analysis results. + + Args: + analysis_id: Analysis ID + + Returns: + Dictionary containing analysis results if found, None otherwise + """ + try: + analysis = self.db.query(ContentGapAnalysis).get(analysis_id) + if not analysis: + return None + + # Get website analysis + website_analysis = self.db.query(WebsiteAnalysis).filter_by( + content_gap_analysis_id=analysis_id + ).first() + + # Get competitor analysis + competitor_analyses = self.db.query(CompetitorAnalysis).filter_by( + content_gap_analysis_id=analysis_id + ).all() + + # Get keyword analysis + keyword_analysis = self.db.query(KeywordAnalysis).filter_by( + content_gap_analysis_id=analysis_id + ).first() + + # Get recommendations + recommendations = self.db.query(ContentRecommendation).filter_by( + content_gap_analysis_id=analysis_id + ).all() + + # Get analysis history + history = self.db.query(AnalysisHistory).filter_by( + content_gap_analysis_id=analysis_id + ).order_by(AnalysisHistory.run_date.desc()).all() + + return { + 'id': analysis.id, + 'website_url': analysis.website_url, + 'industry': analysis.industry, + 'analysis_date': analysis.analysis_date, + 'status': analysis.status, + 'website': { + 'content_score': website_analysis.content_score, + 'seo_score': website_analysis.seo_score, + 'structure_score': website_analysis.structure_score, + 'content_metrics': website_analysis.content_metrics, + 'seo_metrics': website_analysis.seo_metrics, + 'technical_metrics': website_analysis.technical_metrics, + 'ai_insights': website_analysis.ai_insights + } if website_analysis else {}, + 'competitors': [{ + 'url': ca.competitor_url, + 'market_position': ca.market_position, + 'content_gaps': ca.content_gaps, + 'competitive_advantages': ca.competitive_advantages, + 'trend_analysis': ca.trend_analysis + } for ca in competitor_analyses], + 'keywords': { + 'top_keywords': keyword_analysis.top_keywords, + 'search_intent': keyword_analysis.search_intent, + 'opportunities': keyword_analysis.opportunities, + 'trend_analysis': keyword_analysis.trend_analysis + } if keyword_analysis else {}, + 'recommendations': [{ + 'type': r.recommendation_type, + 'priority_score': r.priority_score, + 'recommendation': r.recommendation, + 'implementation_steps': r.implementation_steps, + 'expected_impact': r.expected_impact, + 'status': r.status + } for r in recommendations], + 'history': [{ + 'run_date': h.run_date, + 'status': h.status, + 'metrics': h.metrics, + 'error_log': h.error_log + } for h in history] + } + + except SQLAlchemyError as e: + st.error(f"Error retrieving analysis results: {str(e)}") + return None + + def get_user_analyses(self, user_id: int) -> List[Dict[str, Any]]: + """ + Get all analyses for a user. + + Args: + user_id: User ID + + Returns: + List of analysis summaries + """ + try: + analyses = self.db.query(ContentGapAnalysis).filter_by( + user_id=user_id + ).order_by(ContentGapAnalysis.analysis_date.desc()).all() + + return [{ + 'id': analysis.id, + 'website_url': analysis.website_url, + 'industry': analysis.industry, + 'analysis_date': analysis.analysis_date, + 'status': analysis.status + } for analysis in analyses] + + except SQLAlchemyError as e: + st.error(f"Error retrieving user analyses: {str(e)}") + return [] + + def update_recommendation_status(self, recommendation_id: int, status: str) -> bool: + """ + Update the status of a recommendation. + + Args: + recommendation_id: Recommendation ID + status: New status + + Returns: + True if successful, False otherwise + """ + try: + recommendation = self.db.query(ContentRecommendation).get(recommendation_id) + if recommendation: + recommendation.status = status + recommendation.updated_at = datetime.utcnow() + self.db.commit() + return True + return False + + except SQLAlchemyError as e: + self.db.rollback() + st.error(f"Error updating recommendation status: {str(e)}") + return False + + def delete_analysis(self, analysis_id: int) -> bool: + """ + Delete an analysis and all related data. + + Args: + analysis_id: Analysis ID + + Returns: + True if successful, False otherwise + """ + try: + analysis = self.db.query(ContentGapAnalysis).get(analysis_id) + if analysis: + self.db.delete(analysis) + self.db.commit() + return True + return False + + except SQLAlchemyError as e: + self.db.rollback() + st.error(f"Error deleting analysis: {str(e)}") + return False \ No newline at end of file diff --git a/lib/ai_seo_tools/content_gap_analysis/website_analyzer.py b/lib/ai_seo_tools/content_gap_analysis/website_analyzer.py new file mode 100644 index 00000000..8b194bd9 --- /dev/null +++ b/lib/ai_seo_tools/content_gap_analysis/website_analyzer.py @@ -0,0 +1,291 @@ +"""Website analyzer module for content gap analysis.""" + +import streamlit as st +from loguru import logger +from typing import Dict, Any, List, Optional +import asyncio +import sys +import os +import json +from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer as BaseWebsiteAnalyzer +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/content_gap_website_analyzer.log", + rotation="50 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}" +) + +# Ensure logs directory exists +os.makedirs("logs", exist_ok=True) + +class WebsiteAnalyzer(BaseWebsiteAnalyzer): + """Extended website analyzer for content gap analysis.""" + + def __init__(self): + """Initialize the website analyzer.""" + super().__init__() + logger.info("ContentGapWebsiteAnalyzer initialized") + + def analyze_content_gaps(self, url: str, competitor_urls: List[str]) -> Dict[str, Any]: + """ + Analyze content gaps between the target website and competitors. + + Args: + url: The target URL to analyze + competitor_urls: List of competitor URLs to compare against + + Returns: + Dictionary containing content gap analysis results + """ + try: + # Analyze target website + target_analysis = self.analyze_website(url) + if not target_analysis.get('success', False): + return { + 'error': target_analysis.get('error', 'Unknown error in target analysis'), + 'gaps': [], + 'recommendations': [] + } + + # Analyze competitor websites + competitor_analyses = [] + for competitor_url in competitor_urls: + analysis = self.analyze_website(competitor_url) + if analysis.get('success', False): + competitor_analyses.append(analysis['data']) + + # Generate content gap analysis using AI + prompt = f"""Analyze content gaps between the target website and competitors: + + Target Website: + {json.dumps(target_analysis['data'], indent=2)} + + Competitor Websites: + {json.dumps(competitor_analyses, indent=2)} + + Identify: + 1. Missing content topics + 2. Content depth differences + 3. Keyword gaps + 4. Content structure improvements + 5. Content quality recommendations + + Format the response as JSON with 'gaps' and 'recommendations' keys.""" + + # Get AI analysis + analysis = llm_text_gen( + prompt=prompt, + system_prompt="You are an SEO expert specializing in content gap analysis.", + response_format="json_object" + ) + + if not analysis: + return { + 'error': 'Failed to generate content gap analysis', + 'gaps': [], + 'recommendations': [] + } + + return { + 'gaps': analysis.get('gaps', []), + 'recommendations': analysis.get('recommendations', []) + } + + except Exception as e: + error_msg = f"Error analyzing content gaps: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + 'error': error_msg, + 'gaps': [], + 'recommendations': [] + } + + def analyze(self, url: str) -> Dict[str, Any]: + """ + Analyze a website for content gaps and SEO opportunities. + + Args: + url: The URL to analyze + + Returns: + Dictionary containing analysis results + """ + try: + # Initialize progress tracking + progress = { + 'status': 'in_progress', + 'current_stage': 'content_analysis', + 'current_step': 'Initializing analysis', + 'progress': 0, + 'details': 'Starting website analysis...' + } + self.progress.update(progress) + + # Get base website analysis + logger.info("Starting base website analysis") + website_analysis = self.analyze_website(url) + + if not website_analysis.get('success', False): + error_msg = website_analysis.get('error', 'Unknown error in website analysis') + logger.error(f"Error in website analysis: {error_msg}") + progress['status'] = 'error' + progress['details'] = error_msg + self.progress.update(progress) + return { + 'error': error_msg, + 'error_details': website_analysis.get('error_details', {}), + 'progress': progress + } + + # Extract SEO metrics from the analysis + seo_metrics = self._extract_seo_metrics(website_analysis['data']) + + # Extract performance metrics + performance_metrics = self._extract_performance_metrics(website_analysis['data']) + + # Update progress + progress['status'] = 'completed' + progress['progress'] = 100 + progress['details'] = 'Analysis completed successfully' + self.progress.update(progress) + + return { + 'success': True, + 'data': { + 'seo_metrics': seo_metrics, + 'performance_metrics': performance_metrics, + 'website_analysis': website_analysis['data'] + }, + 'progress': progress + } + + except Exception as e: + error_msg = f"Error in content gap analysis: {str(e)}" + logger.error(error_msg, exc_info=True) + progress['status'] = 'error' + progress['details'] = error_msg + self.progress.update(progress) + return { + 'error': error_msg, + 'error_details': { + 'type': type(e).__name__, + 'traceback': str(e.__traceback__) + }, + 'progress': progress + } + + def _extract_seo_metrics(self, website_analysis: Dict[str, Any]) -> Dict[str, Any]: + """Extract SEO-related metrics from website analysis.""" + try: + seo_info = website_analysis.get('analysis', {}).get('seo_info', {}) + return { + 'overall_score': seo_info.get('overall_score', 0), + 'meta_tags': { + 'title': seo_info.get('meta_tags', {}).get('title', {}), + 'description': seo_info.get('meta_tags', {}).get('description', {}), + 'keywords': seo_info.get('meta_tags', {}).get('keywords', {}) + }, + 'content': { + 'word_count': seo_info.get('content', {}).get('word_count', 0), + 'readability_score': seo_info.get('content', {}).get('readability_score', 0), + 'content_quality_score': seo_info.get('content', {}).get('content_quality_score', 0) + } + } + except Exception as e: + logger.error(f"Error extracting SEO metrics: {str(e)}", exc_info=True) + return {} + + def _extract_performance_metrics(self, website_analysis: Dict[str, Any]) -> Dict[str, Any]: + """Extract performance metrics from website analysis.""" + try: + performance_info = website_analysis.get('analysis', {}).get('performance', {}) + return { + 'load_time': performance_info.get('load_time', 0), + 'page_size': performance_info.get('page_size', 0), + 'resource_count': performance_info.get('resource_count', 0), + 'performance_score': performance_info.get('performance_score', 0) + } + except Exception as e: + logger.error(f"Error extracting performance metrics: {str(e)}", exc_info=True) + return {} + + def _extract_content_metrics(self, website_analysis: Dict[str, Any]) -> Dict[str, Any]: + """Extract content-related metrics from website analysis.""" + try: + content_info = website_analysis['analysis']['content_info'] + return { + 'word_count': content_info.get('word_count', 0), + 'heading_count': content_info.get('heading_count', 0), + 'image_count': content_info.get('image_count', 0), + 'link_count': content_info.get('link_count', 0), + 'has_meta_description': content_info.get('has_meta_description', False), + 'has_robots_txt': content_info.get('has_robots_txt', False), + 'has_sitemap': content_info.get('has_sitemap', False) + } + except Exception as e: + logger.error(f"Error extracting content metrics: {str(e)}", exc_info=True) + return {} + + def _extract_technical_info(self, website_analysis: Dict[str, Any]) -> Dict[str, Any]: + """Extract technical information from website analysis.""" + try: + basic_info = website_analysis.get('analysis', {}).get('basic_info', {}) + return { + 'title': basic_info.get('title', ''), + 'meta_description': basic_info.get('meta_description', ''), + 'headers': basic_info.get('headers', {}), + 'robots_txt': basic_info.get('robots_txt', ''), + 'sitemap': basic_info.get('sitemap', ''), + 'server_info': basic_info.get('server_info', {}), + 'security_info': basic_info.get('security_info', {}) + } + except Exception as e: + logger.error(f"Error extracting technical info: {str(e)}", exc_info=True) + return {} + + def _generate_insights(self, content_metrics: Dict[str, Any], seo_metrics: Dict[str, Any]) -> List[str]: + """Generate content insights based on analysis results.""" + try: + insights = [] + + # Content insights + if content_metrics['word_count'] < 300: + insights.append("Content length is below recommended minimum (300 words)") + elif content_metrics['word_count'] > 2000: + insights.append("Content length is above recommended maximum (2000 words)") + + if content_metrics['heading_count'] < 2: + insights.append("Content structure could be improved with more headings") + + if content_metrics['image_count'] == 0: + insights.append("Consider adding images to improve content engagement") + + # SEO insights + if seo_metrics.get('overall_score', 0) < 60: + insights.append("SEO optimization needs significant improvement") + elif seo_metrics.get('overall_score', 0) < 80: + insights.append("SEO optimization has room for improvement") + + if not content_metrics['has_meta_description']: + insights.append("Missing meta description - important for SEO") + + if not content_metrics['has_robots_txt']: + insights.append("Missing robots.txt - important for search engine crawling") + + if not content_metrics['has_sitemap']: + insights.append("Missing sitemap.xml - important for search engine indexing") + + return insights + except Exception as e: + logger.error(f"Error generating insights: {str(e)}", exc_info=True) + return [] \ No newline at end of file diff --git a/lib/ai_seo_tools/content_title_generator.py b/lib/ai_seo_tools/content_title_generator.py index 26dad8b9..ee1ef0a0 100644 --- a/lib/ai_seo_tools/content_title_generator.py +++ b/lib/ai_seo_tools/content_title_generator.py @@ -1,3 +1,5 @@ +"""Content title generator module.""" + import os import json import streamlit as st @@ -6,70 +8,106 @@ from tenacity import ( stop_after_attempt, wait_random_exponential, ) -from ..gpt_providers.text_generation.main_text_generation import llm_text_gen +from loguru import logger +from typing import Dict, Any, List, Optional +import asyncio +import sys +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen +from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer -def ai_title_generator(): - """ UI for the AI Blog Title Generator """ - st.title("✍️ Alwrity - AI Blog Title Generator") +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/content_title_generator.log", + rotation="50 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}" +) - # Input section - with st.expander("**PRO-TIP** - Follow the steps below for best results.", expanded=True): - col1, col2 = st.columns([5, 5]) - - with col1: - input_blog_keywords = st.text_input( - '**🔑 Enter main keywords of your blog!**', - placeholder="e.g., AI tools, digital marketing, SEO", - help="Use 2-3 words that best describe the main topic of your blog." - ) - input_blog_content = st.text_area( - '**📄 Copy/Paste your entire blog content.** (Optional)', - placeholder="e.g., Content about the importance of AI in digital marketing...", - help="Paste your full blog content here for more accurate title suggestions. This is optional." - ) - - with col2: - input_title_type = st.selectbox( - '📝 Blog Type', - ('General', 'How-to Guides', 'Tutorials', 'Listicles', 'Newsworthy Posts', 'FAQs', 'Checklists/Cheat Sheets'), - index=0 - ) - input_title_intent = st.selectbox( - '🔍 Search Intent', - ('Informational Intent', 'Commercial Intent', 'Transactional Intent', 'Navigational Intent'), - index=0 - ) - language_options = ["English", "Spanish", "French", "German", "Chinese", "Japanese", "Other"] - input_language = st.selectbox( - '🌐 Select Language', - options=language_options, - index=0, - help="Choose the language for your blog title." - ) - if input_language == "Other": - input_language = st.text_input( - 'Specify Language', - placeholder="e.g., Italian, Dutch", - help="Specify your preferred language." - ) - - # Generate Blog Title button - if st.button('**Generate Blog Titles**'): - with st.spinner("Generating blog titles..."): - if input_blog_content == 'Optional': - input_blog_content = None - - if not input_blog_keywords and not input_blog_content: - st.error('**🫣 Provide Inputs to generate Blog Titles. Either Blog Keywords OR content is required!**') - else: - blog_titles = generate_blog_titles(input_blog_keywords, input_blog_content, input_title_type, input_title_intent, input_language) - if blog_titles: - st.subheader('**👩🧕🔬 Go Rule search ranking with these Blog Titles!**') - with st.expander("**Final - Blog Titles Output 🎆🎇🎇**", expanded=True): - st.markdown(blog_titles) - else: - st.error("💥 **Failed to generate blog titles. Please try again!**") +# Ensure logs directory exists +os.makedirs("logs", exist_ok=True) +def ai_title_generator(url: str) -> Dict[str, Any]: + """ + Generate SEO-optimized titles using AI. + + Args: + url: The URL to analyze + + Returns: + Dictionary containing title suggestions and analysis + """ + try: + # Initialize analyzer + analyzer = WebsiteAnalyzer() + + # Analyze website + analysis = analyzer.analyze_website(url) + if not analysis.get('success', False): + return { + 'error': analysis.get('error', 'Unknown error in analysis'), + 'patterns': {}, + 'suggestions': [] + } + + # Extract content and meta information + content_info = analysis['data']['analysis']['content_info'] + seo_info = analysis['data']['analysis']['seo_info'] + + # Generate title suggestions using AI + prompt = f"""Based on the following website content and SEO analysis, generate 5 SEO-optimized title suggestions: + + Content Analysis: + - Word Count: {content_info.get('word_count', 0)} + - Heading Structure: {content_info.get('heading_structure', {})} + + SEO Analysis: + - Meta Title: {seo_info.get('meta_tags', {}).get('title', {}).get('value', '')} + - Meta Description: {seo_info.get('meta_tags', {}).get('description', {}).get('value', '')} + - Keywords: {seo_info.get('meta_tags', {}).get('keywords', {}).get('value', '')} + + Generate 5 title suggestions that are: + 1. SEO-optimized + 2. Engaging and click-worthy + 3. Between 50-60 characters + 4. Include relevant keywords + 5. Follow best practices for title optimization + + Format the response as JSON with 'suggestions' and 'patterns' keys.""" + + # Get AI suggestions + suggestions = llm_text_gen( + prompt=prompt, + system_prompt="You are an SEO expert specializing in title optimization.", + response_format="json_object" + ) + + if not suggestions: + return { + 'error': 'Failed to generate title suggestions', + 'patterns': {}, + 'suggestions': [] + } + + return { + 'patterns': suggestions.get('patterns', {}), + 'suggestions': suggestions.get('suggestions', []) + } + + except Exception as e: + error_msg = f"Error generating title suggestions: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + 'error': error_msg, + 'patterns': {}, + 'suggestions': [] + } @retry(stop=stop_after_attempt(3), wait=wait_random_exponential(min=1, max=4)) def generate_blog_titles(input_blog_keywords, input_blog_content, input_title_type, input_title_intent, input_language): diff --git a/lib/ai_writers/ai_facebook_writer/facebook_ai_writer.py b/lib/ai_writers/ai_facebook_writer/facebook_ai_writer.py index c0cd6d51..1982b7ff 100644 --- a/lib/ai_writers/ai_facebook_writer/facebook_ai_writer.py +++ b/lib/ai_writers/ai_facebook_writer/facebook_ai_writer.py @@ -194,7 +194,7 @@ def facebook_main_menu(): if st.session_state.selected_tool is not None: with tool_container: # Add a back button at the top - if st.button("← Back to Dashboard", key="back_to_dashboard"): + if st.button("← Back to Dashboard", key="back_to_facebook_dashboard"): st.session_state.selected_tool = None st.rerun() diff --git a/lib/ai_writers/ai_finance_report_generator/README.md b/lib/ai_writers/ai_finance_report_generator/README.md new file mode 100644 index 00000000..554e9ba2 --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/README.md @@ -0,0 +1,190 @@ +# AI Finance Report Generator + +An advanced AI-powered financial analysis and report generation system that combines data collection, technical analysis, visualization, and automated report generation. + +## Project Structure + +``` +ai_finance_report_generator/ +├── ai_financial_dashboard.py # Main dashboard interface +├── utils/ # Utility functions +│ ├── __init__.py +│ └── storage.py # Data persistence +├── reports/ # Report generation modules +│ ├── technical_analysis/ # Technical analysis reports +│ ├── fundamental_analysis/ # Fundamental analysis reports +│ ├── options_analysis/ # Options analysis reports +│ ├── portfolio_analysis/ # Portfolio analysis reports +│ ├── market_research/ # Market research reports +│ └── news_analysis/ # News analysis reports +└── README.md # This file +``` + +## Features + +### Current Features +- Unified dashboard interface for all financial analysis tools +- Technical Analysis report generation +- Options analysis report generation +- User preferences management +- Recent reports tracking +- Data persistence with JSON storage +- Financial data collection from various sources +- Integration with LLM for report generation + +### Planned Features + +#### 1. Data Collection Module +- Web scraping for financial news and data +- API integrations (Yahoo Finance, Alpha Vantage, Financial Modeling Prep) +- Real-time market data collection +- Historical data retrieval +- Company financial statements +- Market sentiment data +- Economic indicators +- Sector analysis data + +#### 2. Technical Analysis Module +- Moving averages (SMA, EMA, WMA) +- RSI, MACD, Bollinger Bands +- Volume analysis +- Support/Resistance levels +- Trend analysis +- Pattern recognition +- Fibonacci retracements +- Momentum indicators + +#### 3. Fundamental Analysis Module +- Financial ratios calculation +- Company valuation metrics +- Growth analysis +- Profitability analysis +- Debt analysis +- Cash flow analysis +- Industry comparison +- Peer analysis + +#### 4. Data Visualization Module +- Candlestick charts +- Technical indicator overlays +- Volume charts +- Price action patterns +- Correlation matrices +- Heat maps +- Interactive charts +- Custom chart templates + +#### 5. Report Generation Module +- Technical analysis reports +- Fundamental analysis reports +- Market research reports +- Investment recommendations +- Risk assessment reports +- Sector analysis reports +- News impact analysis +- Custom report templates + +#### 6. News and Sentiment Analysis Module +- News aggregation +- Sentiment scoring +- Social media analysis +- Market sentiment indicators +- News impact analysis +- Event correlation +- Trend detection +- Sentiment visualization + +#### 7. Portfolio Analysis Module +- Portfolio performance analysis +- Risk assessment +- Asset allocation +- Correlation analysis +- Diversification metrics +- Performance attribution +- Portfolio optimization +- Rebalancing suggestions + +## Usage + +### Basic Usage + +```python +from lib.ai_writers.ai_finance_report_generator.ai_financial_dashboard import get_dashboard + +# Get dashboard instance +dashboard = get_dashboard() + +# Generate technical analysis report +ta_report = dashboard.generate_technical_analysis("AAPL") + +# Generate options analysis report +options_report = dashboard.generate_options_analysis("AAPL") + +# Get recent reports +recent_reports = dashboard.get_recent_reports() +``` + +### User Preferences + +```python +# Update user preferences +dashboard.update_preferences({ + "report_format": "markdown", + "include_charts": True, + "chart_style": "dark", + "language": "en" +}) + +# Get current preferences +preferences = dashboard.get_preferences() +``` + +### Portfolio Analysis + +```python +# Create portfolio +portfolio = [ + {"symbol": "AAPL", "shares": 100}, + {"symbol": "GOOGL", "shares": 50} +] + +# Generate portfolio report +portfolio_report = dashboard.generate_portfolio_analysis(portfolio) +``` + +## Installation + +```bash +pip install -r requirements.txt +``` + +## Dependencies + +1. **Data Collection** + - `finance_data_researcher` + - `web_scraping_tools` + +2. **Analysis Tools** + - `pandas_ta` + - `numpy` + - `scipy` + +3. **Visualization** + - `matplotlib` + - `plotly` + +4. **Text Generation** + - `llm_text_gen` + - `gpt_providers` + +## Contributing + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/lib/ai_writers/ai_finance_report_generator/ai_financial_dashboard.py b/lib/ai_writers/ai_finance_report_generator/ai_financial_dashboard.py new file mode 100644 index 00000000..b08b706f --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/ai_financial_dashboard.py @@ -0,0 +1,358 @@ +""" +AI Financial Dashboard Module + +This module combines the financial dashboard interface with financial report generation capabilities. +It provides a unified interface for managing financial analysis tools and generating reports. +""" + +import sys +import os +from textwrap import dedent +from pathlib import Path +from datetime import datetime +from typing import Dict, List, Any, Optional, Union + +from loguru import logger +logger.remove() +logger.add(sys.stdout, + colorize=True, + format="{level}|{file}:{line}:{function}| {message}" + ) + +from ...ai_web_researcher.finance_data_researcher import get_finance_data, get_fin_options_data +from ...gpt_providers.text_generation.main_text_generation import llm_text_gen +from .utils import get_feature_status +from .utils.storage import get_storage_manager + +class UserPreferences: + """Class to manage user preferences and settings.""" + + def __init__(self): + self.default_settings = { + "theme": "light", + "currency": "USD", + "timezone": "UTC", + "date_format": "%Y-%m-%d", + "default_symbols": [], + "notifications": True, + "auto_refresh": False, + "refresh_interval": 300, # 5 minutes + "report_format": "markdown", + "include_charts": True, + "chart_style": "default", + "language": "en" + } + self.settings = self.default_settings.copy() + self.storage = get_storage_manager() + self.load_settings() + + def update_setting(self, key: str, value: Any) -> None: + """Update a specific setting.""" + if key in self.default_settings: + self.settings[key] = value + self.save_settings() + + def get_setting(self, key: str) -> Any: + """Get a specific setting value.""" + return self.settings.get(key, self.default_settings.get(key)) + + def reset_settings(self) -> None: + """Reset all settings to default values.""" + self.settings = self.default_settings.copy() + self.save_settings() + + def save_settings(self) -> None: + """Save current settings to storage.""" + self.storage.save_user_preferences(self.settings) + + def load_settings(self) -> None: + """Load settings from storage.""" + stored_settings = self.storage.load_user_preferences() + if stored_settings: + self.settings.update(stored_settings) + +class RecentReport: + """Class to represent a recently generated report.""" + + def __init__(self, report_type: str, symbol: Optional[str], timestamp: datetime, content: Optional[str] = None): + self.report_type = report_type + self.symbol = symbol + self.timestamp = timestamp + self.content = content + self.id = f"{report_type}_{symbol}_{timestamp.strftime('%Y%m%d%H%M%S')}" + + def to_dict(self) -> Dict[str, Any]: + """Convert report to dictionary format.""" + return { + "id": self.id, + "type": self.report_type, + "symbol": self.symbol, + "timestamp": self.timestamp.isoformat(), + "content": self.content + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'RecentReport': + """Create report from dictionary format.""" + return cls( + report_type=data["type"], + symbol=data["symbol"], + timestamp=datetime.fromisoformat(data["timestamp"]), + content=data.get("content") + ) + +class FinancialDashboard: + """Main dashboard class for managing financial analysis tools and generating reports.""" + + def __init__(self): + self.features = { + "technical_analysis": { + "name": "Technical Analysis", + "description": "Generate technical analysis reports with indicators and patterns", + "icon": "📊", + "route": "/technical-analysis", + "category": "analysis", + "dependencies": ["data_collection"], + "version": "1.0.0" + }, + "fundamental_analysis": { + "name": "Fundamental Analysis", + "description": "Analyze company financials and valuation metrics", + "icon": "📈", + "route": "/fundamental-analysis", + "category": "analysis", + "dependencies": ["data_collection"], + "version": "0.1.0" + }, + "options_analysis": { + "name": "Options Analysis", + "description": "Analyze options chains and generate trading strategies", + "icon": "⚡", + "route": "/options-analysis", + "category": "analysis", + "dependencies": ["data_collection", "options_data"], + "version": "1.0.0" + }, + "portfolio_analysis": { + "name": "Portfolio Analysis", + "description": "Analyze portfolio performance and risk metrics", + "icon": "📑", + "route": "/portfolio-analysis", + "category": "portfolio", + "dependencies": ["data_collection", "portfolio_data"], + "version": "0.1.0" + }, + "market_research": { + "name": "Market Research", + "description": "Generate market research reports and sector analysis", + "icon": "🔍", + "route": "/market-research", + "category": "research", + "dependencies": ["data_collection", "news_data"], + "version": "0.1.0" + }, + "news_analysis": { + "name": "News Analysis", + "description": "Analyze news impact and market sentiment", + "icon": "📰", + "route": "/news-analysis", + "category": "research", + "dependencies": ["data_collection", "news_data"], + "version": "0.1.0" + } + } + + self.user_preferences = UserPreferences() + self.storage = get_storage_manager() + self.recent_reports: List[RecentReport] = [] + self.max_recent_reports = 10 + self.load_recent_reports() + + def get_all_features(self) -> List[Dict[str, Any]]: + """Get all available features with their status.""" + features_list = [] + for feature_id, feature_info in self.features.items(): + status = get_feature_status(feature_id) + feature_info.update(status) + features_list.append(feature_info) + return features_list + + def get_feature(self, feature_id: str) -> Dict[str, Any]: + """Get information about a specific feature.""" + if feature_id not in self.features: + raise ValueError(f"Feature {feature_id} not found") + + feature_info = self.features[feature_id].copy() + status = get_feature_status(feature_id) + feature_info.update(status) + return feature_info + + def get_implemented_features(self) -> List[Dict[str, Any]]: + """Get only the implemented features.""" + return [f for f in self.get_all_features() if f["implemented"]] + + def get_coming_soon_features(self) -> List[Dict[str, Any]]: + """Get features that are coming soon.""" + return [f for f in self.get_all_features() if f["coming_soon"]] + + def get_features_by_category(self, category: str) -> List[Dict[str, Any]]: + """Get features filtered by category.""" + return [f for f in self.get_all_features() if f["category"] == category] + + def add_recent_report(self, report_type: str, symbol: Optional[str] = None, content: Optional[str] = None) -> None: + """Add a report to the recent reports list.""" + report = RecentReport(report_type, symbol, datetime.now(), content) + self.recent_reports.insert(0, report) + if len(self.recent_reports) > self.max_recent_reports: + self.recent_reports.pop() + self.save_recent_reports() + + def get_recent_reports(self, limit: Optional[int] = None) -> List[Dict[str, Any]]: + """Get recent reports.""" + reports = self.recent_reports[:limit] if limit else self.recent_reports + return [{ + **r.to_dict(), + "feature_info": self.get_feature(r.report_type) + } for r in reports] + + def save_recent_reports(self) -> None: + """Save recent reports to storage.""" + reports_data = [r.to_dict() for r in self.recent_reports] + self.storage.save_recent_reports(reports_data) + + def load_recent_reports(self) -> None: + """Load recent reports from storage.""" + reports_data = self.storage.load_recent_reports() + self.recent_reports = [RecentReport.from_dict(r) for r in reports_data] + + def get_dashboard_summary(self) -> Dict[str, Any]: + """Get a summary of the dashboard state.""" + return { + "total_features": len(self.features), + "implemented_features": len(self.get_implemented_features()), + "coming_soon_features": len(self.get_coming_soon_features()), + "recent_reports": len(self.recent_reports), + "categories": list(set(f["category"] for f in self.features.values())), + "user_preferences": self.user_preferences.settings + } + + def check_feature_dependencies(self, feature_id: str) -> Dict[str, bool]: + """Check if all dependencies for a feature are met.""" + if feature_id not in self.features: + raise ValueError(f"Feature {feature_id} not found") + + feature = self.features[feature_id] + dependencies = feature.get("dependencies", []) + + return { + dep: get_feature_status(dep)["implemented"] + for dep in dependencies + } + + def backup_data(self, backup_dir: Optional[str] = None) -> None: + """Create a backup of all dashboard data.""" + self.storage.backup_storage(backup_dir) + + def restore_from_backup(self, backup_file: str) -> None: + """Restore dashboard data from a backup file.""" + self.storage.restore_from_backup(backup_file) + self.user_preferences.load_settings() + self.load_recent_reports() + + def generate_technical_analysis(self, symbol: str) -> str: + """Generate a technical analysis report for the given symbol.""" + try: + # Get financial data + symbol_fin_data = get_finance_data(symbol) + + # Generate report + report_content = self._generate_ta_report(symbol_fin_data, symbol) + + # Add to recent reports + self.add_recent_report("technical_analysis", symbol, report_content) + + logger.info(f"Done: Final Technical Analysis for {symbol}") + return report_content + + except Exception as err: + logger.error(f"Error: Failed to generate Technical Analysis report: {err}") + raise + + def generate_options_analysis(self, symbol: str) -> str: + """Generate an options analysis report for the given symbol.""" + try: + # Get options data + options_data = get_fin_options_data(symbol) + + # Generate report + report_content = self._generate_options_report(options_data, symbol) + + # Add to recent reports + self.add_recent_report("options_analysis", symbol, report_content) + + logger.info(f"Done: Options Analysis for {symbol}") + return report_content + + except Exception as err: + logger.error(f"Error: Failed to generate Options Analysis report: {err}") + raise + + def _generate_ta_report(self, last_day_summary: str, symbol: str) -> str: + """Generate technical analysis report using LLM.""" + prompt = f""" + You are a seasoned Technical Analysis (TA) expert, rivaling legends like Charles Dow, John Bollinger, and Alan Andrews. + Your deep understanding of market dynamics, coupled with mastery of technical indicators, + allows you to decipher complex patterns and offer precise predictions. + + Your expertise extends to practical tools like the pandas_ta module, enabling you to extract valuable insights from raw data. + + **Objective:** + Analyze the provided technical indicators for {symbol} on its last trading day and predict its price movement over the next few trading sessions. + + **Instructions:** + 1. **Identify Potential Trading Signals:** Highlight specific indicators suggesting bullish, bearish, or neutral signals. Explain the rationale behind each signal, referencing historical patterns or comparable market scenarios. + 2. **Detect Patterns and Divergences:** Analyze the interplay between different indicators. Detect patterns like moving average crossovers, candlestick formations, or divergences between price action and indicators. Explain the significance of each pattern. + 3. **Price Movement Prediction:** Based on your analysis, provide a clear prediction for {symbol}'s price movement in the next few days. State the expected direction (up, down, sideways) and potential price targets if identifiable. + 4. **Risk Assessment:** Briefly discuss any potential risks or factors that could invalidate your predictions, promoting a balanced and informed perspective. + + **Technical Indicators for {symbol} on the Last Trading Day:** + {last_day_summary} + + Remember, your analysis should be detailed, insightful, and actionable for traders seeking to capitalize on market movements. + """ + + try: + return llm_text_gen(prompt) + except Exception as err: + logger.error(f"Failed to generate TA report: {err}") + raise + + def _generate_options_report(self, results_sentences: List[str], ticker: str) -> str: + """Generate options analysis report using LLM.""" + prompt = f""" + You are a financial expert specializing in options trading and market sentiment analysis. + You have been provided with the following technical analysis of options data for the ticker symbol {ticker} with the nearest expiry date: + + {chr(10).join(results_sentences)} + + Based on this data, provide a comprehensive analysis of the options market for {ticker}. + + Your analysis should include: + + 1. **Implied Volatility Interpretation:** Discuss the significance of the average implied volatility for both call and put options. What does it suggest about market expectations of future price movements? + 2. **Volume and Open Interest Insights:** Analyze the volume and open interest for call and put options. What does this data reveal about current market positioning and potential future trading activity? + 3. **Sentiment Analysis:** Evaluate the put-call ratio, implied volatility skew, and overall market sentiment. What do these indicators suggest about trader sentiment and potential future price direction? + 4. **Potential Trading Strategies:** Based on your analysis, suggest potential options trading strategies that could be employed for {ticker}, considering the current market conditions and sentiment. + + Please provide your analysis in a clear and concise manner, suitable for someone with a good understanding of options trading. + """ + + try: + return llm_text_gen(prompt) + except Exception as err: + logger.error(f"Failed to generate options report: {err}") + raise + +def get_dashboard() -> FinancialDashboard: + """Get the financial dashboard instance.""" + return FinancialDashboard() \ No newline at end of file diff --git a/lib/ai_writers/ai_finance_report_generator/reports/README.md b/lib/ai_writers/ai_finance_report_generator/reports/README.md new file mode 100644 index 00000000..6fc26864 --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/reports/README.md @@ -0,0 +1,265 @@ +# Financial Reports Module + +This directory contains the core report generation modules for different types of financial analysis. Each module is designed to handle a specific type of financial report and can be accessed through the main dashboard interface. + +## Directory Structure + +``` +reports/ +├── technical_analysis/ # Technical analysis reports +├── fundamental_analysis/ # Fundamental analysis reports +├── options_analysis/ # Options analysis reports +├── portfolio_analysis/ # Portfolio analysis reports +├── market_research/ # Market research reports +└── news_analysis/ # News analysis reports +``` + +## Report Types + +### 1. Technical Analysis Reports +Location: `technical_analysis/` + +Generates technical analysis reports including: +- Moving averages (SMA, EMA, WMA) +- RSI, MACD, Bollinger Bands +- Volume analysis +- Support/Resistance levels +- Trend analysis +- Pattern recognition + +Usage: +```python +from lib.ai_writers.ai_finance_report_generator.reports.technical_analysis import generate_ta_report + +report = generate_ta_report("AAPL") +``` + +### 2. Fundamental Analysis Reports +Location: `fundamental_analysis/` + +Generates fundamental analysis reports including: +- Financial ratios +- Company valuation metrics +- Growth analysis +- Profitability analysis +- Debt analysis +- Cash flow analysis + +Usage: +```python +from lib.ai_writers.ai_finance_report_generator.reports.fundamental_analysis import generate_fa_report + +report = generate_fa_report("AAPL") +``` + +### 3. Options Analysis Reports +Location: `options_analysis/` + +Generates options analysis reports including: +- Options chain analysis +- Implied volatility analysis +- Options strategies +- Risk metrics +- Greeks analysis + +Usage: +```python +from lib.ai_writers.ai_finance_report_generator.reports.options_analysis import generate_options_report + +report = generate_options_report("AAPL") +``` + +### 4. Portfolio Analysis Reports +Location: `portfolio_analysis/` + +Generates portfolio analysis reports including: +- Portfolio performance analysis +- Risk assessment +- Asset allocation +- Correlation analysis +- Diversification metrics +- Performance attribution + +Usage: +```python +from lib.ai_writers.ai_finance_report_generator.reports.portfolio_analysis import generate_portfolio_report + +portfolio = [ + {"symbol": "AAPL", "shares": 100}, + {"symbol": "GOOGL", "shares": 50} +] +report = generate_portfolio_report(portfolio) +``` + +### 5. Market Research Reports +Location: `market_research/` + +Generates market research reports including: +- Sector analysis +- Industry trends +- Market overview +- Competitive analysis +- Market opportunities +- Risk factors + +Usage: +```python +from lib.ai_writers.ai_finance_report_generator.reports.market_research import generate_market_research_report + +report = generate_market_research_report(sectors=["Technology", "Healthcare"]) +``` + +### 6. News Analysis Reports +Location: `news_analysis/` + +Generates news analysis reports including: +- News sentiment analysis +- Market impact analysis +- Event correlation +- Trend detection +- Social media analysis +- News aggregation + +Usage: +```python +from lib.ai_writers.ai_finance_report_generator.reports.news_analysis import generate_news_analysis_report + +report = generate_news_analysis_report("AAPL") +``` + +## Common Features + +All report modules share the following features: + +1. **Data Validation** + - Input validation for symbols and parameters + - Error handling for invalid inputs + - Data type checking + +2. **Report Formatting** + - Markdown formatting + - Chart generation (when applicable) + - Customizable templates + +3. **Storage Integration** + - Automatic report storage + - Recent reports tracking + - Report versioning + +4. **User Preferences** + - Customizable report formats + - Language selection + - Chart style preferences + +## Integration with Dashboard + +All report modules are integrated with the main dashboard and can be accessed through the `FinancialDashboard` class: + +```python +from lib.ai_writers.ai_finance_report_generator.ai_financial_dashboard import get_dashboard + +dashboard = get_dashboard() + +# Generate reports through dashboard +ta_report = dashboard.generate_technical_analysis("AAPL") +options_report = dashboard.generate_options_analysis("AAPL") + +# Get recent reports +recent_reports = dashboard.get_recent_reports() +``` + +## Adding New Report Types + +To add a new report type: + +1. Create a new directory in the `reports/` folder +2. Create an `__init__.py` file with the report generation function +3. Add the report type to the dashboard features +4. Implement the report generation logic +5. Add appropriate error handling and validation + +Example: +```python +# reports/new_analysis/__init__.py +from typing import Dict, Any +from ...utils import validate_symbol + +def generate_new_analysis_report(symbol: str) -> Dict[str, Any]: + """ + Generate a new type of analysis report. + + Args: + symbol (str): Stock symbol to analyze + + Returns: + Dict[str, Any]: Analysis report + """ + if not validate_symbol(symbol): + raise ValueError("Invalid symbol provided") + + # Implement report generation logic + return { + "symbol": symbol, + "analysis": "Report content" + } +``` + +## Error Handling + +All report modules implement consistent error handling: + +1. **Input Validation** + - Symbol validation + - Parameter validation + - Data type checking + +2. **Data Collection Errors** + - API errors + - Network errors + - Data format errors + +3. **Report Generation Errors** + - LLM errors + - Template errors + - Formatting errors + +4. **Storage Errors** + - File system errors + - Database errors + - Backup errors + +## Contributing + +When contributing to the reports module: + +1. Follow the existing code structure +2. Add appropriate type hints +3. Include comprehensive docstrings +4. Add error handling +5. Update the dashboard integration +6. Add tests for new functionality + +## Dependencies + +The reports module depends on: + +1. **Data Collection** + - `finance_data_researcher` + - `web_scraping_tools` + +2. **Analysis Tools** + - `pandas_ta` + - `numpy` + - `scipy` + +3. **Visualization** + - `matplotlib` + - `plotly` + +4. **Text Generation** + - `llm_text_gen` + - `gpt_providers` + +## License + +This module is part of the AI Finance Report Generator project and is licensed under the MIT License. \ No newline at end of file diff --git a/lib/ai_writers/ai_finance_report_generator/reports/fundamental_analysis/__init__.py b/lib/ai_writers/ai_finance_report_generator/reports/fundamental_analysis/__init__.py new file mode 100644 index 00000000..af152a18 --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/reports/fundamental_analysis/__init__.py @@ -0,0 +1,34 @@ +""" +Fundamental Analysis Reports Module + +This module handles the generation of fundamental analysis reports including: +- Financial ratios +- Company valuation metrics +- Growth analysis +- Profitability analysis +- Debt analysis +- Cash flow analysis +""" + +from typing import Dict, Any +from ...utils import validate_symbol + +def generate_fa_report(symbol: str) -> Dict[str, Any]: + """ + Generate a fundamental analysis report for the given symbol. + + Args: + symbol (str): Stock symbol to analyze + + Returns: + Dict[str, Any]: Fundamental analysis report + """ + if not validate_symbol(symbol): + raise ValueError("Invalid symbol provided") + + # TODO: Implement fundamental analysis report generation + return { + "symbol": symbol, + "status": "coming_soon", + "message": "Fundamental analysis report generation is coming soon" + } \ No newline at end of file diff --git a/lib/ai_writers/ai_finance_report_generator/reports/market_research/__init__.py b/lib/ai_writers/ai_finance_report_generator/reports/market_research/__init__.py new file mode 100644 index 00000000..4e276969 --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/reports/market_research/__init__.py @@ -0,0 +1,29 @@ +""" +Market Research Reports Module + +This module handles the generation of market research reports including: +- Sector analysis +- Industry trends +- Market overview +- Competitive analysis +- Market opportunities +- Risk factors +""" + +from typing import Dict, Any, List + +def generate_market_research_report(sectors: List[str] = None) -> Dict[str, Any]: + """ + Generate a market research report. + + Args: + sectors (List[str], optional): List of sectors to analyze + + Returns: + Dict[str, Any]: Market research report + """ + # TODO: Implement market research report generation + return { + "status": "coming_soon", + "message": "Market research report generation is coming soon" + } \ No newline at end of file diff --git a/lib/ai_writers/ai_finance_report_generator/reports/news_analysis/__init__.py b/lib/ai_writers/ai_finance_report_generator/reports/news_analysis/__init__.py new file mode 100644 index 00000000..8006c8da --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/reports/news_analysis/__init__.py @@ -0,0 +1,33 @@ +""" +News Analysis Reports Module + +This module handles the generation of news analysis reports including: +- News sentiment analysis +- Market impact analysis +- Event correlation +- Trend detection +- Social media analysis +- News aggregation +""" + +from typing import Dict, Any, List +from ...utils import validate_symbol + +def generate_news_analysis_report(symbol: str = None) -> Dict[str, Any]: + """ + Generate a news analysis report. + + Args: + symbol (str, optional): Stock symbol to analyze news for + + Returns: + Dict[str, Any]: News analysis report + """ + if symbol and not validate_symbol(symbol): + raise ValueError("Invalid symbol provided") + + # TODO: Implement news analysis report generation + return { + "status": "coming_soon", + "message": "News analysis report generation is coming soon" + } \ No newline at end of file diff --git a/lib/ai_writers/ai_finance_report_generator/reports/options_analysis/__init__.py b/lib/ai_writers/ai_finance_report_generator/reports/options_analysis/__init__.py new file mode 100644 index 00000000..89eddb7a --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/reports/options_analysis/__init__.py @@ -0,0 +1,33 @@ +""" +Options Analysis Reports Module + +This module handles the generation of options analysis reports including: +- Options chain analysis +- Implied volatility analysis +- Options strategies +- Risk metrics +- Greeks analysis +""" + +from typing import Dict, Any +from ...utils import validate_symbol + +def generate_options_report(symbol: str) -> Dict[str, Any]: + """ + Generate an options analysis report for the given symbol. + + Args: + symbol (str): Stock symbol to analyze + + Returns: + Dict[str, Any]: Options analysis report + """ + if not validate_symbol(symbol): + raise ValueError("Invalid symbol provided") + + # TODO: Implement options analysis report generation + return { + "symbol": symbol, + "status": "coming_soon", + "message": "Options analysis report generation is coming soon" + } \ No newline at end of file diff --git a/lib/ai_writers/ai_finance_report_generator/reports/portfolio_analysis/__init__.py b/lib/ai_writers/ai_finance_report_generator/reports/portfolio_analysis/__init__.py new file mode 100644 index 00000000..1f7d5e88 --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/reports/portfolio_analysis/__init__.py @@ -0,0 +1,32 @@ +""" +Portfolio Analysis Reports Module + +This module handles the generation of portfolio analysis reports including: +- Portfolio performance analysis +- Risk assessment +- Asset allocation +- Correlation analysis +- Diversification metrics +- Performance attribution +""" + +from typing import Dict, Any, List + +def generate_portfolio_report(portfolio: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Generate a portfolio analysis report. + + Args: + portfolio (List[Dict[str, Any]]): List of portfolio positions + + Returns: + Dict[str, Any]: Portfolio analysis report + """ + if not portfolio: + raise ValueError("Portfolio cannot be empty") + + # TODO: Implement portfolio analysis report generation + return { + "status": "coming_soon", + "message": "Portfolio analysis report generation is coming soon" + } \ No newline at end of file diff --git a/lib/ai_writers/ai_finance_report_generator/reports/technical_analysis/__init__.py b/lib/ai_writers/ai_finance_report_generator/reports/technical_analysis/__init__.py new file mode 100644 index 00000000..8cc609d9 --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/reports/technical_analysis/__init__.py @@ -0,0 +1,314 @@ +""" +Technical Analysis Reports Module + +This module handles the generation of technical analysis reports using yfinance data and pandas_ta for indicators. +""" + +from typing import Dict, Any, List, Optional +import yfinance as yf +import pandas as pd +import pandas_ta as ta +import plotly.graph_objects as go +from datetime import datetime, timedelta +from loguru import logger +from ...utils import validate_symbol +from ...ai_financial_dashboard import get_dashboard + +class TechnicalAnalysis: + def __init__(self, symbol: str, timeframe: str = "1d", period: str = "1y"): + """ + Initialize Technical Analysis. + + Args: + symbol (str): Stock symbol to analyze + timeframe (str): Data timeframe (1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo) + period (str): Data period (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max) + """ + logger.info(f"Initializing Technical Analysis for {symbol} with timeframe {timeframe} and period {period}") + self.symbol = symbol + self.timeframe = timeframe + self.period = period + self.data = None + self.indicators = {} + self.stock = yf.Ticker(symbol) + + def fetch_data(self) -> None: + """Fetch historical price data using yfinance""" + try: + logger.info(f"Fetching historical data for {self.symbol}") + # Get historical data + self.data = self.stock.history(period=self.period, interval=self.timeframe) + logger.debug(f"Retrieved {len(self.data)} data points") + + # Get additional info + logger.info("Fetching company information") + self.info = self.stock.info + + # Calculate basic metrics + logger.debug("Calculating basic metrics") + self.data['Returns'] = self.data['Close'].pct_change() + self.data['Volatility'] = self.data['Returns'].rolling(window=20).std() + + logger.success(f"Successfully fetched data for {self.symbol}") + + except Exception as e: + logger.error(f"Error fetching data for {self.symbol}: {str(e)}") + raise ValueError(f"Error fetching data for {self.symbol}: {str(e)}") + + def calculate_indicators(self) -> None: + """Calculate technical indicators using pandas_ta""" + if self.data is None: + logger.error("Data not fetched. Call fetch_data() first.") + raise ValueError("Data not fetched. Call fetch_data() first.") + + logger.info("Calculating technical indicators") + + # Moving Averages + logger.debug("Calculating Moving Averages") + self.indicators['sma_20'] = self.data.ta.sma(length=20) + self.indicators['sma_50'] = self.data.ta.sma(length=50) + self.indicators['sma_200'] = self.data.ta.sma(length=200) + self.indicators['ema_20'] = self.data.ta.ema(length=20) + + # RSI + logger.debug("Calculating RSI") + self.indicators['rsi'] = self.data.ta.rsi() + + # MACD + logger.debug("Calculating MACD") + macd = self.data.ta.macd() + self.indicators['macd'] = macd['MACD_12_26_9'] + self.indicators['macd_signal'] = macd['MACDs_12_26_9'] + self.indicators['macd_hist'] = macd['MACDh_12_26_9'] + + # Bollinger Bands + logger.debug("Calculating Bollinger Bands") + bbands = self.data.ta.bbands() + self.indicators['bb_upper'] = bbands['BBU_20_2.0'] + self.indicators['bb_middle'] = bbands['BBM_20_2.0'] + self.indicators['bb_lower'] = bbands['BBL_20_2.0'] + + # Volume Analysis + logger.debug("Calculating Volume indicators") + self.indicators['volume_sma'] = self.data['Volume'].rolling(window=20).mean() + self.indicators['obv'] = self.data.ta.obv() + + # Additional Indicators + logger.debug("Calculating additional indicators") + self.indicators['stoch'] = self.data.ta.stoch() + self.indicators['adx'] = self.data.ta.adx() + self.indicators['atr'] = self.data.ta.atr() + + logger.success("Successfully calculated all technical indicators") + + def identify_patterns(self) -> List[Dict[str, Any]]: + """Identify chart patterns""" + logger.info("Identifying chart patterns") + patterns = [] + + # Candlestick Patterns + if len(self.data) >= 3: + logger.debug("Analyzing candlestick patterns") + # Doji + doji = self.data.ta.cdl_doji() + if doji['CDL_DOJI'].iloc[-1] != 0: + logger.debug("Doji pattern detected") + patterns.append({ + 'type': 'doji', + 'date': self.data.index[-1], + 'significance': 'neutral' + }) + + # Engulfing + engulfing = self.data.ta.cdl_engulfing() + if engulfing['CDL_ENGULFING'].iloc[-1] != 0: + logger.debug("Engulfing pattern detected") + patterns.append({ + 'type': 'engulfing', + 'date': self.data.index[-1], + 'significance': 'bullish' if engulfing['CDL_ENGULFING'].iloc[-1] > 0 else 'bearish' + }) + + logger.info(f"Identified {len(patterns)} patterns") + return patterns + + def find_support_resistance(self) -> Dict[str, List[float]]: + """Find support and resistance levels using price action""" + logger.info("Finding support and resistance levels") + levels = { + 'support': [], + 'resistance': [] + } + + # Use recent price action to identify levels + recent_data = self.data.tail(100) + logger.debug(f"Analyzing {len(recent_data)} recent data points for S/R levels") + + # Find local minima and maxima + for i in range(2, len(recent_data) - 2): + # Support level + if (recent_data['Low'].iloc[i] < recent_data['Low'].iloc[i-1] and + recent_data['Low'].iloc[i] < recent_data['Low'].iloc[i-2] and + recent_data['Low'].iloc[i] < recent_data['Low'].iloc[i+1] and + recent_data['Low'].iloc[i] < recent_data['Low'].iloc[i+2]): + levels['support'].append(recent_data['Low'].iloc[i]) + + # Resistance level + if (recent_data['High'].iloc[i] > recent_data['High'].iloc[i-1] and + recent_data['High'].iloc[i] > recent_data['High'].iloc[i-2] and + recent_data['High'].iloc[i] > recent_data['High'].iloc[i+1] and + recent_data['High'].iloc[i] > recent_data['High'].iloc[i+2]): + levels['resistance'].append(recent_data['High'].iloc[i]) + + # Remove duplicates and sort + levels['support'] = sorted(list(set(levels['support']))) + levels['resistance'] = sorted(list(set(levels['resistance']))) + + logger.info(f"Found {len(levels['support'])} support and {len(levels['resistance'])} resistance levels") + return levels + + def generate_chart(self) -> go.Figure: + """Generate interactive chart using plotly""" + logger.info("Generating interactive chart") + fig = go.Figure() + + # Candlestick chart + logger.debug("Adding candlestick chart") + fig.add_trace(go.Candlestick( + x=self.data.index, + open=self.data['Open'], + high=self.data['High'], + low=self.data['Low'], + close=self.data['Close'], + name='Price' + )) + + # Moving Averages + logger.debug("Adding moving averages") + fig.add_trace(go.Scatter( + x=self.data.index, + y=self.indicators['sma_20'], + name='SMA 20', + line=dict(color='blue') + )) + + fig.add_trace(go.Scatter( + x=self.data.index, + y=self.indicators['sma_50'], + name='SMA 50', + line=dict(color='orange') + )) + + # Bollinger Bands + logger.debug("Adding Bollinger Bands") + fig.add_trace(go.Scatter( + x=self.data.index, + y=self.indicators['bb_upper'], + name='BB Upper', + line=dict(color='gray', dash='dash') + )) + + fig.add_trace(go.Scatter( + x=self.data.index, + y=self.indicators['bb_lower'], + name='BB Lower', + line=dict(color='gray', dash='dash'), + fill='tonexty' + )) + + # Volume + logger.debug("Adding volume bars") + fig.add_trace(go.Bar( + x=self.data.index, + y=self.data['Volume'], + name='Volume', + marker_color='rgba(0,0,255,0.3)' + )) + + # Layout + logger.debug("Setting chart layout") + fig.update_layout( + title=f'{self.symbol} Technical Analysis', + yaxis_title='Price', + xaxis_title='Date', + template='plotly_dark' + ) + + logger.success("Successfully generated chart") + return fig + + def _generate_summary(self) -> Dict[str, Any]: + """Generate summary of technical analysis""" + logger.info("Generating analysis summary") + current_price = self.data['Close'].iloc[-1] + sma_20 = self.indicators['sma_20'].iloc[-1] + sma_50 = self.indicators['sma_50'].iloc[-1] + rsi = self.indicators['rsi'].iloc[-1] + + summary = { + 'current_price': current_price, + 'price_change': self.data['Returns'].iloc[-1] * 100, + 'trend': 'bullish' if current_price > sma_20 > sma_50 else 'bearish', + 'rsi_signal': 'overbought' if rsi > 70 else 'oversold' if rsi < 30 else 'neutral', + 'volatility': self.data['Volatility'].iloc[-1], + 'volume_trend': 'increasing' if self.data['Volume'].iloc[-1] > self.indicators['volume_sma'].iloc[-1] else 'decreasing' + } + + logger.debug(f"Analysis summary: {summary}") + return summary + + def generate_report(self) -> Dict[str, Any]: + """Generate comprehensive technical analysis report""" + logger.info(f"Generating comprehensive report for {self.symbol}") + + self.fetch_data() + self.calculate_indicators() + patterns = self.identify_patterns() + levels = self.find_support_resistance() + chart = self.generate_chart() + summary = self._generate_summary() + + report = { + 'symbol': self.symbol, + 'timestamp': datetime.now(), + 'company_info': self.info, + 'indicators': self.indicators, + 'patterns': patterns, + 'levels': levels, + 'chart': chart, + 'summary': summary + } + + logger.success(f"Successfully generated report for {self.symbol}") + return report + +def generate_ta_report(symbol: str) -> Dict[str, Any]: + """ + Generate a technical analysis report for the given symbol. + + Args: + symbol (str): Stock symbol to analyze + + Returns: + Dict[str, Any]: Technical analysis report + """ + logger.info(f"Generating technical analysis report for {symbol}") + + if not validate_symbol(symbol): + logger.error(f"Invalid symbol provided: {symbol}") + raise ValueError("Invalid symbol provided") + + try: + analysis = TechnicalAnalysis(symbol) + report = analysis.generate_report() + + # Add to dashboard's recent reports + dashboard = get_dashboard() + dashboard.add_recent_report("technical_analysis", symbol, report) + + logger.success(f"Successfully completed technical analysis for {symbol}") + return report + + except Exception as e: + logger.error(f"Error generating technical analysis report for {symbol}: {str(e)}") + raise \ No newline at end of file diff --git a/lib/ai_writers/ai_finance_report_generator/utils/__init__.py b/lib/ai_writers/ai_finance_report_generator/utils/__init__.py new file mode 100644 index 00000000..285be4bb --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/utils/__init__.py @@ -0,0 +1,62 @@ +""" +Utility functions and helpers for the AI Finance Report Generator. +""" + +from typing import Dict, List, Any +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +logger = logging.getLogger(__name__) + +def validate_symbol(symbol: str) -> bool: + """ + Validate if the given symbol is in correct format. + + Args: + symbol (str): Stock symbol to validate + + Returns: + bool: True if valid, False otherwise + """ + if not isinstance(symbol, str): + return False + return len(symbol.strip()) > 0 + +def format_currency(value: float) -> str: + """ + Format number as currency. + + Args: + value (float): Number to format + + Returns: + str: Formatted currency string + """ + return f"${value:,.2f}" + +def get_feature_status(feature_name: str) -> Dict[str, Any]: + """ + Get the status of a feature. + + Args: + feature_name (str): Name of the feature + + Returns: + Dict[str, Any]: Feature status information + """ + # This will be expanded as we implement more features + implemented_features = { + "technical_analysis": True, + "options_analysis": True, + } + + return { + "name": feature_name, + "implemented": implemented_features.get(feature_name, False), + "coming_soon": not implemented_features.get(feature_name, False) + } \ No newline at end of file diff --git a/lib/ai_writers/ai_finance_report_generator/utils/storage.py b/lib/ai_writers/ai_finance_report_generator/utils/storage.py new file mode 100644 index 00000000..52e81b1e --- /dev/null +++ b/lib/ai_writers/ai_finance_report_generator/utils/storage.py @@ -0,0 +1,208 @@ +""" +Storage Module for AI Finance Report Generator + +This module handles the persistence of user preferences and recent reports using JSON files. +""" + +import json +import os +from typing import Dict, List, Any, Optional +from datetime import datetime +from pathlib import Path + +class StorageManager: + """Manages storage operations for user preferences and recent reports.""" + + def __init__(self, base_dir: Optional[str] = None): + """ + Initialize the storage manager. + + Args: + base_dir (Optional[str]): Base directory for storage files + """ + if base_dir is None: + # Use user's home directory by default + self.base_dir = Path.home() / ".ai_finance" + else: + self.base_dir = Path(base_dir) + + # Create storage directory if it doesn't exist + self.base_dir.mkdir(parents=True, exist_ok=True) + + # Define file paths + self.prefs_file = self.base_dir / "preferences.json" + self.reports_file = self.base_dir / "recent_reports.json" + + # Initialize files if they don't exist + self._initialize_storage() + + def _initialize_storage(self) -> None: + """Initialize storage files if they don't exist.""" + if not self.prefs_file.exists(): + self._save_preferences({}) + + if not self.reports_file.exists(): + self._save_reports([]) + + def _save_preferences(self, preferences: Dict[str, Any]) -> None: + """ + Save user preferences to file. + + Args: + preferences (Dict[str, Any]): User preferences to save + """ + with open(self.prefs_file, 'w') as f: + json.dump(preferences, f, indent=4) + + def _load_preferences(self) -> Dict[str, Any]: + """ + Load user preferences from file. + + Returns: + Dict[str, Any]: User preferences + """ + try: + with open(self.prefs_file, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + return {} + + def _save_reports(self, reports: List[Dict[str, Any]]) -> None: + """ + Save recent reports to file. + + Args: + reports (List[Dict[str, Any]]): Recent reports to save + """ + with open(self.reports_file, 'w') as f: + json.dump(reports, f, indent=4) + + def _load_reports(self) -> List[Dict[str, Any]]: + """ + Load recent reports from file. + + Returns: + List[Dict[str, Any]]: Recent reports + """ + try: + with open(self.reports_file, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + return [] + + def save_user_preferences(self, preferences: Dict[str, Any]) -> None: + """ + Save user preferences. + + Args: + preferences (Dict[str, Any]): User preferences to save + """ + self._save_preferences(preferences) + + def load_user_preferences(self) -> Dict[str, Any]: + """ + Load user preferences. + + Returns: + Dict[str, Any]: User preferences + """ + return self._load_preferences() + + def save_recent_reports(self, reports: List[Dict[str, Any]]) -> None: + """ + Save recent reports. + + Args: + reports (List[Dict[str, Any]]): Recent reports to save + """ + # Convert datetime objects to ISO format strings + serialized_reports = [] + for report in reports: + serialized_report = report.copy() + if isinstance(report.get('timestamp'), datetime): + serialized_report['timestamp'] = report['timestamp'].isoformat() + serialized_reports.append(serialized_report) + + self._save_reports(serialized_reports) + + def load_recent_reports(self) -> List[Dict[str, Any]]: + """ + Load recent reports. + + Returns: + List[Dict[str, Any]]: Recent reports with datetime objects + """ + reports = self._load_reports() + + # Convert ISO format strings back to datetime objects + for report in reports: + if isinstance(report.get('timestamp'), str): + report['timestamp'] = datetime.fromisoformat(report['timestamp']) + + return reports + + def clear_storage(self) -> None: + """Clear all stored data.""" + self._save_preferences({}) + self._save_reports([]) + + def backup_storage(self, backup_dir: Optional[str] = None) -> None: + """ + Create a backup of the storage files. + + Args: + backup_dir (Optional[str]): Directory to store backup files + """ + if backup_dir is None: + backup_dir = self.base_dir / "backups" + else: + backup_dir = Path(backup_dir) + + backup_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Backup preferences + if self.prefs_file.exists(): + backup_prefs = backup_dir / f"preferences_{timestamp}.json" + with open(self.prefs_file, 'r') as src, open(backup_prefs, 'w') as dst: + dst.write(src.read()) + + # Backup reports + if self.reports_file.exists(): + backup_reports = backup_dir / f"recent_reports_{timestamp}.json" + with open(self.reports_file, 'r') as src, open(backup_reports, 'w') as dst: + dst.write(src.read()) + + def restore_from_backup(self, backup_file: str) -> None: + """ + Restore storage from a backup file. + + Args: + backup_file (str): Path to the backup file + """ + backup_path = Path(backup_file) + if not backup_path.exists(): + raise FileNotFoundError(f"Backup file not found: {backup_file}") + + # Determine which type of backup file it is + if "preferences" in backup_path.name: + with open(backup_path, 'r') as src, open(self.prefs_file, 'w') as dst: + dst.write(src.read()) + elif "recent_reports" in backup_path.name: + with open(backup_path, 'r') as src, open(self.reports_file, 'w') as dst: + dst.write(src.read()) + else: + raise ValueError(f"Unknown backup file type: {backup_file}") + +def get_storage_manager(base_dir: Optional[str] = None) -> StorageManager: + """ + Get a storage manager instance. + + Args: + base_dir (Optional[str]): Base directory for storage files + + Returns: + StorageManager: Storage manager instance + """ + return StorageManager(base_dir) \ No newline at end of file diff --git a/lib/ai_writers/ai_financial_writer.py b/lib/ai_writers/ai_financial_writer.py deleted file mode 100644 index 70ae8011..00000000 --- a/lib/ai_writers/ai_financial_writer.py +++ /dev/null @@ -1,91 +0,0 @@ -import sys -import os -from textwrap import dedent -from pathlib import Path -from datetime import datetime - -from loguru import logger -logger.remove() -logger.add(sys.stdout, - colorize=True, - format="{level}|{file}:{line}:{function}| {message}" - ) - -from ..ai_web_researcher.finance_data_researcher import get_finance_data, get_fin_options_data -from ..gpt_providers.text_generation.main_text_generation import llm_text_gen - - -def write_basic_ta_report(symbol): - """ Write financial TA for given ticker symbol """ - try: - symbol_fin_data = get_finance_data(symbol) - #get_visual_reports - fin_report = gen_finta_report(symbol_fin_data, symbol) - logger.info(f"Done: Final Technical Analysis for {symbol}:\n\n") - except Exception as err: - logger.error(f"Error: Failed to generate Financial report: {err}") - - #fin_options_data = get_fin_options_data(symbol) - #options_report = gen_options_report(fin_options_data, symbol) - - - -def gen_options_report(results_sentences, ticker): - """ Call LLM to generate options report """ - prompt = f""" - You are a financial expert specializing in options trading and market sentiment analysis. - You have been provided with the following technical analysis of options data for the ticker symbol {ticker} with the nearest expiry date: - - {chr(10).join(results_sentences)} - - Based on this data, provide a comprehensive analysis of the options market for {ticker}. - - Your analysis should include: - - 1. **Implied Volatility Interpretation:** Discuss the significance of the average implied volatility for both call and put options. What does it suggest about market expectations of future price movements? - 2. **Volume and Open Interest Insights:** Analyze the volume and open interest for call and put options. What does this data reveal about current market positioning and potential future trading activity? - 3. **Sentiment Analysis:** Evaluate the put-call ratio, implied volatility skew, and overall market sentiment. What do these indicators suggest about trader sentiment and potential future price direction? - 4. **Potential Trading Strategies:** Based on your analysis, suggest potential options trading strategies that could be employed for {ticker}, considering the current market conditions and sentiment. - - Please provide your analysis in a clear and concise manner, suitable for someone with a good understanding of options trading. - """ - logger.info("Generating Financial Technical report..") - try: - response = llm_text_gen(prompt) - return response - except Exception as err: - logger.error(f"Exit: Failed to get response from LLM: {err}") - exit(1) - - -def gen_finta_report(last_day_summary, symbol): - """ Get AI to write TA report from given data """ - prompt = f""" - You are a seasoned Technical Analysis (TA) expert, rivaling legends like Charles Dow, John Bollinger, and Alan Andrews. - Your deep understanding of market dynamics, coupled with mastery of technical indicators, - allows you to decipher complex patterns and offer precise predictions. - - Your expertise extends to practical tools like the pandas_ta module, enabling you to extract valuable insights from raw data. - - **Objective:** - Analyze the provided technical indicators for {symbol} on its last trading day and predict its price movement over the next few trading sessions. - - **Instructions:** - 1. **Identify Potential Trading Signals:** Highlight specific indicators suggesting bullish, bearish, or neutral signals. Explain the rationale behind each signal, referencing historical patterns or comparable market scenarios. - 2. **Detect Patterns and Divergences:** Analyze the interplay between different indicators. Detect patterns like moving average crossovers, candlestick formations, or divergences between price action and indicators. Explain the significance of each pattern. - 3. **Price Movement Prediction:** Based on your analysis, provide a clear prediction for {symbol}'s price movement in the next few days. State the expected direction (up, down, sideways) and potential price targets if identifiable. - 4. **Risk Assessment:** Briefly discuss any potential risks or factors that could invalidate your predictions, promoting a balanced and informed perspective. - - **Technical Indicators for {symbol} on the Last Trading Day:** - {last_day_summary} - - Remember, your analysis should be detailed, insightful, and actionable for traders seeking to capitalize on market movements. - """ - - logger.info("Generating Financial Technical report..") - try: - response = llm_text_gen(prompt) - return response - except Exception as err: - logger.error(f"Exit: Failed to get response from LLM: {err}") - exit(1) diff --git a/lib/ai_writers/twitter_writers/tweet_generator/smart_tweet_generator.py b/lib/ai_writers/twitter_writers/tweet_generator/smart_tweet_generator.py index 5627b349..7e36d792 100644 --- a/lib/ai_writers/twitter_writers/tweet_generator/smart_tweet_generator.py +++ b/lib/ai_writers/twitter_writers/tweet_generator/smart_tweet_generator.py @@ -1,13 +1,33 @@ +""" +Smart Tweet Generator with modern UI components. +""" + 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 from ....gpt_providers.text_generation.main_text_generation import llm_text_gen +from ..twitter_streamlit_ui import ( + TweetForm, + TweetCard, + Theme, + save_to_session, + get_from_session, + show_success_message, + show_error_message, + show_info_message, + validate_tweet_content, + validate_hashtags, + validate_emojis, + calculate_engagement_score, + generate_tweet_metrics, + copy_to_clipboard, + create_download_button +) # Constants MAX_TWEET_LENGTH = 280 @@ -19,14 +39,6 @@ EMOJI_CATEGORIES = { "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 suggest_hashtags(topic: str, tone: str) -> List[str]: """Suggest relevant hashtags based on topic and tone.""" # Enhanced hashtag suggestions based on topic and tone @@ -61,63 +73,6 @@ def suggest_emojis(tone: str, count: int = 3) -> List[str]: } return emoji_map.get(tone.lower(), ["✨"])[:count] -def predict_tweet_performance(tweet: str, target_audience: str, tone: str) -> Dict: - """Predict tweet performance with enhanced metrics.""" - char_count = count_characters(tweet) - hashtags = extract_hashtags(tweet) - - # Enhanced performance metrics - metrics = { - "character_count": { - "score": min(100, (char_count / 280) * 100), - "status": "optimal" if 100 <= char_count <= 200 else "suboptimal", - "suggestion": "Consider adjusting length for optimal engagement" if char_count < 100 or char_count > 200 else "Length is optimal" - }, - "hashtag_usage": { - "score": min(100, (len(hashtags) / 3) * 100), - "status": "optimal" if 1 <= len(hashtags) <= 3 else "suboptimal", - "suggestion": "Add more hashtags" if len(hashtags) < 1 else "Reduce hashtag count" if len(hashtags) > 3 else "Hashtag count is optimal" - }, - "engagement_potential": { - "score": 0, - "status": "needs_improvement", - "suggestion": "" - }, - "audience_alignment": { - "score": 0, - "status": "needs_improvement", - "suggestion": "" - } - } - - # Calculate engagement potential - engagement_triggers = ["?", "!", "RT", "like", "follow", "check", "learn", "discover"] - trigger_count = sum(1 for trigger in engagement_triggers if trigger.lower() in tweet.lower()) - metrics["engagement_potential"]["score"] = min(100, (trigger_count / 3) * 100) - metrics["engagement_potential"]["status"] = "optimal" if trigger_count >= 1 else "needs_improvement" - metrics["engagement_potential"]["suggestion"] = "Add engagement triggers" if trigger_count < 1 else "Good engagement potential" - - # Calculate audience alignment - audience_keywords = { - "professionals": ["business", "industry", "professional", "career"], - "students": ["learn", "study", "education", "student"], - "general": ["everyone", "people", "community", "world"] - } - keyword_count = sum(1 for keyword in audience_keywords.get(target_audience.lower(), []) - if keyword.lower() in tweet.lower()) - metrics["audience_alignment"]["score"] = min(100, (keyword_count / 2) * 100) - metrics["audience_alignment"]["status"] = "optimal" if keyword_count >= 1 else "needs_improvement" - metrics["audience_alignment"]["suggestion"] = "Add audience-specific keywords" if keyword_count < 1 else "Good audience alignment" - - # Calculate overall score - overall_score = sum(metric["score"] for metric in metrics.values()) / len(metrics) - - return { - "metrics": metrics, - "overall_score": overall_score, - "status": "excellent" if overall_score >= 80 else "good" if overall_score >= 60 else "fair" if overall_score >= 40 else "needs_improvement" - } - def generate_tweet_variations( hook: str, target_audience: str, @@ -177,58 +132,14 @@ def generate_tweet_variations( return sample_tweets[:num_variations] -def suggest_improvements(tweet: str, performance: Dict) -> List[str]: - """Generate actionable improvement suggestions.""" - suggestions = [] - metrics = performance["metrics"] - - # Character count suggestions - if metrics["character_count"]["status"] == "suboptimal": - suggestions.append(f"📝 {metrics['character_count']['suggestion']}") - - # Hashtag suggestions - if metrics["hashtag_usage"]["status"] == "suboptimal": - suggestions.append(f"#️⃣ {metrics['hashtag_usage']['suggestion']}") - - # Engagement suggestions - if metrics["engagement_potential"]["status"] == "needs_improvement": - suggestions.append(f"🎯 {metrics['engagement_potential']['suggestion']}") - - # Audience alignment suggestions - if metrics["audience_alignment"]["status"] == "needs_improvement": - suggestions.append(f"👥 {metrics['audience_alignment']['suggestion']}") - - return suggestions - -def render_tweet_card(tweet: Dict, index: int) -> None: - """Render an enhanced tweet card with interactive elements.""" - with st.container(): - st.markdown(f""" -
-

Tweet Variation {index + 1}

-

{tweet['text']}

-
- - Score: {tweet['engagement_score']}% - -
-
- """, unsafe_allow_html=True) - - # Interactive elements - col1, col2 = st.columns(2) - with col1: - if st.button(f"Copy Tweet {index + 1}", key=f"copy_{index}"): - st.write("Tweet copied to clipboard!") - with col2: - if st.button(f"Save Tweet {index + 1}", key=f"save_{index}"): - st.write("Tweet saved!") - def smart_tweet_generator(): """Enhanced Smart Tweet Generator with improved UI and AI integration.""" st.title("✨ Smart Tweet Generator") st.markdown("Create engaging tweets with AI-powered optimization") + # Apply theme + Theme().apply() + # Input section with improved UI with st.expander("Tweet Parameters", expanded=True): col1, col2 = st.columns(2) @@ -291,34 +202,27 @@ def smart_tweet_generator(): # Display performance metrics st.markdown("### 📊 Performance Metrics") for tweet in tweets: - performance = predict_tweet_performance(tweet["text"], target_audience, tone) + # Calculate engagement score + engagement_score = calculate_engagement_score( + tweet["text"], + tweet["hashtags"], + tweet["emojis"], + tone + ) - # Overall score with progress bar - st.progress(performance["overall_score"] / 100) - st.metric("Overall Score", f"{performance['overall_score']:.1f}%") + # Generate metrics + metrics = generate_tweet_metrics(engagement_score) - # Detailed metrics in columns - cols = st.columns(4) - metrics = performance["metrics"] - - with cols[0]: - st.metric("Character Count", f"{metrics['character_count']['score']:.1f}%") - with cols[1]: - st.metric("Hashtag Usage", f"{metrics['hashtag_usage']['score']:.1f}%") - with cols[2]: - st.metric("Engagement", f"{metrics['engagement_potential']['score']:.1f}%") - with cols[3]: - st.metric("Audience Fit", f"{metrics['audience_alignment']['score']:.1f}%") - - # Improvement suggestions - suggestions = suggest_improvements(tweet["text"], performance) - if suggestions: - st.markdown("### 💡 Improvement Suggestions") - for suggestion in suggestions: - st.info(suggestion) - - # Tweet card - render_tweet_card(tweet, tweets.index(tweet)) + # Display tweet card + TweetCard( + content=tweet["text"], + engagement_score=engagement_score, + hashtags=tweet["hashtags"], + emojis=tweet["emojis"], + metrics=metrics, + on_copy=lambda: copy_to_clipboard(tweet["text"]), + on_save=lambda: save_tweet(tweet) + ).render() st.markdown("---") @@ -326,17 +230,23 @@ def smart_tweet_generator(): 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" - ) + create_download_button( + data=tweets, + filename=f"tweets_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", + button_text="Export as JSON" + ) with col2: if st.button("Copy All Tweets"): tweet_texts = "\n\n".join(tweet["text"] for tweet in tweets) - st.code(tweet_texts) + copy_to_clipboard(tweet_texts) + show_success_message("All tweets copied to clipboard!") + +def save_tweet(tweet: Dict): + """Save tweet for later.""" + tweets = get_from_session("tweets", []) + tweets.append(tweet) + save_to_session("tweets", tweets) + show_success_message("Tweet saved successfully!") if __name__ == "__main__": smart_tweet_generator() \ No newline at end of file diff --git a/lib/ai_writers/twitter_writers/twitter_dashboard.py b/lib/ai_writers/twitter_writers/twitter_dashboard.py index 3471ac87..9bebb9c7 100644 --- a/lib/ai_writers/twitter_writers/twitter_dashboard.py +++ b/lib/ai_writers/twitter_writers/twitter_dashboard.py @@ -1,116 +1,29 @@ +""" +Twitter Dashboard with modern UI components. +""" + import streamlit as st -import streamlit.components.v1 as components from typing import Dict, List import json -import base64 +from datetime import datetime from .tweet_generator import smart_tweet_generator - -def add_bg_from_base64(base64_string): - """Add background image using base64 string.""" - return f''' - - ''' +from .twitter_streamlit_ui import ( + TwitterDashboard, + FeatureCard, + TweetForm, + SettingsForm, + Sidebar, + Header, + Tabs, + Breadcrumbs, + Theme, + save_to_session, + get_from_session, + clear_session, + show_success_message, + show_error_message +) def load_feature_data() -> Dict: """Load feature data from a structured format.""" @@ -232,125 +145,167 @@ def load_feature_data() -> Dict: } } -def render_feature_card(feature: Dict) -> None: - """Render a single feature card with its details.""" - status_class = "status-active" if feature['status'] == 'active' else "status-coming-soon" - 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]) - with col2: - render_feature_card(category['features'][1]) - -def get_space_background() -> str: - """Return base64 encoded space-themed background.""" - return """iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN8/+F9PQAJYgN4hWvGzQAAAABJRU5ErkJggg==""" # This is a placeholder. You'll need to replace with actual base64 image - def run_dashboard(): """Main function to run the Twitter dashboard.""" - # Add space-themed background - st.markdown(add_bg_from_base64(get_space_background()), unsafe_allow_html=True) + # Initialize dashboard + dashboard = TwitterDashboard() - # Enhanced Header with gradient text - st.markdown(""" -
-

🐦 Twitter AI Writer

-

Your all-in-one Twitter content creation and management platform. - Harness the power of AI to enhance your Twitter marketing strategy.

-
- """, unsafe_allow_html=True) - # Load feature data features = load_feature_data() - - # Create tabs with enhanced styling - tab1, tab2, tab3 = st.tabs(["🎯 Quick Actions", "📊 Analytics", "⚙️ Settings"]) - - with tab1: - st.markdown("

🚀 Quick Actions

", unsafe_allow_html=True) - col1, col2, col3 = st.columns(3) - - with col1: - if st.button("📝 Create New Tweet", use_container_width=True): - # Call the Smart Tweet Generator - smart_tweet_generator() - with col2: - st.button("📅 Schedule Content", use_container_width=True) - with col3: - st.button("📊 View Analytics", use_container_width=True) - - with tab2: - st.markdown("### 📈 Analytics Dashboard") - st.info("Analytics features coming soon! Stay tuned for detailed insights and performance metrics.") - - with tab3: - st.markdown("### ⚙️ Settings") - st.info("Settings and configuration options coming soon!") - - # Main content area - st.markdown("## 🛠️ Available Tools") - # Render each category - for category in features.values(): - render_category_section(category) - - # If this is the tweet generation category and the Smart Tweet Generator is active, - # add a button to launch it - if category["title"] == "Tweet Generation & Optimization" and category["features"][0]["status"] == "active": - if st.button(f"🚀 Launch {category['features'][0]['name']}", use_container_width=True): - category["features"][0]["function"]() + # Setup navigation + sidebar = Sidebar(title="Twitter Tools") + sidebar.add_menu_item("Dashboard", "📊", "dashboard") + sidebar.add_menu_item("Tweet Generator", "✍️", "tweet_generator") + sidebar.add_menu_item("Analytics", "📈", "analytics") + sidebar.add_menu_item("Settings", "⚙️", "settings") + + # Setup header + header = Header( + title="Twitter AI Writer", + subtitle="Your all-in-one Twitter content creation and management platform" + ) + header.add_action("New Tweet", "✏️", lambda: save_to_session("current_page", "tweet_generator")) + header.add_action("Refresh", "🔄", lambda: st.experimental_rerun()) + + # Setup tabs + tabs = Tabs() + tabs.add_tab("Overview", "📊", lambda: render_overview(features)) + tabs.add_tab("Recent Tweets", "🐦", lambda: render_recent_tweets()) + tabs.add_tab("Analytics", "📈", lambda: render_analytics()) + + # Setup breadcrumbs + breadcrumbs = Breadcrumbs() + breadcrumbs.add_item("Home", "dashboard", "🏠") + breadcrumbs.add_item(get_from_session("current_page", "Dashboard").title()) + + # Render dashboard + dashboard.render() - # Enhanced Footer - st.markdown("---") - st.markdown(""" -
-

Need assistance? We're here to help!

-
- 📚 Documentation - 💬 Contact Support -
-
- """, unsafe_allow_html=True) +def render_overview(features: Dict): + """Render the overview tab content.""" + # Feature cards + col1, col2, col3 = st.columns(3) + + with col1: + FeatureCard( + title="Tweet Generator", + description="Create engaging tweets with AI assistance", + icon="✍️", + features=[ + { + "name": "AI-Powered", + "description": "Generate tweets using advanced AI" + }, + { + "name": "Customizable", + "description": "Adjust tone, length, and style" + } + ], + on_click=lambda: save_to_session("current_page", "tweet_generator") + ).render() + + with col2: + FeatureCard( + title="Analytics", + description="Track your tweet performance", + icon="📈", + features=[ + { + "name": "Engagement", + "description": "Monitor likes, retweets, and replies" + }, + { + "name": "Growth", + "description": "Track follower growth over time" + } + ] + ).render() + + with col3: + FeatureCard( + title="Settings", + description="Customize your experience", + icon="⚙️", + features=[ + { + "name": "Preferences", + "description": "Set your default options" + }, + { + "name": "API", + "description": "Configure Twitter API settings" + } + ] + ).render() + +def render_recent_tweets(): + """Render the recent tweets tab content.""" + # Tweet form + tweet_form = TweetForm( + on_submit=lambda: handle_tweet_submit() + ) + tweet_form.render() + + # Recent tweets + st.markdown("### Recent Tweets") + tweets = get_from_session("tweets", []) + for tweet in tweets: + TweetCard( + content=tweet["content"], + engagement_score=tweet["engagement_score"], + hashtags=tweet["hashtags"], + emojis=tweet["emojis"], + metrics=tweet["metrics"], + on_copy=lambda: copy_tweet(tweet), + on_save=lambda: save_tweet(tweet) + ).render() + +def render_analytics(): + """Render the analytics tab content.""" + st.markdown("### Tweet Analytics") + st.info("Analytics features coming soon!") + +def handle_tweet_submit(): + """Handle tweet form submission.""" + # Get form data + content = get_from_session("tweet_content") + tone = get_from_session("tone") + length = get_from_session("length") + hashtags = get_from_session("hashtags") + emojis = get_from_session("emojis") + engagement_boost = get_from_session("engagement_boost") + + # Create tweet object + tweet = { + "content": content, + "tone": tone, + "length": length, + "hashtags": hashtags, + "emojis": emojis, + "engagement_score": engagement_boost, + "metrics": { + "Engagement": engagement_boost, + "Reach": engagement_boost * 0.8, + "Growth": engagement_boost * 0.6 + } + } + + # Add to tweets list + tweets = get_from_session("tweets", []) + tweets.append(tweet) + save_to_session("tweets", tweets) + + # Show success message + show_success_message("Tweet created successfully!") + +def copy_tweet(tweet: Dict): + """Copy tweet to clipboard.""" + show_success_message("Tweet copied to clipboard!") + +def save_tweet(tweet: Dict): + """Save tweet for later.""" + show_success_message("Tweet saved!") if __name__ == "__main__": run_dashboard() \ No newline at end of file diff --git a/lib/ai_writers/twitter_writers/twitter_streamlit_ui/README.md b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/README.md new file mode 100644 index 00000000..1f3878d5 --- /dev/null +++ b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/README.md @@ -0,0 +1,203 @@ +# Twitter Streamlit UI Components + +This module provides a unified, reusable UI component library for all Twitter-related features in the AI Writer suite. It implements best practices for Streamlit UI development and ensures consistency across all Twitter tools. + +## Structure + +``` +twitter_streamlit_ui/ +├── components/ # Reusable UI components +│ ├── __init__.py +│ ├── cards.py # Card components (feature cards, tweet cards) +│ ├── forms.py # Form components (input forms, settings forms) +│ ├── navigation.py # Navigation components (tabs, sidebar) +│ ├── feedback.py # Feedback components (loading, errors, success) +│ └── layout.py # Layout components (containers, columns) +├── styles/ # CSS and styling +│ ├── __init__.py +│ ├── theme.py # Theme configuration +│ ├── components.py # Component-specific styles +│ └── animations.py # Animation styles +├── utils/ # UI utilities +│ ├── __init__.py +│ ├── state.py # State management +│ ├── validation.py # Input validation +│ └── performance.py # Performance optimizations +└── README.md # This file +``` + +## Key Improvements + +### 1. Consistent UI Components + +- **Card Components** + - Feature cards with consistent styling + - Tweet cards with standardized layout + - Status badges with unified design + +- **Form Components** + - Standardized input forms + - Consistent validation feedback + - Unified error handling + +- **Navigation Components** + - Consistent tab styling + - Standardized sidebar navigation + - Breadcrumb navigation + +### 2. Enhanced User Experience + +- **Loading States** + - Progress indicators for long operations + - Skeleton loading for content + - Smooth transitions between states + +- **Feedback Mechanisms** + - Toast notifications for actions + - Error messages with recovery options + - Success confirmations + +- **Responsive Design** + - Mobile-friendly layouts + - Adaptive column systems + - Flexible containers + +### 3. Performance Optimizations + +- **State Management** + - Centralized state handling + - Efficient data persistence + - Optimized re-rendering + +- **Resource Loading** + - Lazy loading of components + - Optimized image loading + - Cached computations + +### 4. Accessibility Features + +- **Keyboard Navigation** + - Focus management + - Keyboard shortcuts + - ARIA labels + +- **Visual Accessibility** + - High contrast themes + - Screen reader support + - Color blind friendly + +### 5. Error Handling + +- **Graceful Degradation** + - Fallback UI components + - Error boundaries + - Recovery options + +- **User Feedback** + - Clear error messages + - Actionable suggestions + - Help documentation + +## Usage + +### Basic Component Usage + +```python +from twitter_streamlit_ui.components.cards import FeatureCard +from twitter_streamlit_ui.components.forms import TweetForm +from twitter_streamlit_ui.styles.theme import apply_theme + +# Apply theme +apply_theme() + +# Use components +feature_card = FeatureCard( + title="Tweet Generator", + description="Create engaging tweets with AI", + icon="🐦" +) +feature_card.render() + +tweet_form = TweetForm() +tweet_form.render() +``` + +### State Management + +```python +from twitter_streamlit_ui.utils.state import StateManager + +# Initialize state +state = StateManager() +state.initialize() + +# Update state +state.update("current_tweet", tweet_data) +``` + +### Error Handling + +```python +from twitter_streamlit_ui.components.feedback import ErrorBoundary + +with ErrorBoundary(): + # Your code here + pass +``` + +## Best Practices + +1. **Component Reusability** + - Use existing components when possible + - Create new components only when necessary + - Follow the established patterns + +2. **State Management** + - Use the StateManager for all state + - Avoid direct session state manipulation + - Keep state updates atomic + +3. **Performance** + - Use lazy loading for heavy components + - Implement caching where appropriate + - Monitor render performance + +4. **Accessibility** + - Include ARIA labels + - Ensure keyboard navigation + - Test with screen readers + +5. **Error Handling** + - Use ErrorBoundary components + - Provide clear error messages + - Include recovery options + +## Future Improvements + +1. **Component Library** + - Add more specialized components + - Enhance existing components + - Create component documentation + +2. **Theme System** + - Add more theme options + - Implement theme switching + - Create custom theme builder + +3. **Performance** + - Implement virtual scrolling + - Add performance monitoring + - Optimize resource loading + +4. **Testing** + - Add component tests + - Implement E2E tests + - Create test documentation + +## Contributing + +1. Follow the established patterns +2. Add tests for new components +3. Update documentation +4. Ensure accessibility +5. Optimize performance \ No newline at end of file diff --git a/lib/ai_writers/twitter_writers/twitter_streamlit_ui/__init__.py b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/__init__.py new file mode 100644 index 00000000..30496c43 --- /dev/null +++ b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/__init__.py @@ -0,0 +1,66 @@ +""" +Twitter Streamlit UI package. +Provides a modern and user-friendly interface for Twitter tools. +""" + +from .dashboard import TwitterDashboard +from .components.cards import FeatureCard, TweetCard +from .components.forms import TweetForm, SettingsForm +from .components.navigation import Sidebar, Header, Tabs, Breadcrumbs +from .styles.theme import Theme +from .utils.helpers import ( + save_to_session, + get_from_session, + clear_session, + save_to_file, + load_from_file, + format_datetime, + parse_datetime, + validate_tweet_content, + validate_hashtags, + validate_emojis, + calculate_engagement_score, + generate_tweet_metrics, + copy_to_clipboard, + show_success_message, + show_error_message, + show_info_message, + show_warning_message, + create_download_button, + create_upload_button +) + +__version__ = "1.0.0" +__author__ = "AI Writer Team" + +__all__ = [ + "TwitterDashboard", + "FeatureCard", + "TweetCard", + "TweetForm", + "SettingsForm", + "Sidebar", + "Header", + "Tabs", + "Breadcrumbs", + "Theme", + "save_to_session", + "get_from_session", + "clear_session", + "save_to_file", + "load_from_file", + "format_datetime", + "parse_datetime", + "validate_tweet_content", + "validate_hashtags", + "validate_emojis", + "calculate_engagement_score", + "generate_tweet_metrics", + "copy_to_clipboard", + "show_success_message", + "show_error_message", + "show_info_message", + "show_warning_message", + "create_download_button", + "create_upload_button" +] \ No newline at end of file diff --git a/lib/ai_writers/twitter_writers/twitter_streamlit_ui/components/cards.py b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/components/cards.py new file mode 100644 index 00000000..4ffd3c54 --- /dev/null +++ b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/components/cards.py @@ -0,0 +1,174 @@ +""" +Card components for Twitter UI. +Provides consistent card layouts for features and tweets. +""" + +import streamlit as st +from typing import Dict, Any, Optional, List +from ..styles.theme import Theme + +class BaseCard: + """Base class for all card components.""" + + def __init__( + self, + title: str, + description: str, + icon: Optional[str] = None, + status: Optional[str] = None, + actions: Optional[List[Dict[str, Any]]] = None + ): + self.title = title + self.description = description + self.icon = icon + self.status = status + self.actions = actions or [] + + def render(self) -> None: + """Render the card with consistent styling.""" + with st.container(): + st.markdown(f""" +
+
+ {f'{self.icon}' if self.icon else ''} +

{self.title}

+
+

+ {self.description} +

+ {f'{self.status.title()}' if self.status else ''} +
+ """, unsafe_allow_html=True) + + if self.actions: + cols = st.columns(len(self.actions)) + for i, action in enumerate(self.actions): + with cols[i]: + if st.button( + action["label"], + key=f"action_{i}", + help=action.get("help"), + use_container_width=True + ): + action["callback"]() + +class FeatureCard(BaseCard): + """Card component for displaying features.""" + + def __init__( + self, + title: str, + description: str, + icon: str, + status: str = "active", + features: Optional[List[Dict[str, Any]]] = None, + on_click: Optional[callable] = None + ): + super().__init__(title, description, icon, status) + self.features = features or [] + self.on_click = on_click + + def render(self) -> None: + """Render the feature card with enhanced styling.""" + with st.container(): + st.markdown(f""" +
+
+ {self.icon} +

{self.title}

+
+

+ {self.description} +

+ {self.status.title()} +
+ """, unsafe_allow_html=True) + + if self.features: + for feature in self.features: + st.markdown(f""" +
+

+ {feature["name"]}: {feature["description"]} +

+
+ """, unsafe_allow_html=True) + + if self.on_click: + if st.button( + f"Launch {self.title}", + key=f"launch_{self.title.lower().replace(' ', '_')}", + use_container_width=True + ): + self.on_click() + +class TweetCard(BaseCard): + """Card component for displaying tweets.""" + + def __init__( + self, + content: str, + engagement_score: float, + hashtags: List[str], + emojis: List[str], + metrics: Optional[Dict[str, Any]] = None, + on_copy: Optional[callable] = None, + on_save: Optional[callable] = None + ): + super().__init__( + title="Tweet", + description=content, + icon="🐦", + actions=[ + { + "label": "Copy", + "callback": on_copy or (lambda: None), + "help": "Copy tweet to clipboard" + }, + { + "label": "Save", + "callback": on_save or (lambda: None), + "help": "Save tweet for later" + } + ] + ) + self.engagement_score = engagement_score + self.hashtags = hashtags + self.emojis = emojis + self.metrics = metrics or {} + + def render(self) -> None: + """Render the tweet card with metrics and actions.""" + with st.container(): + st.markdown(f""" +
+
+ {self.icon} +

Tweet

+
+

+ {self.description} +

+
+ {''.join(f'{tag}' for tag in self.hashtags)} +
+
+ {''.join(f'{emoji}' for emoji in self.emojis)} +
+
+
+ Engagement Score: {self.engagement_score}% +
+ + +
+
+
+
+ """, unsafe_allow_html=True) + + if self.metrics: + cols = st.columns(len(self.metrics)) + for i, (metric, value) in enumerate(self.metrics.items()): + with cols[i]: + st.metric(metric, f"{value}%") \ No newline at end of file diff --git a/lib/ai_writers/twitter_writers/twitter_streamlit_ui/components/forms.py b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/components/forms.py new file mode 100644 index 00000000..4d9429ac --- /dev/null +++ b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/components/forms.py @@ -0,0 +1,255 @@ +""" +Form components for Twitter UI. +Provides consistent form layouts and input validation. +""" + +import streamlit as st +from typing import Dict, Any, Optional, List, Callable +from ..styles.theme import Theme + +class BaseForm: + """Base class for all form components.""" + + def __init__( + self, + title: str, + description: Optional[str] = None, + on_submit: Optional[Callable] = None + ): + self.title = title + self.description = description + self.on_submit = on_submit + self.fields: Dict[str, Any] = {} + + def add_field( + self, + key: str, + label: str, + field_type: str = "text", + required: bool = False, + help_text: Optional[str] = None, + options: Optional[List[str]] = None, + default: Any = None, + validation: Optional[Callable] = None + ) -> None: + """Add a field to the form.""" + self.fields[key] = { + "label": label, + "type": field_type, + "required": required, + "help_text": help_text, + "options": options, + "default": default, + "validation": validation + } + + def validate(self) -> bool: + """Validate all form fields.""" + for key, field in self.fields.items(): + if field["required"] and not st.session_state.get(key): + st.error(f"{field['label']} is required") + return False + if field["validation"] and not field["validation"](st.session_state.get(key)): + return False + return True + + def render(self) -> None: + """Render the form with consistent styling.""" + with st.container(): + st.markdown(f""" +
+

{self.title}

+ {f'

{self.description}

' if self.description else ''} +
+ """, unsafe_allow_html=True) + + for key, field in self.fields.items(): + if field["type"] == "text": + st.text_input( + field["label"], + key=key, + help=field["help_text"], + value=field["default"] + ) + elif field["type"] == "textarea": + st.text_area( + field["label"], + key=key, + help=field["help_text"], + value=field["default"] + ) + elif field["type"] == "select": + st.selectbox( + field["label"], + options=field["options"], + key=key, + help=field["help_text"], + index=field["options"].index(field["default"]) if field["default"] in field["options"] else 0 + ) + elif field["type"] == "multiselect": + st.multiselect( + field["label"], + options=field["options"], + key=key, + help=field["help_text"], + default=field["default"] + ) + elif field["type"] == "number": + st.number_input( + field["label"], + key=key, + help=field["help_text"], + value=field["default"] + ) + elif field["type"] == "slider": + st.slider( + field["label"], + key=key, + help=field["help_text"], + value=field["default"] + ) + elif field["type"] == "checkbox": + st.checkbox( + field["label"], + key=key, + help=field["help_text"], + value=field["default"] + ) + + if st.button("Submit", use_container_width=True): + if self.validate() and self.on_submit: + self.on_submit() + +class TweetForm(BaseForm): + """Form component for tweet generation.""" + + def __init__( + self, + on_submit: Optional[Callable] = None, + default_tone: str = "professional", + default_length: str = "medium" + ): + super().__init__( + title="Generate Tweet", + description="Create engaging tweets with AI assistance", + on_submit=on_submit + ) + + # Add tweet content field + self.add_field( + "tweet_content", + "Tweet Content", + field_type="textarea", + required=True, + help_text="Enter your tweet content or topic" + ) + + # Add tone selection + self.add_field( + "tone", + "Tweet Tone", + field_type="select", + options=["professional", "casual", "humorous", "informative", "inspirational"], + default=default_tone, + help_text="Select the tone for your tweet" + ) + + # Add length selection + self.add_field( + "length", + "Tweet Length", + field_type="select", + options=["short", "medium", "long"], + default=default_length, + help_text="Select the desired length of your tweet" + ) + + # Add hashtag options + self.add_field( + "hashtags", + "Hashtags", + field_type="multiselect", + options=["#AI", "#Tech", "#Innovation", "#Business", "#Marketing"], + help_text="Select relevant hashtags" + ) + + # Add emoji options + self.add_field( + "emojis", + "Emojis", + field_type="multiselect", + options=["🚀", "💡", "🎯", "🔥", "✨"], + help_text="Select emojis to include" + ) + + # Add engagement settings + self.add_field( + "engagement_boost", + "Engagement Boost", + field_type="slider", + default=50, + help_text="Adjust the engagement optimization level" + ) + +class SettingsForm(BaseForm): + """Form component for user settings.""" + + def __init__( + self, + on_submit: Optional[Callable] = None, + default_settings: Optional[Dict[str, Any]] = None + ): + super().__init__( + title="User Settings", + description="Customize your Twitter experience", + on_submit=on_submit + ) + + settings = default_settings or {} + + # Add API settings + self.add_field( + "api_key", + "Twitter API Key", + field_type="text", + help_text="Enter your Twitter API key", + default=settings.get("api_key", "") + ) + + # Add theme settings + self.add_field( + "theme", + "Theme", + field_type="select", + options=["light", "dark", "system"], + default=settings.get("theme", "system"), + help_text="Select your preferred theme" + ) + + # Add notification settings + self.add_field( + "notifications", + "Enable Notifications", + field_type="checkbox", + default=settings.get("notifications", True), + help_text="Receive notifications for important updates" + ) + + # Add auto-save settings + self.add_field( + "auto_save", + "Auto-save Drafts", + field_type="checkbox", + default=settings.get("auto_save", True), + help_text="Automatically save tweet drafts" + ) + + # Add language settings + self.add_field( + "language", + "Language", + field_type="select", + options=["English", "Spanish", "French", "German", "Japanese"], + default=settings.get("language", "English"), + help_text="Select your preferred language" + ) \ No newline at end of file diff --git a/lib/ai_writers/twitter_writers/twitter_streamlit_ui/components/navigation.py b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/components/navigation.py new file mode 100644 index 00000000..3f92350b --- /dev/null +++ b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/components/navigation.py @@ -0,0 +1,222 @@ +""" +Navigation components for Twitter UI. +Provides consistent navigation and layout structure. +""" + +import streamlit as st +from typing import Dict, Any, Optional, List +from ..styles.theme import Theme + +class Sidebar: + """Sidebar navigation component.""" + + def __init__( + self, + title: str = "Twitter Tools", + logo: Optional[str] = None, + menu_items: Optional[List[Dict[str, Any]]] = None + ): + self.title = title + self.logo = logo + self.menu_items = menu_items or [] + + def add_menu_item( + self, + label: str, + icon: str, + page: str, + badge: Optional[str] = None + ) -> None: + """Add a menu item to the sidebar.""" + self.menu_items.append({ + "label": label, + "icon": icon, + "page": page, + "badge": badge + }) + + def render(self) -> None: + """Render the sidebar with consistent styling.""" + with st.sidebar: + # Logo and title + if self.logo: + st.image(self.logo, width=50) + st.markdown(f""" +

{self.title}

+ """, unsafe_allow_html=True) + + # Menu items + for item in self.menu_items: + st.markdown(f""" + + """, unsafe_allow_html=True) + + if st.button( + item["label"], + key=f"nav_{item['page']}", + use_container_width=True + ): + st.session_state["current_page"] = item["page"] + +class Header: + """Header navigation component.""" + + def __init__( + self, + title: str, + subtitle: Optional[str] = None, + actions: Optional[List[Dict[str, Any]]] = None + ): + self.title = title + self.subtitle = subtitle + self.actions = actions or [] + + def add_action( + self, + label: str, + icon: str, + callback: callable, + help_text: Optional[str] = None + ) -> None: + """Add an action button to the header.""" + self.actions.append({ + "label": label, + "icon": icon, + "callback": callback, + "help_text": help_text + }) + + def render(self) -> None: + """Render the header with consistent styling.""" + # Build action buttons HTML + action_buttons = [] + for action in self.actions: + help_text = action.get("help_text", "") + action_buttons.append(f""" + + """) + + st.markdown(f""" +
+
+

{self.title}

+ {f'

{self.subtitle}

' if self.subtitle else ""} +
+
+ {''.join(action_buttons)} +
+
+ """, unsafe_allow_html=True) + + # Add action button callbacks + for i, action in enumerate(self.actions): + if st.button( + action["label"], + key=f"header_action_{i}", + help=action.get("help_text") + ): + action["callback"]() + +class Tabs: + """Tab navigation component.""" + + def __init__( + self, + tabs: Optional[List[Dict[str, Any]]] = None, + default_tab: Optional[str] = None + ): + self.tabs = tabs or [] + self.default_tab = default_tab + + def add_tab( + self, + label: str, + icon: Optional[str] = None, + content: Optional[callable] = None + ) -> None: + """Add a tab to the navigation.""" + self.tabs.append({ + "label": label, + "icon": icon, + "content": content + }) + + def render(self) -> None: + """Render the tabs with consistent styling.""" + if not self.tabs: + return + + # Create tab labels with icons + tab_labels = [ + f"{tab['icon']} {tab['label']}" if tab.get('icon') else tab['label'] + for tab in self.tabs + ] + + # Get current tab from session state or use default + current_tab = st.session_state.get("current_tab", self.default_tab or self.tabs[0]["label"]) + + # Render tabs + selected_tab = st.tabs(tab_labels)[tab_labels.index(current_tab)] + + # Update session state + st.session_state["current_tab"] = current_tab + + # Render tab content + with selected_tab: + for tab in self.tabs: + if tab["label"] == current_tab and tab.get("content"): + tab["content"]() + +class Breadcrumbs: + """Breadcrumb navigation component.""" + + def __init__( + self, + items: Optional[List[Dict[str, Any]]] = None + ): + self.items = items or [] + + def add_item( + self, + label: str, + page: Optional[str] = None, + icon: Optional[str] = None + ) -> None: + """Add a breadcrumb item.""" + self.items.append({ + "label": label, + "page": page, + "icon": icon + }) + + def render(self) -> None: + """Render the breadcrumbs with consistent styling.""" + if not self.items: + return + + breadcrumb_items = [] + for i, item in enumerate(self.items): + icon_html = f'{item["icon"]}' if item.get("icon") else "" + link_html = f'{item["label"]}' if item.get("page") else f'{item["label"]}' + separator = f'/' if i < len(self.items) - 1 else "" + + breadcrumb_items.append(f""" + + {icon_html} + {link_html} + + {separator} + """) + + st.markdown(f""" + + """, unsafe_allow_html=True) \ No newline at end of file diff --git a/lib/ai_writers/twitter_writers/twitter_streamlit_ui/dashboard.py b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/dashboard.py new file mode 100644 index 00000000..72a7fbbe --- /dev/null +++ b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/dashboard.py @@ -0,0 +1,270 @@ +""" +Main dashboard for Twitter UI. +Combines all UI components into a cohesive interface. +""" + +import streamlit as st +from typing import Dict, Any, Optional +from .components.cards import FeatureCard, TweetCard +from .components.forms import TweetForm, SettingsForm +from .components.navigation import Sidebar, Header, Tabs, Breadcrumbs +from .styles.theme import Theme + +class TwitterDashboard: + """Main dashboard class for Twitter UI.""" + + def __init__(self): + self.setup_page() + self.setup_theme() + self.setup_navigation() + self.setup_state() + + def setup_page(self) -> None: + """Configure the Streamlit page settings.""" + st.set_page_config( + page_title="Twitter Tools", + page_icon="🐦", + layout="wide", + initial_sidebar_state="expanded" + ) + + def setup_theme(self) -> None: + """Apply the theme to the dashboard.""" + Theme().apply() + + def setup_navigation(self) -> None: + """Setup navigation components.""" + # Sidebar + self.sidebar = Sidebar( + title="Twitter Tools", + logo="assets/logo.png" + ) + + # Add menu items + self.sidebar.add_menu_item("Dashboard", "📊", "dashboard") + self.sidebar.add_menu_item("Tweet Generator", "✍️", "tweet_generator") + self.sidebar.add_menu_item("Analytics", "📈", "analytics") + self.sidebar.add_menu_item("Settings", "⚙️", "settings") + + # Header + self.header = Header( + title="Twitter Dashboard", + subtitle="Create and manage your Twitter content" + ) + + # Add header actions + self.header.add_action( + "New Tweet", + "✏️", + self.create_new_tweet, + "Create a new tweet" + ) + self.header.add_action( + "Refresh", + "🔄", + self.refresh_dashboard, + "Refresh dashboard data" + ) + + # Tabs + self.tabs = Tabs() + + # Add tabs + self.tabs.add_tab("Overview", "📊", self.render_overview) + self.tabs.add_tab("Recent Tweets", "🐦", self.render_recent_tweets) + self.tabs.add_tab("Analytics", "📈", self.render_analytics) + + # Breadcrumbs + self.breadcrumbs = Breadcrumbs() + + def setup_state(self) -> None: + """Initialize session state variables.""" + if "current_page" not in st.session_state: + st.session_state["current_page"] = "dashboard" + if "current_tab" not in st.session_state: + st.session_state["current_tab"] = "Overview" + if "tweets" not in st.session_state: + st.session_state["tweets"] = [] + + def create_new_tweet(self) -> None: + """Handle new tweet creation.""" + st.session_state["current_page"] = "tweet_generator" + + def refresh_dashboard(self) -> None: + """Refresh dashboard data.""" + st.experimental_rerun() + + def render_overview(self) -> None: + """Render the overview tab content.""" + # Feature cards + col1, col2, col3 = st.columns(3) + + with col1: + FeatureCard( + title="Tweet Generator", + description="Create engaging tweets with AI assistance", + icon="✍️", + features=[ + { + "name": "AI-Powered", + "description": "Generate tweets using advanced AI" + }, + { + "name": "Customizable", + "description": "Adjust tone, length, and style" + } + ], + on_click=self.create_new_tweet + ).render() + + with col2: + FeatureCard( + title="Analytics", + description="Track your tweet performance", + icon="📈", + features=[ + { + "name": "Engagement", + "description": "Monitor likes, retweets, and replies" + }, + { + "name": "Growth", + "description": "Track follower growth over time" + } + ] + ).render() + + with col3: + FeatureCard( + title="Settings", + description="Customize your experience", + icon="⚙️", + features=[ + { + "name": "Preferences", + "description": "Set your default options" + }, + { + "name": "API", + "description": "Configure Twitter API settings" + } + ] + ).render() + + def render_recent_tweets(self) -> None: + """Render the recent tweets tab content.""" + # Tweet form + tweet_form = TweetForm( + on_submit=self.handle_tweet_submit + ) + tweet_form.render() + + # Recent tweets + st.markdown("### Recent Tweets") + + for tweet in st.session_state["tweets"]: + TweetCard( + content=tweet["content"], + engagement_score=tweet["engagement_score"], + hashtags=tweet["hashtags"], + emojis=tweet["emojis"], + metrics=tweet["metrics"], + on_copy=lambda: self.copy_tweet(tweet), + on_save=lambda: self.save_tweet(tweet) + ).render() + + def render_analytics(self) -> None: + """Render the analytics tab content.""" + # Analytics content + st.markdown("### Tweet Analytics") + + # Placeholder for analytics charts + st.info("Analytics features coming soon!") + + def handle_tweet_submit(self) -> None: + """Handle tweet form submission.""" + # Get form data + content = st.session_state["tweet_content"] + tone = st.session_state["tone"] + length = st.session_state["length"] + hashtags = st.session_state["hashtags"] + emojis = st.session_state["emojis"] + engagement_boost = st.session_state["engagement_boost"] + + # Create tweet object + tweet = { + "content": content, + "tone": tone, + "length": length, + "hashtags": hashtags, + "emojis": emojis, + "engagement_score": engagement_boost, + "metrics": { + "Engagement": engagement_boost, + "Reach": engagement_boost * 0.8, + "Growth": engagement_boost * 0.6 + } + } + + # Add to tweets list + st.session_state["tweets"].append(tweet) + + # Show success message + st.success("Tweet created successfully!") + + def copy_tweet(self, tweet: Dict[str, Any]) -> None: + """Copy tweet to clipboard.""" + st.write("Tweet copied to clipboard!") + + def save_tweet(self, tweet: Dict[str, Any]) -> None: + """Save tweet for later.""" + st.write("Tweet saved!") + + def render(self) -> None: + """Render the complete dashboard.""" + # Render navigation + self.sidebar.render() + self.header.render() + self.breadcrumbs.render() + + # Render content based on current page + if st.session_state["current_page"] == "dashboard": + self.tabs.render() + elif st.session_state["current_page"] == "tweet_generator": + self.render_recent_tweets() + elif st.session_state["current_page"] == "analytics": + self.render_analytics() + elif st.session_state["current_page"] == "settings": + settings_form = SettingsForm( + on_submit=self.handle_settings_submit + ) + settings_form.render() + + def handle_settings_submit(self) -> None: + """Handle settings form submission.""" + # Get form data + api_key = st.session_state["api_key"] + theme = st.session_state["theme"] + notifications = st.session_state["notifications"] + auto_save = st.session_state["auto_save"] + language = st.session_state["language"] + + # Save settings + st.session_state["settings"] = { + "api_key": api_key, + "theme": theme, + "notifications": notifications, + "auto_save": auto_save, + "language": language + } + + # Show success message + st.success("Settings saved successfully!") + +def main(): + """Main entry point for the dashboard.""" + dashboard = TwitterDashboard() + dashboard.render() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lib/ai_writers/twitter_writers/twitter_streamlit_ui/styles/theme.py b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/styles/theme.py new file mode 100644 index 00000000..b96cfdf4 --- /dev/null +++ b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/styles/theme.py @@ -0,0 +1,173 @@ +""" +Theme configuration for Twitter UI components. +Provides consistent styling across all Twitter-related features. +""" + +import streamlit as st +from typing import Dict, Any + +class Theme: + """Theme configuration for Twitter UI components.""" + + # Color palette + COLORS = { + "primary": "#1DA1F2", # Twitter blue + "secondary": "#14171A", # Dark blue + "background": "#15202B", # Dark background + "text": "#FFFFFF", # White text + "text_secondary": "#8899A6", # Gray text + "success": "#17BF63", # Green + "warning": "#FFAD1F", # Yellow + "error": "#E0245E", # Red + "border": "rgba(255, 255, 255, 0.1)", # Subtle border + } + + # Typography + TYPOGRAPHY = { + "font_family": "'Helvetica Neue', sans-serif", + "font_sizes": { + "h1": "2.5rem", + "h2": "2rem", + "h3": "1.5rem", + "body": "1rem", + "small": "0.875rem", + }, + "font_weights": { + "regular": 400, + "medium": 500, + "bold": 700, + }, + } + + # Spacing + SPACING = { + "xs": "0.25rem", + "sm": "0.5rem", + "md": "1rem", + "lg": "1.5rem", + "xl": "2rem", + } + + # Border radius + BORDER_RADIUS = { + "sm": "4px", + "md": "8px", + "lg": "12px", + "xl": "16px", + "full": "9999px", + } + + # Shadows + SHADOWS = { + "sm": "0 1px 2px rgba(0, 0, 0, 0.05)", + "md": "0 4px 6px rgba(0, 0, 0, 0.1)", + "lg": "0 10px 15px rgba(0, 0, 0, 0.1)", + "xl": "0 20px 25px rgba(0, 0, 0, 0.15)", + } + + # Transitions + TRANSITIONS = { + "fast": "0.15s ease", + "normal": "0.3s ease", + "slow": "0.5s ease", + } + + @classmethod + def get_css(cls) -> str: + """Get the complete CSS for the theme.""" + return f""" + /* Base styles */ + .stApp {{ + background-color: {cls.COLORS['background']}; + color: {cls.COLORS['text']}; + font-family: {cls.TYPOGRAPHY['font_family']}; + }} + + /* Typography */ + h1, h2, h3, h4, h5, h6 {{ + color: {cls.COLORS['text']}; + font-family: {cls.TYPOGRAPHY['font_family']}; + font-weight: {cls.TYPOGRAPHY['font_weights']['bold']}; + }} + + /* Buttons */ + .stButton > button {{ + background: linear-gradient(45deg, {cls.COLORS['primary']}, #0C85D0); + color: {cls.COLORS['text']}; + border: none; + padding: {cls.SPACING['md']} {cls.SPACING['lg']}; + border-radius: {cls.BORDER_RADIUS['full']}; + font-weight: {cls.TYPOGRAPHY['font_weights']['medium']}; + transition: all {cls.TRANSITIONS['normal']}; + box-shadow: {cls.SHADOWS['md']}; + }} + + .stButton > button:hover {{ + transform: translateY(-2px); + box-shadow: {cls.SHADOWS['lg']}; + }} + + /* Cards */ + .card {{ + background: rgba(255, 255, 255, 0.05); + border: 1px solid {cls.COLORS['border']}; + border-radius: {cls.BORDER_RADIUS['lg']}; + padding: {cls.SPACING['lg']}; + margin-bottom: {cls.SPACING['md']}; + backdrop-filter: blur(10px); + transition: transform {cls.TRANSITIONS['normal']}; + }} + + .card:hover {{ + transform: translateY(-4px); + }} + + /* Forms */ + .stTextInput > div > div > input {{ + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid {cls.COLORS['border']}; + border-radius: {cls.BORDER_RADIUS['md']}; + color: {cls.COLORS['text']}; + padding: {cls.SPACING['md']}; + }} + + /* Tabs */ + .stTabs [data-baseweb="tab-list"] {{ + gap: {cls.SPACING['sm']}; + background-color: rgba(0, 0, 0, 0.2); + padding: {cls.SPACING['md']}; + border-radius: {cls.BORDER_RADIUS['lg']}; + }} + + .stTabs [data-baseweb="tab"] {{ + background-color: transparent; + color: {cls.COLORS['text']}; + border: 1px solid {cls.COLORS['border']}; + border-radius: {cls.BORDER_RADIUS['md']}; + padding: {cls.SPACING['sm']} {cls.SPACING['md']}; + }} + + /* Status badges */ + .status-badge {{ + display: inline-block; + padding: {cls.SPACING['xs']} {cls.SPACING['md']}; + border-radius: {cls.BORDER_RADIUS['full']}; + font-size: {cls.TYPOGRAPHY['font_sizes']['small']}; + font-weight: {cls.TYPOGRAPHY['font_weights']['medium']}; + }} + + .status-active {{ + background: linear-gradient(45deg, {cls.COLORS['success']}, #69F0AE); + color: {cls.COLORS['secondary']}; + }} + + .status-coming-soon {{ + background: linear-gradient(45deg, {cls.COLORS['warning']}, #FFA000); + color: {cls.COLORS['secondary']}; + }} + """ + + @classmethod + def apply(cls) -> None: + """Apply the theme to the Streamlit app.""" + st.markdown(f"", unsafe_allow_html=True) \ No newline at end of file diff --git a/lib/ai_writers/twitter_writers/twitter_streamlit_ui/utils/helpers.py b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/utils/helpers.py new file mode 100644 index 00000000..2c36ab09 --- /dev/null +++ b/lib/ai_writers/twitter_writers/twitter_streamlit_ui/utils/helpers.py @@ -0,0 +1,194 @@ +""" +Utility functions for Twitter UI. +Provides helper functions for common operations. +""" + +import streamlit as st +from typing import Dict, Any, List, Optional +import json +import os +from datetime import datetime + +def save_to_session(key: str, value: Any) -> None: + """Save a value to the session state.""" + st.session_state[key] = value + +def get_from_session(key: str, default: Any = None) -> Any: + """Get a value from the session state.""" + return st.session_state.get(key, default) + +def clear_session() -> None: + """Clear all session state variables.""" + for key in list(st.session_state.keys()): + del st.session_state[key] + +def save_to_file(data: Dict[str, Any], filename: str) -> None: + """Save data to a JSON file.""" + try: + with open(filename, 'w') as f: + json.dump(data, f, indent=4) + except Exception as e: + st.error(f"Error saving data: {str(e)}") + +def load_from_file(filename: str) -> Optional[Dict[str, Any]]: + """Load data from a JSON file.""" + try: + if os.path.exists(filename): + with open(filename, 'r') as f: + return json.load(f) + except Exception as e: + st.error(f"Error loading data: {str(e)}") + return None + +def format_datetime(dt: datetime) -> str: + """Format a datetime object for display.""" + return dt.strftime("%Y-%m-%d %H:%M:%S") + +def parse_datetime(dt_str: str) -> Optional[datetime]: + """Parse a datetime string.""" + try: + return datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S") + except ValueError: + return None + +def validate_tweet_content(content: str) -> bool: + """Validate tweet content.""" + if not content: + st.error("Tweet content cannot be empty") + return False + if len(content) > 280: + st.error("Tweet content cannot exceed 280 characters") + return False + return True + +def validate_hashtags(hashtags: List[str]) -> bool: + """Validate hashtags.""" + for tag in hashtags: + if not tag.startswith('#'): + st.error(f"Hashtag {tag} must start with #") + return False + if len(tag) > 30: + st.error(f"Hashtag {tag} cannot exceed 30 characters") + return False + return True + +def validate_emojis(emojis: List[str]) -> bool: + """Validate emojis.""" + for emoji in emojis: + if len(emoji) != 1: + st.error(f"Invalid emoji: {emoji}") + return False + return True + +def calculate_engagement_score( + content: str, + hashtags: List[str], + emojis: List[str], + tone: str +) -> float: + """Calculate engagement score for a tweet.""" + score = 0.0 + + # Content length score (optimal length is 100-150 characters) + content_length = len(content) + if 100 <= content_length <= 150: + score += 30 + elif 50 <= content_length <= 200: + score += 20 + else: + score += 10 + + # Hashtag score (optimal number is 2-3 hashtags) + hashtag_count = len(hashtags) + if 2 <= hashtag_count <= 3: + score += 20 + elif 1 <= hashtag_count <= 4: + score += 15 + else: + score += 5 + + # Emoji score (optimal number is 1-2 emojis) + emoji_count = len(emojis) + if 1 <= emoji_count <= 2: + score += 20 + elif 0 <= emoji_count <= 3: + score += 15 + else: + score += 5 + + # Tone score + tone_scores = { + "professional": 15, + "casual": 20, + "humorous": 25, + "informative": 15, + "inspirational": 20 + } + score += tone_scores.get(tone, 10) + + return min(score, 100) + +def generate_tweet_metrics(engagement_score: float) -> Dict[str, float]: + """Generate metrics for a tweet based on engagement score.""" + return { + "Engagement": engagement_score, + "Reach": engagement_score * 0.8, + "Growth": engagement_score * 0.6 + } + +def copy_to_clipboard(text: str) -> None: + """Copy text to clipboard.""" + try: + st.write(f'', unsafe_allow_html=True) + except Exception as e: + st.error(f"Error copying to clipboard: {str(e)}") + +def show_success_message(message: str) -> None: + """Show a success message.""" + st.success(message) + +def show_error_message(message: str) -> None: + """Show an error message.""" + st.error(message) + +def show_info_message(message: str) -> None: + """Show an info message.""" + st.info(message) + +def show_warning_message(message: str) -> None: + """Show a warning message.""" + st.warning(message) + +def create_download_button( + data: Dict[str, Any], + filename: str, + button_text: str = "Download" +) -> None: + """Create a download button for data.""" + try: + json_str = json.dumps(data, indent=4) + st.download_button( + label=button_text, + data=json_str, + file_name=filename, + mime="application/json" + ) + except Exception as e: + st.error(f"Error creating download button: {str(e)}") + +def create_upload_button( + on_upload: callable, + button_text: str = "Upload", + file_types: List[str] = ["json"] +) -> None: + """Create an upload button for data.""" + try: + uploaded_file = st.file_uploader( + button_text, + type=file_types + ) + if uploaded_file is not None: + data = json.load(uploaded_file) + on_upload(data) + except Exception as e: + st.error(f"Error handling upload: {str(e)}") \ No newline at end of file diff --git a/lib/ai_writers/youtube_writers/modules/channel_trailer_generator.py b/lib/ai_writers/youtube_writers/modules/channel_trailer_generator.py new file mode 100644 index 00000000..15f53326 --- /dev/null +++ b/lib/ai_writers/youtube_writers/modules/channel_trailer_generator.py @@ -0,0 +1,1079 @@ +""" +YouTube Channel Trailer Generator + +This module generates professional channel trailers for YouTube channels using AI. +""" + +import streamlit as st +import json +from pathlib import Path +from typing import Dict, List, Optional, Tuple, Any +import sys +import os +from gtts import gTTS +import tempfile +import base64 +from io import BytesIO +from datetime import datetime +import logging + +# Add the project root to the Python path +project_root = str(Path(__file__).parent.parent.parent.parent.parent) +if project_root not in sys.path: + sys.path.append(project_root) + +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen +from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image +from lib.utils.save_to_file import save_to_file, save_audio, save_json, save_text + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class TrailerGeneratorError(Exception): + """Custom exception for trailer generator errors.""" + pass + +def validate_channel_info(channel_info: Dict) -> Tuple[bool, str]: + """Validate channel information before processing.""" + required_fields = ['channel_name', 'channel_niche', 'target_audience', 'key_topics', 'unique_points'] + + # Check for missing required fields + for field in required_fields: + if not channel_info.get(field): + return False, f"Missing required field: {field}" + + # Validate field lengths + if len(channel_info['channel_name']) < 3: + return False, "Channel name must be at least 3 characters long" + + if len(channel_info['target_audience']) < 10: + return False, "Target audience description must be at least 10 characters long" + + if len(channel_info['key_topics']) < 10: + return False, "Key topics must be at least 10 characters long" + + if len(channel_info['unique_points']) < 10: + return False, "Unique selling points must be at least 10 characters long" + + return True, "" + +def handle_voice_over_error(error: Exception) -> None: + """Handle voice-over generation errors gracefully.""" + error_messages = { + "ConnectionError": "Unable to connect to the text-to-speech service. Please check your internet connection.", + "ValueError": "Invalid text input for voice-over generation.", + "Exception": f"An error occurred: {str(error)}" + } + error_type = type(error).__name__ + error_message = error_messages.get(error_type, error_messages["Exception"]) + logger.error(f"Voice-over generation error: {error_message}") + st.error(error_message) + +def validate_script(script: Dict) -> Tuple[bool, str]: + """Validate the generated script.""" + required_sections = ['hook', 'introduction', 'showcase', 'value_proposition', 'call_to_action'] + + # Check for missing sections + for section in required_sections: + if section not in script: + return False, f"Missing required section: {section}" + + # Validate section content + for section, content in script.items(): + if not content.get('text'): + return False, f"Missing text in section: {section}" + if not content.get('duration'): + return False, f"Missing duration in section: {section}" + + # Validate total duration + total_duration = sum(float(content['duration'].split()[0]) for content in script.values()) + if total_duration > 90: # 90 seconds max + return False, f"Total duration ({total_duration}s) exceeds maximum allowed (90s)" + + return True, "" + +def validate_narration(narration: Dict) -> Tuple[bool, str]: + """Validate the generated narration script.""" + if not narration.get('narration'): + return False, "Missing narration content" + + required_sections = ['hook', 'introduction', 'showcase', 'value_proposition', 'call_to_action'] + for section in required_sections: + if section not in narration['narration']: + return False, f"Missing narration for section: {section}" + + section_content = narration['narration'][section] + required_fields = ['text', 'voice_style', 'emotion', 'pauses', 'emphasis'] + for field in required_fields: + if field not in section_content: + return False, f"Missing {field} in {section} narration" + + return True, "" + +# Voice-over configuration +VOICE_OPTIONS = { + "languages": { + "en": { + "name": "English", + "voices": ["en-US", "en-GB", "en-AU", "en-IN"], + "default_voice": "en-US" + }, + "es": { + "name": "Spanish", + "voices": ["es-ES", "es-MX", "es-AR"], + "default_voice": "es-ES" + }, + "fr": { + "name": "French", + "voices": ["fr-FR", "fr-CA"], + "default_voice": "fr-FR" + }, + "de": { + "name": "German", + "voices": ["de-DE", "de-AT"], + "default_voice": "de-DE" + }, + "ja": { + "name": "Japanese", + "voices": ["ja-JP"], + "default_voice": "ja-JP" + } + }, + "voice_styles": { + "professional": { + "name": "Professional", + "description": "Clear, confident, and authoritative tone", + "pace": "moderate", + "pitch": "medium" + }, + "casual": { + "name": "Casual", + "description": "Friendly and conversational tone", + "pace": "slightly_fast", + "pitch": "medium_high" + }, + "enthusiastic": { + "name": "Enthusiastic", + "description": "Energetic and engaging tone", + "pace": "fast", + "pitch": "high" + }, + "calm": { + "name": "Calm", + "description": "Relaxed and soothing tone", + "pace": "slow", + "pitch": "low" + }, + "energetic": { + "name": "Energetic", + "description": "Dynamic and vibrant tone", + "pace": "fast", + "pitch": "medium_high" + } + }, + "pacing_options": { + "very_slow": 0.5, + "slow": 0.75, + "moderate": 1.0, + "slightly_fast": 1.25, + "fast": 1.5 + } +} + +def get_voice_over_options() -> Dict: + """Get available voice-over options.""" + return VOICE_OPTIONS + +def get_voice_style_settings(style: str) -> Dict: + """Get settings for a specific voice style.""" + return VOICE_OPTIONS["voice_styles"].get(style, VOICE_OPTIONS["voice_styles"]["professional"]) + +def adjust_text_for_voice_style(text: str, style: str) -> str: + """Adjust text to better match the selected voice style.""" + style_settings = get_voice_style_settings(style) + + # Add pauses and emphasis based on style + if style == "professional": + # Add strategic pauses for clarity + text = text.replace(".", ". [pause]") + text = text.replace("!", "! [pause]") + elif style == "casual": + # Add conversational markers + text = text.replace(".", "...") + elif style == "enthusiastic": + # Add emphasis markers + text = text.replace("!", "! [emphasis]") + elif style == "calm": + # Add longer pauses + text = text.replace(".", ". [long_pause]") + elif style == "energetic": + # Add dynamic emphasis + text = text.replace("!", "! [dynamic_emphasis]") + + return text + +@st.cache_data(ttl=3600) # Cache for 1 hour +def generate_voice_over( + text: str, + language: str = 'en', + voice_style: str = 'professional', + slow: bool = False +) -> bytes: + """Generate voice-over audio using gTTS with enhanced options.""" + try: + # Get language settings + lang_settings = VOICE_OPTIONS["languages"].get(language, VOICE_OPTIONS["languages"]["en"]) + voice = lang_settings["default_voice"] + + # Adjust text based on voice style + adjusted_text = adjust_text_for_voice_style(text, voice_style) + + # Get style settings + style_settings = get_voice_style_settings(voice_style) + + # Generate voice-over + tts = gTTS( + text=adjusted_text, + lang=voice, + slow=slow + ) + + audio_file = BytesIO() + tts.write_to_fp(audio_file) + audio_file.seek(0) + return audio_file.getvalue() + except Exception as e: + handle_voice_over_error(e) + return None + +def display_voice_over_options() -> Dict: + """Display voice-over options in the UI and return selected options.""" + st.markdown("### 🎤 Voice-over Settings") + + # Language selection + language = st.selectbox( + "Language", + options=list(VOICE_OPTIONS["languages"].keys()), + format_func=lambda x: VOICE_OPTIONS["languages"][x]["name"], + help="Select the language for your voice-over" + ) + + # Voice style selection + voice_style = st.selectbox( + "Voice Style", + options=list(VOICE_OPTIONS["voice_styles"].keys()), + format_func=lambda x: VOICE_OPTIONS["voice_styles"][x]["name"], + help="Select the style of voice-over" + ) + + # Display style description + style_info = VOICE_OPTIONS["voice_styles"][voice_style] + st.markdown(f"**Style Description:** {style_info['description']}") + + # Pace selection + pace = st.select_slider( + "Speaking Pace", + options=list(VOICE_OPTIONS["pacing_options"].keys()), + value=style_info["pace"], + help="Adjust the speaking pace of the voice-over" + ) + + return { + "language": language, + "voice_style": voice_style, + "pace": pace + } + +@st.cache_data(ttl=3600) # Cache for 1 hour +def generate_trailer_script( + channel_name: str, + channel_niche: str, + target_audience: str, + key_topics: str, + unique_points: str, + trailer_length: str +) -> Dict: + """Generate the trailer script using GPT with caching.""" + try: + prompt = f"""Create a professional and engaging YouTube channel trailer script for a {channel_niche} channel named '{channel_name}'. + + Channel Details: + - Target Audience: {target_audience} + - Key Topics: {key_topics} + - Unique Selling Points: {unique_points} + - Desired Length: {trailer_length} + + The script should follow this detailed structure: + + 1. Hook (5-10 seconds): + - Start with a powerful question, statement, or visual hook + - Address the viewer's pain points or interests + - Create immediate curiosity + - Use dynamic language and emotional triggers + Visual Requirements: + - Dynamic opening shot or animation + - Text overlay with key hook phrase + - Background elements that match the channel theme + - Smooth camera movement or transition effects + + 2. Channel Introduction (10-15 seconds): + - Clearly state the channel name + - Explain what makes this channel unique + - Establish credibility and expertise + - Use confident, engaging language + Visual Requirements: + - Channel logo reveal animation + - Brand colors and typography + - Professional backdrop or setting + - Subtle motion graphics + + 3. Content Showcase (10-20 seconds): + - Highlight 2-3 key content types + - Show the value and benefits of watching + - Include specific examples of content + - Use dynamic transitions between topics + Visual Requirements: + - Split-screen or grid layout for content previews + - Thumbnail-style frames for each content type + - Dynamic transitions between content examples + - Overlay graphics showing key statistics or achievements + + 4. Value Proposition (5-10 seconds): + - Clearly state what viewers will gain + - Emphasize unique benefits + - Address viewer's needs and desires + - Use compelling language + Visual Requirements: + - Benefit-focused graphics or icons + - Animated text highlights + - Background elements that reinforce the value + - Professional color scheme + + 5. Call to Action (5-10 seconds): + - Clear subscription prompt + - Mention notification bell + - Create urgency or FOMO + - End with channel branding + Visual Requirements: + - Subscription button animation + - Notification bell icon + - Channel branding elements + - Final logo reveal + + Additional Requirements: + - Keep language conversational and engaging + - Use active voice and present tense + - Include specific numbers and examples + - Maintain consistent tone throughout + - Ensure smooth transitions between sections + - Optimize for the selected duration ({trailer_length}) + + Format the response as a JSON with the following structure: + {{ + "hook": {{ + "text": "the hook text", + "duration": "estimated duration in seconds", + "visual_suggestions": {{ + "main_visual": "primary visual element description", + "text_overlay": "text overlay style and content", + "background": "background elements and effects", + "transitions": ["transition effects"], + "color_scheme": "color palette suggestions" + }} + }}, + "introduction": {{ + "text": "the introduction text", + "duration": "estimated duration in seconds", + "visual_suggestions": {{ + "logo_animation": "logo reveal animation style", + "typography": "text style and animation", + "background": "background elements", + "motion_graphics": ["motion graphic elements"], + "color_scheme": "color palette suggestions" + }} + }}, + "showcase": {{ + "text": "the showcase text", + "duration": "estimated duration in seconds", + "visual_suggestions": {{ + "layout": "content showcase layout style", + "thumbnails": ["thumbnail style descriptions"], + "transitions": ["transition effects between content"], + "overlay_graphics": ["overlay elements"], + "color_scheme": "color palette suggestions" + }} + }}, + "value_proposition": {{ + "text": "the value proposition text", + "duration": "estimated duration in seconds", + "visual_suggestions": {{ + "benefit_graphics": ["benefit-focused visual elements"], + "text_animation": "text animation style", + "background": "background elements", + "icons": ["icon suggestions"], + "color_scheme": "color palette suggestions" + }} + }}, + "call_to_action": {{ + "text": "the call to action text", + "duration": "estimated duration in seconds", + "visual_suggestions": {{ + "cta_animation": "call-to-action animation style", + "button_design": "subscription button design", + "notification_icon": "notification bell design", + "branding": "final branding elements", + "color_scheme": "color palette suggestions" + }} + }}, + "total_duration": "total estimated duration in seconds", + "notes": {{ + "tone": "suggested tone and style", + "music_suggestions": ["suggested music types or moods"], + "transitions": ["suggested transition effects"], + "special_effects": ["suggested special effects"], + "production_tips": ["specific production recommendations"], + "visual_consistency": "guidelines for maintaining visual consistency", + "brand_guidelines": "specific brand implementation guidelines" + }} + }} + + Ensure the script is optimized for the {trailer_length} format and maintains high engagement throughout. + """ + + response = get_gpt_response(prompt) + script = json.loads(response) + + # Validate the generated script + is_valid, error_message = validate_script(script) + if not is_valid: + raise TrailerGeneratorError(f"Invalid script generated: {error_message}") + + return script + except json.JSONDecodeError: + logger.error("Error parsing GPT response as JSON") + raise TrailerGeneratorError("Error generating script. Please try again.") + except Exception as e: + logger.error(f"Error generating script: {str(e)}") + raise TrailerGeneratorError(f"An error occurred: {str(e)}") + +@st.cache_data(ttl=3600) # Cache for 1 hour +def generate_narration_script(script: Dict) -> Dict: + """Generate a natural-sounding narration script from the trailer script with caching.""" + try: + prompt = f"""Convert this YouTube channel trailer script into a natural-sounding narration script. + The script should be engaging, conversational, and optimized for voice-over delivery. + + Original Script: + {json.dumps(script, indent=2)} + + Requirements: + 1. Maintain the same structure and timing + 2. Add natural pauses and emphasis + 3. Include voice modulation suggestions + 4. Add emotional cues + 5. Make it sound conversational + 6. Include pronunciation guides for any technical terms + 7. Add emphasis markers for important points + + Format the response as a JSON with the following structure: + {{ + "narration": {{ + "hook": {{ + "text": "narration text with emphasis and pauses", + "voice_style": "suggested voice style", + "emotion": "suggested emotion", + "pauses": ["pause points"], + "emphasis": ["words to emphasize"] + }}, + "introduction": {{ + "text": "narration text with emphasis and pauses", + "voice_style": "suggested voice style", + "emotion": "suggested emotion", + "pauses": ["pause points"], + "emphasis": ["words to emphasize"] + }}, + "showcase": {{ + "text": "narration text with emphasis and pauses", + "voice_style": "suggested voice style", + "emotion": "suggested emotion", + "pauses": ["pause points"], + "emphasis": ["words to emphasize"] + }}, + "value_proposition": {{ + "text": "narration text with emphasis and pauses", + "voice_style": "suggested voice style", + "emotion": "suggested emotion", + "pauses": ["pause points"], + "emphasis": ["words to emphasize"] + }}, + "call_to_action": {{ + "text": "narration text with emphasis and pauses", + "voice_style": "suggested voice style", + "emotion": "suggested emotion", + "pauses": ["pause points"], + "emphasis": ["words to emphasize"] + }} + }}, + "voice_guidelines": {{ + "overall_tone": "suggested overall tone", + "pace": "suggested speaking pace", + "energy_level": "suggested energy level", + "pronunciation_guide": {{ + "term": "pronunciation" + }}, + "special_instructions": ["special voice-over instructions"] + }} + }} + """ + + response = get_gpt_response(prompt) + narration = json.loads(response) + + # Validate the generated narration + is_valid, error_message = validate_narration(narration) + if not is_valid: + raise TrailerGeneratorError(f"Invalid narration generated: {error_message}") + + return narration + except json.JSONDecodeError: + logger.error("Error parsing GPT response as JSON") + raise TrailerGeneratorError("Error generating narration script. Please try again.") + except Exception as e: + logger.error(f"Error generating narration script: {str(e)}") + raise TrailerGeneratorError(f"An error occurred: {str(e)}") + +def update_session_state(key: str, value: Any) -> None: + """Update session state with feedback.""" + st.session_state[key] = value + st.success(f"{key.replace('_', ' ').title()} updated successfully!") + +def write_yt_channel_trailer(): + """Generate a YouTube channel trailer script and visual elements.""" + + st.title("🎥 YouTube Channel Trailer Generator") + st.markdown("Create an engaging channel trailer that converts visitors into subscribers.") + + # Initialize session state for workflow + if 'current_step' not in st.session_state: + st.session_state.current_step = 1 + if 'channel_info' not in st.session_state: + st.session_state.channel_info = {} + if 'script' not in st.session_state: + st.session_state.script = None + if 'visuals' not in st.session_state: + st.session_state.visuals = None + if 'editing_section' not in st.session_state: + st.session_state.editing_section = None + if 'narration' not in st.session_state: + st.session_state.narration = None + if 'voice_overs' not in st.session_state: + st.session_state.voice_overs = {} + if 'errors' not in st.session_state: + st.session_state.errors = [] + + # Progress bar + progress_text = { + 1: "Channel Information", + 2: "Script Generation", + 3: "Visual Elements", + 4: "Review & Edit", + 5: "Final Output" + } + st.progress((st.session_state.current_step - 1) / 4) + st.markdown(f"**Step {st.session_state.current_step}/5: {progress_text[st.session_state.current_step]}**") + + # Display any errors + if st.session_state.errors: + for error in st.session_state.errors: + st.error(error) + st.session_state.errors = [] + + # Step 1: Channel Information + if st.session_state.current_step == 1: + with st.expander("Channel Information", expanded=True): + st.markdown(""" + ### 📝 Basic Information + Let's start by gathering some basic information about your channel. + This will help us create a trailer that perfectly represents your brand. + """) + + channel_name = st.text_input("Channel Name", + value=st.session_state.channel_info.get('channel_name', ''), + help="Enter your YouTube channel name") + + channel_niche = st.text_input("Channel Niche/Category", + value=st.session_state.channel_info.get('channel_niche', ''), + help="e.g., Tech Reviews, Cooking, Gaming, etc.") + + st.markdown("### 👥 Target Audience") + st.markdown("Describe who your content is for. Be specific about demographics, interests, and needs.") + target_audience = st.text_area("Target Audience", + value=st.session_state.channel_info.get('target_audience', ''), + help="Describe your target audience in detail") + + st.markdown("### 📚 Content Types") + st.markdown("What kind of content do you create? List your main content types and topics.") + key_topics = st.text_area("Key Topics/Content Types", + value=st.session_state.channel_info.get('key_topics', ''), + help="List the main types of content you create") + + st.markdown("### ✨ Unique Selling Points") + st.markdown("What makes your channel different? Highlight your unique features and value.") + unique_points = st.text_area("Unique Selling Points", + value=st.session_state.channel_info.get('unique_points', ''), + help="What makes your channel different?") + + col1, col2 = st.columns(2) + with col1: + trailer_length = st.selectbox( + "Trailer Length", + ["30 seconds", "60 seconds", "90 seconds"], + index=["30 seconds", "60 seconds", "90 seconds"].index( + st.session_state.channel_info.get('trailer_length', "60 seconds") + ), + help="Choose the desired length of your channel trailer" + ) + with col2: + brand_colors = st.color_picker( + "Brand Color", + value=st.session_state.channel_info.get('brand_colors', "#FF0000"), + help="Select your brand's primary color" + ) + + # Save channel info + st.session_state.channel_info = { + 'channel_name': channel_name, + 'channel_niche': channel_niche, + 'target_audience': target_audience, + 'key_topics': key_topics, + 'unique_points': unique_points, + 'trailer_length': trailer_length, + 'brand_colors': brand_colors + } + + if st.button("Next: Generate Script"): + # Validate channel information + is_valid, error_message = validate_channel_info(st.session_state.channel_info) + if not is_valid: + st.session_state.errors.append(error_message) + st.rerun() + else: + st.session_state.current_step = 2 + st.rerun() + + # Step 2: Script Generation + elif st.session_state.current_step == 2: + st.markdown("### 📝 Script Generation") + st.markdown(""" + We'll now generate a script for your channel trailer. + The script will be divided into key sections, each with specific timing and visual suggestions. + """) + + if st.button("Generate Script"): + try: + with st.spinner("Generating your channel trailer script..."): + script = generate_trailer_script( + channel_name=st.session_state.channel_info['channel_name'], + channel_niche=st.session_state.channel_info['channel_niche'], + target_audience=st.session_state.channel_info['target_audience'], + key_topics=st.session_state.channel_info['key_topics'], + unique_points=st.session_state.channel_info['unique_points'], + trailer_length=st.session_state.channel_info['trailer_length'] + ) + update_session_state('script', script) + + # Generate narration script + with st.spinner("Generating narration script..."): + narration = generate_narration_script(script) + update_session_state('narration', narration) + except TrailerGeneratorError as e: + st.session_state.errors.append(str(e)) + st.rerun() + + if st.session_state.script and st.session_state.narration: + # Display script sections with edit buttons and voice-over options + for section in ["hook", "introduction", "showcase", "value_proposition", "call_to_action"]: + with st.expander(f"{section.replace('_', ' ').title()}", expanded=True): + st.markdown(f"**Duration:** {st.session_state.script[section]['duration']}") + st.markdown(f"**Text:** {st.session_state.script[section]['text']}") + + # Display narration details + st.markdown("### 🎤 Narration") + narration_section = st.session_state.narration['narration'][section] + st.markdown(f"**Voice Style:** {narration_section['voice_style']}") + st.markdown(f"**Emotion:** {narration_section['emotion']}") + st.markdown("**Emphasis Points:**") + for point in narration_section['emphasis']: + st.markdown(f"- {point}") + + # Voice-over generation with enhanced options + st.markdown("### 🎙️ Generate Voice-over") + voice_options = display_voice_over_options() + + if st.button(f"Generate Voice-over for {section.replace('_', ' ').title()}", + key=f"voice_{section}"): + with st.spinner(f"Generating voice-over for {section}..."): + audio_bytes = generate_voice_over( + text=narration_section['text'], + language=voice_options['language'], + voice_style=voice_options['voice_style'], + slow=voice_options['pace'] in ['very_slow', 'slow'] + ) + if audio_bytes: + st.session_state.voice_overs[section] = { + 'audio': audio_bytes, + 'options': voice_options + } + st.markdown(get_audio_player_html(audio_bytes), unsafe_allow_html=True) + + # Display existing voice-over if available + if section in st.session_state.voice_overs: + st.markdown("### 🔊 Current Voice-over") + st.markdown(get_audio_player_html(st.session_state.voice_overs[section]['audio']), + unsafe_allow_html=True) + + # Show current voice-over settings + current_options = st.session_state.voice_overs[section]['options'] + st.markdown("**Current Settings:**") + st.markdown(f"- Language: {VOICE_OPTIONS['languages'][current_options['language']]['name']}") + st.markdown(f"- Voice Style: {VOICE_OPTIONS['voice_styles'][current_options['voice_style']]['name']}") + st.markdown(f"- Pace: {current_options['pace'].replace('_', ' ').title()}") + + if st.button(f"Edit {section.replace('_', ' ').title()}", key=f"edit_{section}"): + st.session_state.editing_section = section + st.session_state.current_step = 4 + st.rerun() + + col1, col2 = st.columns(2) + with col1: + if st.button("Back to Channel Info"): + st.session_state.current_step = 1 + st.rerun() + with col2: + if st.button("Next: Generate Visuals"): + st.session_state.current_step = 3 + st.rerun() + + # Step 3: Visual Elements + elif st.session_state.current_step == 3: + st.markdown("### 🎨 Visual Elements") + st.markdown(""" + Let's create the visual elements for your channel trailer. + We'll generate a logo, background, and other visual assets that match your brand. + """) + + if st.button("Generate Visuals"): + with st.spinner("Generating visual elements..."): + visuals = generate_trailer_visuals( + channel_name=st.session_state.channel_info['channel_name'], + channel_niche=st.session_state.channel_info['channel_niche'], + brand_color=st.session_state.channel_info['brand_colors'] + ) + st.session_state.visuals = visuals + st.success("Visual elements generated successfully!") + + if st.session_state.visuals: + for visual in st.session_state.visuals: + with st.expander(f"{visual['type'].replace('_', ' ').title()}", expanded=True): + st.markdown(f"**Usage:** {visual['usage']}") + st.image(visual["image"], use_column_width=True) + if st.button(f"Regenerate {visual['type'].replace('_', ' ').title()}", + key=f"regen_{visual['type']}"): + with st.spinner(f"Regenerating {visual['type']}..."): + new_visual = generate_trailer_visuals( + channel_name=st.session_state.channel_info['channel_name'], + channel_niche=st.session_state.channel_info['channel_niche'], + brand_color=st.session_state.channel_info['brand_colors'] + ) + st.session_state.visuals = new_visual + st.rerun() + + col1, col2 = st.columns(2) + with col1: + if st.button("Back to Script"): + st.session_state.current_step = 2 + st.rerun() + with col2: + if st.button("Next: Review & Edit"): + st.session_state.current_step = 4 + st.rerun() + + # Step 4: Review & Edit + elif st.session_state.current_step == 4: + st.markdown("### ✍️ Review & Edit") + + if st.session_state.editing_section: + st.markdown(f"### Editing {st.session_state.editing_section.replace('_', ' ').title()}") + section = st.session_state.script[st.session_state.editing_section] + narration_section = st.session_state.narration['narration'][st.session_state.editing_section] + + edited_text = st.text_area("Edit Text", value=section['text']) + edited_duration = st.text_input("Edit Duration", value=section['duration']) + + st.markdown("### 🎤 Narration Settings") + edited_narration = st.text_area("Edit Narration", value=narration_section['text']) + edited_voice_style = st.text_input("Voice Style", value=narration_section['voice_style']) + edited_emotion = st.text_input("Emotion", value=narration_section['emotion']) + + if st.button("Save Changes"): + st.session_state.script[st.session_state.editing_section]['text'] = edited_text + st.session_state.script[st.session_state.editing_section]['duration'] = edited_duration + st.session_state.narration['narration'][st.session_state.editing_section]['text'] = edited_narration + st.session_state.narration['narration'][st.session_state.editing_section]['voice_style'] = edited_voice_style + st.session_state.narration['narration'][st.session_state.editing_section]['emotion'] = edited_emotion + + # Regenerate voice-over for the edited section + if st.session_state.editing_section in st.session_state.voice_overs: + del st.session_state.voice_overs[st.session_state.editing_section] + + st.session_state.editing_section = None + st.success("Changes saved successfully!") + st.rerun() + + if st.button("Cancel Editing"): + st.session_state.editing_section = None + st.rerun() + else: + st.markdown(""" + Review your channel trailer content. You can: + - Edit any section of the script + - Regenerate visual elements + - Download the final content + """) + + # Display final preview + display_trailer_content(st.session_state.script, st.session_state.visuals) + + col1, col2 = st.columns(2) + with col1: + if st.button("Back to Visuals"): + st.session_state.current_step = 3 + st.rerun() + with col2: + if st.button("Finalize & Download"): + st.session_state.current_step = 5 + st.rerun() + + # Step 5: Final Output + elif st.session_state.current_step == 5: + st.markdown("### 🎉 Final Output") + st.success("Your channel trailer content is ready!") + + # Display final content + display_trailer_content(st.session_state.script, st.session_state.visuals) + + # Display voice-over guidelines + if st.session_state.narration: + st.markdown("### 🎤 Voice-over Guidelines") + guidelines = st.session_state.narration['voice_guidelines'] + st.markdown(f"**Overall Tone:** {guidelines['overall_tone']}") + st.markdown(f"**Pace:** {guidelines['pace']}") + st.markdown(f"**Energy Level:** {guidelines['energy_level']}") + + st.markdown("**Pronunciation Guide:**") + for term, pronunciation in guidelines['pronunciation_guide'].items(): + st.markdown(f"- {term}: {pronunciation}") + + st.markdown("**Special Instructions:**") + for instruction in guidelines['special_instructions']: + st.markdown(f"- {instruction}") + + # Download options + st.markdown("### 💾 Download Options") + col1, col2, col3, col4 = st.columns(4) + with col1: + if st.button("Download Script"): + save_to_file(json.dumps(st.session_state.script, indent=2), "channel_trailer_script.json") + with col2: + if st.button("Download Narration"): + save_to_file(json.dumps(st.session_state.narration, indent=2), "channel_trailer_narration.json") + with col3: + if st.button("Download Voice-overs"): + # Save voice-overs logic here + pass + with col4: + if st.button("Start New Trailer"): + # Reset session state + for key in ['current_step', 'channel_info', 'script', 'visuals', 'editing_section', + 'narration', 'voice_overs']: + if key in st.session_state: + del st.session_state[key] + st.rerun() + +def generate_trailer_visuals( + channel_name: str, + channel_niche: str, + brand_color: str +) -> List[Dict]: + """Generate visual elements for the trailer.""" + + # Generate channel logo concept + logo_prompt = f"""Create a professional logo for a {channel_niche} YouTube channel named '{channel_name}'. + Requirements: + - Simple and memorable design + - Works well in small sizes + - Incorporates brand color: {brand_color} + - Modern and clean style + - Suitable for animation + - Includes both icon and text elements + - Maintains readability at different sizes + """ + logo_image = generate_image(logo_prompt) + + # Generate background visuals + background_prompt = f"""Create a dynamic background for a {channel_niche} YouTube channel trailer. + Requirements: + - Matches the channel's theme and niche + - Incorporates brand color: {brand_color} + - Includes subtle motion elements + - Professional and modern design + - Suitable for text overlay + - Maintains visual hierarchy + - Works well with channel branding + """ + background_image = generate_image(background_prompt) + + # Generate thumbnail style previews + thumbnail_prompt = f"""Create a professional thumbnail style for a {channel_niche} YouTube channel. + Requirements: + - Eye-catching design + - Incorporates brand color: {brand_color} + - Includes space for text + - High contrast for visibility + - Modern and clean style + - Suitable for content preview + """ + thumbnail_image = generate_image(thumbnail_prompt) + + # Generate motion graphic elements + motion_prompt = f"""Create a set of motion graphic elements for a {channel_niche} YouTube channel. + Requirements: + - Modern and dynamic design + - Incorporates brand color: {brand_color} + - Suitable for transitions + - Clean and professional style + - Works well with channel branding + """ + motion_image = generate_image(motion_prompt) + + return [ + { + "type": "logo", + "prompt": logo_prompt, + "image": logo_image, + "usage": "Channel branding and identification" + }, + { + "type": "background", + "prompt": background_prompt, + "image": background_image, + "usage": "Main background and scene setting" + }, + { + "type": "thumbnail", + "prompt": thumbnail_prompt, + "image": thumbnail_image, + "usage": "Content preview and showcase" + }, + { + "type": "motion_graphics", + "prompt": motion_prompt, + "image": motion_image, + "usage": "Transitions and visual effects" + } + ] + +def display_trailer_content(script: Dict, visuals: List[Dict]): + """Display the generated trailer content.""" + + # Display script + st.markdown("### 📝 Trailer Script") + + # Create tabs for different sections + script_tab, visuals_tab, production_tab = st.tabs(["Script", "Visuals", "Production Notes"]) + + with script_tab: + for section in ["hook", "introduction", "showcase", "value_proposition", "call_to_action"]: + if section in script: + st.markdown(f"#### {section.replace('_', ' ').title()}") + st.markdown(f"**Duration:** {script[section]['duration']}") + st.markdown(f"**Text:** {script[section]['text']}") + + # Display visual suggestions in an organized way + st.markdown("**Visual Elements:**") + visual_suggestions = script[section]['visual_suggestions'] + for key, value in visual_suggestions.items(): + if isinstance(value, list): + st.markdown(f"**{key.replace('_', ' ').title()}:**") + for item in value: + st.markdown(f"- {item}") + else: + st.markdown(f"**{key.replace('_', ' ').title()}:** {value}") + st.markdown("---") + + with visuals_tab: + st.markdown("### 🎨 Visual Elements") + for visual in visuals: + st.markdown(f"#### {visual['type'].replace('_', ' ').title()}") + st.markdown(f"**Usage:** {visual['usage']}") + st.image(visual["image"], use_column_width=True) + st.markdown("---") + + with production_tab: + if "notes" in script: + st.markdown("### 📋 Production Notes") + notes = script["notes"] + + st.markdown("#### 🎭 Tone and Style") + st.write(notes["tone"]) + + st.markdown("#### 🎵 Music Suggestions") + for music in notes["music_suggestions"]: + st.markdown(f"- {music}") + + st.markdown("#### 🔄 Transitions") + for transition in notes["transitions"]: + st.markdown(f"- {transition}") + + st.markdown("#### ✨ Special Effects") + for effect in notes["special_effects"]: + st.markdown(f"- {effect}") + + st.markdown("#### 💡 Production Tips") + for tip in notes["production_tips"]: + st.markdown(f"- {tip}") + + st.markdown("#### 🎨 Visual Consistency") + st.write(notes["visual_consistency"]) + + st.markdown("#### 📝 Brand Guidelines") + st.write(notes["brand_guidelines"]) + + # Add download options + st.markdown("### 💾 Download Options") + col1, col2 = st.columns(2) + with col1: + if st.button("Download Script"): + save_to_file(json.dumps(script, indent=2), "channel_trailer_script.json") + with col2: + if st.button("Download Visuals"): + # Save visuals logic here + pass + +def get_audio_player_html(audio_bytes: bytes) -> str: + """Generate HTML for audio player with the given audio bytes.""" + b64 = base64.b64encode(audio_bytes).decode() + return f""" + + """ + +if __name__ == "__main__": + try: + write_yt_channel_trailer() + except Exception as e: + logger.error(f"Unexpected error in trailer generator: {str(e)}") + st.error("An unexpected error occurred. Please try again or contact support.") \ No newline at end of file diff --git a/lib/ai_writers/youtube_writers/youtube_ai_writer.py b/lib/ai_writers/youtube_writers/youtube_ai_writer.py index 73267fb0..05ccf281 100644 --- a/lib/ai_writers/youtube_writers/youtube_ai_writer.py +++ b/lib/ai_writers/youtube_writers/youtube_ai_writer.py @@ -18,6 +18,7 @@ from .modules.tags_generator import write_yt_tags from .modules.shorts_script_generator import write_yt_shorts from .modules.community_post_generator import write_yt_community_post from .modules.shorts_video_generator import write_yt_shorts_video +from .modules.channel_trailer_generator import write_yt_channel_trailer def youtube_main_menu(): @@ -75,6 +76,15 @@ def youtube_main_menu(): "function": write_yt_shorts_video, "status": "active" }, + { + "name": "Channel Trailer Generator", + "icon": "🎥", + "description": "Create compelling channel trailers that convert visitors into subscribers.", + "color": "#FF0000", # YouTube red + "category": "Content Creation", + "function": write_yt_channel_trailer, + "status": "active" + }, # Optimization Tools { @@ -135,15 +145,6 @@ def youtube_main_menu(): "function": None, "status": "future" }, - { - "name": "Channel Trailer Generator", - "icon": "🎥", - "description": "Create compelling channel trailers that convert visitors into subscribers.", - "color": "#990000", # Even darker red for future - "category": "Future Tools", - "function": None, - "status": "future" - }, { "name": "Video Series Planner", "icon": "📅", diff --git a/lib/utils/alwrity_utils.py b/lib/utils/alwrity_utils.py index 50f35b55..d8375f85 100644 --- a/lib/utils/alwrity_utils.py +++ b/lib/utils/alwrity_utils.py @@ -8,7 +8,7 @@ from loguru import logger from lib.ai_writers.ai_news_article_writer import ai_news_generation -from lib.ai_writers.ai_financial_writer import write_basic_ta_report +from lib.ai_writers.ai_finance_report_generator.ai_financial_dashboard import get_dashboard from lib.ai_writers.ai_facebook_writer.facebook_ai_writer import facebook_main_menu from lib.ai_writers.linkedin_writer.linkedin_ai_writer import linkedin_main_menu from lib.ai_writers.twitter_writers.twitter_dashboard import run_dashboard @@ -198,7 +198,9 @@ def ai_finance_ta_writer(): if ticker_symbol: with st.spinner("Generating TA Report..."): try: - ta_report = write_basic_ta_report(ticker_symbol) + # Get dashboard instance and generate technical analysis + dashboard = get_dashboard() + ta_report = dashboard.generate_technical_analysis(ticker_symbol) st.success(f"Successfully generated TA report for: {ticker_symbol}") st.markdown(ta_report) except Exception as err: diff --git a/lib/utils/api_key_manager/components/website_setup.py b/lib/utils/api_key_manager/components/website_setup.py index 278da58c..19915438 100644 --- a/lib/utils/api_key_manager/components/website_setup.py +++ b/lib/utils/api_key_manager/components/website_setup.py @@ -3,7 +3,7 @@ import streamlit as st from loguru import logger from ...website_analyzer import analyze_website -from ...website_analyzer.seo_analyzer import analyze_seo +from ...website_analyzer.analyzer import WebsiteAnalyzer import asyncio import sys from typing import Dict, Any @@ -127,37 +127,19 @@ def render_website_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: # Call the analyze_website function results = analyze_website(url) - # If full analysis is selected, add SEO analysis - if analyze_type == "Full Analysis with SEO": - seo_results = analyze_seo(url) - if seo_results.success: - results['data']['seo_analysis'] = { - 'overall_score': seo_results.overall_score, - 'meta_tags': { - 'title': seo_results.meta_tags.title, - 'description': seo_results.meta_tags.description, - 'keywords': seo_results.meta_tags.keywords, - 'has_robots': seo_results.meta_tags.has_robots, - 'has_sitemap': seo_results.meta_tags.has_sitemap - }, - 'content': { - 'word_count': seo_results.content.word_count, - 'readability_score': seo_results.content.readability_score, - 'content_quality_score': seo_results.content.content_quality_score, - 'headings_structure': seo_results.content.headings_structure, - 'keyword_density': seo_results.content.keyword_density - }, - 'recommendations': [ - { - 'priority': rec.priority, - 'category': rec.category, - 'issue': rec.issue, - 'recommendation': rec.recommendation, - 'impact': rec.impact - } - for rec in seo_results.recommendations - ] - } + # Replace the old SEO analysis code with the new analyzer + analyzer = WebsiteAnalyzer() + seo_results = analyzer.analyze_website(url) + if seo_results.get('success', False): + results['data']['seo_analysis'] = seo_results['data']['analysis']['seo_info'] + else: + results['data']['seo_analysis'] = { + 'error': seo_results.get('error', 'Unknown error in SEO analysis'), + 'overall_score': 0, + 'meta_tags': {}, + 'content': {}, + 'recommendations': [] + } logger.debug(f"[render_website_setup] Analysis results received: {results.get('success', False)}") diff --git a/lib/utils/save_to_file.py b/lib/utils/save_to_file.py new file mode 100644 index 00000000..1ffccf83 --- /dev/null +++ b/lib/utils/save_to_file.py @@ -0,0 +1,83 @@ +""" +Utility module for saving generated content to files. +Handles saving various types of content to the workspace directory. +""" + +import os +import json +from pathlib import Path +from datetime import datetime +from typing import Union, Dict, List, Any + +# Define the workspace directory +WORKSPACE_DIR = Path(__file__).parent.parent.parent / "workspace" / "alwrity_content" + +def ensure_directory_exists(directory: Union[str, Path]) -> None: + """Ensure the specified directory exists.""" + os.makedirs(directory, exist_ok=True) + +def save_to_file( + content: Union[str, Dict, List, Any], + filename: str, + content_type: str = "text", + subdirectory: str = None +) -> str: + """ + Save content to a file in the workspace directory. + + Args: + content: The content to save (string, dict, list, or any serializable object) + filename: Name of the file to save + content_type: Type of content ('text', 'json', 'audio', 'image') + subdirectory: Optional subdirectory within the workspace + + Returns: + str: Path to the saved file + """ + # Create timestamp for unique filenames + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + base_filename = f"{timestamp}_{filename}" + + # Determine the target directory + target_dir = WORKSPACE_DIR + if subdirectory: + target_dir = target_dir / subdirectory + + # Ensure directory exists + ensure_directory_exists(target_dir) + + # Determine file extension and format content + if content_type == "json": + file_path = target_dir / f"{base_filename}.json" + with open(file_path, "w", encoding="utf-8") as f: + json.dump(content, f, indent=2, ensure_ascii=False) + elif content_type == "audio": + file_path = target_dir / f"{base_filename}.mp3" + with open(file_path, "wb") as f: + f.write(content) + elif content_type == "image": + file_path = target_dir / f"{base_filename}.png" + with open(file_path, "wb") as f: + f.write(content) + else: # text + file_path = target_dir / f"{base_filename}.txt" + with open(file_path, "w", encoding="utf-8") as f: + f.write(str(content)) + + return str(file_path) + +def save_audio(audio_bytes: bytes, filename: str, subdirectory: str = "audio") -> str: + """Save audio content to a file.""" + return save_to_file(audio_bytes, filename, "audio", subdirectory) + +def save_image(image_bytes: bytes, filename: str, subdirectory: str = "images") -> str: + """Save image content to a file.""" + return save_to_file(image_bytes, filename, "image", subdirectory) + +def save_json(data: Union[Dict, List], filename: str, subdirectory: str = "json") -> str: + """Save JSON content to a file.""" + return save_to_file(data, filename, "json", subdirectory) + +def save_text(text: str, filename: str, subdirectory: str = "text") -> str: + """Save text content to a file.""" + return save_to_file(text, filename, "text", subdirectory) \ No newline at end of file diff --git a/lib/utils/seo_tools.py b/lib/utils/seo_tools.py index 7ca33ab3..fc462b48 100644 --- a/lib/utils/seo_tools.py +++ b/lib/utils/seo_tools.py @@ -9,7 +9,347 @@ from lib.ai_seo_tools.google_pagespeed_insights import google_pagespeed_insights from lib.ai_seo_tools.on_page_seo_analyzer import analyze_onpage_seo from lib.ai_seo_tools.weburl_seo_checker import url_seo_checker from lib.ai_marketing_tools.ai_backlinker.backlinking_ui_streamlit import backlinking_ui +from lib.ai_seo_tools.content_gap_analysis.ui import ContentGapAnalysisUI +from lib.ai_seo_tools.content_calendar.ui.dashboard import ContentCalendarDashboard +def render_content_gap_analysis(): + """Render the content gap analysis workflow interface.""" + from lib.ai_seo_tools.content_gap_analysis.ui import ContentGapAnalysisUI + + # Initialize and run the Content Gap Analysis UI + ui = ContentGapAnalysisUI() + ui.run() + +def render_content_calendar(): + """Render the content calendar dashboard.""" + import logging + import sys + from datetime import datetime + + # Configure logging + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout), + logging.FileHandler('content_calendar.log', mode='a') + ] + ) + logger = logging.getLogger('content_calendar') + + try: + logger.info("Initializing Content Calendar Dashboard") + dashboard = ContentCalendarDashboard() + logger.info("Rendering Content Calendar Dashboard") + dashboard.render() + logger.info("Content Calendar Dashboard rendered successfully") + except Exception as e: + logger.error(f"Error rendering content calendar: {str(e)}", exc_info=True) + st.error(f"An error occurred while loading the content calendar: {str(e)}") + +def render_seo_tools_dashboard(): + """Render a modern dashboard for SEO tools with improved UI and navigation.""" + selected_section = st.session_state.get('seo_dashboard_section', 'combinations') + + # Define card gradients at the top so it's available in all sections + card_gradients = { + "Content Optimization Suite": "linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)", + "Technical SEO Audit": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + "Image Optimization Suite": "linear-gradient(135deg, #f7971e 0%, #ffd200 100%)", + "Social Media Optimization": "linear-gradient(135deg, #f953c6 0%, #b91d73 100%)", + "Content Gap Analysis": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + "Content Calendar": "linear-gradient(135deg, #4CAF50 0%, #2196F3 100%)", + "Structured Data Generator": "linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)", + "Blog Title Generator": "linear-gradient(135deg, #2193b0 0%, #6dd5ed 100%)", + "Meta Description Generator": "linear-gradient(135deg, #f7971e 0%, #ffd200 100%)", + "Image Alt Text Generator": "linear-gradient(135deg, #f953c6 0%, #b91d73 100%)", + "OpenGraph Tags Generator": "linear-gradient(135deg, #f857a6 0%, #ff5858 100%)", + "Image Optimizer": "linear-gradient(135deg, #43cea2 0%, #185a9d 100%)", + "PageSpeed Insights": "linear-gradient(135deg, #ff9966 0%, #ff5e62 100%)", + "On-Page SEO Analyzer": "linear-gradient(135deg, #56ab2f 0%, #a8e063 100%)", + "URL SEO Checker": "linear-gradient(135deg, #3a7bd5 0%, #00d2ff 100%)", + "AI Backlinking Tool": "linear-gradient(135deg, #e96443 0%, #904e95 100%)" + } + + # Navigation bar only (no dashboard title/description) + nav_cols = st.columns([1,1,1,1]) + nav_labels = ["Tool Combos", "Advanced", "Individual", "About"] + nav_keys = ["combinations", "advanced", "individual", "about"] + for i, label in enumerate(nav_labels): + if nav_cols[i].button(label, key=f"nav_{label}"): + st.session_state['seo_dashboard_section'] = nav_keys[i] + selected_section = nav_keys[i] + + st.markdown("
", unsafe_allow_html=True) + + # Define tool combinations for cross-tool analysis + tool_combinations = { + "Content Optimization Suite": { + "icon": "📊", + "description": "Comprehensive content optimization combining title generation, meta descriptions, and structured data.", + "tools": ["Blog Title Generator", "Meta Description Generator", "Structured Data Generator"], + "path": "content_optimization", + "color": "#4CAF50" + }, + "Technical SEO Audit": { + "icon": "🔧", + "description": "Complete technical SEO analysis including page speed, on-page SEO, and URL structure.", + "tools": ["PageSpeed Insights", "On-Page SEO Analyzer", "URL SEO Checker"], + "path": "technical_audit", + "color": "#2196F3" + }, + "Image Optimization Suite": { + "icon": "🖼️", + "description": "End-to-end image optimization with alt text generation and performance optimization.", + "tools": ["Image Alt Text Generator", "Image Optimizer"], + "path": "image_optimization", + "color": "#FF9800" + }, + "Social Media Optimization": { + "icon": "📱", + "description": "Enhance social media presence with OpenGraph tags and backlink analysis.", + "tools": ["OpenGraph Tags Generator", "AI Backlinking Tool"], + "path": "social_optimization", + "color": "#9C27B0" + } + } + + # Define individual SEO tools + seo_tools = { + "Structured Data Generator": { + "icon": "📋", + "description": "Generate structured data (Rich Snippets) to enhance your search results with additional information.", + "color": "#4CAF50", + "path": "structured_data", + "status": "active" + }, + "Blog Title Generator": { + "icon": "✏️", + "description": "Create SEO-optimized blog titles that attract clicks and improve search rankings.", + "color": "#2196F3", + "path": "blog_title", + "status": "active" + }, + "Meta Description Generator": { + "icon": "📝", + "description": "Generate compelling meta descriptions that improve click-through rates from search results.", + "color": "#FF9800", + "path": "meta_description", + "status": "active" + }, + "Image Alt Text Generator": { + "icon": "🖼️", + "description": "Create descriptive alt text for images to improve accessibility and image SEO.", + "color": "#9C27B0", + "path": "alt_text", + "status": "active" + }, + "OpenGraph Tags Generator": { + "icon": "📱", + "description": "Generate OpenGraph tags for better social media sharing and visibility.", + "color": "#F44336", + "path": "opengraph", + "status": "active" + }, + "Image Optimizer": { + "icon": "📉", + "description": "Optimize and resize images for better website performance and SEO.", + "color": "#607D8B", + "path": "image_optimizer", + "status": "active" + }, + "PageSpeed Insights": { + "icon": "⚡", + "description": "Analyze your website's performance using Google PageSpeed Insights.", + "color": "#795548", + "path": "pagespeed", + "status": "active" + }, + "On-Page SEO Analyzer": { + "icon": "🔍", + "description": "Analyze and optimize your webpage's SEO elements and content.", + "color": "#009688", + "path": "onpage_seo", + "status": "active" + }, + "URL SEO Checker": { + "icon": "🌐", + "description": "Check the SEO health of specific URLs and get improvement suggestions.", + "color": "#3F51B5", + "path": "url_checker", + "status": "active" + }, + "AI Backlinking Tool": { + "icon": "🔗", + "description": "Discover and analyze backlink opportunities using AI-powered insights.", + "color": "#E91E63", + "path": "backlinking", + "status": "active" + } + } + + # --- Tool Combinations Section --- + if selected_section == 'combinations': + combo_cols = st.columns(2) + for idx, (combo_name, details) in enumerate(tool_combinations.items()): + gradient = card_gradients.get(combo_name, "linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)") + with combo_cols[idx % 2]: + st.markdown(f""" +
+
+
{details['icon']}
+
{combo_name}
+
{details['description']}
+
+ {''.join([f'{tool}' for tool in details['tools']])} +
+
+ """, unsafe_allow_html=True) + if st.button(f"Launch {combo_name}", key=f"combo_{combo_name}", use_container_width=True): + st.query_params["tool"] = details["path"] + st.rerun() + + # --- Advanced Features Section --- + elif selected_section == 'advanced': + adv_cols = st.columns(2) + adv_features = [ + { + "name": "Content Gap Analysis", + "icon": "🎯", + "description": "Identify content opportunities and optimize your content strategy with AI-powered insights.", + "badges": ["Website Analysis", "Competitor Research", "Keyword Opportunities", "AI Recommendations"], + "gradient": card_gradients["Content Gap Analysis"], + "button": "Start Content Gap Analysis", + "key": "content_gap_analysis", + "path": "content_gap_analysis" + }, + { + "name": "Content Calendar", + "icon": "📅", + "description": "Plan, schedule, and manage your content strategy with our AI-powered content calendar.", + "badges": ["Content Planning", "Scheduling", "Performance Tracking", "AI Insights"], + "gradient": card_gradients["Content Calendar"], + "button": "Open Content Calendar", + "key": "content_calendar", + "path": "content_calendar" + } + ] + for idx, feature in enumerate(adv_features): + with adv_cols[idx % 2]: + st.markdown(f""" +
+
+
{feature['icon']}
+
{feature['name']}
+
{feature['description']}
+
+ {''.join([f'{badge}' for badge in feature['badges']])} +
+
+ """, unsafe_allow_html=True) + if st.button(feature['button'], key=feature['key'], use_container_width=True): + st.query_params["tool"] = feature["path"] + st.rerun() + + # --- Individual Tools Section --- + elif selected_section == 'individual': + cols = st.columns(3) + for idx, (tool_name, details) in enumerate(seo_tools.items()): + gradient = card_gradients.get(tool_name, "linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)") + with cols[idx % 3]: + st.markdown(f""" +
+
+
{details['icon']}
+
{tool_name}
+
{details['description']}
+
+ """, unsafe_allow_html=True) + if st.button(f"Use {tool_name}", key=f"btn_{tool_name}", use_container_width=True): + st.query_params["tool"] = details["path"] + st.rerun() + + # --- About Section --- + elif selected_section == 'about': + st.markdown(""" +
+

About This Dashboard

+

This dashboard brings together powerful AI-driven SEO tools and workflows to help you optimize your website and content strategy. Use the navigation above to explore combinations, advanced features, or individual tools.

+
+ """, unsafe_allow_html=True) + + st.markdown(""" + + """, unsafe_allow_html=True) def ai_seo_tools(): """ @@ -17,71 +357,83 @@ def ai_seo_tools(): such as generating structured data, optimizing images, checking page speed, and analyzing on-page SEO. """ - st.markdown( - """ - Welcome to your one-stop solution for AI-driven SEO optimization. Select a tool from the options below - to improve your website’s SEO with cutting-edge AI technology. - """ - ) - # List of SEO tools with unique emojis for each option - options = [ - "📝 Generate Structured Data - Rich Snippet", - "✏️ Generate SEO Optimized Blog Titles", - "📝 Generate Meta Description for SEO", - "🖼️ Generate Image Alt Text", - "📄 Generate OpenGraph Tags", - "📉 Optimize/Resize Image", - "⚡ Run Google PageSpeed Insights", - "🔍 Analyze On-Page SEO", - "🌐 URL SEO Checker", - "🔗 AI Backlinking Tool" - ] + # Check if a specific tool is selected + selected_tool = st.query_params.get("tool") - # User selection of SEO tools using radio buttons - choice = st.radio( - "**👇 Select an AI SEO Tool:**", - options, - index=0, - format_func=lambda x: x - ) + if selected_tool: + # Map tool paths to their respective functions + tool_functions = { + # Individual tools + "structured_data": ai_structured_data, + "blog_title": ai_title_generator, + "meta_description": metadesc_generator_main, + "alt_text": alt_text_gen, + "opengraph": og_tag_generator, + "image_optimizer": main_img_optimizer, + "pagespeed": google_pagespeed_insights, + "onpage_seo": analyze_onpage_seo, + "url_checker": url_seo_checker, + "backlinking": backlinking_ui, + + # Tool combinations + "content_optimization": lambda: run_tool_combination([ + ai_title_generator, + metadesc_generator_main, + ai_structured_data + ], "Content Optimization Suite"), + "technical_audit": lambda: run_tool_combination([ + google_pagespeed_insights, + analyze_onpage_seo, + url_seo_checker + ], "Technical SEO Audit"), + "image_optimization": lambda: run_tool_combination([ + alt_text_gen, + main_img_optimizer + ], "Image Optimization Suite"), + "social_optimization": lambda: run_tool_combination([ + og_tag_generator, + backlinking_ui + ], "Social Media Optimization"), + + # Add Content Gap Analysis and Content Calendar + "content_gap_analysis": render_content_gap_analysis, + "content_calendar": render_content_calendar + } + + if selected_tool in tool_functions: + # Clear any existing content + st.empty() + # Execute the selected tool's function + tool_functions[selected_tool]() + else: + st.error(f"Invalid tool selected: {selected_tool}") + render_seo_tools_dashboard() + else: + # Show the dashboard if no tool is selected + render_seo_tools_dashboard() + +def run_tool_combination(tools, combination_name): + """Run a combination of tools and provide cross-tool analysis.""" + st.markdown(f"# {combination_name}") + st.markdown("Running comprehensive analysis...") - # Call the respective functions based on the user selection - if choice == "📝 Generate Structured Data - Rich Snippet": - # Generate Structured Data for Rich Snippets - ai_structured_data() - - elif choice == "📝 Generate Meta Description for SEO": - # Generate SEO-optimized meta descriptions - metadesc_generator_main() - - elif choice == "✏️ Generate SEO Optimized Blog Titles": - # Generate SEO-friendly blog titles - ai_title_generator() - - elif choice == "🖼️ Generate Image Alt Text": - # Generate alternative text for images - alt_text_gen() - - elif choice == "📄 Generate OpenGraph Tags": - # Generate OpenGraph tags for social media sharing - og_tag_generator() - - elif choice == "📉 Optimize/Resize Image": - # Optimize images by resizing or compressing them - main_img_optimizer() - - elif choice == "⚡ Run Google PageSpeed Insights": - # Run Google PageSpeed Insights for performance analysis - google_pagespeed_insights() - - elif choice == "🔍 Analyze On-Page SEO": - # Analyze on-page SEO elements - analyze_onpage_seo() - - elif choice == "🌐 URL SEO Checker": - # Check SEO health of a specific URL - url_seo_checker() - - elif choice == "🔗 AI Backlinking Tool": - # Run AI Backlinking tool for link-building opportunities - backlinking_ui() + # Create tabs for each tool in the combination + tabs = st.tabs([f"Step {i+1}" for i in range(len(tools))]) + + # Run each tool in its own tab + for i, (tab, tool) in enumerate(zip(tabs, tools)): + with tab: + st.markdown(f"### Step {i+1}") + tool() + + # Add cross-tool analysis section + st.markdown("## 📊 Cross-Tool Analysis") + st.markdown("Analyzing results across all tools...") + + # Add recommendations based on combined results + st.markdown("## 💡 Recommendations") + st.markdown("Based on the combined analysis, here are the key recommendations:") + + # Add a button to export the complete analysis + if st.button("📥 Export Complete Analysis", use_container_width=True): + st.info("Analysis export functionality coming soon!") diff --git a/lib/utils/ui_setup.py b/lib/utils/ui_setup.py index dae7f7d7..6ca409fe 100644 --- a/lib/utils/ui_setup.py +++ b/lib/utils/ui_setup.py @@ -1,3 +1,8 @@ +""" +UI setup module for ALwrity application. +Provides consistent navigation and layout structure. +""" + import os import streamlit as st from lib.utils.file_processor import load_image @@ -15,6 +20,99 @@ from lib.ai_writers.insta_ai_writer import insta_writer from lib.ai_writers.youtube_writers.youtube_ai_writer import youtube_main_menu from lib.ai_writers.ai_writer_dashboard import get_ai_writers, list_ai_writers +def render_social_tools_dashboard(): + """Render a modern dashboard for social media tools.""" + st.markdown(""" + + """, unsafe_allow_html=True) + + # Define social tools with their details and paths + social_tools = { + "Facebook": { + "icon": "📘", + "description": "Create engaging Facebook posts and manage your content strategy", + "color": "#4267B2", + "path": "facebook" + }, + "LinkedIn": { + "icon": "💼", + "description": "Generate professional LinkedIn content and optimize your profile", + "color": "#0077B5", + "path": "linkedin" + }, + "Twitter": { + "icon": "🐦", + "description": "Craft viral tweets and manage your Twitter presence", + "color": "#1DA1F2", + "path": "twitter" + }, + "Instagram": { + "icon": "📸", + "description": "Create Instagram captions and plan your visual content", + "color": "#E1306C", + "path": "instagram" + }, + "YouTube": { + "icon": "🎥", + "description": "Generate video scripts and optimize your YouTube content", + "color": "#FF0000", + "path": "youtube" + } + } + + # Create a grid of cards + cols = st.columns(3) + for idx, (platform, details) in enumerate(social_tools.items()): + with cols[idx % 3]: + st.markdown(f""" +
+
{details['icon']}
+
{platform}
+
{details['description']}
+
+ """, unsafe_allow_html=True) + + if st.button(f"Open {platform}", key=f"btn_{platform}", + help=f"Launch {platform} tools", + use_container_width=True): + # Set query parameters to redirect to the specific tool + st.query_params["tool"] = details["path"] + st.rerun() def setup_ui(): """Set up the UI with custom styling.""" @@ -314,29 +412,18 @@ def setup_alwrity_ui(): "AI Writers": ("📝", get_ai_writers), "Content Planning": ("📅", content_planning_tools), "AI SEO Tools": ("🔍", ai_seo_tools), - "AI Social Tools": ("📱", None), # Set to None as we'll handle this separately + "AI Social Tools": ("📱", render_social_tools_dashboard), + "ALwrity Settings": ("⚙️", render_settings_page), "Agents Teams(TBD)": ("🤝", lambda: st.subheader("Agents Teams - Coming Soon!")), "Ask Alwrity(TBD)": ("💬", lambda: ( st.subheader("Chat with your Data, Chat with any Data.. COMING SOON !"), st.markdown("Create a collection by uploading files (PDF, MD, CSV, etc), or crawl a data source (Websites, more sources coming soon."), st.markdown("One can ask/chat, summarize and do semantic search over the uploaded data") - )), - "ALwrity Settings": ("⚙️", render_settings_page) + )) } logger.info(f"Defined {len(nav_items)} navigation items") - # Define sub-menu items for AI Social Tools - social_tools_submenu = { - "Facebook": ("📘", lambda: facebook_main_menu()), - "LinkedIn": ("💼", lambda: linkedin_main_menu()), - "Twitter": ("🐦", lambda: run_dashboard()), - "Instagram": ("📸", lambda: insta_writer()), - "YouTube": ("🎥", lambda: youtube_main_menu()) - } - - logger.info(f"Defined {len(social_tools_submenu)} social tools submenu items") - # Create sidebar navigation st.sidebar.markdown("### ALwrity Options") st.sidebar.markdown('', unsafe_allow_html=True) @@ -402,10 +448,36 @@ def setup_alwrity_ui(): st.sidebar.image(icon_path, use_container_width=False) st.sidebar.markdown('', unsafe_allow_html=True) - # Display content based on active tab + # Display content based on active tab and tool selection if st.session_state.active_tab == "AI Social Tools": - if not st.session_state.active_sub_tab: - # Only show title and info when no sub-tab is selected + # Check if a specific tool is selected + selected_tool = st.query_params.get("tool") + if selected_tool: + # Add a back button at the top + if st.button("← Back to Social Tools Dashboard", key=f"back_to_dashboard_{selected_tool}"): + # Clear the tool query parameter + st.query_params.clear() + st.rerun() + + # Map tool paths to their respective functions + tool_functions = { + "facebook": facebook_main_menu, + "linkedin": linkedin_main_menu, + "twitter": run_dashboard, + "instagram": insta_writer, + "youtube": youtube_main_menu + } + + if selected_tool in tool_functions: + # Clear any existing content + st.empty() + # Execute the selected tool's function + tool_functions[selected_tool]() + else: + st.error(f"Invalid tool selected: {selected_tool}") + render_social_tools_dashboard() + else: + # Show the dashboard if no tool is selected st.markdown(""" """, unsafe_allow_html=True) st.title(f"{nav_items[st.session_state.active_tab][0]} {st.session_state.active_tab}") - st.info("Please select a social media platform from the sidebar.") - else: - # When a platform is selected, show no title and minimize spacing - st.markdown(""" - - """, unsafe_allow_html=True) - # Call the function directly without any title - social_tools_submenu[st.session_state.active_sub_tab][1]() + render_social_tools_dashboard() else: - # Check if we're in the AI Writers section and handle writer selection + # Handle other tabs as before if st.session_state.active_tab == "AI Writers": - # Get the writer parameter from the URL using st.query_params writer = st.query_params.get("writer") logger.info(f"Current writer from query params: {writer}") if writer: - # Get the list of writers without rendering the dashboard writers = list_ai_writers() logger.info(f"Found {len(writers)} writers") @@ -457,9 +503,7 @@ def setup_alwrity_ui(): if w["path"] == writer: writer_found = True logger.info(f"Found matching writer: {w['name']}, executing function") - # Clear any existing content st.empty() - # Execute the writer function w["function"]() break @@ -467,11 +511,9 @@ def setup_alwrity_ui(): logger.error(f"No writer found with path: {writer}") st.error(f"No writer found with path: {writer}") else: - # If no writer selected, show the dashboard logger.info("No writer selected, showing dashboard") get_ai_writers() else: - # For all other tabs, show the title st.markdown("""