Content Calendar, Content Gap Analysis, and Content Optimization
This commit is contained in:
@@ -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
|
||||||
27
Getting Started/Option_2_Python_Users/README.md
Normal file
27
Getting Started/Option_2_Python_Users/README.md
Normal file
@@ -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
|
||||||
|
|
||||||
|
---
|
||||||
26
Getting Started/Option_4_Mac_Users/Dockerfile
Normal file
26
Getting Started/Option_4_Mac_Users/Dockerfile
Normal file
@@ -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"]
|
||||||
23
Getting Started/Option_4_Mac_Users/README.md
Normal file
23
Getting Started/Option_4_Mac_Users/README.md
Normal file
@@ -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
|
||||||
78
Getting Started/Option_4_Mac_Users/setup.py
Normal file
78
Getting Started/Option_4_Mac_Users/setup.py
Normal file
@@ -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}")
|
||||||
@@ -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)**
|
## Option 3: Docker Installation
|
||||||
- **Best for:** Anyone who wants a hassle-free, one-command setup on Windows, Mac, or Linux.
|
- For advanced users and developers
|
||||||
- **Why choose Docker?**
|
- Containerized environment
|
||||||
- No need to install Python, Rust, or system libraries manually.
|
- Platform-independent setup
|
||||||
- Everything runs in a safe, isolated environment.
|
→ [Go to Docker Setup](./Option_3_Docker_Install)
|
||||||
- Consistent experience across all operating systems.
|
|
||||||
- **How to use:**
|
|
||||||
- See [README_dockerfile.md](./README_dockerfile.md) for step-by-step instructions.
|
|
||||||
|
|
||||||
### 2. **Windows One-Click Installer (`install_alwrity.bat`)**
|
## Need Help?
|
||||||
- **Best for:** Windows users who prefer a simple double-click installer.
|
- Visit our [GitHub Issues](https://github.com/AJaySi/AI-Writer/issues) page
|
||||||
- **Why choose this?**
|
- Check our [Documentation](../docs)
|
||||||
- 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!
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
85
calendar_data.json
Normal file
85
calendar_data.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,6 +160,142 @@ Stores reusable content templates.
|
|||||||
# Relationships
|
# Relationships
|
||||||
user = relationship("User")
|
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
|
Vector Database Schema
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|||||||
167
lib/ai_seo_tools/content_calendar/README.md
Normal file
167
lib/ai_seo_tools/content_calendar/README.md
Normal file
@@ -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.
|
||||||
798
lib/ai_seo_tools/content_calendar/core/ai_generator.py
Normal file
798
lib/ai_seo_tools/content_calendar/core/ai_generator.py
Normal file
@@ -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
|
||||||
196
lib/ai_seo_tools/content_calendar/core/calendar_manager.py
Normal file
196
lib/ai_seo_tools/content_calendar/core/calendar_manager.py
Normal file
@@ -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)
|
||||||
151
lib/ai_seo_tools/content_calendar/core/content_brief.py
Normal file
151
lib/ai_seo_tools/content_calendar/core/content_brief.py
Normal file
@@ -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': {}
|
||||||
|
}
|
||||||
323
lib/ai_seo_tools/content_calendar/core/content_generator.py
Normal file
323
lib/ai_seo_tools/content_calendar/core/content_generator.py
Normal file
@@ -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 {}
|
||||||
80
lib/ai_seo_tools/content_calendar/examples/calendar_usage.py
Normal file
80
lib/ai_seo_tools/content_calendar/examples/calendar_usage.py
Normal file
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
127
lib/ai_seo_tools/content_calendar/integrations/gap_analyzer.py
Normal file
127
lib/ai_seo_tools/content_calendar/integrations/gap_analyzer.py
Normal file
@@ -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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan> | <yellow>{function}</yellow> | {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
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
}
|
||||||
219
lib/ai_seo_tools/content_calendar/integrations/seo_optimizer.py
Normal file
219
lib/ai_seo_tools/content_calendar/integrations/seo_optimizer.py
Normal file
@@ -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)
|
||||||
|
}
|
||||||
143
lib/ai_seo_tools/content_calendar/integrations/seo_tools.py
Normal file
143
lib/ai_seo_tools/content_calendar/integrations/seo_tools.py
Normal file
@@ -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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
237
lib/ai_seo_tools/content_calendar/models/calendar.py
Normal file
237
lib/ai_seo_tools/content_calendar/models/calendar.py
Normal file
@@ -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')
|
||||||
|
)
|
||||||
185
lib/ai_seo_tools/content_calendar/tests/test_ai_generator.py
Normal file
185
lib/ai_seo_tools/content_calendar/tests/test_ai_generator.py
Normal file
@@ -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()
|
||||||
132
lib/ai_seo_tools/content_calendar/tests/test_content_brief.py
Normal file
132
lib/ai_seo_tools/content_calendar/tests/test_content_brief.py
Normal file
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
132
lib/ai_seo_tools/content_calendar/tests/test_seo_optimizer.py
Normal file
132
lib/ai_seo_tools/content_calendar/tests/test_seo_optimizer.py
Normal file
@@ -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()
|
||||||
21
lib/ai_seo_tools/content_calendar/ui/add_content_modal.py
Normal file
21
lib/ai_seo_tools/content_calendar/ui/add_content_modal.py
Normal file
@@ -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
|
||||||
137
lib/ai_seo_tools/content_calendar/ui/ai_suggestions_modal.py
Normal file
137
lib/ai_seo_tools/content_calendar/ui/ai_suggestions_modal.py
Normal file
@@ -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}")
|
||||||
51
lib/ai_seo_tools/content_calendar/ui/calendar_view.py
Normal file
51
lib/ai_seo_tools/content_calendar/ui/calendar_view.py
Normal file
@@ -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.")
|
||||||
231
lib/ai_seo_tools/content_calendar/ui/components/ab_testing.py
Normal file
231
lib/ai_seo_tools/content_calendar/ui/components/ab_testing.py
Normal file
@@ -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}")
|
||||||
2
lib/ai_seo_tools/content_calendar/ui/components/badge.py
Normal file
2
lib/ai_seo_tools/content_calendar/ui/components/badge.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
def render_badge(platform_disp, platform_icon, type_disp, status_disp):
|
||||||
|
return f"<span class='badge-content-calendar badge-platform-{platform_disp.lower()}'>{platform_icon} {platform_disp} | {type_disp} | <span class='chip-status chip-status-{status_disp.lower()}'>{status_disp}</span></span>"
|
||||||
@@ -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"<div class='card-content-calendar'>", unsafe_allow_html=True)
|
||||||
|
st.markdown(f"<div style='display:flex;align-items:center;justify-content:space-between;gap:8px;'>", unsafe_allow_html=True)
|
||||||
|
st.markdown(f"<div style='display:flex;align-items:center;gap:8px;min-width:0;flex:1;'>"
|
||||||
|
f"{type_icon}<span class='content-title'>{row['title']}</span></div>", unsafe_allow_html=True)
|
||||||
|
st.markdown("<div style='display:flex;align-items:center;gap:4px;'>", 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("</div>", unsafe_allow_html=True)
|
||||||
|
st.markdown("</div>", unsafe_allow_html=True)
|
||||||
|
st.markdown(f"<div class='content-meta'><span class='badge-content-calendar badge-platform-{platform_disp.lower()}'>{platform_icon} {platform_disp} | {type_disp} | <span class='chip-status chip-status-{status_disp.lower()}'>{status_disp}</span></span></div>", unsafe_allow_html=True)
|
||||||
|
st.markdown("</div>", unsafe_allow_html=True)
|
||||||
@@ -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.")
|
||||||
@@ -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()
|
||||||
@@ -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)}")
|
||||||
634
lib/ai_seo_tools/content_calendar/ui/dashboard.py
Normal file
634
lib/ai_seo_tools/content_calendar/ui/dashboard.py
Normal file
@@ -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("""
|
||||||
|
<style>
|
||||||
|
/* Add your custom CSS here if needed */
|
||||||
|
</style>
|
||||||
|
""", 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()
|
||||||
30
lib/ai_seo_tools/content_calendar/ui/filters.py
Normal file
30
lib/ai_seo_tools/content_calendar/ui/filters.py
Normal file
@@ -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
|
||||||
198
lib/ai_seo_tools/content_calendar/utils/date_utils.py
Normal file
198
lib/ai_seo_tools/content_calendar/utils/date_utils.py
Normal file
@@ -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
|
||||||
154
lib/ai_seo_tools/content_calendar/utils/error_handling.py
Normal file
154
lib/ai_seo_tools/content_calendar/utils/error_handling.py
Normal file
@@ -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)}")
|
||||||
182
lib/ai_seo_tools/content_gap_analysis/README.md
Normal file
182
lib/ai_seo_tools/content_gap_analysis/README.md
Normal file
@@ -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
|
||||||
36
lib/ai_seo_tools/content_gap_analysis/__init__.py
Normal file
36
lib/ai_seo_tools/content_gap_analysis/__init__.py
Normal file
@@ -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()
|
||||||
711
lib/ai_seo_tools/content_gap_analysis/competitor_analyzer.py
Normal file
711
lib/ai_seo_tools/content_gap_analysis/competitor_analyzer.py
Normal file
@@ -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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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'
|
||||||
|
])
|
||||||
649
lib/ai_seo_tools/content_gap_analysis/keyword_researcher.py
Normal file
649
lib/ai_seo_tools/content_gap_analysis/keyword_researcher.py
Normal file
@@ -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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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': {}
|
||||||
|
}
|
||||||
361
lib/ai_seo_tools/content_gap_analysis/main.py
Normal file
361
lib/ai_seo_tools/content_gap_analysis/main.py
Normal file
@@ -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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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 {}
|
||||||
41
lib/ai_seo_tools/content_gap_analysis/navigation.py
Normal file
41
lib/ai_seo_tools/content_gap_analysis/navigation.py
Normal file
@@ -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
|
||||||
440
lib/ai_seo_tools/content_gap_analysis/recommendation_engine.py
Normal file
440
lib/ai_seo_tools/content_gap_analysis/recommendation_engine.py
Normal file
@@ -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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
769
lib/ai_seo_tools/content_gap_analysis/ui.py
Normal file
769
lib/ai_seo_tools/content_gap_analysis/ui.py
Normal file
@@ -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()
|
||||||
249
lib/ai_seo_tools/content_gap_analysis/utils/README.md
Normal file
249
lib/ai_seo_tools/content_gap_analysis/utils/README.md
Normal file
@@ -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.
|
||||||
13
lib/ai_seo_tools/content_gap_analysis/utils/__init__.py
Normal file
13
lib/ai_seo_tools/content_gap_analysis/utils/__init__.py
Normal file
@@ -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'
|
||||||
|
]
|
||||||
1134
lib/ai_seo_tools/content_gap_analysis/utils/ai_processor.py
Normal file
1134
lib/ai_seo_tools/content_gap_analysis/utils/ai_processor.py
Normal file
File diff suppressed because it is too large
Load Diff
236
lib/ai_seo_tools/content_gap_analysis/utils/content_parser.py
Normal file
236
lib/ai_seo_tools/content_gap_analysis/utils/content_parser.py
Normal file
@@ -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
|
||||||
|
}
|
||||||
112
lib/ai_seo_tools/content_gap_analysis/utils/data_collector.py
Normal file
112
lib/ai_seo_tools/content_gap_analysis/utils/data_collector.py
Normal file
@@ -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
|
||||||
237
lib/ai_seo_tools/content_gap_analysis/utils/seo_analyzer.py
Normal file
237
lib/ai_seo_tools/content_gap_analysis/utils/seo_analyzer.py
Normal file
@@ -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
|
||||||
|
}
|
||||||
270
lib/ai_seo_tools/content_gap_analysis/utils/storage.py
Normal file
270
lib/ai_seo_tools/content_gap_analysis/utils/storage.py
Normal file
@@ -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
|
||||||
291
lib/ai_seo_tools/content_gap_analysis/website_analyzer.py
Normal file
291
lib/ai_seo_tools/content_gap_analysis/website_analyzer.py
Normal file
@@ -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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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 []
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""Content title generator module."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
@@ -6,70 +8,106 @@ from tenacity import (
|
|||||||
stop_after_attempt,
|
stop_after_attempt,
|
||||||
wait_random_exponential,
|
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():
|
# Configure logger
|
||||||
""" UI for the AI Blog Title Generator """
|
logger.remove() # Remove default handler
|
||||||
st.title("✍️ Alwrity - AI Blog Title Generator")
|
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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||||
|
)
|
||||||
|
|
||||||
# Input section
|
# Ensure logs directory exists
|
||||||
with st.expander("**PRO-TIP** - Follow the steps below for best results.", expanded=True):
|
os.makedirs("logs", exist_ok=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!**")
|
|
||||||
|
|
||||||
|
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))
|
@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):
|
def generate_blog_titles(input_blog_keywords, input_blog_content, input_title_type, input_title_intent, input_language):
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ def facebook_main_menu():
|
|||||||
if st.session_state.selected_tool is not None:
|
if st.session_state.selected_tool is not None:
|
||||||
with tool_container:
|
with tool_container:
|
||||||
# Add a back button at the top
|
# 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.session_state.selected_tool = None
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
|
|||||||
190
lib/ai_writers/ai_finance_report_generator/README.md
Normal file
190
lib/ai_writers/ai_finance_report_generator/README.md
Normal file
@@ -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.
|
||||||
@@ -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>{level}</level>|<green>{file}:{line}:{function}</green>| {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()
|
||||||
265
lib/ai_writers/ai_finance_report_generator/reports/README.md
Normal file
265
lib/ai_writers/ai_finance_report_generator/reports/README.md
Normal file
@@ -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.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
62
lib/ai_writers/ai_finance_report_generator/utils/__init__.py
Normal file
62
lib/ai_writers/ai_finance_report_generator/utils/__init__.py
Normal file
@@ -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)
|
||||||
|
}
|
||||||
208
lib/ai_writers/ai_finance_report_generator/utils/storage.py
Normal file
208
lib/ai_writers/ai_finance_report_generator/utils/storage.py
Normal file
@@ -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)
|
||||||
@@ -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>{level}</level>|<green>{file}:{line}:{function}</green>| {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)
|
|
||||||
@@ -1,13 +1,33 @@
|
|||||||
|
"""
|
||||||
|
Smart Tweet Generator with modern UI components.
|
||||||
|
"""
|
||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
from typing import Dict, List, Tuple, Optional
|
from typing import Dict, List, Tuple, Optional
|
||||||
import random
|
import random
|
||||||
import emoji
|
import emoji
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from ....gpt_providers.text_generation.main_text_generation import llm_text_gen
|
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
|
# Constants
|
||||||
MAX_TWEET_LENGTH = 280
|
MAX_TWEET_LENGTH = 280
|
||||||
@@ -19,14 +39,6 @@ EMOJI_CATEGORIES = {
|
|||||||
"Casual": ["👋", "👍", "🙋", "💁", "🤗", "👌", "✌️", "🤝", "👊", "🙏"]
|
"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]:
|
def suggest_hashtags(topic: str, tone: str) -> List[str]:
|
||||||
"""Suggest relevant hashtags based on topic and tone."""
|
"""Suggest relevant hashtags based on topic and tone."""
|
||||||
# Enhanced hashtag suggestions 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]
|
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(
|
def generate_tweet_variations(
|
||||||
hook: str,
|
hook: str,
|
||||||
target_audience: str,
|
target_audience: str,
|
||||||
@@ -177,58 +132,14 @@ def generate_tweet_variations(
|
|||||||
|
|
||||||
return sample_tweets[:num_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"""
|
|
||||||
<div style='padding: 20px; border-radius: 10px; background-color: #f0f2f6; margin-bottom: 20px;'>
|
|
||||||
<h3 style='margin: 0;'>Tweet Variation {index + 1}</h3>
|
|
||||||
<p style='margin: 10px 0;'>{tweet['text']}</p>
|
|
||||||
<div style='display: flex; gap: 10px;'>
|
|
||||||
<span style='background-color: #e1e4e8; padding: 5px 10px; border-radius: 15px; font-size: 0.8em;'>
|
|
||||||
Score: {tweet['engagement_score']}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
""", 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():
|
def smart_tweet_generator():
|
||||||
"""Enhanced Smart Tweet Generator with improved UI and AI integration."""
|
"""Enhanced Smart Tweet Generator with improved UI and AI integration."""
|
||||||
st.title("✨ Smart Tweet Generator")
|
st.title("✨ Smart Tweet Generator")
|
||||||
st.markdown("Create engaging tweets with AI-powered optimization")
|
st.markdown("Create engaging tweets with AI-powered optimization")
|
||||||
|
|
||||||
|
# Apply theme
|
||||||
|
Theme().apply()
|
||||||
|
|
||||||
# Input section with improved UI
|
# Input section with improved UI
|
||||||
with st.expander("Tweet Parameters", expanded=True):
|
with st.expander("Tweet Parameters", expanded=True):
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
@@ -291,34 +202,27 @@ def smart_tweet_generator():
|
|||||||
# Display performance metrics
|
# Display performance metrics
|
||||||
st.markdown("### 📊 Performance Metrics")
|
st.markdown("### 📊 Performance Metrics")
|
||||||
for tweet in tweets:
|
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
|
# Generate metrics
|
||||||
st.progress(performance["overall_score"] / 100)
|
metrics = generate_tweet_metrics(engagement_score)
|
||||||
st.metric("Overall Score", f"{performance['overall_score']:.1f}%")
|
|
||||||
|
|
||||||
# Detailed metrics in columns
|
# Display tweet card
|
||||||
cols = st.columns(4)
|
TweetCard(
|
||||||
metrics = performance["metrics"]
|
content=tweet["text"],
|
||||||
|
engagement_score=engagement_score,
|
||||||
with cols[0]:
|
hashtags=tweet["hashtags"],
|
||||||
st.metric("Character Count", f"{metrics['character_count']['score']:.1f}%")
|
emojis=tweet["emojis"],
|
||||||
with cols[1]:
|
metrics=metrics,
|
||||||
st.metric("Hashtag Usage", f"{metrics['hashtag_usage']['score']:.1f}%")
|
on_copy=lambda: copy_to_clipboard(tweet["text"]),
|
||||||
with cols[2]:
|
on_save=lambda: save_tweet(tweet)
|
||||||
st.metric("Engagement", f"{metrics['engagement_potential']['score']:.1f}%")
|
).render()
|
||||||
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))
|
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
@@ -326,17 +230,23 @@ def smart_tweet_generator():
|
|||||||
st.markdown("### 📥 Export Options")
|
st.markdown("### 📥 Export Options")
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
with col1:
|
with col1:
|
||||||
if st.button("Export as JSON"):
|
create_download_button(
|
||||||
st.download_button(
|
data=tweets,
|
||||||
"Download JSON",
|
filename=f"tweets_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
|
||||||
data=json.dumps(tweets, indent=2),
|
button_text="Export as JSON"
|
||||||
file_name=f"tweets_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
|
)
|
||||||
mime="application/json"
|
|
||||||
)
|
|
||||||
with col2:
|
with col2:
|
||||||
if st.button("Copy All Tweets"):
|
if st.button("Copy All Tweets"):
|
||||||
tweet_texts = "\n\n".join(tweet["text"] for tweet in 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__":
|
if __name__ == "__main__":
|
||||||
smart_tweet_generator()
|
smart_tweet_generator()
|
||||||
@@ -1,116 +1,29 @@
|
|||||||
|
"""
|
||||||
|
Twitter Dashboard with modern UI components.
|
||||||
|
"""
|
||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import streamlit.components.v1 as components
|
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
import json
|
import json
|
||||||
import base64
|
from datetime import datetime
|
||||||
|
|
||||||
from .tweet_generator import smart_tweet_generator
|
from .tweet_generator import smart_tweet_generator
|
||||||
|
from .twitter_streamlit_ui import (
|
||||||
def add_bg_from_base64(base64_string):
|
TwitterDashboard,
|
||||||
"""Add background image using base64 string."""
|
FeatureCard,
|
||||||
return f'''
|
TweetForm,
|
||||||
<style>
|
SettingsForm,
|
||||||
.stApp {{
|
Sidebar,
|
||||||
background-image: url("data:image/png;base64,{base64_string}");
|
Header,
|
||||||
background-size: cover;
|
Tabs,
|
||||||
background-position: center;
|
Breadcrumbs,
|
||||||
background-repeat: no-repeat;
|
Theme,
|
||||||
background-attachment: fixed;
|
save_to_session,
|
||||||
}}
|
get_from_session,
|
||||||
|
clear_session,
|
||||||
/* Enhanced styling for containers */
|
show_success_message,
|
||||||
.element-container, .stMarkdown, .stButton {{
|
show_error_message
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
)
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 10px 0;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* Typography enhancements */
|
|
||||||
h1, h2, h3 {{
|
|
||||||
color: #ffffff !important;
|
|
||||||
font-family: 'Helvetica Neue', sans-serif;
|
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
|
||||||
}}
|
|
||||||
|
|
||||||
p, li {{
|
|
||||||
color: #e0e0e0 !important;
|
|
||||||
font-family: 'Helvetica Neue', sans-serif;
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* Button styling */
|
|
||||||
.stButton > button {{
|
|
||||||
background: linear-gradient(45deg, #1DA1F2, #0C85D0);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stButton > button:hover {{
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* Tab styling */
|
|
||||||
.stTabs [data-baseweb="tab-list"] {{
|
|
||||||
gap: 8px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stTabs [data-baseweb="tab"] {{
|
|
||||||
background-color: transparent;
|
|
||||||
color: #ffffff;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 5px;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.stTabs [data-baseweb="tab"]:hover {{
|
|
||||||
background-color: rgba(29, 161, 242, 0.2);
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* Feature card styling */
|
|
||||||
.feature-card {{
|
|
||||||
background: linear-gradient(135deg, rgba(29, 161, 242, 0.1), rgba(0, 0, 0, 0.3));
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.feature-card:hover {{
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* Status badge styling */
|
|
||||||
.status-badge {{
|
|
||||||
display: inline-block;
|
|
||||||
padding: 5px 15px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.status-active {{
|
|
||||||
background: linear-gradient(45deg, #00C853, #69F0AE);
|
|
||||||
color: #000000;
|
|
||||||
}}
|
|
||||||
|
|
||||||
.status-coming-soon {{
|
|
||||||
background: linear-gradient(45deg, #FFD700, #FFA000);
|
|
||||||
color: #000000;
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
'''
|
|
||||||
|
|
||||||
def load_feature_data() -> Dict:
|
def load_feature_data() -> Dict:
|
||||||
"""Load feature data from a structured format."""
|
"""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"""
|
|
||||||
<div class='feature-card'>
|
|
||||||
<h3 style='color: #ffffff; margin: 0;'>{feature['icon']} {feature['name']}</h3>
|
|
||||||
<p style='color: #e0e0e0; margin: 10px 0;'>{feature['description']}</p>
|
|
||||||
<span class='status-badge {status_class}'>
|
|
||||||
{feature['status'].title()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
""", 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():
|
def run_dashboard():
|
||||||
"""Main function to run the Twitter dashboard."""
|
"""Main function to run the Twitter dashboard."""
|
||||||
# Add space-themed background
|
# Initialize dashboard
|
||||||
st.markdown(add_bg_from_base64(get_space_background()), unsafe_allow_html=True)
|
dashboard = TwitterDashboard()
|
||||||
|
|
||||||
# Enhanced Header with gradient text
|
|
||||||
st.markdown("""
|
|
||||||
<div style='text-align: center; padding: 40px 0;'>
|
|
||||||
<h1 style='
|
|
||||||
font-size: 3em;
|
|
||||||
background: linear-gradient(45deg, #1DA1F2, #ffffff);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
'>🐦 Twitter AI Writer</h1>
|
|
||||||
<p style='
|
|
||||||
font-size: 1.2em;
|
|
||||||
color: #e0e0e0;
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
'>Your all-in-one Twitter content creation and management platform.
|
|
||||||
Harness the power of AI to enhance your Twitter marketing strategy.</p>
|
|
||||||
</div>
|
|
||||||
""", unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# Load feature data
|
# Load feature data
|
||||||
features = 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("<h2 style='color: #ffffff;'>🚀 Quick Actions</h2>", 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
|
# Setup navigation
|
||||||
for category in features.values():
|
sidebar = Sidebar(title="Twitter Tools")
|
||||||
render_category_section(category)
|
sidebar.add_menu_item("Dashboard", "📊", "dashboard")
|
||||||
|
sidebar.add_menu_item("Tweet Generator", "✍️", "tweet_generator")
|
||||||
# If this is the tweet generation category and the Smart Tweet Generator is active,
|
sidebar.add_menu_item("Analytics", "📈", "analytics")
|
||||||
# add a button to launch it
|
sidebar.add_menu_item("Settings", "⚙️", "settings")
|
||||||
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):
|
# Setup header
|
||||||
category["features"][0]["function"]()
|
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
|
def render_overview(features: Dict):
|
||||||
st.markdown("---")
|
"""Render the overview tab content."""
|
||||||
st.markdown("""
|
# Feature cards
|
||||||
<div style='text-align: center; padding: 20px; background: rgba(0, 0, 0, 0.5); border-radius: 10px;'>
|
col1, col2, col3 = st.columns(3)
|
||||||
<p style='color: #ffffff; margin-bottom: 10px;'>Need assistance? We're here to help!</p>
|
|
||||||
<div style='display: flex; justify-content: center; gap: 20px;'>
|
with col1:
|
||||||
<a href='#' style='
|
FeatureCard(
|
||||||
text-decoration: none;
|
title="Tweet Generator",
|
||||||
color: #1DA1F2;
|
description="Create engaging tweets with AI assistance",
|
||||||
background: rgba(255, 255, 255, 0.1);
|
icon="✍️",
|
||||||
padding: 8px 20px;
|
features=[
|
||||||
border-radius: 20px;
|
{
|
||||||
transition: all 0.3s ease;
|
"name": "AI-Powered",
|
||||||
'>📚 Documentation</a>
|
"description": "Generate tweets using advanced AI"
|
||||||
<a href='#' style='
|
},
|
||||||
text-decoration: none;
|
{
|
||||||
color: #1DA1F2;
|
"name": "Customizable",
|
||||||
background: rgba(255, 255, 255, 0.1);
|
"description": "Adjust tone, length, and style"
|
||||||
padding: 8px 20px;
|
}
|
||||||
border-radius: 20px;
|
],
|
||||||
transition: all 0.3s ease;
|
on_click=lambda: save_to_session("current_page", "tweet_generator")
|
||||||
'>💬 Contact Support</a>
|
).render()
|
||||||
</div>
|
|
||||||
</div>
|
with col2:
|
||||||
""", unsafe_allow_html=True)
|
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__":
|
if __name__ == "__main__":
|
||||||
run_dashboard()
|
run_dashboard()
|
||||||
203
lib/ai_writers/twitter_writers/twitter_streamlit_ui/README.md
Normal file
203
lib/ai_writers/twitter_writers/twitter_streamlit_ui/README.md
Normal file
@@ -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
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
@@ -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"""
|
||||||
|
<div class="card">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: {Theme.SPACING["sm"]};">
|
||||||
|
{f'<span style="font-size: 1.5em; margin-right: {Theme.SPACING["sm"]};">{self.icon}</span>' if self.icon else ''}
|
||||||
|
<h3 style="margin: 0;">{self.title}</h3>
|
||||||
|
</div>
|
||||||
|
<p style="color: {Theme.COLORS["text_secondary"]}; margin: {Theme.SPACING["sm"]} 0;">
|
||||||
|
{self.description}
|
||||||
|
</p>
|
||||||
|
{f'<span class="status-badge status-{self.status}">{self.status.title()}</span>' if self.status else ''}
|
||||||
|
</div>
|
||||||
|
""", 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"""
|
||||||
|
<div class="card feature-card">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: {Theme.SPACING["sm"]};">
|
||||||
|
<span style="font-size: 1.5em; margin-right: {Theme.SPACING["sm"]};">{self.icon}</span>
|
||||||
|
<h3 style="margin: 0;">{self.title}</h3>
|
||||||
|
</div>
|
||||||
|
<p style="color: {Theme.COLORS["text_secondary"]}; margin: {Theme.SPACING["sm"]} 0;">
|
||||||
|
{self.description}
|
||||||
|
</p>
|
||||||
|
<span class="status-badge status-{self.status}">{self.status.title()}</span>
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
if self.features:
|
||||||
|
for feature in self.features:
|
||||||
|
st.markdown(f"""
|
||||||
|
<div style="margin-left: {Theme.SPACING["lg"]}; margin-top: {Theme.SPACING["sm"]};">
|
||||||
|
<p style="margin: 0;">
|
||||||
|
<strong>{feature["name"]}</strong>: {feature["description"]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
""", 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"""
|
||||||
|
<div class="card tweet-card">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: {Theme.SPACING["sm"]};">
|
||||||
|
<span style="font-size: 1.5em; margin-right: {Theme.SPACING["sm"]};">{self.icon}</span>
|
||||||
|
<h3 style="margin: 0;">Tweet</h3>
|
||||||
|
</div>
|
||||||
|
<p style="color: {Theme.COLORS["text"]}; margin: {Theme.SPACING["sm"]} 0;">
|
||||||
|
{self.description}
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; gap: {Theme.SPACING["sm"]}; margin: {Theme.SPACING["sm"]} 0;">
|
||||||
|
{''.join(f'<span style="color: {Theme.COLORS["primary"]};">{tag}</span>' for tag in self.hashtags)}
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: {Theme.SPACING["sm"]}; margin: {Theme.SPACING["sm"]} 0;">
|
||||||
|
{''.join(f'<span>{emoji}</span>' for emoji in self.emojis)}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: {Theme.SPACING["md"]};">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<span>Engagement Score: {self.engagement_score}%</span>
|
||||||
|
<div style="display: flex; gap: {Theme.SPACING["sm"]};">
|
||||||
|
<button class="stButton" onclick="copyTweet()">Copy</button>
|
||||||
|
<button class="stButton" onclick="saveTweet()">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""", 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}%")
|
||||||
@@ -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"""
|
||||||
|
<div class="form-container">
|
||||||
|
<h3 style="margin-bottom: {Theme.SPACING['sm']};">{self.title}</h3>
|
||||||
|
{f'<p style="color: {Theme.COLORS["text_secondary"]}; margin-bottom: {Theme.SPACING["md"]};">{self.description}</p>' if self.description else ''}
|
||||||
|
</div>
|
||||||
|
""", 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"
|
||||||
|
)
|
||||||
@@ -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"""
|
||||||
|
<h2 style="margin: {Theme.SPACING["sm"]} 0;">{self.title}</h2>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# Menu items
|
||||||
|
for item in self.menu_items:
|
||||||
|
st.markdown(f"""
|
||||||
|
<div class="menu-item">
|
||||||
|
<span style="font-size: 1.2em; margin-right: {Theme.SPACING["sm"]};">{item["icon"]}</span>
|
||||||
|
<span>{item["label"]}</span>
|
||||||
|
{f'<span class="badge">{item["badge"]}</span>' if item.get("badge") else ""}
|
||||||
|
</div>
|
||||||
|
""", 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"""
|
||||||
|
<button class="header-action" title="{help_text}">
|
||||||
|
<span style="font-size: 1.2em; margin-right: {Theme.SPACING["xs"]};">{action["icon"]}</span>
|
||||||
|
{action["label"]}
|
||||||
|
</button>
|
||||||
|
""")
|
||||||
|
|
||||||
|
st.markdown(f"""
|
||||||
|
<div class="header">
|
||||||
|
<div>
|
||||||
|
<h1 style="margin: 0;">{self.title}</h1>
|
||||||
|
{f'<p style="color: {Theme.COLORS["text_secondary"]}; margin: {Theme.SPACING["xs"]} 0;">{self.subtitle}</p>' if self.subtitle else ""}
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: {Theme.SPACING["sm"]};">
|
||||||
|
{''.join(action_buttons)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""", 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'<span style="font-size: 1.2em; margin-right: {Theme.SPACING["xs"]};">{item["icon"]}</span>' if item.get("icon") else ""
|
||||||
|
link_html = f'<a href="#" onclick="setPage(\'{item["page"]}\')">{item["label"]}</a>' if item.get("page") else f'<span>{item["label"]}</span>'
|
||||||
|
separator = f'<span style="margin: 0 {Theme.SPACING["xs"]};">/</span>' if i < len(self.items) - 1 else ""
|
||||||
|
|
||||||
|
breadcrumb_items.append(f"""
|
||||||
|
<span class="breadcrumb-item">
|
||||||
|
{icon_html}
|
||||||
|
{link_html}
|
||||||
|
</span>
|
||||||
|
{separator}
|
||||||
|
""")
|
||||||
|
|
||||||
|
st.markdown(f"""
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
{''.join(breadcrumb_items)}
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
270
lib/ai_writers/twitter_writers/twitter_streamlit_ui/dashboard.py
Normal file
270
lib/ai_writers/twitter_writers/twitter_streamlit_ui/dashboard.py
Normal file
@@ -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()
|
||||||
@@ -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"<style>{cls.get_css()}</style>", unsafe_allow_html=True)
|
||||||
@@ -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'<script>navigator.clipboard.writeText("{text}")</script>', 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)}")
|
||||||
1079
lib/ai_writers/youtube_writers/modules/channel_trailer_generator.py
Normal file
1079
lib/ai_writers/youtube_writers/modules/channel_trailer_generator.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ from .modules.tags_generator import write_yt_tags
|
|||||||
from .modules.shorts_script_generator import write_yt_shorts
|
from .modules.shorts_script_generator import write_yt_shorts
|
||||||
from .modules.community_post_generator import write_yt_community_post
|
from .modules.community_post_generator import write_yt_community_post
|
||||||
from .modules.shorts_video_generator import write_yt_shorts_video
|
from .modules.shorts_video_generator import write_yt_shorts_video
|
||||||
|
from .modules.channel_trailer_generator import write_yt_channel_trailer
|
||||||
|
|
||||||
|
|
||||||
def youtube_main_menu():
|
def youtube_main_menu():
|
||||||
@@ -75,6 +76,15 @@ def youtube_main_menu():
|
|||||||
"function": write_yt_shorts_video,
|
"function": write_yt_shorts_video,
|
||||||
"status": "active"
|
"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
|
# Optimization Tools
|
||||||
{
|
{
|
||||||
@@ -135,15 +145,6 @@ def youtube_main_menu():
|
|||||||
"function": None,
|
"function": None,
|
||||||
"status": "future"
|
"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",
|
"name": "Video Series Planner",
|
||||||
"icon": "📅",
|
"icon": "📅",
|
||||||
|
|||||||
@@ -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_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.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.linkedin_writer.linkedin_ai_writer import linkedin_main_menu
|
||||||
from lib.ai_writers.twitter_writers.twitter_dashboard import run_dashboard
|
from lib.ai_writers.twitter_writers.twitter_dashboard import run_dashboard
|
||||||
@@ -198,7 +198,9 @@ def ai_finance_ta_writer():
|
|||||||
if ticker_symbol:
|
if ticker_symbol:
|
||||||
with st.spinner("Generating TA Report..."):
|
with st.spinner("Generating TA Report..."):
|
||||||
try:
|
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.success(f"Successfully generated TA report for: {ticker_symbol}")
|
||||||
st.markdown(ta_report)
|
st.markdown(ta_report)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from ...website_analyzer import analyze_website
|
from ...website_analyzer import analyze_website
|
||||||
from ...website_analyzer.seo_analyzer import analyze_seo
|
from ...website_analyzer.analyzer import WebsiteAnalyzer
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict, Any
|
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
|
# Call the analyze_website function
|
||||||
results = analyze_website(url)
|
results = analyze_website(url)
|
||||||
|
|
||||||
# If full analysis is selected, add SEO analysis
|
# Replace the old SEO analysis code with the new analyzer
|
||||||
if analyze_type == "Full Analysis with SEO":
|
analyzer = WebsiteAnalyzer()
|
||||||
seo_results = analyze_seo(url)
|
seo_results = analyzer.analyze_website(url)
|
||||||
if seo_results.success:
|
if seo_results.get('success', False):
|
||||||
results['data']['seo_analysis'] = {
|
results['data']['seo_analysis'] = seo_results['data']['analysis']['seo_info']
|
||||||
'overall_score': seo_results.overall_score,
|
else:
|
||||||
'meta_tags': {
|
results['data']['seo_analysis'] = {
|
||||||
'title': seo_results.meta_tags.title,
|
'error': seo_results.get('error', 'Unknown error in SEO analysis'),
|
||||||
'description': seo_results.meta_tags.description,
|
'overall_score': 0,
|
||||||
'keywords': seo_results.meta_tags.keywords,
|
'meta_tags': {},
|
||||||
'has_robots': seo_results.meta_tags.has_robots,
|
'content': {},
|
||||||
'has_sitemap': seo_results.meta_tags.has_sitemap
|
'recommendations': []
|
||||||
},
|
}
|
||||||
'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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(f"[render_website_setup] Analysis results received: {results.get('success', False)}")
|
logger.debug(f"[render_website_setup] Analysis results received: {results.get('success', False)}")
|
||||||
|
|
||||||
|
|||||||
83
lib/utils/save_to_file.py
Normal file
83
lib/utils/save_to_file.py
Normal file
@@ -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)
|
||||||
@@ -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.on_page_seo_analyzer import analyze_onpage_seo
|
||||||
from lib.ai_seo_tools.weburl_seo_checker import url_seo_checker
|
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_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("<hr style='margin:1.5rem 0;'>", 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"""
|
||||||
|
<div class="seo-card" style="background: {gradient}; position: relative; overflow: hidden;">
|
||||||
|
<div class="seo-card-overlay"></div>
|
||||||
|
<div class="seo-icon">{details['icon']}</div>
|
||||||
|
<div class="seo-title">{combo_name}</div>
|
||||||
|
<div class="seo-description">{details['description']}</div>
|
||||||
|
<div>
|
||||||
|
{''.join([f'<span class="tool-badge">{tool}</span>' for tool in details['tools']])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""", 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"""
|
||||||
|
<div class="seo-card" style="background: {feature['gradient']}; position: relative; overflow: hidden;">
|
||||||
|
<div class="seo-card-overlay"></div>
|
||||||
|
<div class="seo-icon">{feature['icon']}</div>
|
||||||
|
<div class="seo-title">{feature['name']}</div>
|
||||||
|
<div class="seo-description">{feature['description']}</div>
|
||||||
|
<div>
|
||||||
|
{''.join([f'<span class="tool-badge">{badge}</span>' for badge in feature['badges']])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""", 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"""
|
||||||
|
<div class="seo-card" style="background: {gradient}; position: relative; overflow: hidden;">
|
||||||
|
<div class="seo-card-overlay"></div>
|
||||||
|
<div class="seo-icon">{details['icon']}</div>
|
||||||
|
<div class="seo-title">{tool_name}</div>
|
||||||
|
<div class="seo-description">{details['description']}</div>
|
||||||
|
</div>
|
||||||
|
""", 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("""
|
||||||
|
<div style='text-align: center; margin: 2rem 0;'>
|
||||||
|
<h2>About This Dashboard</h2>
|
||||||
|
<p style='color: #666;'>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.</p>
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.markdown("""
|
||||||
|
<style>
|
||||||
|
.seo-card {
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 4px 16px rgba(44, 62, 80, 0.10), 0 1.5px 4px rgba(44,62,80,0.06);
|
||||||
|
transition: transform 0.2s cubic-bezier(.4,2,.6,1), box-shadow 0.2s;
|
||||||
|
height: 100%;
|
||||||
|
border: 1.5px solid #e3e8ee;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.seo-card-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(255,255,255,0.72);
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 8px rgba(44,62,80,0.08);
|
||||||
|
}
|
||||||
|
.seo-card:hover {
|
||||||
|
transform: translateY(-6px) scale(1.025);
|
||||||
|
box-shadow: 0 8px 32px rgba(44, 62, 80, 0.18), 0 2px 8px rgba(44,62,80,0.10);
|
||||||
|
border-color: #4CAF50;
|
||||||
|
}
|
||||||
|
.seo-icon {
|
||||||
|
font-size: 2.7rem;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
text-shadow: 0 2px 8px rgba(44,62,80,0.10);
|
||||||
|
}
|
||||||
|
.seo-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #222b45;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
text-shadow: 0 2px 8px rgba(44,62,80,0.10);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
.seo-description {
|
||||||
|
color: #34495e;
|
||||||
|
font-size: 1.08rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-shadow: 0 1px 4px rgba(44,62,80,0.08);
|
||||||
|
}
|
||||||
|
.tool-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
color: #2196F3;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid #e3e8ee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
def ai_seo_tools():
|
def ai_seo_tools():
|
||||||
"""
|
"""
|
||||||
@@ -17,71 +357,83 @@ def ai_seo_tools():
|
|||||||
such as generating structured data, optimizing images, checking page speed,
|
such as generating structured data, optimizing images, checking page speed,
|
||||||
and analyzing on-page SEO.
|
and analyzing on-page SEO.
|
||||||
"""
|
"""
|
||||||
st.markdown(
|
# Check if a specific tool is selected
|
||||||
"""
|
selected_tool = st.query_params.get("tool")
|
||||||
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"
|
|
||||||
]
|
|
||||||
|
|
||||||
# User selection of SEO tools using radio buttons
|
if selected_tool:
|
||||||
choice = st.radio(
|
# Map tool paths to their respective functions
|
||||||
"**👇 Select an AI SEO Tool:**",
|
tool_functions = {
|
||||||
options,
|
# Individual tools
|
||||||
index=0,
|
"structured_data": ai_structured_data,
|
||||||
format_func=lambda x: x
|
"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
|
# Create tabs for each tool in the combination
|
||||||
if choice == "📝 Generate Structured Data - Rich Snippet":
|
tabs = st.tabs([f"Step {i+1}" for i in range(len(tools))])
|
||||||
# Generate Structured Data for Rich Snippets
|
|
||||||
ai_structured_data()
|
# Run each tool in its own tab
|
||||||
|
for i, (tab, tool) in enumerate(zip(tabs, tools)):
|
||||||
elif choice == "📝 Generate Meta Description for SEO":
|
with tab:
|
||||||
# Generate SEO-optimized meta descriptions
|
st.markdown(f"### Step {i+1}")
|
||||||
metadesc_generator_main()
|
tool()
|
||||||
|
|
||||||
elif choice == "✏️ Generate SEO Optimized Blog Titles":
|
# Add cross-tool analysis section
|
||||||
# Generate SEO-friendly blog titles
|
st.markdown("## 📊 Cross-Tool Analysis")
|
||||||
ai_title_generator()
|
st.markdown("Analyzing results across all tools...")
|
||||||
|
|
||||||
elif choice == "🖼️ Generate Image Alt Text":
|
# Add recommendations based on combined results
|
||||||
# Generate alternative text for images
|
st.markdown("## 💡 Recommendations")
|
||||||
alt_text_gen()
|
st.markdown("Based on the combined analysis, here are the key recommendations:")
|
||||||
|
|
||||||
elif choice == "📄 Generate OpenGraph Tags":
|
# Add a button to export the complete analysis
|
||||||
# Generate OpenGraph tags for social media sharing
|
if st.button("📥 Export Complete Analysis", use_container_width=True):
|
||||||
og_tag_generator()
|
st.info("Analysis export functionality coming soon!")
|
||||||
|
|
||||||
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()
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""
|
||||||
|
UI setup module for ALwrity application.
|
||||||
|
Provides consistent navigation and layout structure.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
from lib.utils.file_processor import load_image
|
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.youtube_writers.youtube_ai_writer import youtube_main_menu
|
||||||
from lib.ai_writers.ai_writer_dashboard import get_ai_writers, list_ai_writers
|
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("""
|
||||||
|
<style>
|
||||||
|
.social-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.social-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
.social-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.social-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.social-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.social-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""", 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"""
|
||||||
|
<div class="social-card">
|
||||||
|
<div class="social-icon">{details['icon']}</div>
|
||||||
|
<div class="social-title">{platform}</div>
|
||||||
|
<div class="social-description">{details['description']}</div>
|
||||||
|
</div>
|
||||||
|
""", 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():
|
def setup_ui():
|
||||||
"""Set up the UI with custom styling."""
|
"""Set up the UI with custom styling."""
|
||||||
@@ -314,29 +412,18 @@ def setup_alwrity_ui():
|
|||||||
"AI Writers": ("📝", get_ai_writers),
|
"AI Writers": ("📝", get_ai_writers),
|
||||||
"Content Planning": ("📅", content_planning_tools),
|
"Content Planning": ("📅", content_planning_tools),
|
||||||
"AI SEO Tools": ("🔍", ai_seo_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!")),
|
"Agents Teams(TBD)": ("🤝", lambda: st.subheader("Agents Teams - Coming Soon!")),
|
||||||
"Ask Alwrity(TBD)": ("💬", lambda: (
|
"Ask Alwrity(TBD)": ("💬", lambda: (
|
||||||
st.subheader("Chat with your Data, Chat with any Data.. COMING SOON !"),
|
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("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")
|
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")
|
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
|
# Create sidebar navigation
|
||||||
st.sidebar.markdown("### ALwrity Options")
|
st.sidebar.markdown("### ALwrity Options")
|
||||||
st.sidebar.markdown('<div class="sidebar-nav">', unsafe_allow_html=True)
|
st.sidebar.markdown('<div class="sidebar-nav">', unsafe_allow_html=True)
|
||||||
@@ -345,53 +432,12 @@ def setup_alwrity_ui():
|
|||||||
for name, (icon, func) in nav_items.items():
|
for name, (icon, func) in nav_items.items():
|
||||||
button_class = "nav-button active" if st.session_state.active_tab == name else "nav-button"
|
button_class = "nav-button active" if st.session_state.active_tab == name else "nav-button"
|
||||||
|
|
||||||
if name == "AI Social Tools":
|
if st.sidebar.button(f"{icon} {name}", key=f"nav_{name}",
|
||||||
# For AI Social Tools, we'll create a button that toggles the sub-menu
|
help=f"Navigate to {name}", use_container_width=True):
|
||||||
if st.sidebar.button(f"{icon} {name}", key=f"nav_{name}",
|
st.session_state.active_tab = name
|
||||||
help=f"Navigate to {name}", use_container_width=True):
|
# Reset sub-tab when main tab changes
|
||||||
st.session_state.active_tab = name
|
st.session_state.active_sub_tab = None
|
||||||
# Reset sub-tab when main tab changes
|
logger.info(f"Selected main tab: {name}")
|
||||||
st.session_state.active_sub_tab = None
|
|
||||||
logger.info(f"Selected main tab: {name}")
|
|
||||||
|
|
||||||
# If AI Social Tools is active, show the sub-menu
|
|
||||||
if st.session_state.active_tab == "AI Social Tools":
|
|
||||||
st.sidebar.markdown('<div class="sub-menu">', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# Create sub-menu buttons
|
|
||||||
for sub_name, (sub_icon, sub_func) in social_tools_submenu.items():
|
|
||||||
# Create the button with a custom key that includes the platform name
|
|
||||||
button_key = f"sub_{sub_name}"
|
|
||||||
|
|
||||||
# Determine if this button is active
|
|
||||||
is_active = st.session_state.active_sub_tab == sub_name
|
|
||||||
|
|
||||||
# Create a container with the platform-specific class
|
|
||||||
platform_class = f"{sub_name.lower()}-button"
|
|
||||||
if is_active:
|
|
||||||
platform_class += " active"
|
|
||||||
|
|
||||||
# Add the platform-specific class to the button container
|
|
||||||
st.sidebar.markdown(f'<div class="{platform_class}">', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# Create the button
|
|
||||||
if st.sidebar.button(f"{sub_icon} {sub_name}", key=button_key,
|
|
||||||
help=f"Navigate to {sub_name}", use_container_width=True):
|
|
||||||
st.session_state.active_sub_tab = sub_name
|
|
||||||
logger.info(f"Selected social tool: {sub_name}")
|
|
||||||
|
|
||||||
# Close the div
|
|
||||||
st.sidebar.markdown('</div>', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
st.sidebar.markdown('</div>', unsafe_allow_html=True)
|
|
||||||
else:
|
|
||||||
# For other navigation items, create regular buttons
|
|
||||||
if st.sidebar.button(f"{icon} {name}", key=f"nav_{name}",
|
|
||||||
help=f"Navigate to {name}", use_container_width=True):
|
|
||||||
st.session_state.active_tab = name
|
|
||||||
# Reset sub-tab when main tab changes
|
|
||||||
st.session_state.active_sub_tab = None
|
|
||||||
logger.info(f"Selected main tab: {name}")
|
|
||||||
|
|
||||||
st.sidebar.markdown('</div>', unsafe_allow_html=True)
|
st.sidebar.markdown('</div>', unsafe_allow_html=True)
|
||||||
|
|
||||||
@@ -402,10 +448,36 @@ def setup_alwrity_ui():
|
|||||||
st.sidebar.image(icon_path, use_container_width=False)
|
st.sidebar.image(icon_path, use_container_width=False)
|
||||||
st.sidebar.markdown('</div>', unsafe_allow_html=True)
|
st.sidebar.markdown('</div>', 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 st.session_state.active_tab == "AI Social Tools":
|
||||||
if not st.session_state.active_sub_tab:
|
# Check if a specific tool is selected
|
||||||
# Only show title and info when no sub-tab 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("""
|
st.markdown("""
|
||||||
<style>
|
<style>
|
||||||
.main .block-container {
|
.main .block-container {
|
||||||
@@ -414,40 +486,14 @@ def setup_alwrity_ui():
|
|||||||
</style>
|
</style>
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
st.title(f"{nav_items[st.session_state.active_tab][0]} {st.session_state.active_tab}")
|
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.")
|
render_social_tools_dashboard()
|
||||||
else:
|
|
||||||
# When a platform is selected, show no title and minimize spacing
|
|
||||||
st.markdown("""
|
|
||||||
<style>
|
|
||||||
.main .block-container {
|
|
||||||
padding-top: 0 !important;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove all margins and padding from content area */
|
|
||||||
.element-container {
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide any automatic headers */
|
|
||||||
.main .block-container > div:first-child {
|
|
||||||
margin-top: 0 !important;
|
|
||||||
padding-top: 0 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
""", unsafe_allow_html=True)
|
|
||||||
# Call the function directly without any title
|
|
||||||
social_tools_submenu[st.session_state.active_sub_tab][1]()
|
|
||||||
else:
|
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":
|
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")
|
writer = st.query_params.get("writer")
|
||||||
logger.info(f"Current writer from query params: {writer}")
|
logger.info(f"Current writer from query params: {writer}")
|
||||||
|
|
||||||
if writer:
|
if writer:
|
||||||
# Get the list of writers without rendering the dashboard
|
|
||||||
writers = list_ai_writers()
|
writers = list_ai_writers()
|
||||||
logger.info(f"Found {len(writers)} writers")
|
logger.info(f"Found {len(writers)} writers")
|
||||||
|
|
||||||
@@ -457,9 +503,7 @@ def setup_alwrity_ui():
|
|||||||
if w["path"] == writer:
|
if w["path"] == writer:
|
||||||
writer_found = True
|
writer_found = True
|
||||||
logger.info(f"Found matching writer: {w['name']}, executing function")
|
logger.info(f"Found matching writer: {w['name']}, executing function")
|
||||||
# Clear any existing content
|
|
||||||
st.empty()
|
st.empty()
|
||||||
# Execute the writer function
|
|
||||||
w["function"]()
|
w["function"]()
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -467,11 +511,9 @@ def setup_alwrity_ui():
|
|||||||
logger.error(f"No writer found with path: {writer}")
|
logger.error(f"No writer found with path: {writer}")
|
||||||
st.error(f"No writer found with path: {writer}")
|
st.error(f"No writer found with path: {writer}")
|
||||||
else:
|
else:
|
||||||
# If no writer selected, show the dashboard
|
|
||||||
logger.info("No writer selected, showing dashboard")
|
logger.info("No writer selected, showing dashboard")
|
||||||
get_ai_writers()
|
get_ai_writers()
|
||||||
else:
|
else:
|
||||||
# For all other tabs, show the title
|
|
||||||
st.markdown("""
|
st.markdown("""
|
||||||
<style>
|
<style>
|
||||||
.main .block-container {
|
.main .block-container {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Website analyzer module for AI-powered website analysis."""
|
"""Website analyzer module for AI-powered website analysis."""
|
||||||
|
|
||||||
from .analyzer import analyze_website
|
from .analyzer import analyze_website, WebsiteAnalyzer
|
||||||
from .seo_analyzer import analyze_seo
|
|
||||||
from .models import SEOAnalysisResult
|
from .models import SEOAnalysisResult
|
||||||
|
|
||||||
__all__ = ['analyze_seo', 'SEOAnalysisResult', 'analyze_website']
|
__all__ = ['analyze_website', 'WebsiteAnalyzer', 'SEOAnalysisResult']
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Website scraping and AI analysis module."""
|
"""Website and SEO analysis module."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Tuple
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import urljoin, urlparse
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
@@ -21,51 +21,29 @@ import whois
|
|||||||
import dns.resolver
|
import dns.resolver
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from .models import (
|
||||||
|
SEOAnalysisResult,
|
||||||
|
MetaTagAnalysis,
|
||||||
|
ContentAnalysis,
|
||||||
|
SEORecommendation
|
||||||
|
)
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG,
|
level=logging.INFO,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
handlers=[
|
handlers=[
|
||||||
logging.StreamHandler(),
|
logging.StreamHandler(),
|
||||||
logging.FileHandler('logs/website_analyzer.log')
|
logging.FileHandler('logs/website_analyzer.log')
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create a logger for the website analyzer
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def analyze_website(url: str) -> Dict:
|
# Create a separate logger for scraping operations
|
||||||
"""
|
scraping_logger = logging.getLogger('website_analyzer.scraping')
|
||||||
Analyze a website and return comprehensive results.
|
scraping_logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
Args:
|
|
||||||
url (str): The URL to analyze
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict: Analysis results including various metrics and checks
|
|
||||||
"""
|
|
||||||
logger.info(f"Starting website analysis for URL: {url}")
|
|
||||||
try:
|
|
||||||
analyzer = WebsiteAnalyzer()
|
|
||||||
results = analyzer.analyze_website(url)
|
|
||||||
|
|
||||||
# Add success status to results
|
|
||||||
if "error" in results:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": results["error"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add success status and wrap results
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"data": results
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in analyze_website: {str(e)}", exc_info=True)
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebsiteAnalyzer:
|
class WebsiteAnalyzer:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -89,13 +67,17 @@ class WebsiteAnalyzer:
|
|||||||
try:
|
try:
|
||||||
# Validate URL
|
# Validate URL
|
||||||
if not self._validate_url(url):
|
if not self._validate_url(url):
|
||||||
logger.error(f"Invalid URL format: {url}")
|
error_msg = f"Invalid URL format: {url}"
|
||||||
return {"error": "Invalid URL format"}
|
logger.error(error_msg)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {"stage": "url_validation"}
|
||||||
|
}
|
||||||
|
|
||||||
# Basic URL parsing
|
# Basic URL parsing
|
||||||
parsed_url = urlparse(url)
|
parsed_url = urlparse(url)
|
||||||
domain = parsed_url.netloc
|
domain = parsed_url.netloc
|
||||||
logger.debug(f"Parsed domain: {domain}")
|
|
||||||
|
|
||||||
# Initialize results dictionary
|
# Initialize results dictionary
|
||||||
results = {
|
results = {
|
||||||
@@ -107,36 +89,105 @@ class WebsiteAnalyzer:
|
|||||||
|
|
||||||
# Perform various analyses
|
# Perform various analyses
|
||||||
with ThreadPoolExecutor(max_workers=4) as executor:
|
with ThreadPoolExecutor(max_workers=4) as executor:
|
||||||
|
logger.info("Starting parallel analysis tasks")
|
||||||
|
|
||||||
# Basic website info
|
# Basic website info
|
||||||
|
logger.info("Starting basic info analysis")
|
||||||
basic_info = executor.submit(self._get_basic_info, url).result()
|
basic_info = executor.submit(self._get_basic_info, url).result()
|
||||||
|
if "error" in basic_info:
|
||||||
|
error_msg = f"Basic info analysis failed: {basic_info['error']}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"stage": "basic_info",
|
||||||
|
"details": basic_info.get("error_details", {})
|
||||||
|
}
|
||||||
|
}
|
||||||
results["analysis"]["basic_info"] = basic_info
|
results["analysis"]["basic_info"] = basic_info
|
||||||
|
|
||||||
# SSL/TLS info
|
# SSL/TLS info
|
||||||
|
logger.info("Starting SSL analysis")
|
||||||
ssl_info = executor.submit(self._check_ssl, domain).result()
|
ssl_info = executor.submit(self._check_ssl, domain).result()
|
||||||
results["analysis"]["ssl_info"] = ssl_info
|
results["analysis"]["ssl_info"] = ssl_info
|
||||||
|
|
||||||
# DNS info
|
# DNS info
|
||||||
|
logger.info("Starting DNS analysis")
|
||||||
dns_info = executor.submit(self._check_dns, domain).result()
|
dns_info = executor.submit(self._check_dns, domain).result()
|
||||||
results["analysis"]["dns_info"] = dns_info
|
results["analysis"]["dns_info"] = dns_info
|
||||||
|
|
||||||
# WHOIS info
|
# WHOIS info
|
||||||
|
logger.info("Starting WHOIS analysis")
|
||||||
whois_info = executor.submit(self._get_whois_info, domain).result()
|
whois_info = executor.submit(self._get_whois_info, domain).result()
|
||||||
results["analysis"]["whois_info"] = whois_info
|
results["analysis"]["whois_info"] = whois_info
|
||||||
|
|
||||||
# Content analysis
|
# Content analysis
|
||||||
|
logger.info("Starting content analysis")
|
||||||
content_info = executor.submit(self._analyze_content, url).result()
|
content_info = executor.submit(self._analyze_content, url).result()
|
||||||
|
if "error" in content_info:
|
||||||
|
error_msg = f"Content analysis failed: {content_info['error']}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"stage": "content_analysis",
|
||||||
|
"details": content_info.get("error_details", {})
|
||||||
|
}
|
||||||
|
}
|
||||||
results["analysis"]["content_info"] = content_info
|
results["analysis"]["content_info"] = content_info
|
||||||
|
|
||||||
# Performance metrics
|
# Performance metrics
|
||||||
|
logger.info("Starting performance analysis")
|
||||||
performance = executor.submit(self._check_performance, url).result()
|
performance = executor.submit(self._check_performance, url).result()
|
||||||
|
if "error" in performance:
|
||||||
|
error_msg = f"Performance analysis failed: {performance['error']}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"stage": "performance_analysis",
|
||||||
|
"details": performance.get("error_details", {})
|
||||||
|
}
|
||||||
|
}
|
||||||
results["analysis"]["performance"] = performance
|
results["analysis"]["performance"] = performance
|
||||||
|
|
||||||
|
# SEO analysis
|
||||||
|
logger.info("Starting SEO analysis")
|
||||||
|
seo_analysis = executor.submit(self._analyze_seo, url).result()
|
||||||
|
if "error" in seo_analysis:
|
||||||
|
error_msg = f"SEO analysis failed: {seo_analysis['error']}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"stage": "seo_analysis",
|
||||||
|
"details": seo_analysis.get("error_details", {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results["analysis"]["seo_info"] = seo_analysis
|
||||||
|
|
||||||
logger.info(f"Analysis completed successfully for {url}")
|
logger.info(f"Analysis completed successfully for {url}")
|
||||||
return results
|
logger.debug(f"Final results: {json.dumps(results, indent=2)}")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": results
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during website analysis: {str(e)}", exc_info=True)
|
error_msg = f"Error during website analysis: {str(e)}"
|
||||||
return {"error": str(e)}
|
logger.error(error_msg, exc_info=True)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"type": type(e).__name__,
|
||||||
|
"traceback": str(e.__traceback__)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def _validate_url(self, url: str) -> bool:
|
def _validate_url(self, url: str) -> bool:
|
||||||
"""Validate URL format."""
|
"""Validate URL format."""
|
||||||
@@ -149,7 +200,7 @@ class WebsiteAnalyzer:
|
|||||||
|
|
||||||
def _get_basic_info(self, url: str) -> Dict:
|
def _get_basic_info(self, url: str) -> Dict:
|
||||||
"""Get basic website information."""
|
"""Get basic website information."""
|
||||||
logger.debug(f"Getting basic info for {url}")
|
scraping_logger.debug(f"Getting basic info for {url}")
|
||||||
try:
|
try:
|
||||||
response = self.session.get(url, timeout=10)
|
response = self.session.get(url, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -165,13 +216,31 @@ class WebsiteAnalyzer:
|
|||||||
"robots_txt": self._get_robots_txt(url),
|
"robots_txt": self._get_robots_txt(url),
|
||||||
"sitemap": self._get_sitemap(url)
|
"sitemap": self._get_sitemap(url)
|
||||||
}
|
}
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
error_msg = f"Request error in basic info: {str(e)}"
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
return {
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"type": "RequestException",
|
||||||
|
"status_code": getattr(e.response, 'status_code', None) if hasattr(e, 'response') else None,
|
||||||
|
"url": url
|
||||||
|
}
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting basic info: {str(e)}", exc_info=True)
|
error_msg = f"Error getting basic info: {str(e)}"
|
||||||
return {"error": str(e)}
|
logger.error(error_msg, exc_info=True)
|
||||||
|
return {
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"type": type(e).__name__,
|
||||||
|
"traceback": str(e.__traceback__)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def _check_ssl(self, domain: str) -> Dict:
|
def _check_ssl(self, domain: str) -> Dict:
|
||||||
"""Check SSL/TLS certificate information."""
|
"""Check SSL/TLS certificate information."""
|
||||||
logger.debug(f"Checking SSL for {domain}")
|
scraping_logger.debug(f"Checking SSL for {domain}")
|
||||||
try:
|
try:
|
||||||
context = ssl.create_default_context()
|
context = ssl.create_default_context()
|
||||||
with socket.create_connection((domain, 443)) as sock:
|
with socket.create_connection((domain, 443)) as sock:
|
||||||
@@ -190,7 +259,7 @@ class WebsiteAnalyzer:
|
|||||||
|
|
||||||
def _check_dns(self, domain: str) -> Dict:
|
def _check_dns(self, domain: str) -> Dict:
|
||||||
"""Check DNS records."""
|
"""Check DNS records."""
|
||||||
logger.debug(f"Checking DNS for {domain}")
|
scraping_logger.debug(f"Checking DNS for {domain}")
|
||||||
try:
|
try:
|
||||||
records = {}
|
records = {}
|
||||||
for record_type in ['A', 'AAAA', 'MX', 'NS', 'TXT']:
|
for record_type in ['A', 'AAAA', 'MX', 'NS', 'TXT']:
|
||||||
@@ -200,7 +269,7 @@ class WebsiteAnalyzer:
|
|||||||
except dns.resolver.NoAnswer:
|
except dns.resolver.NoAnswer:
|
||||||
records[record_type] = []
|
records[record_type] = []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error resolving {record_type} record: {str(e)}")
|
scraping_logger.warning(f"Error resolving {record_type} record: {str(e)}")
|
||||||
records[record_type] = []
|
records[record_type] = []
|
||||||
return records
|
return records
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -209,6 +278,7 @@ class WebsiteAnalyzer:
|
|||||||
|
|
||||||
def _get_whois_info(self, domain: str) -> Dict:
|
def _get_whois_info(self, domain: str) -> Dict:
|
||||||
"""Get WHOIS information for a domain."""
|
"""Get WHOIS information for a domain."""
|
||||||
|
scraping_logger.debug(f"Getting WHOIS info for {domain}")
|
||||||
try:
|
try:
|
||||||
w = whois.whois(domain)
|
w = whois.whois(domain)
|
||||||
|
|
||||||
@@ -240,7 +310,7 @@ class WebsiteAnalyzer:
|
|||||||
|
|
||||||
def _analyze_content(self, url: str) -> Dict:
|
def _analyze_content(self, url: str) -> Dict:
|
||||||
"""Analyze website content."""
|
"""Analyze website content."""
|
||||||
logger.debug(f"Analyzing content for {url}")
|
scraping_logger.debug(f"Analyzing content for {url}")
|
||||||
try:
|
try:
|
||||||
response = self.session.get(url, timeout=10)
|
response = self.session.get(url, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -255,6 +325,14 @@ class WebsiteAnalyzer:
|
|||||||
|
|
||||||
# Count headings
|
# Count headings
|
||||||
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
||||||
|
heading_counts = {
|
||||||
|
'h1': len(soup.find_all('h1')),
|
||||||
|
'h2': len(soup.find_all('h2')),
|
||||||
|
'h3': len(soup.find_all('h3')),
|
||||||
|
'h4': len(soup.find_all('h4')),
|
||||||
|
'h5': len(soup.find_all('h5')),
|
||||||
|
'h6': len(soup.find_all('h6'))
|
||||||
|
}
|
||||||
|
|
||||||
# Count images
|
# Count images
|
||||||
images = soup.find_all('img')
|
images = soup.find_all('img')
|
||||||
@@ -262,22 +340,52 @@ class WebsiteAnalyzer:
|
|||||||
# Count links
|
# Count links
|
||||||
links = soup.find_all('a')
|
links = soup.find_all('a')
|
||||||
|
|
||||||
|
# Count paragraphs
|
||||||
|
paragraphs = soup.find_all('p')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"word_count": word_count,
|
"word_count": word_count,
|
||||||
"heading_count": len(headings),
|
"heading_count": len(headings),
|
||||||
|
"heading_structure": heading_counts,
|
||||||
"image_count": len(images),
|
"image_count": len(images),
|
||||||
"link_count": len(links),
|
"link_count": len(links),
|
||||||
|
"paragraph_count": len(paragraphs),
|
||||||
"has_meta_description": bool(self._get_meta_description(soup)),
|
"has_meta_description": bool(self._get_meta_description(soup)),
|
||||||
"has_robots_txt": bool(self._get_robots_txt(url)),
|
"has_robots_txt": bool(self._get_robots_txt(url)),
|
||||||
"has_sitemap": bool(self._get_sitemap(url))
|
"has_sitemap": bool(self._get_sitemap(url))
|
||||||
}
|
}
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Request error in content analysis: {str(e)}", exc_info=True)
|
||||||
|
return {
|
||||||
|
"word_count": 0,
|
||||||
|
"heading_count": 0,
|
||||||
|
"heading_structure": {'h1': 0, 'h2': 0, 'h3': 0, 'h4': 0, 'h5': 0, 'h6': 0},
|
||||||
|
"image_count": 0,
|
||||||
|
"link_count": 0,
|
||||||
|
"paragraph_count": 0,
|
||||||
|
"has_meta_description": False,
|
||||||
|
"has_robots_txt": False,
|
||||||
|
"has_sitemap": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Content analysis error: {str(e)}", exc_info=True)
|
logger.error(f"Content analysis error: {str(e)}", exc_info=True)
|
||||||
return {"error": str(e)}
|
return {
|
||||||
|
"word_count": 0,
|
||||||
|
"heading_count": 0,
|
||||||
|
"heading_structure": {'h1': 0, 'h2': 0, 'h3': 0, 'h4': 0, 'h5': 0, 'h6': 0},
|
||||||
|
"image_count": 0,
|
||||||
|
"link_count": 0,
|
||||||
|
"paragraph_count": 0,
|
||||||
|
"has_meta_description": False,
|
||||||
|
"has_robots_txt": False,
|
||||||
|
"has_sitemap": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
def _check_performance(self, url: str) -> Dict:
|
def _check_performance(self, url: str) -> Dict:
|
||||||
"""Check website performance metrics."""
|
"""Check website performance metrics."""
|
||||||
logger.debug(f"Checking performance for {url}")
|
scraping_logger.debug(f"Checking performance for {url}")
|
||||||
try:
|
try:
|
||||||
start_time = datetime.now()
|
start_time = datetime.now()
|
||||||
response = self.session.get(url, timeout=10)
|
response = self.session.get(url, timeout=10)
|
||||||
@@ -289,11 +397,29 @@ class WebsiteAnalyzer:
|
|||||||
"load_time": load_time,
|
"load_time": load_time,
|
||||||
"status_code": response.status_code,
|
"status_code": response.status_code,
|
||||||
"content_length": len(response.content),
|
"content_length": len(response.content),
|
||||||
"headers": dict(response.headers)
|
"headers": dict(response.headers),
|
||||||
|
"response_time": response.elapsed.total_seconds()
|
||||||
|
}
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Request error in performance check: {str(e)}", exc_info=True)
|
||||||
|
return {
|
||||||
|
"load_time": 0,
|
||||||
|
"status_code": 0,
|
||||||
|
"content_length": 0,
|
||||||
|
"headers": {},
|
||||||
|
"response_time": 0,
|
||||||
|
"error": str(e)
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Performance check error: {str(e)}", exc_info=True)
|
logger.error(f"Performance check error: {str(e)}", exc_info=True)
|
||||||
return {"error": str(e)}
|
return {
|
||||||
|
"load_time": 0,
|
||||||
|
"status_code": 0,
|
||||||
|
"content_length": 0,
|
||||||
|
"headers": {},
|
||||||
|
"response_time": 0,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
def _get_meta_description(self, soup: BeautifulSoup) -> Optional[str]:
|
def _get_meta_description(self, soup: BeautifulSoup) -> Optional[str]:
|
||||||
"""Extract meta description from HTML."""
|
"""Extract meta description from HTML."""
|
||||||
@@ -308,7 +434,7 @@ class WebsiteAnalyzer:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.text
|
return response.text
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error fetching robots.txt: {str(e)}")
|
scraping_logger.warning(f"Error fetching robots.txt: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_sitemap(self, url: str) -> Optional[str]:
|
def _get_sitemap(self, url: str) -> Optional[str]:
|
||||||
@@ -319,5 +445,253 @@ class WebsiteAnalyzer:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.text
|
return response.text
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error fetching sitemap.xml: {str(e)}")
|
scraping_logger.warning(f"Error fetching sitemap.xml: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _analyze_seo(self, url: str) -> Dict:
|
||||||
|
"""Analyze website SEO."""
|
||||||
|
try:
|
||||||
|
# Extract content
|
||||||
|
content, soup, extract_errors = self._extract_content(url)
|
||||||
|
if not content or not soup:
|
||||||
|
return {
|
||||||
|
"error": "Failed to extract content",
|
||||||
|
"error_details": {"errors": extract_errors}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Analyze meta tags
|
||||||
|
meta_analysis = self._analyze_meta_tags(soup)
|
||||||
|
|
||||||
|
# Analyze content with AI
|
||||||
|
content_analysis, recommendations = self._analyze_content_with_ai(content)
|
||||||
|
|
||||||
|
# Calculate overall score
|
||||||
|
meta_score = sum([
|
||||||
|
1 if meta_analysis.title['status'] == 'good' else 0,
|
||||||
|
1 if meta_analysis.description['status'] == 'good' else 0,
|
||||||
|
1 if meta_analysis.keywords['status'] == 'good' else 0,
|
||||||
|
1 if meta_analysis.has_robots else 0,
|
||||||
|
1 if meta_analysis.has_sitemap else 0
|
||||||
|
]) * 20 # Scale to 100
|
||||||
|
|
||||||
|
overall_score = (
|
||||||
|
meta_score * 0.3 + # 30% weight for meta tags
|
||||||
|
content_analysis.readability_score * 0.3 + # 30% weight for readability
|
||||||
|
content_analysis.content_quality_score * 0.4 # 40% weight for content quality
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"overall_score": overall_score,
|
||||||
|
"meta_tags": meta_analysis.__dict__,
|
||||||
|
"content": content_analysis.__dict__,
|
||||||
|
"recommendations": [rec.__dict__ for rec in recommendations]
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error in SEO analysis: {str(e)}"
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
return {
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"type": type(e).__name__,
|
||||||
|
"traceback": str(e.__traceback__)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_content(self, url: str) -> Tuple[Optional[str], Optional[BeautifulSoup], List[str]]:
|
||||||
|
"""Extract content from URL."""
|
||||||
|
errors = []
|
||||||
|
try:
|
||||||
|
response = self.session.get(url, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
return response.text, soup, errors
|
||||||
|
except requests.RequestException as e:
|
||||||
|
error_msg = f"Error fetching URL: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
errors.append(error_msg)
|
||||||
|
return None, None, errors
|
||||||
|
|
||||||
|
def _analyze_meta_tags(self, soup: BeautifulSoup) -> MetaTagAnalysis:
|
||||||
|
"""Analyze meta tags using BeautifulSoup."""
|
||||||
|
# Title analysis
|
||||||
|
title = soup.title.string if soup.title else ""
|
||||||
|
title_analysis = {
|
||||||
|
'status': 'good' if title and 30 <= len(title) <= 60 else 'needs_improvement',
|
||||||
|
'value': title,
|
||||||
|
'recommendation': '' if title and 30 <= len(title) <= 60 else 'Title should be between 30-60 characters'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Meta description analysis
|
||||||
|
meta_desc = soup.find('meta', attrs={'name': 'description'})
|
||||||
|
desc = meta_desc.get('content', '') if meta_desc else ""
|
||||||
|
desc_analysis = {
|
||||||
|
'status': 'good' if desc and 120 <= len(desc) <= 160 else 'needs_improvement',
|
||||||
|
'value': desc,
|
||||||
|
'recommendation': '' if desc and 120 <= len(desc) <= 160 else 'Description should be between 120-160 characters'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keywords analysis
|
||||||
|
meta_keywords = soup.find('meta', attrs={'name': 'keywords'})
|
||||||
|
keywords = meta_keywords.get('content', '') if meta_keywords else ""
|
||||||
|
keywords_analysis = {
|
||||||
|
'status': 'good' if keywords else 'needs_improvement',
|
||||||
|
'value': keywords,
|
||||||
|
'recommendation': '' if keywords else 'Add relevant keywords meta tag'
|
||||||
|
}
|
||||||
|
|
||||||
|
return MetaTagAnalysis(
|
||||||
|
title=title_analysis,
|
||||||
|
description=desc_analysis,
|
||||||
|
keywords=keywords_analysis,
|
||||||
|
has_robots=bool(soup.find('meta', attrs={'name': 'robots'})),
|
||||||
|
has_sitemap=bool(soup.find('link', attrs={'rel': 'sitemap'}))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _analyze_content_with_ai(self, content: str) -> Tuple[ContentAnalysis, List[SEORecommendation]]:
|
||||||
|
"""Analyze content using AI."""
|
||||||
|
try:
|
||||||
|
# Prepare prompt for content analysis
|
||||||
|
prompt = f"""Analyze the following webpage content for SEO and provide a structured analysis:
|
||||||
|
Content: {content[:4000]}... # Truncate to avoid token limits
|
||||||
|
|
||||||
|
Provide analysis in the following format:
|
||||||
|
1. Word count
|
||||||
|
2. Heading structure analysis
|
||||||
|
3. Keyword density for main topics
|
||||||
|
4. Readability score (0-100)
|
||||||
|
5. Content quality score (0-100)
|
||||||
|
6. List of SEO recommendations with priority (high/medium/low), category, issue, recommendation, and impact
|
||||||
|
|
||||||
|
Format the response as JSON."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get AI analysis using llm_text_gen
|
||||||
|
analysis = llm_text_gen(
|
||||||
|
prompt=prompt,
|
||||||
|
system_prompt="You are an SEO expert analyzing website content.",
|
||||||
|
response_format="json_object"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not analysis:
|
||||||
|
logger.error("Empty response from AI analysis")
|
||||||
|
return self._get_fallback_analysis(content)
|
||||||
|
|
||||||
|
# Create ContentAnalysis object
|
||||||
|
content_analysis = ContentAnalysis(
|
||||||
|
word_count=len(content.split()),
|
||||||
|
headings_structure=analysis.get('heading_structure', {}),
|
||||||
|
keyword_density=analysis.get('keyword_density', {}),
|
||||||
|
readability_score=analysis.get('readability_score', 0),
|
||||||
|
content_quality_score=analysis.get('content_quality_score', 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create recommendations
|
||||||
|
recommendations = [
|
||||||
|
SEORecommendation(
|
||||||
|
priority=rec['priority'],
|
||||||
|
category=rec['category'],
|
||||||
|
issue=rec['issue'],
|
||||||
|
recommendation=rec['recommendation'],
|
||||||
|
impact=rec['impact']
|
||||||
|
)
|
||||||
|
for rec in analysis.get('recommendations', [])
|
||||||
|
]
|
||||||
|
|
||||||
|
return content_analysis, recommendations
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in AI analysis: {str(e)}")
|
||||||
|
return self._get_fallback_analysis(content)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in AI analysis setup: {str(e)}")
|
||||||
|
return self._get_fallback_analysis(content)
|
||||||
|
|
||||||
|
def _get_fallback_analysis(self, content: str) -> Tuple[ContentAnalysis, List[SEORecommendation]]:
|
||||||
|
"""Provide fallback analysis when AI analysis is not available."""
|
||||||
|
try:
|
||||||
|
# Basic content analysis
|
||||||
|
words = content.split()
|
||||||
|
word_count = len(words)
|
||||||
|
|
||||||
|
# Simple readability score based on word count
|
||||||
|
readability_score = min(100, max(0, word_count / 10))
|
||||||
|
|
||||||
|
# Basic content quality score
|
||||||
|
content_quality_score = min(100, max(0, word_count / 20))
|
||||||
|
|
||||||
|
# Create basic recommendations
|
||||||
|
recommendations = [
|
||||||
|
SEORecommendation(
|
||||||
|
priority="high",
|
||||||
|
category="content",
|
||||||
|
issue="AI analysis unavailable",
|
||||||
|
recommendation="Consider running the analysis again with a valid API key for more detailed insights",
|
||||||
|
impact="Limited analysis capabilities"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return ContentAnalysis(
|
||||||
|
word_count=word_count,
|
||||||
|
headings_structure={},
|
||||||
|
keyword_density={},
|
||||||
|
readability_score=readability_score,
|
||||||
|
content_quality_score=content_quality_score
|
||||||
|
), recommendations
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in fallback analysis: {str(e)}")
|
||||||
|
return ContentAnalysis(
|
||||||
|
word_count=0,
|
||||||
|
headings_structure={},
|
||||||
|
keyword_density={},
|
||||||
|
readability_score=0,
|
||||||
|
content_quality_score=0
|
||||||
|
), []
|
||||||
|
|
||||||
|
def analyze_website(url: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Analyze a website and return comprehensive results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): The URL to analyze
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: Analysis results including various metrics and checks
|
||||||
|
"""
|
||||||
|
logger.info(f"Starting website analysis for URL: {url}")
|
||||||
|
try:
|
||||||
|
analyzer = WebsiteAnalyzer()
|
||||||
|
|
||||||
|
results = analyzer.analyze_website(url)
|
||||||
|
|
||||||
|
# Add success status to results
|
||||||
|
if "error" in results:
|
||||||
|
error_msg = f"Error in base analysis: {results['error']}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
logger.error(f"Error details: {json.dumps(results.get('error_details', {}), indent=2)}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": results.get("error_details", {})
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add success status and wrap results
|
||||||
|
logger.info("Analysis completed successfully")
|
||||||
|
logger.debug(f"Analysis results: {json.dumps(results, indent=2)}")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": results
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error in analyze_website: {str(e)}"
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"type": type(e).__name__,
|
||||||
|
"traceback": str(e.__traceback__)
|
||||||
|
}
|
||||||
|
}
|
||||||
134
lib/utils/website_analyzer/content_gap_analyzer.py
Normal file
134
lib/utils/website_analyzer/content_gap_analyzer.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
from typing import Dict
|
||||||
|
import json
|
||||||
|
|
||||||
|
class ContentGapAnalyzer:
|
||||||
|
def __init__(self, analyzer):
|
||||||
|
self.analyzer = analyzer
|
||||||
|
|
||||||
|
def analyze(self, url: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Analyze content gaps for a given URL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): The URL to analyze
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: Analysis results including content gaps and recommendations
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get base analysis
|
||||||
|
logger.info(f"Starting content gap analysis for URL: {url}")
|
||||||
|
base_analysis = self.analyzer.analyze_website(url)
|
||||||
|
|
||||||
|
# Check for errors in base analysis
|
||||||
|
if not base_analysis.get("success", False):
|
||||||
|
error_msg = base_analysis.get("error", "Unknown error in website analysis")
|
||||||
|
error_details = base_analysis.get("error_details", {})
|
||||||
|
logger.error(f"Base analysis failed: {error_msg}")
|
||||||
|
logger.error(f"Error details: {json.dumps(error_details, indent=2)}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": error_details,
|
||||||
|
"stage": "base_analysis"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract required sections
|
||||||
|
analysis_data = base_analysis.get("data", {}).get("analysis", {})
|
||||||
|
required_sections = ["content_info", "basic_info", "performance"]
|
||||||
|
missing_sections = [section for section in required_sections if section not in analysis_data]
|
||||||
|
|
||||||
|
if missing_sections:
|
||||||
|
error_msg = f"Missing required analysis sections: {', '.join(missing_sections)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
logger.error(f"Available sections: {list(analysis_data.keys())}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"missing_sections": missing_sections,
|
||||||
|
"available_sections": list(analysis_data.keys())
|
||||||
|
},
|
||||||
|
"stage": "section_validation"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract content metrics
|
||||||
|
try:
|
||||||
|
content_info = analysis_data["content_info"]
|
||||||
|
basic_info = analysis_data["basic_info"]
|
||||||
|
performance = analysis_data["performance"]
|
||||||
|
except KeyError as e:
|
||||||
|
error_msg = f"Error extracting analysis section: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"type": "KeyError",
|
||||||
|
"missing_key": str(e),
|
||||||
|
"available_keys": list(analysis_data.keys())
|
||||||
|
},
|
||||||
|
"stage": "data_extraction"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Analyze content gaps
|
||||||
|
try:
|
||||||
|
gaps = self._analyze_content_gaps(content_info, basic_info, performance)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error analyzing content gaps: {str(e)}"
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"type": type(e).__name__,
|
||||||
|
"traceback": str(e.__traceback__)
|
||||||
|
},
|
||||||
|
"stage": "gap_analysis"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate recommendations
|
||||||
|
try:
|
||||||
|
recommendations = self._generate_recommendations(gaps)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error generating recommendations: {str(e)}"
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"type": type(e).__name__,
|
||||||
|
"traceback": str(e.__traceback__)
|
||||||
|
},
|
||||||
|
"stage": "recommendation_generation"
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"content_gaps": gaps,
|
||||||
|
"recommendations": recommendations,
|
||||||
|
"metrics": {
|
||||||
|
"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),
|
||||||
|
"paragraph_count": content_info.get("paragraph_count", 0),
|
||||||
|
"load_time": performance.get("load_time", 0),
|
||||||
|
"response_time": performance.get("response_time", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error in content gap analysis: {str(e)}"
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"error_details": {
|
||||||
|
"type": type(e).__name__,
|
||||||
|
"traceback": str(e.__traceback__)
|
||||||
|
},
|
||||||
|
"stage": "general"
|
||||||
|
}
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
"""SEO analyzer module with AI integration."""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Dict, List, Tuple, Optional
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
import openai
|
|
||||||
from loguru import logger
|
|
||||||
import os
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from .models import (
|
|
||||||
SEOAnalysisResult,
|
|
||||||
MetaTagAnalysis,
|
|
||||||
ContentAnalysis,
|
|
||||||
SEORecommendation
|
|
||||||
)
|
|
||||||
|
|
||||||
def extract_content(url: str) -> Tuple[Optional[str], Optional[BeautifulSoup], List[str]]:
|
|
||||||
"""Extract content from URL."""
|
|
||||||
errors = []
|
|
||||||
try:
|
|
||||||
response = requests.get(url, timeout=10)
|
|
||||||
response.raise_for_status()
|
|
||||||
soup = BeautifulSoup(response.text, 'html.parser')
|
|
||||||
return response.text, soup, errors
|
|
||||||
except requests.RequestException as e:
|
|
||||||
error_msg = f"Error fetching URL: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
errors.append(error_msg)
|
|
||||||
return None, None, errors
|
|
||||||
|
|
||||||
def analyze_meta_tags(soup: BeautifulSoup) -> MetaTagAnalysis:
|
|
||||||
"""Analyze meta tags using BeautifulSoup."""
|
|
||||||
# Title analysis
|
|
||||||
title = soup.title.string if soup.title else ""
|
|
||||||
title_analysis = {
|
|
||||||
'status': 'good' if title and 30 <= len(title) <= 60 else 'needs_improvement',
|
|
||||||
'value': title,
|
|
||||||
'recommendation': '' if title and 30 <= len(title) <= 60 else 'Title should be between 30-60 characters'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Meta description analysis
|
|
||||||
meta_desc = soup.find('meta', attrs={'name': 'description'})
|
|
||||||
desc = meta_desc.get('content', '') if meta_desc else ""
|
|
||||||
desc_analysis = {
|
|
||||||
'status': 'good' if desc and 120 <= len(desc) <= 160 else 'needs_improvement',
|
|
||||||
'value': desc,
|
|
||||||
'recommendation': '' if desc and 120 <= len(desc) <= 160 else 'Description should be between 120-160 characters'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Keywords analysis
|
|
||||||
meta_keywords = soup.find('meta', attrs={'name': 'keywords'})
|
|
||||||
keywords = meta_keywords.get('content', '') if meta_keywords else ""
|
|
||||||
keywords_analysis = {
|
|
||||||
'status': 'good' if keywords else 'needs_improvement',
|
|
||||||
'value': keywords,
|
|
||||||
'recommendation': '' if keywords else 'Add relevant keywords meta tag'
|
|
||||||
}
|
|
||||||
|
|
||||||
return MetaTagAnalysis(
|
|
||||||
title=title_analysis,
|
|
||||||
description=desc_analysis,
|
|
||||||
keywords=keywords_analysis,
|
|
||||||
has_robots=bool(soup.find('meta', attrs={'name': 'robots'})),
|
|
||||||
has_sitemap=bool(soup.find('link', attrs={'rel': 'sitemap'}))
|
|
||||||
)
|
|
||||||
|
|
||||||
def analyze_content_with_ai(content: str) -> Tuple[ContentAnalysis, List[SEORecommendation]]:
|
|
||||||
"""Analyze content using AI."""
|
|
||||||
try:
|
|
||||||
# Load environment variables
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Get API key from environment
|
|
||||||
api_key = os.getenv('OPENAI_API_KEY')
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError("OpenAI API key not found in environment variables")
|
|
||||||
|
|
||||||
# Initialize OpenAI client
|
|
||||||
client = openai.OpenAI(api_key=api_key)
|
|
||||||
|
|
||||||
# Prepare prompt for content analysis
|
|
||||||
prompt = f"""Analyze the following webpage content for SEO and provide a structured analysis:
|
|
||||||
Content: {content[:4000]}... # Truncate to avoid token limits
|
|
||||||
|
|
||||||
Provide analysis in the following format:
|
|
||||||
1. Word count
|
|
||||||
2. Heading structure analysis
|
|
||||||
3. Keyword density for main topics
|
|
||||||
4. Readability score (0-100)
|
|
||||||
5. Content quality score (0-100)
|
|
||||||
6. List of SEO recommendations with priority (high/medium/low), category, issue, recommendation, and impact
|
|
||||||
|
|
||||||
Format the response as JSON."""
|
|
||||||
|
|
||||||
# Get AI analysis
|
|
||||||
response = client.chat.completions.create(
|
|
||||||
model="gpt-4",
|
|
||||||
messages=[
|
|
||||||
{"role": "system", "content": "You are an SEO expert analyzing website content."},
|
|
||||||
{"role": "user", "content": prompt}
|
|
||||||
],
|
|
||||||
response_format={"type": "json_object"}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Parse AI response
|
|
||||||
analysis = response.choices[0].message.content
|
|
||||||
|
|
||||||
# Create ContentAnalysis object
|
|
||||||
content_analysis = ContentAnalysis(
|
|
||||||
word_count=len(content.split()),
|
|
||||||
headings_structure=analysis.get('heading_structure', {}),
|
|
||||||
keyword_density=analysis.get('keyword_density', {}),
|
|
||||||
readability_score=analysis.get('readability_score', 0),
|
|
||||||
content_quality_score=analysis.get('content_quality_score', 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create recommendations
|
|
||||||
recommendations = [
|
|
||||||
SEORecommendation(
|
|
||||||
priority=rec['priority'],
|
|
||||||
category=rec['category'],
|
|
||||||
issue=rec['issue'],
|
|
||||||
recommendation=rec['recommendation'],
|
|
||||||
impact=rec['impact']
|
|
||||||
)
|
|
||||||
for rec in analysis.get('recommendations', [])
|
|
||||||
]
|
|
||||||
|
|
||||||
return content_analysis, recommendations
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in AI analysis: {str(e)}")
|
|
||||||
return ContentAnalysis(
|
|
||||||
word_count=len(content.split()),
|
|
||||||
headings_structure={},
|
|
||||||
keyword_density={},
|
|
||||||
readability_score=0,
|
|
||||||
content_quality_score=0
|
|
||||||
), []
|
|
||||||
|
|
||||||
def analyze_seo(url: str) -> SEOAnalysisResult:
|
|
||||||
"""Main function to analyze website SEO."""
|
|
||||||
errors = []
|
|
||||||
warnings = []
|
|
||||||
|
|
||||||
# Validate URL
|
|
||||||
try:
|
|
||||||
parsed_url = urlparse(url)
|
|
||||||
if not all([parsed_url.scheme, parsed_url.netloc]):
|
|
||||||
errors.append("Invalid URL format")
|
|
||||||
raise ValueError("Invalid URL format")
|
|
||||||
except Exception as e:
|
|
||||||
errors.append(f"URL parsing error: {str(e)}")
|
|
||||||
return SEOAnalysisResult(
|
|
||||||
url=url,
|
|
||||||
analyzed_at=datetime.now(),
|
|
||||||
overall_score=0,
|
|
||||||
meta_tags=None,
|
|
||||||
content=None,
|
|
||||||
recommendations=[],
|
|
||||||
errors=errors,
|
|
||||||
warnings=warnings,
|
|
||||||
success=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract content
|
|
||||||
content, soup, extract_errors = extract_content(url)
|
|
||||||
errors.extend(extract_errors)
|
|
||||||
|
|
||||||
if not content or not soup:
|
|
||||||
return SEOAnalysisResult(
|
|
||||||
url=url,
|
|
||||||
analyzed_at=datetime.now(),
|
|
||||||
overall_score=0,
|
|
||||||
meta_tags=None,
|
|
||||||
content=None,
|
|
||||||
recommendations=[],
|
|
||||||
errors=errors,
|
|
||||||
warnings=warnings,
|
|
||||||
success=False
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Analyze meta tags
|
|
||||||
meta_analysis = analyze_meta_tags(soup)
|
|
||||||
|
|
||||||
# Analyze content with AI
|
|
||||||
content_analysis, recommendations = analyze_content_with_ai(content)
|
|
||||||
|
|
||||||
# Calculate overall score
|
|
||||||
meta_score = sum([
|
|
||||||
1 if meta_analysis.title['status'] == 'good' else 0,
|
|
||||||
1 if meta_analysis.description['status'] == 'good' else 0,
|
|
||||||
1 if meta_analysis.keywords['status'] == 'good' else 0,
|
|
||||||
1 if meta_analysis.has_robots else 0,
|
|
||||||
1 if meta_analysis.has_sitemap else 0
|
|
||||||
]) * 20 # Scale to 100
|
|
||||||
|
|
||||||
overall_score = (
|
|
||||||
meta_score * 0.3 + # 30% weight for meta tags
|
|
||||||
content_analysis.readability_score * 0.3 + # 30% weight for readability
|
|
||||||
content_analysis.content_quality_score * 0.4 # 40% weight for content quality
|
|
||||||
)
|
|
||||||
|
|
||||||
return SEOAnalysisResult(
|
|
||||||
url=url,
|
|
||||||
analyzed_at=datetime.now(),
|
|
||||||
overall_score=overall_score,
|
|
||||||
meta_tags=meta_analysis,
|
|
||||||
content=content_analysis,
|
|
||||||
recommendations=recommendations,
|
|
||||||
errors=errors,
|
|
||||||
warnings=warnings,
|
|
||||||
success=True
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Error in SEO analysis: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
errors.append(error_msg)
|
|
||||||
return SEOAnalysisResult(
|
|
||||||
url=url,
|
|
||||||
analyzed_at=datetime.now(),
|
|
||||||
overall_score=0,
|
|
||||||
meta_tags=None,
|
|
||||||
content=None,
|
|
||||||
recommendations=[],
|
|
||||||
errors=errors,
|
|
||||||
warnings=warnings,
|
|
||||||
success=False
|
|
||||||
)
|
|
||||||
@@ -673,4 +673,19 @@ select option {
|
|||||||
.search-option.disabled h4,
|
.search-option.disabled h4,
|
||||||
.search-option.disabled p {
|
.search-option.disabled p {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Move main content upwards and reduce free space at the top */
|
||||||
|
.main .block-container {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
/* Optionally, reduce margin for the main title container if used */
|
||||||
|
.seo-main-title, .main-title, .dashboard-title {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
padding-top: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -21,8 +21,8 @@ imageio-ffmpeg==0.4.5
|
|||||||
decorator==4.4.2
|
decorator==4.4.2
|
||||||
reportlab==4.4.0
|
reportlab==4.4.0
|
||||||
textblob==0.19.0
|
textblob==0.19.0
|
||||||
numpy>=1.22.4,<2.0.0
|
numpy>=1.24.0
|
||||||
pandas>=2.0.3
|
pandas>=2.0.0
|
||||||
scikit-learn>=1.3.2
|
scikit-learn>=1.3.2
|
||||||
matplotlib>=3.8.2
|
matplotlib>=3.8.2
|
||||||
plotly>=5.18.0
|
plotly>=5.18.0
|
||||||
@@ -40,7 +40,7 @@ lxml_html_clean>=0.4.1
|
|||||||
streamlit>=1.44.0
|
streamlit>=1.44.0
|
||||||
Authlib>=1.3.2
|
Authlib>=1.3.2
|
||||||
yfinance>=0.2.36
|
yfinance>=0.2.36
|
||||||
pandas_ta>=0.3.14b0
|
pandas-ta>=0.3.14b0
|
||||||
firecrawl-py>=1.14.1
|
firecrawl-py>=1.14.1
|
||||||
gTTS>=2.5.1
|
gTTS>=2.5.1
|
||||||
streamlit-mic-recorder>=0.0.8
|
streamlit-mic-recorder>=0.0.8
|
||||||
@@ -52,4 +52,6 @@ tavily-python>=0.2.8
|
|||||||
tinify>=1.6.0
|
tinify>=1.6.0
|
||||||
validators>=0.20.0
|
validators>=0.20.0
|
||||||
python-whois==0.9.5
|
python-whois==0.9.5
|
||||||
dnspython
|
dnspython
|
||||||
|
sqlalchemy==2.0.41
|
||||||
|
scipy>=1.10.0
|
||||||
Reference in New Issue
Block a user