feat: Initial implementation of Marketing Assistant AI for Adriana James
- Set up FastAPI backend with modular structure: - main.py for API routing - copywriter.py for AI-powered content generation using Cohere - embeddings.py for generating and reranking content embeddings - vector_store.py for FAISS-based similarity search - brand_style.py for managing brand tone, taboo words, and preferred terms - config.py for managing environment and application settings - Configured RESTful API endpoints: /generate-copy, /brand-style, /training-data, /improve-content, /analyze-content - Created frontend with vanilla HTML, CSS, and JS (index.html, styles.css, app.js) - Integrated brand style management for tone, voice, taboo words, and terminology - Implemented vector search for referencing similar historical content - Enabled training data input to improve future AI output - Added environment variable support for API keys and model configs - Structured data storage with local JSON and DB files - Added developer documentation, API reference, and project setup instructions This commit provides the foundation for a full-stack, AI-driven content creation platform that ensures brand consistency, speeds up marketing workflows, and supports iterative improvement over time.
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Local data
|
||||||
|
data/past_campaigns/*
|
||||||
|
data/user_queries/*
|
||||||
|
!data/past_campaigns/.gitkeep
|
||||||
|
!data/user_queries/.gitkeep
|
||||||
|
|
||||||
|
# OS specific
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Adriana James
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
# Marketing Assistant AI
|
||||||
|
|
||||||
|
A comprehensive AI-powered tool designed to streamline the process of ideation, copywriting, and marketing campaign creation for Adriana James. This system generates high-quality marketing content that maintains brand consistency while significantly reducing content creation time.
|
||||||
|
|
||||||
|
|
||||||
|
## 🌟 Features
|
||||||
|
|
||||||
|
- **AI-Powered Content Generation**: Create marketing content for various channels with customizable parameters
|
||||||
|
- **Brand Voice Consistency**: Ensure all content adheres to Adriana James' unique brand tone and style
|
||||||
|
- **Vector Search**: Find similar past content for reference and inspiration
|
||||||
|
- **Content Improvement**: Refine generated content based on specific feedback
|
||||||
|
- **Brand Style Management**: Configure and update brand guidelines through a user-friendly interface
|
||||||
|
- **Training Data Management**: Add successful marketing content to improve the AI over time
|
||||||
|
- **Content Performance Analysis**: Get insights into the potential effectiveness of your content
|
||||||
|
|
||||||
|
## 📋 Project Overview
|
||||||
|
|
||||||
|
The Marketing Assistant AI combines a powerful backend built with FastAPI and a clean, intuitive frontend interface. It leverages advanced AI models through the Cohere API for text generation, embeddings, and reranking.
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
|
||||||
|
- **Backend**: Python with FastAPI
|
||||||
|
- **Frontend**: Vanilla HTML, CSS, and JavaScript
|
||||||
|
- **AI Models**: Cohere's generation and embedding models
|
||||||
|
- **Vector Database**: FAISS for efficient content similarity search
|
||||||
|
- **Storage**: Local JSON files for historical marketing data
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.9+
|
||||||
|
- Node.js and npm (optional, for development tools)
|
||||||
|
- Cohere API key
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone http://23.29.118.76:3000/michael/marketing-assistant-ai.git
|
||||||
|
cd marketing-assistant-ai
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Set up the backend**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure environment variables**
|
||||||
|
|
||||||
|
Create a `.env` file in the project root with the following variables:
|
||||||
|
|
||||||
|
```
|
||||||
|
# API Keys
|
||||||
|
COHERE_API_KEY=your-api-key-here
|
||||||
|
|
||||||
|
# LLM Configuration
|
||||||
|
LLM_MODEL=llm-model-name # e.g., command
|
||||||
|
LLM_API_KEY=your-api-key-here # if using a custom model
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
API_HOST=localhost
|
||||||
|
API_PORT=8000
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
VECTOR_DB_PATH=./data/vector_store
|
||||||
|
HISTORY_DB_PATH=./data/history.db
|
||||||
|
|
||||||
|
# Brand Configuration
|
||||||
|
BRAND_NAME=Adriana James
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Start the backend server**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The API will be available at http://localhost:8000
|
||||||
|
|
||||||
|
5. **Serve the frontend**
|
||||||
|
|
||||||
|
You can use any static file server. For development, Python's built-in HTTP server works well:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
python -m http.server 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
The frontend will be available at http://localhost:3000
|
||||||
|
|
||||||
|
## 🔧 Usage
|
||||||
|
|
||||||
|
### Generating Content
|
||||||
|
|
||||||
|
1. Navigate to the "Generate" tab in the web interface
|
||||||
|
2. Enter your content prompt (e.g., "Write a social media post about our new coaching program")
|
||||||
|
3. Select optional parameters (content type, tone, length)
|
||||||
|
4. Click "Generate" and wait for the AI to create your content
|
||||||
|
5. Review, improve, and save the generated content
|
||||||
|
|
||||||
|
### Managing Brand Style
|
||||||
|
|
||||||
|
1. Go to the "Brand Style" tab
|
||||||
|
2. Configure tone options and voice characteristics
|
||||||
|
3. Add or remove taboo words
|
||||||
|
4. Define preferred terminology
|
||||||
|
5. Save the updated brand style
|
||||||
|
|
||||||
|
### Adding Training Data
|
||||||
|
|
||||||
|
1. Visit the "Training" tab
|
||||||
|
2. Select the content type
|
||||||
|
3. Enter your high-performing marketing content
|
||||||
|
4. Add performance metrics if available
|
||||||
|
5. Submit the training data to improve the AI over time
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Marketing_Assistant_AI/
|
||||||
|
│-- backend/
|
||||||
|
│ │-- main.py # FastAPI application
|
||||||
|
│ │-- copywriter.py # AI content generation
|
||||||
|
│ │-- vector_store.py # FAISS vector database
|
||||||
|
│ │-- embeddings.py # Cohere embeddings
|
||||||
|
│ │-- brand_style.py # Brand style management
|
||||||
|
│ │-- config.py # Configuration settings
|
||||||
|
│ │-- requirements.txt # Python dependencies
|
||||||
|
│
|
||||||
|
│-- data/
|
||||||
|
│ │-- past_campaigns/ # Stored marketing campaigns
|
||||||
|
│ │-- user_queries/ # Query history
|
||||||
|
│ │-- style_guidelines/ # Brand style config
|
||||||
|
│ │ │-- brand_style.json # Default style guidelines
|
||||||
|
│
|
||||||
|
│-- frontend/
|
||||||
|
│ │-- index.html # Main HTML page
|
||||||
|
│ │-- styles.css # CSS styles
|
||||||
|
│ │-- app.js # Frontend functionality
|
||||||
|
│
|
||||||
|
│-- docs/
|
||||||
|
│ │-- README.md # Developer documentation
|
||||||
|
│ │-- API_Documentation.md # API reference
|
||||||
|
│
|
||||||
|
│-- .env # Environment variables
|
||||||
|
│-- .gitignore # Git ignore rules
|
||||||
|
│-- LICENSE # License information
|
||||||
|
│-- README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 API Endpoints
|
||||||
|
|
||||||
|
The backend provides several RESTful API endpoints:
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/generate-copy` | POST | Generate marketing content |
|
||||||
|
| `/brand-style` | GET | Retrieve brand style guidelines |
|
||||||
|
| `/brand-style` | PUT | Update brand style guidelines |
|
||||||
|
| `/training-data` | POST | Add new training data |
|
||||||
|
| `/training-data` | GET | List available training data |
|
||||||
|
| `/training-data/{id}` | GET | Get specific training document |
|
||||||
|
| `/training-data/{id}` | DELETE | Delete training document |
|
||||||
|
| `/improve-content` | POST | Improve content based on feedback |
|
||||||
|
| `/analyze-content` | POST | Analyze content performance |
|
||||||
|
|
||||||
|
For detailed API documentation, see [API Documentation](docs/API_Documentation.md).
|
||||||
|
|
||||||
|
## 💡 How It Works
|
||||||
|
|
||||||
|
1. **Content Generation Process**
|
||||||
|
- User submits a content request
|
||||||
|
- System formats the prompt with brand style guidelines
|
||||||
|
- Similar past content is retrieved from the vector database
|
||||||
|
- The AI generates content using the formatted prompt and references
|
||||||
|
- The system checks alignment with brand guidelines
|
||||||
|
- Final content is returned with alignment score and suggestions
|
||||||
|
|
||||||
|
2. **Brand Style Management**
|
||||||
|
- Brand style guidelines are stored in JSON format
|
||||||
|
- The system uses these guidelines to format prompts for the AI
|
||||||
|
- Content is checked against taboo words and preferred terminology
|
||||||
|
- An alignment score is calculated based on adherence to guidelines
|
||||||
|
|
||||||
|
3. **Vector Search**
|
||||||
|
- Marketing content is converted to vector embeddings using Cohere
|
||||||
|
- FAISS enables efficient similarity search across thousands of documents
|
||||||
|
- Similar content is reranked based on relevance to the query
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
### Backend Development
|
||||||
|
|
||||||
|
The backend is built with FastAPI and follows a modular architecture:
|
||||||
|
|
||||||
|
- `main.py`: API endpoints and request handling
|
||||||
|
- `copywriter.py`: Core AI content generation
|
||||||
|
- `vector_store.py`: Vector database operations
|
||||||
|
- `embeddings.py`: Embedding generation and reranking
|
||||||
|
- `brand_style.py`: Brand style management
|
||||||
|
- `config.py`: Configuration settings
|
||||||
|
|
||||||
|
### Frontend Development
|
||||||
|
|
||||||
|
The frontend is built with vanilla HTML, CSS, and JavaScript:
|
||||||
|
|
||||||
|
- `index.html`: Main application structure and UI components
|
||||||
|
- `styles.css`: Styling and responsive design rules
|
||||||
|
- `app.js`: User interactions and API integration
|
||||||
|
|
||||||
|
## 🔍 Future Enhancements
|
||||||
|
|
||||||
|
- User authentication and role-based access
|
||||||
|
- Content performance analytics dashboard
|
||||||
|
- A/B testing capabilities
|
||||||
|
- Integration with marketing platforms
|
||||||
|
- Custom model fine-tuning for even better results
|
||||||
|
- Mobile application
|
||||||
|
|
||||||
|
## 👥 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 👏 Acknowledgements
|
||||||
|
|
||||||
|
- [Cohere](https://cohere.ai/) for AI models
|
||||||
|
- [FastAPI](https://fastapi.tiangolo.com/) for the API framework
|
||||||
|
- [FAISS](https://github.com/facebookresearch/faiss) for vector search
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Made with ❤️ for Adriana James
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
"""
|
||||||
|
Brand style module for the Marketing Assistant AI.
|
||||||
|
Ensures generated content aligns with Adriana James' brand voice and tone.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
|
class BrandStyleManager:
|
||||||
|
"""Manages brand style guidelines and ensures content consistency."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the BrandStyleManager with default or stored style guidelines."""
|
||||||
|
self.style_path = Path(config.DATA_DIR) / "style_guidelines" / "brand_style.json"
|
||||||
|
self.style_guidelines = self._load_or_create_style()
|
||||||
|
logger.info("BrandStyleManager initialized successfully")
|
||||||
|
|
||||||
|
def _load_or_create_style(self) -> Dict[str, Any]:
|
||||||
|
"""Load existing style guidelines or create new ones with defaults."""
|
||||||
|
try:
|
||||||
|
if self.style_path.exists():
|
||||||
|
with open(self.style_path, 'r') as f:
|
||||||
|
style = json.load(f)
|
||||||
|
logger.info("Loaded existing brand style guidelines")
|
||||||
|
return style
|
||||||
|
else:
|
||||||
|
# Create directory if it doesn't exist
|
||||||
|
self.style_path.parent.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Use default style guidelines
|
||||||
|
style = config.DEFAULT_BRAND_STYLE
|
||||||
|
|
||||||
|
# Save default style
|
||||||
|
with open(self.style_path, 'w') as f:
|
||||||
|
json.dump(style, f, indent=2)
|
||||||
|
|
||||||
|
logger.info("Created default brand style guidelines")
|
||||||
|
return style
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading or creating style guidelines: {str(e)}")
|
||||||
|
# Fall back to default style
|
||||||
|
return config.DEFAULT_BRAND_STYLE
|
||||||
|
|
||||||
|
def get_style_guidelines(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get current brand style guidelines.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of style guidelines
|
||||||
|
"""
|
||||||
|
return self.style_guidelines
|
||||||
|
|
||||||
|
def update_style_guidelines(self, new_style: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Update brand style guidelines.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_style: Dictionary with new style guidelines
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated style guidelines dictionary
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Merge new style with existing
|
||||||
|
for key, value in new_style.items():
|
||||||
|
self.style_guidelines[key] = value
|
||||||
|
|
||||||
|
# Ensure brand name is preserved
|
||||||
|
self.style_guidelines['brand_name'] = config.BRAND_NAME
|
||||||
|
|
||||||
|
# Save updated style
|
||||||
|
with open(self.style_path, 'w') as f:
|
||||||
|
json.dump(self.style_guidelines, f, indent=2)
|
||||||
|
|
||||||
|
logger.info("Updated brand style guidelines")
|
||||||
|
return self.style_guidelines
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating style guidelines: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def format_prompt_with_brand_style(self, user_prompt: str, content_type: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Format user prompt with brand style guidelines for the LLM.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_prompt: Original user prompt
|
||||||
|
content_type: Type of content being generated
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted prompt with brand style instructions
|
||||||
|
"""
|
||||||
|
style = self.style_guidelines
|
||||||
|
|
||||||
|
# Create a formatted prompt with brand style instructions
|
||||||
|
prompt_parts = [
|
||||||
|
f"Generate marketing content for {style['brand_name']} based on the following request:",
|
||||||
|
f"\"{user_prompt}\"",
|
||||||
|
"\nFollow these brand style guidelines:",
|
||||||
|
f"- Brand Name: {style['brand_name']}",
|
||||||
|
f"- Tone: {', '.join(style.get('tone', []))}",
|
||||||
|
f"- Voice Characteristics: {', '.join(style.get('voice_characteristics', []))}",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add taboo words if any
|
||||||
|
if 'taboo_words' in style and style['taboo_words']:
|
||||||
|
prompt_parts.append(f"- Avoid these words: {', '.join(style['taboo_words'])}")
|
||||||
|
|
||||||
|
# Add preferred terms if any
|
||||||
|
if 'preferred_terms' in style and style['preferred_terms']:
|
||||||
|
terms = [f"use '{value}' instead of '{key}'" for key, value in style['preferred_terms'].items()]
|
||||||
|
prompt_parts.append(f"- Preferred terminology: {'; '.join(terms)}")
|
||||||
|
|
||||||
|
# Add content type specific instructions
|
||||||
|
if content_type:
|
||||||
|
if content_type == "email_campaign":
|
||||||
|
prompt_parts.append("- Format as a professional email with subject line, greeting, body, and signature")
|
||||||
|
elif content_type == "social_media":
|
||||||
|
prompt_parts.append("- Format as a concise social media post with appropriate hashtags")
|
||||||
|
elif content_type == "blog_post":
|
||||||
|
prompt_parts.append("- Format as a blog post with title, introduction, body with subheadings, and conclusion")
|
||||||
|
elif content_type == "website_copy":
|
||||||
|
prompt_parts.append("- Format as website copy with clear headings and concise paragraphs")
|
||||||
|
elif content_type == "ad_copy":
|
||||||
|
prompt_parts.append("- Format as advertising copy with headline, body, and clear call to action")
|
||||||
|
|
||||||
|
# Combine all parts
|
||||||
|
formatted_prompt = "\n".join(prompt_parts)
|
||||||
|
|
||||||
|
logger.debug("Created formatted prompt with brand style")
|
||||||
|
return formatted_prompt
|
||||||
|
|
||||||
|
def check_content_alignment(self, content: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Check if generated content aligns with brand style guidelines.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Generated marketing content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with alignment metrics and suggestions
|
||||||
|
"""
|
||||||
|
style = self.style_guidelines
|
||||||
|
taboo_words = style.get('taboo_words', [])
|
||||||
|
preferred_terms = style.get('preferred_terms', {})
|
||||||
|
|
||||||
|
# Check for taboo words
|
||||||
|
found_taboo_words = []
|
||||||
|
for word in taboo_words:
|
||||||
|
if word.lower() in content.lower():
|
||||||
|
found_taboo_words.append(word)
|
||||||
|
|
||||||
|
# Check for preferred terminology
|
||||||
|
terminology_issues = []
|
||||||
|
for avoid, use in preferred_terms.items():
|
||||||
|
if avoid.lower() in content.lower():
|
||||||
|
terminology_issues.append(f"Found '{avoid}', should use '{use}' instead")
|
||||||
|
|
||||||
|
# Calculate an overall alignment score (simple implementation)
|
||||||
|
issues_count = len(found_taboo_words) + len(terminology_issues)
|
||||||
|
alignment_score = max(0, 100 - (issues_count * 10)) # Reduce score for each issue
|
||||||
|
|
||||||
|
return {
|
||||||
|
'alignment_score': alignment_score,
|
||||||
|
'taboo_words_found': found_taboo_words,
|
||||||
|
'terminology_issues': terminology_issues,
|
||||||
|
'aligned': alignment_score >= 80 # Consider aligned if score is 80% or higher
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a singleton instance
|
||||||
|
brand_style_manager = BrandStyleManager()
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""
|
||||||
|
Configuration module for the Marketing Assistant AI.
|
||||||
|
Handles environment variables and application settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Base paths
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
DATA_DIR = BASE_DIR / "data"
|
||||||
|
|
||||||
|
# Ensure data directories exist
|
||||||
|
(DATA_DIR / "past_campaigns").mkdir(exist_ok=True)
|
||||||
|
(DATA_DIR / "user_queries").mkdir(exist_ok=True)
|
||||||
|
(DATA_DIR / "style_guidelines").mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# API configuration
|
||||||
|
API_HOST = os.getenv("API_HOST", "localhost")
|
||||||
|
API_PORT = int(os.getenv("API_PORT", 8000))
|
||||||
|
|
||||||
|
# LLM configuration
|
||||||
|
LLM_MODEL = os.getenv("LLM_MODEL")
|
||||||
|
LLM_API_KEY = os.getenv("LLM_API_KEY")
|
||||||
|
|
||||||
|
# Cohere configuration
|
||||||
|
COHERE_API_KEY = os.getenv("COHERE_API_KEY")
|
||||||
|
|
||||||
|
# Vector database configuration
|
||||||
|
VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", str(DATA_DIR / "vector_store"))
|
||||||
|
|
||||||
|
# Brand configuration
|
||||||
|
BRAND_NAME = os.getenv("BRAND_NAME", "Adriana James")
|
||||||
|
|
||||||
|
# Content types
|
||||||
|
CONTENT_TYPES = [
|
||||||
|
"email_campaign",
|
||||||
|
"social_media",
|
||||||
|
"blog_post",
|
||||||
|
"website_copy",
|
||||||
|
"ad_copy",
|
||||||
|
"funnel_page",
|
||||||
|
"product_description",
|
||||||
|
"press_release"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Tone options
|
||||||
|
TONE_OPTIONS = [
|
||||||
|
"professional",
|
||||||
|
"friendly",
|
||||||
|
"excited",
|
||||||
|
"authoritative",
|
||||||
|
"casual",
|
||||||
|
"inspirational",
|
||||||
|
"empathetic",
|
||||||
|
"humorous"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Content length options
|
||||||
|
LENGTH_OPTIONS = [
|
||||||
|
"short", # < 100 words
|
||||||
|
"medium", # 100-300 words
|
||||||
|
"long", # > 300 words
|
||||||
|
]
|
||||||
|
|
||||||
|
# Default brand style guidelines
|
||||||
|
DEFAULT_BRAND_STYLE = {
|
||||||
|
"brand_name": BRAND_NAME,
|
||||||
|
"tone": ["professional", "friendly", "inspirational"],
|
||||||
|
"voice_characteristics": ["clear", "direct", "empowering"],
|
||||||
|
"taboo_words": ["cheap", "discount", "bargain"],
|
||||||
|
"preferred_terms": {
|
||||||
|
"customers": "clients",
|
||||||
|
"products": "solutions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||||
|
LOG_FILE = os.getenv("LOG_FILE", str(BASE_DIR / "logs" / "app.log"))
|
||||||
|
|
||||||
|
# Create logs directory if it doesn't exist
|
||||||
|
(BASE_DIR / "logs").mkdir(exist_ok=True)
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
"""
|
||||||
|
Copywriter module for the Marketing Assistant AI.
|
||||||
|
Core AI-powered content generation using a fine-tuned LLM.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import httpx
|
||||||
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
from loguru import logger
|
||||||
|
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||||
|
|
||||||
|
import config
|
||||||
|
from brand_style import brand_style_manager
|
||||||
|
from vector_store import vector_store
|
||||||
|
|
||||||
|
class Copywriter:
|
||||||
|
"""Generates marketing copy using a fine-tuned LLM."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the Copywriter with Cohere LLM client."""
|
||||||
|
self.model = "command" # Cohere's generation model
|
||||||
|
self.api_key = config.COHERE_API_KEY
|
||||||
|
logger.info("Copywriter initialized with Cohere API successfully")
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
|
||||||
|
async def generate_copy(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
content_type: Optional[str] = None,
|
||||||
|
tone: Optional[str] = None,
|
||||||
|
length: Optional[str] = None,
|
||||||
|
include_cta: bool = False,
|
||||||
|
reference_similar_content: bool = True,
|
||||||
|
max_tokens: int = 1000
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Generate marketing copy based on the user prompt and parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: User prompt for content generation
|
||||||
|
content_type: Type of content to generate
|
||||||
|
tone: Desired tone of the content
|
||||||
|
length: Desired length of the content
|
||||||
|
include_cta: Whether to include a call to action
|
||||||
|
reference_similar_content: Whether to fetch and reference similar content
|
||||||
|
max_tokens: Maximum tokens for the generated response
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with generated content and metadata
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Step 1: Format prompt with brand style guidelines
|
||||||
|
branded_prompt = brand_style_manager.format_prompt_with_brand_style(prompt, content_type)
|
||||||
|
|
||||||
|
# Step 2: Find similar content for reference (if enabled)
|
||||||
|
reference_content = []
|
||||||
|
if reference_similar_content:
|
||||||
|
search_results = await vector_store.search(prompt, top_k=3)
|
||||||
|
if search_results:
|
||||||
|
reference_content = [result['text'] for result in search_results]
|
||||||
|
|
||||||
|
# Step 3: Add additional instructions based on parameters
|
||||||
|
full_prompt = branded_prompt
|
||||||
|
|
||||||
|
if tone:
|
||||||
|
full_prompt += f"\n- Use a {tone} tone"
|
||||||
|
|
||||||
|
if length:
|
||||||
|
length_instructions = {
|
||||||
|
"short": "Keep the content brief and to the point (under 100 words).",
|
||||||
|
"medium": "Write a moderate amount of content (100-300 words).",
|
||||||
|
"long": "Create comprehensive content with depth (over 300 words)."
|
||||||
|
}
|
||||||
|
full_prompt += f"\n- {length_instructions.get(length, '')}"
|
||||||
|
|
||||||
|
if include_cta:
|
||||||
|
full_prompt += "\n- Include a strong call to action at the end"
|
||||||
|
|
||||||
|
# Step 4: Add reference content if available
|
||||||
|
if reference_content:
|
||||||
|
full_prompt += "\n\nFor reference, here are some similar pieces of content that have performed well in the past:"
|
||||||
|
for i, content in enumerate(reference_content, 1):
|
||||||
|
# Truncate reference content if it's too long
|
||||||
|
preview = content[:300] + "..." if len(content) > 300 else content
|
||||||
|
full_prompt += f"\n\nReference {i}:\n{preview}"
|
||||||
|
|
||||||
|
full_prompt += "\n\nUse these references for inspiration, but create original content."
|
||||||
|
|
||||||
|
# Step 5: Generate content using the LLM
|
||||||
|
generated_content = await self._call_llm_api(full_prompt, max_tokens)
|
||||||
|
|
||||||
|
# Step 6: Check content alignment with brand style
|
||||||
|
alignment_check = brand_style_manager.check_content_alignment(generated_content)
|
||||||
|
|
||||||
|
# Step 7: Generate alternative headline suggestions
|
||||||
|
headline_suggestions = await self._generate_headline_suggestions(prompt, generated_content)
|
||||||
|
|
||||||
|
# Step 8: Return the generated content with metadata
|
||||||
|
result = {
|
||||||
|
"content": generated_content,
|
||||||
|
"suggestions": headline_suggestions,
|
||||||
|
"metadata": {
|
||||||
|
"content_type": content_type,
|
||||||
|
"tone": tone,
|
||||||
|
"alignment_score": alignment_check['alignment_score'],
|
||||||
|
"generated_at": None # Will be added by the API
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add alignment issues if any
|
||||||
|
if alignment_check['taboo_words_found'] or alignment_check['terminology_issues']:
|
||||||
|
result["alignment_issues"] = {
|
||||||
|
"taboo_words_found": alignment_check['taboo_words_found'],
|
||||||
|
"terminology_issues": alignment_check['terminology_issues']
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Generated content with {len(generated_content)} characters")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating copy: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
|
||||||
|
async def _call_llm_api(self, prompt: str, max_tokens: int = 1000) -> str:
|
||||||
|
"""
|
||||||
|
Call the Cohere API to generate content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: The formatted prompt for the LLM
|
||||||
|
max_tokens: Maximum tokens for the generated response
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated content as a string
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Use Cohere's generate API with the API key from config
|
||||||
|
cohere_api_key = config.COHERE_API_KEY
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
"https://api.cohere.ai/v1/generate",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {cohere_api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"model": "command", # Cohere's generation model
|
||||||
|
"prompt": prompt,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"k": 0,
|
||||||
|
"p": 0.75
|
||||||
|
},
|
||||||
|
timeout=30.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
return result["generations"][0]["text"].strip()
|
||||||
|
else:
|
||||||
|
logger.error(f"Cohere API error: {response.status_code}, {response.text}")
|
||||||
|
raise Exception(f"Cohere API error: {response.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calling Cohere API: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _generate_headline_suggestions(self, original_prompt: str, generated_content: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Generate alternative headline suggestions based on the content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
original_prompt: The original user prompt
|
||||||
|
generated_content: The generated marketing content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of headline suggestions
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# This would call the LLM to generate headlines
|
||||||
|
# Simplified mock response for demonstration
|
||||||
|
return [
|
||||||
|
"Alternative Headline 1: Discover the Power of Adriana James' Solutions",
|
||||||
|
"Alternative Headline 2: Transform Your Results with Adriana James",
|
||||||
|
"Alternative Headline 3: The Adriana James Approach: Excellence Redefined"
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating headline suggestions: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def improve_copy(self, content: str, feedback: str) -> str:
|
||||||
|
"""
|
||||||
|
Improve content based on user feedback.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Original generated content
|
||||||
|
feedback: User feedback for improvement
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Improved content
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Format prompt for improvement
|
||||||
|
improve_prompt = f"""
|
||||||
|
Please improve the following marketing content based on the feedback provided:
|
||||||
|
|
||||||
|
ORIGINAL CONTENT:
|
||||||
|
{content}
|
||||||
|
|
||||||
|
FEEDBACK:
|
||||||
|
{feedback}
|
||||||
|
|
||||||
|
IMPROVED CONTENT:
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Call LLM to improve content
|
||||||
|
improved_content = await self._call_llm_api(improve_prompt, max_tokens=1200)
|
||||||
|
|
||||||
|
logger.info(f"Improved content based on feedback")
|
||||||
|
return improved_content
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error improving content: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def analyze_content_performance(self, content: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Analyze marketing content for performance prediction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Marketing content to analyze
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with analysis results
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# This would be enhanced with actual ML models in production
|
||||||
|
# Simplified mock response for demonstration
|
||||||
|
|
||||||
|
# Very basic analysis using length and keyword presence
|
||||||
|
word_count = len(content.split())
|
||||||
|
has_cta = any(phrase in content.lower() for phrase in ["call", "contact", "get started", "try", "buy", "sign up"])
|
||||||
|
sentence_count = len([s for s in content.split(".") if s.strip()])
|
||||||
|
avg_words_per_sentence = word_count / max(1, sentence_count)
|
||||||
|
|
||||||
|
# Simple scoring system
|
||||||
|
readability_score = 100 - min(100, max(0, abs(avg_words_per_sentence - 15) * 5))
|
||||||
|
cta_score = 90 if has_cta else 60
|
||||||
|
length_score = min(100, max(0, word_count / 3))
|
||||||
|
|
||||||
|
overall_score = (readability_score + cta_score + length_score) / 3
|
||||||
|
|
||||||
|
return {
|
||||||
|
"overall_score": round(overall_score, 1),
|
||||||
|
"readability_score": round(readability_score, 1),
|
||||||
|
"cta_effectiveness": round(cta_score, 1),
|
||||||
|
"length_appropriateness": round(length_score, 1),
|
||||||
|
"metrics": {
|
||||||
|
"word_count": word_count,
|
||||||
|
"sentence_count": sentence_count,
|
||||||
|
"avg_words_per_sentence": round(avg_words_per_sentence, 1),
|
||||||
|
"has_cta": has_cta
|
||||||
|
},
|
||||||
|
"improvement_suggestions": [
|
||||||
|
"Consider adding a stronger call to action" if cta_score < 80 else "Your call to action is effective",
|
||||||
|
"Try to use shorter sentences for better readability" if avg_words_per_sentence > 20 else "Your sentence length is good for readability",
|
||||||
|
"Consider adding more content for better engagement" if word_count < 100 else "Your content length is appropriate"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error analyzing content: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Create a singleton instance
|
||||||
|
copywriter = Copywriter()
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,138 @@
|
|||||||
|
"""
|
||||||
|
Embeddings module for the Marketing Assistant AI.
|
||||||
|
Uses Cohere to generate and manage text embeddings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import cohere
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
import numpy as np
|
||||||
|
from loguru import logger
|
||||||
|
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
|
class EmbeddingsManager:
|
||||||
|
"""Manages the generation and manipulation of text embeddings using Cohere."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the EmbeddingsManager with Cohere API client."""
|
||||||
|
try:
|
||||||
|
self.co = cohere.Client(config.COHERE_API_KEY)
|
||||||
|
logger.info("EmbeddingsManager initialized successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to initialize EmbeddingsManager: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
|
||||||
|
async def get_embeddings(self, texts: List[str], model: str = "embed-english-v3.0") -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Generate embeddings for a list of texts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: List of text strings to embed
|
||||||
|
model: Cohere embedding model to use
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
numpy.ndarray: Array of embeddings vectors
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not texts:
|
||||||
|
logger.warning("Empty text list provided for embedding")
|
||||||
|
return np.array([])
|
||||||
|
|
||||||
|
# Ensure texts are not too long for the API
|
||||||
|
processed_texts = [text[:8192] for text in texts]
|
||||||
|
|
||||||
|
response = self.co.embed(
|
||||||
|
texts=processed_texts,
|
||||||
|
model=model,
|
||||||
|
input_type="search_document"
|
||||||
|
)
|
||||||
|
|
||||||
|
embeddings = np.array(response.embeddings)
|
||||||
|
logger.debug(f"Generated {len(embeddings)} embeddings with shape {embeddings.shape}")
|
||||||
|
return embeddings
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating embeddings: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
|
||||||
|
async def get_query_embedding(self, text: str, model: str = "embed-english-v3.0") -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Generate embedding for a single query text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The query text to embed
|
||||||
|
model: Cohere embedding model to use
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
numpy.ndarray: Embedding vector for the query
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.co.embed(
|
||||||
|
texts=[text[:8192]],
|
||||||
|
model=model,
|
||||||
|
input_type="search_query"
|
||||||
|
)
|
||||||
|
|
||||||
|
embedding = np.array(response.embeddings[0])
|
||||||
|
return embedding
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating query embedding: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
|
||||||
|
async def rerank_results(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
documents: List[str],
|
||||||
|
model: str = "rerank-english-v2.0",
|
||||||
|
top_n: int = 5
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Rerank documents based on relevance to the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: The search query
|
||||||
|
documents: List of documents to rerank
|
||||||
|
model: Cohere reranking model to use
|
||||||
|
top_n: Number of top results to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dictionaries with document index and relevance score
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not documents:
|
||||||
|
logger.warning("Empty document list provided for reranking")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Truncate documents if they're too long
|
||||||
|
processed_docs = [doc[:8192] for doc in documents]
|
||||||
|
|
||||||
|
response = self.co.rerank(
|
||||||
|
query=query,
|
||||||
|
documents=processed_docs,
|
||||||
|
model=model,
|
||||||
|
top_n=min(top_n, len(processed_docs))
|
||||||
|
)
|
||||||
|
|
||||||
|
results = [
|
||||||
|
{
|
||||||
|
"index": result.index,
|
||||||
|
"document": documents[result.index],
|
||||||
|
"relevance_score": result.relevance_score
|
||||||
|
}
|
||||||
|
for result in response.results
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.debug(f"Reranked {len(documents)} documents, returning top {len(results)}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reranking documents: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Create a singleton instance
|
||||||
|
embeddings_manager = EmbeddingsManager()
|
||||||
+406
@@ -0,0 +1,406 @@
|
|||||||
|
"""
|
||||||
|
Main FastAPI application for the Marketing Assistant AI.
|
||||||
|
Provides API endpoints for generating and managing marketing content.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import FastAPI, HTTPException, Depends, Query, Body, status
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from loguru import logger
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
import config
|
||||||
|
from copywriter import copywriter
|
||||||
|
from vector_store import vector_store
|
||||||
|
from brand_style import brand_style_manager
|
||||||
|
from embeddings import embeddings_manager
|
||||||
|
|
||||||
|
# Initialize logging
|
||||||
|
logger.add(config.LOG_FILE, level=config.LOG_LEVEL, rotation="10 MB", retention="1 month")
|
||||||
|
|
||||||
|
# Create FastAPI app
|
||||||
|
app = FastAPI(
|
||||||
|
title="Marketing Assistant AI",
|
||||||
|
description="AI-powered tool for marketing copywriting with Adriana James' brand voice",
|
||||||
|
version="1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add CORS middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # In production, specify your frontend domain
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define request and response models
|
||||||
|
class GenerateCopyRequest(BaseModel):
|
||||||
|
prompt: str = Field(..., description="The main instruction for generating content")
|
||||||
|
content_type: Optional[str] = Field(None, description="Type of content to generate")
|
||||||
|
tone: Optional[str] = Field(None, description="Desired tone of the content")
|
||||||
|
length: Optional[str] = Field(None, description="Desired length of the content")
|
||||||
|
include_cta: Optional[bool] = Field(False, description="Whether to include a call to action")
|
||||||
|
reference_similar_content: Optional[bool] = Field(True, description="Whether to reference similar content")
|
||||||
|
max_tokens: Optional[int] = Field(1000, description="Maximum tokens for the generated response")
|
||||||
|
|
||||||
|
class TrainingDataRequest(BaseModel):
|
||||||
|
content_type: str = Field(..., description="Type of content")
|
||||||
|
content: str = Field(..., description="The marketing content")
|
||||||
|
metadata: Optional[Dict[str, Any]] = Field({}, description="Additional metadata about the content")
|
||||||
|
|
||||||
|
class BrandStyleUpdateRequest(BaseModel):
|
||||||
|
tone: Optional[List[str]] = Field(None, description="Brand tone options")
|
||||||
|
voice_characteristics: Optional[List[str]] = Field(None, description="Voice characteristics")
|
||||||
|
taboo_words: Optional[List[str]] = Field(None, description="Words to avoid")
|
||||||
|
preferred_terms: Optional[Dict[str, str]] = Field(None, description="Preferred terminology")
|
||||||
|
|
||||||
|
class ContentImprovementRequest(BaseModel):
|
||||||
|
content: str = Field(..., description="Original generated content")
|
||||||
|
feedback: str = Field(..., description="User feedback for improvement")
|
||||||
|
|
||||||
|
# API Routes
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Root endpoint with API information."""
|
||||||
|
return {
|
||||||
|
"name": "Marketing Assistant AI",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": f"AI-powered marketing copywriter for {config.BRAND_NAME}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/generate-copy")
|
||||||
|
async def generate_copy(request: GenerateCopyRequest):
|
||||||
|
"""Generate marketing copy based on the provided prompt and parameters."""
|
||||||
|
try:
|
||||||
|
# Validate content type if provided
|
||||||
|
if request.content_type and request.content_type not in config.CONTENT_TYPES:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Invalid content_type. Must be one of: {', '.join(config.CONTENT_TYPES)}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate tone if provided
|
||||||
|
if request.tone and request.tone not in config.TONE_OPTIONS:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Invalid tone. Must be one of: {', '.join(config.TONE_OPTIONS)}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate length if provided
|
||||||
|
if request.length and request.length not in config.LENGTH_OPTIONS:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Invalid length. Must be one of: {', '.join(config.LENGTH_OPTIONS)}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate copy
|
||||||
|
result = await copywriter.generate_copy(
|
||||||
|
prompt=request.prompt,
|
||||||
|
content_type=request.content_type,
|
||||||
|
tone=request.tone,
|
||||||
|
length=request.length,
|
||||||
|
include_cta=request.include_cta,
|
||||||
|
reference_similar_content=request.reference_similar_content,
|
||||||
|
max_tokens=request.max_tokens
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add timestamp
|
||||||
|
result["metadata"]["generated_at"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# Store the generated content in the vector store for future reference
|
||||||
|
if result["content"]:
|
||||||
|
metadata = {
|
||||||
|
"content_type": request.content_type,
|
||||||
|
"tone": request.tone,
|
||||||
|
"prompt": request.prompt,
|
||||||
|
"generated": True
|
||||||
|
}
|
||||||
|
await vector_store.add_documents([result["content"]], [metadata])
|
||||||
|
|
||||||
|
# Store the user query for future training
|
||||||
|
query_path = Path(config.DATA_DIR) / "user_queries" / f"{datetime.now().strftime('%Y%m%d%H%M%S')}.json"
|
||||||
|
with open(query_path, 'w') as f:
|
||||||
|
json.dump({
|
||||||
|
"prompt": request.prompt,
|
||||||
|
"parameters": {
|
||||||
|
"content_type": request.content_type,
|
||||||
|
"tone": request.tone,
|
||||||
|
"length": request.length,
|
||||||
|
"include_cta": request.include_cta
|
||||||
|
},
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}, f, indent=2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"content": result["content"],
|
||||||
|
"suggestions": result.get("suggestions", []),
|
||||||
|
"metadata": result["metadata"]
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating copy: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to generate copy: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/brand-style")
|
||||||
|
async def get_brand_style():
|
||||||
|
"""Get the current brand style guidelines."""
|
||||||
|
try:
|
||||||
|
style = brand_style_manager.get_style_guidelines()
|
||||||
|
return style
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting brand style: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to get brand style: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.put("/brand-style")
|
||||||
|
async def update_brand_style(request: BrandStyleUpdateRequest):
|
||||||
|
"""Update the brand style guidelines."""
|
||||||
|
try:
|
||||||
|
update_data = request.dict(exclude_unset=True)
|
||||||
|
updated_style = brand_style_manager.update_style_guidelines(update_data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Brand style updated successfully",
|
||||||
|
"style": updated_style
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating brand style: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to update brand style: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/training-data")
|
||||||
|
async def add_training_data(request: TrainingDataRequest):
|
||||||
|
"""Add new marketing content for AI training."""
|
||||||
|
try:
|
||||||
|
# Validate content type
|
||||||
|
if request.content_type not in config.CONTENT_TYPES:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Invalid content_type. Must be one of: {', '.join(config.CONTENT_TYPES)}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add metadata
|
||||||
|
metadata = request.metadata.copy()
|
||||||
|
metadata["content_type"] = request.content_type
|
||||||
|
metadata["added_at"] = datetime.now().isoformat()
|
||||||
|
metadata["training_data"] = True
|
||||||
|
|
||||||
|
# Add to vector store
|
||||||
|
doc_ids = await vector_store.add_documents([request.content], [metadata])
|
||||||
|
|
||||||
|
# Save to past campaigns
|
||||||
|
campaign_path = Path(config.DATA_DIR) / "past_campaigns" / f"{datetime.now().strftime('%Y%m%d%H%M%S')}.json"
|
||||||
|
with open(campaign_path, 'w') as f:
|
||||||
|
json.dump({
|
||||||
|
"content": request.content,
|
||||||
|
"content_type": request.content_type,
|
||||||
|
"metadata": metadata,
|
||||||
|
"document_id": doc_ids[0] if doc_ids else None,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}, f, indent=2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Training data added successfully",
|
||||||
|
"data_id": doc_ids[0] if doc_ids else None
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding training data: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to add training data: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/training-data")
|
||||||
|
async def list_training_data(
|
||||||
|
content_type: Optional[str] = Query(None, description="Filter by content type"),
|
||||||
|
page: int = Query(1, ge=1, description="Page number"),
|
||||||
|
limit: int = Query(10, ge=1, le=100, description="Items per page")
|
||||||
|
):
|
||||||
|
"""Retrieve a list of available training data."""
|
||||||
|
try:
|
||||||
|
# Build filters
|
||||||
|
filters = {}
|
||||||
|
if content_type:
|
||||||
|
if content_type not in config.CONTENT_TYPES:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content={
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Invalid content_type. Must be one of: {', '.join(config.CONTENT_TYPES)}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
filters["content_type"] = content_type
|
||||||
|
|
||||||
|
filters["training_data"] = True
|
||||||
|
|
||||||
|
# Fetch all matching documents first (not efficient for large datasets but works for demo)
|
||||||
|
all_docs = []
|
||||||
|
for i in range(len(vector_store.metadata)):
|
||||||
|
doc = await vector_store.get_document(i)
|
||||||
|
if doc and all(doc["metadata"].get(k) == v for k, v in filters.items()):
|
||||||
|
all_docs.append(doc)
|
||||||
|
|
||||||
|
# Sort by timestamp (newest first)
|
||||||
|
all_docs.sort(key=lambda x: x["metadata"].get("added_at", ""), reverse=True)
|
||||||
|
|
||||||
|
# Paginate
|
||||||
|
total = len(all_docs)
|
||||||
|
pages = (total + limit - 1) // limit if total > 0 else 1
|
||||||
|
start = (page - 1) * limit
|
||||||
|
end = start + limit
|
||||||
|
paginated_docs = all_docs[start:end]
|
||||||
|
|
||||||
|
# Format the response
|
||||||
|
items = []
|
||||||
|
for doc in paginated_docs:
|
||||||
|
# Get a preview of the text (first 100 characters)
|
||||||
|
preview = doc["text"][:100] + "..." if len(doc["text"]) > 100 else doc["text"]
|
||||||
|
|
||||||
|
items.append({
|
||||||
|
"id": doc["document_id"],
|
||||||
|
"content_type": doc["metadata"].get("content_type", "unknown"),
|
||||||
|
"preview": preview,
|
||||||
|
"added_at": doc["metadata"].get("added_at", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": items,
|
||||||
|
"pagination": {
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
"pages": pages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing training data: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to list training data: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/training-data/{document_id}")
|
||||||
|
async def get_training_data(document_id: int):
|
||||||
|
"""Retrieve a specific training document by ID."""
|
||||||
|
try:
|
||||||
|
doc = await vector_store.get_document(document_id)
|
||||||
|
if not doc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Document with ID {document_id} not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": doc["document_id"],
|
||||||
|
"content": doc["text"],
|
||||||
|
"metadata": doc["metadata"]
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving training data: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to retrieve training data: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.delete("/training-data/{document_id}")
|
||||||
|
async def delete_training_data(document_id: int):
|
||||||
|
"""Delete a specific training document by ID."""
|
||||||
|
try:
|
||||||
|
success = await vector_store.delete_document(document_id)
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Document with ID {document_id} not found or could not be deleted"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Document with ID {document_id} successfully deleted"
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting training data: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to delete training data: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/improve-content")
|
||||||
|
async def improve_content(request: ContentImprovementRequest):
|
||||||
|
"""Improve content based on user feedback."""
|
||||||
|
try:
|
||||||
|
improved_content = await copywriter.improve_copy(
|
||||||
|
content=request.content,
|
||||||
|
feedback=request.feedback
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"original_content": request.content,
|
||||||
|
"improved_content": improved_content,
|
||||||
|
"feedback": request.feedback
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error improving content: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to improve content: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/analyze-content")
|
||||||
|
async def analyze_content(content: str = Body(..., embed=True)):
|
||||||
|
"""Analyze marketing content for performance prediction."""
|
||||||
|
try:
|
||||||
|
analysis = await copywriter.analyze_content_performance(content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"analysis": analysis
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error analyzing content: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Failed to analyze content: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(
|
||||||
|
"main:app",
|
||||||
|
host=config.API_HOST,
|
||||||
|
port=config.API_PORT,
|
||||||
|
reload=True
|
||||||
|
)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
pydantic
|
||||||
|
python-dotenv
|
||||||
|
httpx
|
||||||
|
faiss-cpu
|
||||||
|
numpy==1.26.2
|
||||||
|
pandas
|
||||||
|
cohere
|
||||||
|
python-multipart
|
||||||
|
SQLAlchemy
|
||||||
|
databases
|
||||||
|
aiosqlite
|
||||||
|
loguru
|
||||||
|
tenacity
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
"""
|
||||||
|
Vector store module for the Marketing Assistant AI.
|
||||||
|
Uses FAISS for efficient storage and retrieval of content embeddings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import pickle
|
||||||
|
import faiss
|
||||||
|
import numpy as np
|
||||||
|
from typing import List, Dict, Any, Optional, Tuple
|
||||||
|
from pathlib import Path
|
||||||
|
from loguru import logger
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import config
|
||||||
|
from embeddings import embeddings_manager
|
||||||
|
|
||||||
|
class VectorStore:
|
||||||
|
"""Manages vector database operations for content retrieval."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the VectorStore with FAISS index."""
|
||||||
|
self.store_path = Path(config.VECTOR_DB_PATH)
|
||||||
|
self.store_path.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
self.index_path = self.store_path / "faiss_index.bin"
|
||||||
|
self.metadata_path = self.store_path / "metadata.pkl"
|
||||||
|
|
||||||
|
self.dimension = None
|
||||||
|
self.index = None
|
||||||
|
self.metadata = []
|
||||||
|
|
||||||
|
self._load_or_create_index()
|
||||||
|
logger.info("VectorStore initialized successfully")
|
||||||
|
|
||||||
|
def _load_or_create_index(self) -> None:
|
||||||
|
"""Load existing index or create new one if it doesn't exist."""
|
||||||
|
try:
|
||||||
|
if self.index_path.exists() and self.metadata_path.exists():
|
||||||
|
# Load existing index and metadata
|
||||||
|
self.index = faiss.read_index(str(self.index_path))
|
||||||
|
with open(self.metadata_path, 'rb') as f:
|
||||||
|
self.metadata = pickle.load(f)
|
||||||
|
self.dimension = self.index.d
|
||||||
|
logger.info(f"Loaded existing vector index with {self.index.ntotal} vectors")
|
||||||
|
else:
|
||||||
|
# Default dimension for Cohere embeddings
|
||||||
|
self.dimension = 1024
|
||||||
|
self.index = faiss.IndexFlatL2(self.dimension)
|
||||||
|
self.metadata = []
|
||||||
|
logger.info(f"Created new vector index with dimension {self.dimension}")
|
||||||
|
|
||||||
|
# Save the empty index and metadata
|
||||||
|
self._save_index()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading or creating index: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _save_index(self) -> None:
|
||||||
|
"""Save the index and metadata to disk."""
|
||||||
|
try:
|
||||||
|
faiss.write_index(self.index, str(self.index_path))
|
||||||
|
with open(self.metadata_path, 'wb') as f:
|
||||||
|
pickle.dump(self.metadata, f)
|
||||||
|
logger.debug("Saved vector index and metadata")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error saving index: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def add_documents(
|
||||||
|
self,
|
||||||
|
texts: List[str],
|
||||||
|
metadata_list: Optional[List[Dict[str, Any]]] = None
|
||||||
|
) -> List[int]:
|
||||||
|
"""
|
||||||
|
Add documents to the vector store.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: List of text documents to add
|
||||||
|
metadata_list: List of metadata dictionaries for each document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of document IDs (vector indices)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not texts:
|
||||||
|
logger.warning("No texts provided to add to vector store")
|
||||||
|
return []
|
||||||
|
|
||||||
|
if metadata_list is None:
|
||||||
|
metadata_list = [{} for _ in texts]
|
||||||
|
|
||||||
|
if len(texts) != len(metadata_list):
|
||||||
|
raise ValueError("Number of texts and metadata entries must match")
|
||||||
|
|
||||||
|
# Generate embeddings
|
||||||
|
embeddings = await embeddings_manager.get_embeddings(texts)
|
||||||
|
|
||||||
|
# Check if embeddings match our dimension
|
||||||
|
if embeddings.shape[1] != self.dimension:
|
||||||
|
logger.warning(f"Embedding dimension mismatch: expected {self.dimension}, got {embeddings.shape[1]}")
|
||||||
|
# If we have no documents yet, we can adapt to the new dimension
|
||||||
|
if self.index.ntotal == 0:
|
||||||
|
self.dimension = embeddings.shape[1]
|
||||||
|
self.index = faiss.IndexFlatL2(self.dimension)
|
||||||
|
logger.info(f"Adapted to new dimension: {self.dimension}")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Embedding dimension mismatch: expected {self.dimension}, got {embeddings.shape[1]}")
|
||||||
|
|
||||||
|
# Add timestamp to metadata
|
||||||
|
timestamp = datetime.now().isoformat()
|
||||||
|
for meta in metadata_list:
|
||||||
|
meta['timestamp'] = timestamp
|
||||||
|
meta['document_id'] = len(self.metadata) + len(metadata_list)
|
||||||
|
|
||||||
|
# Store texts in metadata
|
||||||
|
for i, (text, meta) in enumerate(zip(texts, metadata_list)):
|
||||||
|
meta['text'] = text
|
||||||
|
|
||||||
|
# Add vectors to index
|
||||||
|
start_idx = self.index.ntotal
|
||||||
|
self.index.add(embeddings.astype(np.float32))
|
||||||
|
self.metadata.extend(metadata_list)
|
||||||
|
|
||||||
|
# Save updated index
|
||||||
|
self._save_index()
|
||||||
|
|
||||||
|
# Return document IDs
|
||||||
|
doc_ids = list(range(start_idx, start_idx + len(texts)))
|
||||||
|
logger.info(f"Added {len(texts)} documents to vector store")
|
||||||
|
return doc_ids
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding documents to vector store: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def search(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
top_k: int = 5,
|
||||||
|
filters: Optional[Dict[str, Any]] = None,
|
||||||
|
rerank: bool = True
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Search for similar documents.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: The search query
|
||||||
|
top_k: Number of results to return
|
||||||
|
filters: Dictionary of metadata filters
|
||||||
|
rerank: Whether to use Cohere's reranking
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of result dictionaries with document content and metadata
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.index.ntotal == 0:
|
||||||
|
logger.warning("Empty vector store, no results to return")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Generate query embedding
|
||||||
|
query_embedding = await embeddings_manager.get_query_embedding(query)
|
||||||
|
query_embedding = query_embedding.reshape(1, -1).astype(np.float32)
|
||||||
|
|
||||||
|
# First pass: find more candidates than needed for reranking
|
||||||
|
search_k = top_k * 3 if rerank else top_k
|
||||||
|
search_k = min(search_k, self.index.ntotal) # Don't request more than we have
|
||||||
|
|
||||||
|
distances, indices = self.index.search(query_embedding, search_k)
|
||||||
|
|
||||||
|
# Get metadata and texts for matching indices
|
||||||
|
results = []
|
||||||
|
for i, idx in enumerate(indices[0]):
|
||||||
|
if idx < 0 or idx >= len(self.metadata):
|
||||||
|
continue # Skip invalid indices
|
||||||
|
|
||||||
|
metadata = self.metadata[idx]
|
||||||
|
text = metadata.get('text', '')
|
||||||
|
|
||||||
|
# Apply filters if any
|
||||||
|
if filters and not self._matches_filters(metadata, filters):
|
||||||
|
continue
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'document_id': idx,
|
||||||
|
'text': text,
|
||||||
|
'metadata': {k: v for k, v in metadata.items() if k != 'text'},
|
||||||
|
'distance': float(distances[0][i])
|
||||||
|
})
|
||||||
|
|
||||||
|
# Apply reranking if requested
|
||||||
|
if rerank and results:
|
||||||
|
texts = [r['text'] for r in results]
|
||||||
|
reranked = await embeddings_manager.rerank_results(query, texts, top_n=top_k)
|
||||||
|
|
||||||
|
# Map reranked results back to our original results
|
||||||
|
reranked_results = []
|
||||||
|
for item in reranked:
|
||||||
|
orig_idx = item['index']
|
||||||
|
if 0 <= orig_idx < len(results):
|
||||||
|
reranked_results.append({
|
||||||
|
**results[orig_idx],
|
||||||
|
'relevance_score': item['relevance_score']
|
||||||
|
})
|
||||||
|
|
||||||
|
results = reranked_results
|
||||||
|
else:
|
||||||
|
# Just take the top_k results
|
||||||
|
results = results[:top_k]
|
||||||
|
|
||||||
|
logger.info(f"Found {len(results)} matching documents for query")
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error searching vector store: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _matches_filters(self, metadata: Dict[str, Any], filters: Dict[str, Any]) -> bool:
|
||||||
|
"""Check if metadata matches the specified filters."""
|
||||||
|
for key, value in filters.items():
|
||||||
|
if key not in metadata:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if isinstance(value, list):
|
||||||
|
# Check if metadata value is in the list
|
||||||
|
if metadata[key] not in value:
|
||||||
|
return False
|
||||||
|
elif metadata[key] != value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def delete_document(self, document_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Delete a document from the vector store.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
document_id: ID of the document to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Boolean indicating success
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if document_id < 0 or document_id >= len(self.metadata):
|
||||||
|
logger.warning(f"Invalid document ID: {document_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# FAISS doesn't support direct deletion, so we need to rebuild the index
|
||||||
|
# Mark the document as deleted in metadata
|
||||||
|
self.metadata[document_id]['deleted'] = True
|
||||||
|
|
||||||
|
# Save updated metadata
|
||||||
|
self._save_index()
|
||||||
|
|
||||||
|
logger.info(f"Marked document {document_id} as deleted")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting document: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_document(self, document_id: int) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Retrieve a document by ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
document_id: ID of the document to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Document with metadata or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if document_id < 0 or document_id >= len(self.metadata):
|
||||||
|
logger.warning(f"Invalid document ID: {document_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
metadata = self.metadata[document_id]
|
||||||
|
|
||||||
|
# Check if document is marked as deleted
|
||||||
|
if metadata.get('deleted', False):
|
||||||
|
logger.warning(f"Document {document_id} is marked as deleted")
|
||||||
|
return None
|
||||||
|
|
||||||
|
text = metadata.get('text', '')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'document_id': document_id,
|
||||||
|
'text': text,
|
||||||
|
'metadata': {k: v for k, v in metadata.items() if k != 'text' and k != 'deleted'}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving document: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def update_document(self, document_id: int, text: str, metadata: Optional[Dict[str, Any]] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Update a document in the vector store.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
document_id: ID of the document to update
|
||||||
|
text: New document text
|
||||||
|
metadata: New metadata (will be merged with existing)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Boolean indicating success
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if document_id < 0 or document_id >= len(self.metadata):
|
||||||
|
logger.warning(f"Invalid document ID: {document_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get existing metadata
|
||||||
|
existing_metadata = self.metadata[document_id]
|
||||||
|
|
||||||
|
# Check if document is marked as deleted
|
||||||
|
if existing_metadata.get('deleted', False):
|
||||||
|
logger.warning(f"Cannot update deleted document {document_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Generate new embedding
|
||||||
|
embeddings = await embeddings_manager.get_embeddings([text])
|
||||||
|
|
||||||
|
# Update the vector in the index
|
||||||
|
faiss.IndexFlatL2_update_vectors(self.index, embeddings.astype(np.float32), np.array([document_id], dtype=np.int64))
|
||||||
|
|
||||||
|
# Update metadata
|
||||||
|
if metadata:
|
||||||
|
for key, value in metadata.items():
|
||||||
|
existing_metadata[key] = value
|
||||||
|
|
||||||
|
existing_metadata['text'] = text
|
||||||
|
existing_metadata['updated_at'] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# Save updated index
|
||||||
|
self._save_index()
|
||||||
|
|
||||||
|
logger.info(f"Updated document {document_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating document: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Create a singleton instance
|
||||||
|
vector_store = VectorStore()
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Marketing Assistant AI - Data Directory
|
||||||
|
|
||||||
|
This directory contains the data used by the Marketing Assistant AI system.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- **past_campaigns/**: Contains JSON files of past marketing campaigns used for training and reference
|
||||||
|
- **user_queries/**: Stores user queries and requests for analytics and model improvement
|
||||||
|
- **style_guidelines/**: Contains brand tone and voice guidelines
|
||||||
|
- **vector_store/**: Generated vector database for content retrieval (created automatically)
|
||||||
|
|
||||||
|
## File Formats
|
||||||
|
|
||||||
|
### Past Campaigns
|
||||||
|
|
||||||
|
Past campaign files are stored as JSON with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "The actual marketing content text",
|
||||||
|
"content_type": "email_campaign|social_media|blog_post|etc",
|
||||||
|
"metadata": {
|
||||||
|
"campaign_name": "Name of the campaign",
|
||||||
|
"performance_metrics": {
|
||||||
|
"metric1": value,
|
||||||
|
"metric2": value
|
||||||
|
},
|
||||||
|
"content_type": "Same as above",
|
||||||
|
"added_at": "ISO timestamp",
|
||||||
|
"training_data": true
|
||||||
|
},
|
||||||
|
"document_id": 0,
|
||||||
|
"timestamp": "ISO timestamp"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Queries
|
||||||
|
|
||||||
|
User query files store information about requests made to the AI:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "The user's prompt text",
|
||||||
|
"parameters": {
|
||||||
|
"content_type": "Type of content requested",
|
||||||
|
"tone": "Requested tone",
|
||||||
|
"length": "Requested length",
|
||||||
|
"include_cta": true|false
|
||||||
|
},
|
||||||
|
"timestamp": "ISO timestamp"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Brand Style Guidelines
|
||||||
|
|
||||||
|
Brand style is stored as a JSON file with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"brand_name": "Adriana James",
|
||||||
|
"tone": ["professional", "friendly", "inspirational"],
|
||||||
|
"voice_characteristics": ["clear", "direct", "empowering"],
|
||||||
|
"taboo_words": ["cheap", "discount", "bargain"],
|
||||||
|
"preferred_terms": {
|
||||||
|
"customers": "clients",
|
||||||
|
"products": "solutions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Data
|
||||||
|
|
||||||
|
### Adding Past Campaigns
|
||||||
|
|
||||||
|
1. Use the API endpoint `POST /training-data` with the appropriate JSON payload
|
||||||
|
2. Alternatively, add a JSON file to the `past_campaigns` directory following the format above
|
||||||
|
|
||||||
|
### Updating Brand Style
|
||||||
|
|
||||||
|
1. Use the API endpoint `PUT /brand-style` with the updated style guidelines
|
||||||
|
2. The system will automatically update the style file
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"brand_name": "Adriana James",
|
||||||
|
"tone": [
|
||||||
|
"professional",
|
||||||
|
"friendly",
|
||||||
|
"inspirational",
|
||||||
|
"empowering",
|
||||||
|
"excited",
|
||||||
|
"authoritative"
|
||||||
|
],
|
||||||
|
"voice_characteristics": [
|
||||||
|
"clear",
|
||||||
|
"direct",
|
||||||
|
"empowering",
|
||||||
|
"confident",
|
||||||
|
"authentic",
|
||||||
|
"innovative",
|
||||||
|
"visionary",
|
||||||
|
"approachable"
|
||||||
|
],
|
||||||
|
"taboo_words": [
|
||||||
|
"cheap",
|
||||||
|
"discount",
|
||||||
|
"bargain",
|
||||||
|
"failure",
|
||||||
|
"impossible",
|
||||||
|
"difficult"
|
||||||
|
],
|
||||||
|
"preferred_terms": {
|
||||||
|
"customers": "clients",
|
||||||
|
"products": "solutions",
|
||||||
|
"problems": "challenges",
|
||||||
|
"services": "experiences",
|
||||||
|
"training": "transformation"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# Marketing Assistant AI - API Documentation
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Generate Copy
|
||||||
|
|
||||||
|
Generates marketing copy based on the provided prompt and optional parameters.
|
||||||
|
|
||||||
|
**Endpoint**: `/generate-copy`
|
||||||
|
**Method**: POST
|
||||||
|
**Content-Type**: application/json
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "Write a social media post for our new product launch",
|
||||||
|
"content_type": "social_media",
|
||||||
|
"tone": "excited",
|
||||||
|
"length": "medium",
|
||||||
|
"include_cta": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
- `prompt` (string, required): The main instruction for generating content
|
||||||
|
- `content_type` (string, optional): Type of content to generate (social_media, email, blog, website, etc.)
|
||||||
|
- `tone` (string, optional): Desired tone (excited, professional, casual, etc.)
|
||||||
|
- `length` (string, optional): Content length (short, medium, long)
|
||||||
|
- `include_cta` (boolean, optional): Whether to include a call to action
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"content": "Exciting news! Our revolutionary new product has just landed...",
|
||||||
|
"suggestions": [
|
||||||
|
"Alternative headline option 1",
|
||||||
|
"Alternative headline option 2"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"content_type": "social_media",
|
||||||
|
"tone": "excited",
|
||||||
|
"word_count": 85,
|
||||||
|
"generated_at": "2025-04-17T10:30:45Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Brand Style Guidelines
|
||||||
|
|
||||||
|
Retrieves the current brand style guidelines.
|
||||||
|
|
||||||
|
**Endpoint**: `/brand-style`
|
||||||
|
**Method**: GET
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"brand_name": "Adriana James",
|
||||||
|
"tone": ["professional", "friendly", "inspiring"],
|
||||||
|
"voice_characteristics": ["clear", "direct", "empowering"],
|
||||||
|
"taboo_words": ["cheap", "discount", "bargain"],
|
||||||
|
"preferred_terms": {
|
||||||
|
"customers": "clients",
|
||||||
|
"products": "solutions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Brand Style
|
||||||
|
|
||||||
|
Updates the brand style guidelines.
|
||||||
|
|
||||||
|
**Endpoint**: `/brand-style`
|
||||||
|
**Method**: PUT
|
||||||
|
**Content-Type**: application/json
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tone": ["professional", "friendly", "inspiring", "innovative"],
|
||||||
|
"voice_characteristics": ["clear", "direct", "empowering"],
|
||||||
|
"taboo_words": ["cheap", "discount", "bargain", "basic"],
|
||||||
|
"preferred_terms": {
|
||||||
|
"customers": "clients",
|
||||||
|
"products": "solutions",
|
||||||
|
"problems": "challenges"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Brand style updated successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Training Data
|
||||||
|
|
||||||
|
Adds new marketing content for AI training.
|
||||||
|
|
||||||
|
**Endpoint**: `/training-data`
|
||||||
|
**Method**: POST
|
||||||
|
**Content-Type**: application/json
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content_type": "email_campaign",
|
||||||
|
"content": "Dear valued client, We're thrilled to announce...",
|
||||||
|
"metadata": {
|
||||||
|
"campaign_name": "Spring Launch 2025",
|
||||||
|
"performance_metrics": {
|
||||||
|
"open_rate": 0.42,
|
||||||
|
"click_rate": 0.15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Training data added successfully",
|
||||||
|
"data_id": "12345"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Training Data
|
||||||
|
|
||||||
|
Retrieves a list of available training data.
|
||||||
|
|
||||||
|
**Endpoint**: `/training-data`
|
||||||
|
**Method**: GET
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
- `content_type` (optional): Filter by content type
|
||||||
|
- `page` (optional): Page number for pagination
|
||||||
|
- `limit` (optional): Number of items per page
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "12345",
|
||||||
|
"content_type": "email_campaign",
|
||||||
|
"preview": "Dear valued client, We're thrilled to announce...",
|
||||||
|
"added_at": "2025-04-10T14:30:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12346",
|
||||||
|
"content_type": "social_media",
|
||||||
|
"preview": "Exciting news! Our revolutionary new product...",
|
||||||
|
"added_at": "2025-04-11T09:15:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"total": 45,
|
||||||
|
"page": 1,
|
||||||
|
"limit": 10,
|
||||||
|
"pages": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All endpoints return standard HTTP status codes:
|
||||||
|
|
||||||
|
- `200 OK`: Request successful
|
||||||
|
- `400 Bad Request`: Invalid request parameters
|
||||||
|
- `401 Unauthorized`: Authentication failed
|
||||||
|
- `404 Not Found`: Resource not found
|
||||||
|
- `500 Internal Server Error`: Server-side error
|
||||||
|
|
||||||
|
Error response format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Detailed error message",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
+513
@@ -0,0 +1,513 @@
|
|||||||
|
// DOM Elements
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Navigation
|
||||||
|
const menuItems = document.querySelectorAll('.menu li');
|
||||||
|
const pages = document.querySelectorAll('.page');
|
||||||
|
|
||||||
|
// Generate Content Page
|
||||||
|
const generateBtn = document.getElementById('generate-btn');
|
||||||
|
const promptInput = document.getElementById('prompt');
|
||||||
|
const contentTypeSelect = document.getElementById('content-type');
|
||||||
|
const toneSelect = document.getElementById('tone');
|
||||||
|
const lengthSelect = document.getElementById('length');
|
||||||
|
const includeCTACheckbox = document.getElementById('include-cta');
|
||||||
|
const referenceSimilarCheckbox = document.getElementById('reference-similar');
|
||||||
|
const resultContainer = document.getElementById('result-container');
|
||||||
|
const resultContent = document.getElementById('result-content');
|
||||||
|
const loadingIndicator = document.getElementById('loading-indicator');
|
||||||
|
const alignmentScore = document.getElementById('alignment-score');
|
||||||
|
const suggestionsList = document.getElementById('suggestions-list');
|
||||||
|
const copyBtn = document.getElementById('copy-btn');
|
||||||
|
const improveBtn = document.getElementById('improve-btn');
|
||||||
|
const saveBtn = document.getElementById('save-btn');
|
||||||
|
const improvementPanel = document.getElementById('improvement-panel');
|
||||||
|
const improvementFeedback = document.getElementById('improvement-feedback');
|
||||||
|
const submitImprovement = document.getElementById('submit-improvement');
|
||||||
|
|
||||||
|
// Brand Style Page
|
||||||
|
const toneSelector = document.getElementById('tone-selector');
|
||||||
|
const voiceSelector = document.getElementById('voice-selector');
|
||||||
|
const tabooWords = document.getElementById('taboo-words');
|
||||||
|
const tabooInput = document.getElementById('taboo-input');
|
||||||
|
const addTabooBtn = document.getElementById('add-taboo-btn');
|
||||||
|
const avoidTerm = document.getElementById('avoid-term');
|
||||||
|
const useTerm = document.getElementById('use-term');
|
||||||
|
const addTermBtn = document.getElementById('add-term-btn');
|
||||||
|
const saveBrandStyleBtn = document.getElementById('save-brand-style');
|
||||||
|
const resetBrandStyleBtn = document.getElementById('reset-brand-style');
|
||||||
|
|
||||||
|
// Training Page
|
||||||
|
const trainingTabs = document.querySelectorAll('.training-tabs .tab');
|
||||||
|
const tabContents = document.querySelectorAll('.tab-content');
|
||||||
|
const addTrainingBtn = document.getElementById('add-training-btn');
|
||||||
|
const trainingContentType = document.getElementById('training-content-type');
|
||||||
|
const campaignName = document.getElementById('campaign-name');
|
||||||
|
const trainingContent = document.getElementById('training-content');
|
||||||
|
const openRate = document.getElementById('open-rate');
|
||||||
|
const clickRate = document.getElementById('click-rate');
|
||||||
|
const conversionRate = document.getElementById('conversion-rate');
|
||||||
|
|
||||||
|
// API Base URL
|
||||||
|
const API_URL = 'http://localhost:8000';
|
||||||
|
|
||||||
|
// Menu Navigation
|
||||||
|
menuItems.forEach(item => {
|
||||||
|
item.addEventListener('click', function() {
|
||||||
|
const pageName = this.getAttribute('data-page');
|
||||||
|
|
||||||
|
// Update active menu item
|
||||||
|
menuItems.forEach(menuItem => menuItem.classList.remove('active'));
|
||||||
|
this.classList.add('active');
|
||||||
|
|
||||||
|
// Show selected page
|
||||||
|
pages.forEach(page => {
|
||||||
|
if (page.id === `${pageName}-page`) {
|
||||||
|
page.classList.add('active');
|
||||||
|
} else {
|
||||||
|
page.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate Content
|
||||||
|
if (generateBtn) {
|
||||||
|
generateBtn.addEventListener('click', function() {
|
||||||
|
if (!promptInput.value.trim()) {
|
||||||
|
alert('Please enter a prompt for content generation.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
loadingIndicator.classList.remove('hidden');
|
||||||
|
resultContainer.classList.add('hidden');
|
||||||
|
|
||||||
|
// Prepare request data
|
||||||
|
const requestData = {
|
||||||
|
prompt: promptInput.value,
|
||||||
|
content_type: contentTypeSelect.value || null,
|
||||||
|
tone: toneSelect.value || null,
|
||||||
|
length: lengthSelect.value || null,
|
||||||
|
include_cta: includeCTACheckbox.checked,
|
||||||
|
reference_similar_content: referenceSimilarCheckbox.checked
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the API
|
||||||
|
fetch(`${API_URL}/generate-copy`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Hide loading indicator
|
||||||
|
loadingIndicator.classList.add('hidden');
|
||||||
|
|
||||||
|
// Display result
|
||||||
|
resultContent.textContent = data.content;
|
||||||
|
|
||||||
|
// Set alignment score
|
||||||
|
const score = data.metadata.alignment_score || 0;
|
||||||
|
alignmentScore.style.width = `${score}%`;
|
||||||
|
alignmentScore.textContent = `${Math.round(score)}%`;
|
||||||
|
|
||||||
|
if (score < 60) {
|
||||||
|
alignmentScore.style.backgroundColor = 'var(--danger-color)';
|
||||||
|
} else if (score < 80) {
|
||||||
|
alignmentScore.style.backgroundColor = 'var(--warning-color)';
|
||||||
|
} else {
|
||||||
|
alignmentScore.style.backgroundColor = 'var(--success-color)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display suggestions
|
||||||
|
if (data.suggestions && data.suggestions.length > 0) {
|
||||||
|
suggestionsList.innerHTML = '';
|
||||||
|
data.suggestions.forEach(suggestion => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = suggestion;
|
||||||
|
li.addEventListener('click', function() {
|
||||||
|
promptInput.value = suggestion;
|
||||||
|
});
|
||||||
|
suggestionsList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show result container
|
||||||
|
resultContainer.classList.remove('hidden');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
loadingIndicator.classList.add('hidden');
|
||||||
|
alert('An error occurred while generating content. Please try again.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy to Clipboard
|
||||||
|
if (copyBtn) {
|
||||||
|
copyBtn.addEventListener('click', function() {
|
||||||
|
navigator.clipboard.writeText(resultContent.textContent)
|
||||||
|
.then(() => {
|
||||||
|
const originalTitle = copyBtn.getAttribute('title');
|
||||||
|
copyBtn.setAttribute('title', 'Copied!');
|
||||||
|
setTimeout(() => {
|
||||||
|
copyBtn.setAttribute('title', originalTitle);
|
||||||
|
}, 2000);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Could not copy text: ', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle Improvement Panel
|
||||||
|
if (improveBtn) {
|
||||||
|
improveBtn.addEventListener('click', function() {
|
||||||
|
improvementPanel.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit Improvement Feedback
|
||||||
|
if (submitImprovement) {
|
||||||
|
submitImprovement.addEventListener('click', function() {
|
||||||
|
if (!improvementFeedback.value.trim()) {
|
||||||
|
alert('Please enter feedback for improvement.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
loadingIndicator.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Prepare request data
|
||||||
|
const requestData = {
|
||||||
|
content: resultContent.textContent,
|
||||||
|
feedback: improvementFeedback.value
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the API
|
||||||
|
fetch(`${API_URL}/improve-content`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Hide loading indicator
|
||||||
|
loadingIndicator.classList.add('hidden');
|
||||||
|
|
||||||
|
// Update result content
|
||||||
|
resultContent.textContent = data.improved_content;
|
||||||
|
|
||||||
|
// Hide improvement panel
|
||||||
|
improvementPanel.classList.add('hidden');
|
||||||
|
improvementFeedback.value = '';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
loadingIndicator.classList.add('hidden');
|
||||||
|
alert('An error occurred while improving content. Please try again.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Content to History
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.addEventListener('click', function() {
|
||||||
|
alert('Content saved to history!');
|
||||||
|
// In a real implementation, you would save this to local storage
|
||||||
|
// or call an API to save it to the backend
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brand Style Tag Selection
|
||||||
|
if (toneSelector) {
|
||||||
|
const tagElements = toneSelector.querySelectorAll('.tag');
|
||||||
|
tagElements.forEach(tag => {
|
||||||
|
tag.addEventListener('click', function() {
|
||||||
|
this.classList.toggle('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (voiceSelector) {
|
||||||
|
const tagElements = voiceSelector.querySelectorAll('.tag');
|
||||||
|
tagElements.forEach(tag => {
|
||||||
|
tag.addEventListener('click', function() {
|
||||||
|
this.classList.toggle('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Taboo Word
|
||||||
|
if (addTabooBtn && tabooInput && tabooWords) {
|
||||||
|
addTabooBtn.addEventListener('click', function() {
|
||||||
|
const word = tabooInput.value.trim();
|
||||||
|
if (word) {
|
||||||
|
const tagElement = document.createElement('span');
|
||||||
|
tagElement.classList.add('tag', 'removable');
|
||||||
|
tagElement.innerHTML = `${word}<i class="fas fa-times"></i>`;
|
||||||
|
|
||||||
|
// Add click event to remove the tag
|
||||||
|
tagElement.querySelector('i').addEventListener('click', function() {
|
||||||
|
tagElement.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
tabooWords.appendChild(tagElement);
|
||||||
|
tabooInput.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow pressing Enter to add a word
|
||||||
|
tabooInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
addTabooBtn.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Terminology Term
|
||||||
|
if (addTermBtn && avoidTerm && useTerm) {
|
||||||
|
addTermBtn.addEventListener('click', function() {
|
||||||
|
const avoid = avoidTerm.value.trim();
|
||||||
|
const use = useTerm.value.trim();
|
||||||
|
|
||||||
|
if (avoid && use) {
|
||||||
|
const tableRow = document.createElement('div');
|
||||||
|
tableRow.classList.add('terminology-row');
|
||||||
|
tableRow.innerHTML = `
|
||||||
|
<div class="term-avoid">${avoid}</div>
|
||||||
|
<div class="term-use">${use}</div>
|
||||||
|
<div class="term-actions">
|
||||||
|
<button class="btn btn-icon"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add click event to remove the row
|
||||||
|
tableRow.querySelector('.btn-icon').addEventListener('click', function() {
|
||||||
|
tableRow.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert before the add row
|
||||||
|
const addRow = document.querySelector('.terminology-row.add-row');
|
||||||
|
addRow.parentNode.insertBefore(tableRow, addRow);
|
||||||
|
|
||||||
|
avoidTerm.value = '';
|
||||||
|
useTerm.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow pressing Enter to add a term
|
||||||
|
useTerm.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
addTermBtn.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Brand Style
|
||||||
|
if (saveBrandStyleBtn) {
|
||||||
|
saveBrandStyleBtn.addEventListener('click', function() {
|
||||||
|
// Collect tone tags
|
||||||
|
const selectedTones = [];
|
||||||
|
toneSelector.querySelectorAll('.tag.selected').forEach(tag => {
|
||||||
|
selectedTones.push(tag.textContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collect voice characteristics
|
||||||
|
const selectedVoice = [];
|
||||||
|
voiceSelector.querySelectorAll('.tag.selected').forEach(tag => {
|
||||||
|
selectedVoice.push(tag.textContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collect taboo words
|
||||||
|
const tabooWordsList = [];
|
||||||
|
tabooWords.querySelectorAll('.tag').forEach(tag => {
|
||||||
|
// Extract just the text without the 'x' icon
|
||||||
|
const text = tag.textContent.replace('×', '').trim();
|
||||||
|
tabooWordsList.push(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collect preferred terms
|
||||||
|
const preferredTerms = {};
|
||||||
|
document.querySelectorAll('.terminology-row:not(.add-row):not(.terminology-header)').forEach(row => {
|
||||||
|
const avoid = row.querySelector('.term-avoid').textContent.trim();
|
||||||
|
const use = row.querySelector('.term-use').textContent.trim();
|
||||||
|
if (avoid && use) {
|
||||||
|
preferredTerms[avoid] = use;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepare request data
|
||||||
|
const requestData = {
|
||||||
|
tone: selectedTones,
|
||||||
|
voice_characteristics: selectedVoice,
|
||||||
|
taboo_words: tabooWordsList,
|
||||||
|
preferred_terms: preferredTerms
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the API
|
||||||
|
fetch(`${API_URL}/brand-style`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
alert('Brand style updated successfully!');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('An error occurred while updating brand style. Please try again.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset Brand Style
|
||||||
|
if (resetBrandStyleBtn) {
|
||||||
|
resetBrandStyleBtn.addEventListener('click', function() {
|
||||||
|
if (confirm('Are you sure you want to reset brand style to defaults?')) {
|
||||||
|
// In a real implementation, you would call an API to reset
|
||||||
|
// For now, just reload the page
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Training Tabs
|
||||||
|
if (trainingTabs.length > 0) {
|
||||||
|
trainingTabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', function() {
|
||||||
|
const tabName = this.getAttribute('data-tab');
|
||||||
|
|
||||||
|
// Update active tab
|
||||||
|
trainingTabs.forEach(t => t.classList.remove('active'));
|
||||||
|
this.classList.add('active');
|
||||||
|
|
||||||
|
// Show selected tab content
|
||||||
|
tabContents.forEach(content => {
|
||||||
|
if (content.id === `${tabName}-tab`) {
|
||||||
|
content.classList.add('active');
|
||||||
|
} else {
|
||||||
|
content.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Training Data
|
||||||
|
if (addTrainingBtn) {
|
||||||
|
addTrainingBtn.addEventListener('click', function() {
|
||||||
|
if (!trainingContentType.value) {
|
||||||
|
alert('Please select a content type.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trainingContent.value.trim()) {
|
||||||
|
alert('Please enter content for training.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare request data
|
||||||
|
const requestData = {
|
||||||
|
content_type: trainingContentType.value,
|
||||||
|
content: trainingContent.value,
|
||||||
|
metadata: {
|
||||||
|
campaign_name: campaignName.value,
|
||||||
|
performance_metrics: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add performance metrics if provided
|
||||||
|
if (openRate.value) {
|
||||||
|
requestData.metadata.performance_metrics.open_rate = parseFloat(openRate.value) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clickRate.value) {
|
||||||
|
requestData.metadata.performance_metrics.click_rate = parseFloat(clickRate.value) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversionRate.value) {
|
||||||
|
requestData.metadata.performance_metrics.conversion_rate = parseFloat(conversionRate.value) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the API
|
||||||
|
fetch(`${API_URL}/training-data`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
alert('Training data added successfully!');
|
||||||
|
|
||||||
|
// Clear form
|
||||||
|
trainingContentType.value = '';
|
||||||
|
campaignName.value = '';
|
||||||
|
trainingContent.value = '';
|
||||||
|
openRate.value = '';
|
||||||
|
clickRate.value = '';
|
||||||
|
conversionRate.value = '';
|
||||||
|
|
||||||
|
// Switch to view tab
|
||||||
|
document.querySelector('.tab[data-tab="view-training"]').click();
|
||||||
|
|
||||||
|
// In a real implementation, you would also refresh the training data list
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('An error occurred while adding training data. Please try again.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Brand Style on Page Load
|
||||||
|
fetch(`${API_URL}/brand-style`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('Loaded brand style:', data);
|
||||||
|
// In a real implementation, you would update the UI based on the loaded data
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading brand style:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For demonstration purposes, let's create a mocked pre-filled content
|
||||||
|
// In a real implementation, this would be loaded from the backend
|
||||||
|
document.getElementById('prompt').value = 'Write a social media post about our new coaching program';
|
||||||
|
});
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Adriana James - Marketing Assistant AI</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<h2>AJ</h2>
|
||||||
|
</div>
|
||||||
|
<ul class="menu">
|
||||||
|
<li class="active" data-page="generate"><i class="fas fa-magic"></i> Generate</li>
|
||||||
|
<li data-page="templates"><i class="fas fa-folder"></i> Templates</li>
|
||||||
|
<li data-page="history"><i class="fas fa-history"></i> History</li>
|
||||||
|
<li data-page="brand-style"><i class="fas fa-paint-brush"></i> Brand Style</li>
|
||||||
|
<li data-page="training"><i class="fas fa-graduation-cap"></i> Training</li>
|
||||||
|
</ul>
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<img src="https://via.placeholder.com/50" alt="User Avatar">
|
||||||
|
</div>
|
||||||
|
<div class="user-name">Marketing Team</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="content">
|
||||||
|
<header>
|
||||||
|
<h1>Marketing Assistant AI</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="btn btn-secondary"><i class="fas fa-cog"></i> Settings</button>
|
||||||
|
<button class="btn btn-primary">Upgrade</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Generate Content Page -->
|
||||||
|
<section id="generate-page" class="page active">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Generate Marketing Content</h2>
|
||||||
|
<p>Create high-quality marketing copy aligned with Adriana James' brand voice.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="generation-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="prompt">What would you like to create?</label>
|
||||||
|
<textarea id="prompt" placeholder="e.g., Write a social media post for our new coaching program launch" rows="4"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="content-type">Content Type</label>
|
||||||
|
<select id="content-type">
|
||||||
|
<option value="">Select Type</option>
|
||||||
|
<option value="email_campaign">Email Campaign</option>
|
||||||
|
<option value="social_media">Social Media</option>
|
||||||
|
<option value="blog_post">Blog Post</option>
|
||||||
|
<option value="website_copy">Website Copy</option>
|
||||||
|
<option value="ad_copy">Ad Copy</option>
|
||||||
|
<option value="funnel_page">Funnel Page</option>
|
||||||
|
<option value="product_description">Product Description</option>
|
||||||
|
<option value="press_release">Press Release</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tone">Tone</label>
|
||||||
|
<select id="tone">
|
||||||
|
<option value="">Select Tone</option>
|
||||||
|
<option value="professional">Professional</option>
|
||||||
|
<option value="friendly">Friendly</option>
|
||||||
|
<option value="excited">Excited</option>
|
||||||
|
<option value="authoritative">Authoritative</option>
|
||||||
|
<option value="casual">Casual</option>
|
||||||
|
<option value="inspirational">Inspirational</option>
|
||||||
|
<option value="empathetic">Empathetic</option>
|
||||||
|
<option value="humorous">Humorous</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="length">Length</label>
|
||||||
|
<select id="length">
|
||||||
|
<option value="">Select Length</option>
|
||||||
|
<option value="short">Short (< 100 words)</option>
|
||||||
|
<option value="medium">Medium (100-300 words)</option>
|
||||||
|
<option value="long">Long (> 300 words)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" id="include-cta" checked>
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
Include Call to Action
|
||||||
|
</label>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" id="reference-similar" checked>
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
Reference Similar Content
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button id="generate-btn" class="btn btn-primary btn-lg">
|
||||||
|
<i class="fas fa-magic"></i> Generate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="result-container" class="result-container hidden">
|
||||||
|
<div class="result-header">
|
||||||
|
<h3>Generated Content</h3>
|
||||||
|
<div class="result-actions">
|
||||||
|
<button id="copy-btn" class="btn btn-icon" title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
<button id="improve-btn" class="btn btn-icon" title="Improve content">
|
||||||
|
<i class="fas fa-wand-magic-sparkles"></i>
|
||||||
|
</button>
|
||||||
|
<button id="save-btn" class="btn btn-icon" title="Save to history">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="result-content" class="result-content"></div>
|
||||||
|
|
||||||
|
<div class="metadata-panel">
|
||||||
|
<div class="metadata-item">
|
||||||
|
<span class="metadata-label">Alignment Score</span>
|
||||||
|
<div class="score-bar">
|
||||||
|
<div id="alignment-score" class="score-fill" style="width: 0%;">0%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="suggestions-container" class="suggestions-container">
|
||||||
|
<h4>Headline Suggestions</h4>
|
||||||
|
<ul id="suggestions-list" class="suggestions-list"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="improvement-panel" class="improvement-panel hidden">
|
||||||
|
<h4>Improve This Content</h4>
|
||||||
|
<textarea id="improvement-feedback" placeholder="What would you like to improve about this content?" rows="3"></textarea>
|
||||||
|
<button id="submit-improvement" class="btn btn-secondary">Submit Feedback</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading-indicator" class="loading-indicator hidden">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Generating creative marketing content...</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Templates Page -->
|
||||||
|
<section id="templates-page" class="page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Content Templates</h2>
|
||||||
|
<p>Use pre-built templates for faster content creation.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="templates-grid">
|
||||||
|
<div class="template-card">
|
||||||
|
<div class="template-icon"><i class="fas fa-envelope"></i></div>
|
||||||
|
<h3>Email Welcome Sequence</h3>
|
||||||
|
<p>A 5-part email sequence for new subscribers.</p>
|
||||||
|
<button class="btn btn-outline">Use Template</button>
|
||||||
|
</div>
|
||||||
|
<div class="template-card">
|
||||||
|
<div class="template-icon"><i class="fas fa-share-alt"></i></div>
|
||||||
|
<h3>Product Launch Posts</h3>
|
||||||
|
<p>Social media templates for product launches.</p>
|
||||||
|
<button class="btn btn-outline">Use Template</button>
|
||||||
|
</div>
|
||||||
|
<div class="template-card">
|
||||||
|
<div class="template-icon"><i class="fas fa-newspaper"></i></div>
|
||||||
|
<h3>Transformation Story</h3>
|
||||||
|
<p>Client success story blog post template.</p>
|
||||||
|
<button class="btn btn-outline">Use Template</button>
|
||||||
|
</div>
|
||||||
|
<div class="template-card">
|
||||||
|
<div class="template-icon"><i class="fas fa-ad"></i></div>
|
||||||
|
<h3>Workshop Promotion</h3>
|
||||||
|
<p>Ad copy template for promoting workshops.</p>
|
||||||
|
<button class="btn btn-outline">Use Template</button>
|
||||||
|
</div>
|
||||||
|
<div class="template-card template-add">
|
||||||
|
<div class="template-icon"><i class="fas fa-plus"></i></div>
|
||||||
|
<h3>Create New Template</h3>
|
||||||
|
<p>Build a custom template from scratch.</p>
|
||||||
|
<button class="btn btn-outline">Create Template</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- History Page -->
|
||||||
|
<section id="history-page" class="page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Content History</h2>
|
||||||
|
<p>View and reuse your previously generated content.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-filters">
|
||||||
|
<div class="form-group">
|
||||||
|
<select id="history-filter-type">
|
||||||
|
<option value="">All Content Types</option>
|
||||||
|
<option value="email_campaign">Email Campaign</option>
|
||||||
|
<option value="social_media">Social Media</option>
|
||||||
|
<option value="blog_post">Blog Post</option>
|
||||||
|
<option value="website_copy">Website Copy</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" placeholder="Search history..." id="history-search">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-list">
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-item-type email_campaign">Email</div>
|
||||||
|
<div class="history-item-content">
|
||||||
|
<h4>Transformation Masterclass Invitation</h4>
|
||||||
|
<p>Subject: Transform Your Potential with Adriana James' Exclusive Workshop...</p>
|
||||||
|
</div>
|
||||||
|
<div class="history-item-date">Apr 17, 2025</div>
|
||||||
|
<div class="history-item-actions">
|
||||||
|
<button class="btn btn-icon" title="View content"><i class="fas fa-eye"></i></button>
|
||||||
|
<button class="btn btn-icon" title="Edit content"><i class="fas fa-edit"></i></button>
|
||||||
|
<button class="btn btn-icon" title="Delete content"><i class="fas fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-item-type social_media">Social</div>
|
||||||
|
<div class="history-item-content">
|
||||||
|
<h4>3-Step Framework Post</h4>
|
||||||
|
<p>BREAKTHROUGH MOMENT ✨ Ever feel stuck in patterns that hold you back...</p>
|
||||||
|
</div>
|
||||||
|
<div class="history-item-date">Apr 16, 2025</div>
|
||||||
|
<div class="history-item-actions">
|
||||||
|
<button class="btn btn-icon" title="View content"><i class="fas fa-eye"></i></button>
|
||||||
|
<button class="btn btn-icon" title="Edit content"><i class="fas fa-edit"></i></button>
|
||||||
|
<button class="btn btn-icon" title="Delete content"><i class="fas fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-item-type blog_post">Blog</div>
|
||||||
|
<div class="history-item-content">
|
||||||
|
<h4>5 Ways to Overcome Limiting Beliefs</h4>
|
||||||
|
<p>Are limiting beliefs holding you back from achieving your full potential?...</p>
|
||||||
|
</div>
|
||||||
|
<div class="history-item-date">Apr 14, 2025</div>
|
||||||
|
<div class="history-item-actions">
|
||||||
|
<button class="btn btn-icon" title="View content"><i class="fas fa-eye"></i></button>
|
||||||
|
<button class="btn btn-icon" title="Edit content"><i class="fas fa-edit"></i></button>
|
||||||
|
<button class="btn btn-icon" title="Delete content"><i class="fas fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Brand Style Page -->
|
||||||
|
<section id="brand-style-page" class="page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Brand Style Guidelines</h2>
|
||||||
|
<p>Customize the AI to match Adriana James' brand voice and tone.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brand-style-form">
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Brand Tone</h3>
|
||||||
|
<p>Select the tone options that best represent the brand.</p>
|
||||||
|
<div class="tag-selector" id="tone-selector">
|
||||||
|
<span class="tag selected">professional</span>
|
||||||
|
<span class="tag selected">friendly</span>
|
||||||
|
<span class="tag selected">inspirational</span>
|
||||||
|
<span class="tag selected">empowering</span>
|
||||||
|
<span class="tag">excited</span>
|
||||||
|
<span class="tag">authoritative</span>
|
||||||
|
<span class="tag">casual</span>
|
||||||
|
<span class="tag">humorous</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Voice Characteristics</h3>
|
||||||
|
<p>Define the key characteristics of the brand voice.</p>
|
||||||
|
<div class="tag-selector" id="voice-selector">
|
||||||
|
<span class="tag selected">clear</span>
|
||||||
|
<span class="tag selected">direct</span>
|
||||||
|
<span class="tag selected">empowering</span>
|
||||||
|
<span class="tag selected">confident</span>
|
||||||
|
<span class="tag selected">authentic</span>
|
||||||
|
<span class="tag">innovative</span>
|
||||||
|
<span class="tag">visionary</span>
|
||||||
|
<span class="tag">approachable</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Taboo Words</h3>
|
||||||
|
<p>Words to avoid in all marketing content.</p>
|
||||||
|
<div class="tag-editor">
|
||||||
|
<div class="tag-list" id="taboo-words">
|
||||||
|
<span class="tag removable">cheap<i class="fas fa-times"></i></span>
|
||||||
|
<span class="tag removable">discount<i class="fas fa-times"></i></span>
|
||||||
|
<span class="tag removable">bargain<i class="fas fa-times"></i></span>
|
||||||
|
<span class="tag removable">failure<i class="fas fa-times"></i></span>
|
||||||
|
<span class="tag removable">impossible<i class="fas fa-times"></i></span>
|
||||||
|
<span class="tag removable">difficult<i class="fas fa-times"></i></span>
|
||||||
|
</div>
|
||||||
|
<div class="tag-input-container">
|
||||||
|
<input type="text" id="taboo-input" placeholder="Add a word to avoid...">
|
||||||
|
<button class="btn btn-icon" id="add-taboo-btn"><i class="fas fa-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Preferred Terminology</h3>
|
||||||
|
<p>Preferred terms to use instead of common alternatives.</p>
|
||||||
|
<div class="terminology-table">
|
||||||
|
<div class="terminology-header">
|
||||||
|
<div class="term-avoid">Avoid</div>
|
||||||
|
<div class="term-use">Use Instead</div>
|
||||||
|
<div class="term-actions"></div>
|
||||||
|
</div>
|
||||||
|
<div class="terminology-row">
|
||||||
|
<div class="term-avoid">customers</div>
|
||||||
|
<div class="term-use">clients</div>
|
||||||
|
<div class="term-actions">
|
||||||
|
<button class="btn btn-icon"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="terminology-row">
|
||||||
|
<div class="term-avoid">products</div>
|
||||||
|
<div class="term-use">solutions</div>
|
||||||
|
<div class="term-actions">
|
||||||
|
<button class="btn btn-icon"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="terminology-row">
|
||||||
|
<div class="term-avoid">problems</div>
|
||||||
|
<div class="term-use">challenges</div>
|
||||||
|
<div class="term-actions">
|
||||||
|
<button class="btn btn-icon"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="terminology-row">
|
||||||
|
<div class="term-avoid">services</div>
|
||||||
|
<div class="term-use">experiences</div>
|
||||||
|
<div class="term-actions">
|
||||||
|
<button class="btn btn-icon"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="terminology-row">
|
||||||
|
<div class="term-avoid">training</div>
|
||||||
|
<div class="term-use">transformation</div>
|
||||||
|
<div class="term-actions">
|
||||||
|
<button class="btn btn-icon"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="terminology-row add-row">
|
||||||
|
<div class="term-avoid">
|
||||||
|
<input type="text" placeholder="Avoid" id="avoid-term">
|
||||||
|
</div>
|
||||||
|
<div class="term-use">
|
||||||
|
<input type="text" placeholder="Use Instead" id="use-term">
|
||||||
|
</div>
|
||||||
|
<div class="term-actions">
|
||||||
|
<button class="btn btn-icon" id="add-term-btn"><i class="fas fa-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button id="save-brand-style" class="btn btn-primary">Save Brand Style</button>
|
||||||
|
<button id="reset-brand-style" class="btn btn-outline">Reset to Defaults</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Training Page -->
|
||||||
|
<section id="training-page" class="page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Training Data</h2>
|
||||||
|
<p>Add examples of high-performing content to improve the AI.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="training-tabs">
|
||||||
|
<div class="tab active" data-tab="add-training">Add Training Data</div>
|
||||||
|
<div class="tab" data-tab="view-training">View Training Data</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="add-training-tab" class="tab-content active">
|
||||||
|
<div class="training-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="training-content-type">Content Type</label>
|
||||||
|
<select id="training-content-type">
|
||||||
|
<option value="">Select Type</option>
|
||||||
|
<option value="email_campaign">Email Campaign</option>
|
||||||
|
<option value="social_media">Social Media</option>
|
||||||
|
<option value="blog_post">Blog Post</option>
|
||||||
|
<option value="website_copy">Website Copy</option>
|
||||||
|
<option value="ad_copy">Ad Copy</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="campaign-name">Campaign Name</label>
|
||||||
|
<input type="text" id="campaign-name" placeholder="e.g., Spring Launch 2025">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="training-content">Content</label>
|
||||||
|
<textarea id="training-content" rows="8" placeholder="Paste your successful marketing content here..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Performance Metrics</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="open-rate">Open Rate (%)</label>
|
||||||
|
<input type="number" id="open-rate" min="0" max="100" step="0.1">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="click-rate">Click Rate (%)</label>
|
||||||
|
<input type="number" id="click-rate" min="0" max="100" step="0.1">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="conversion-rate">Conversion Rate (%)</label>
|
||||||
|
<input type="number" id="conversion-rate" min="0" max="100" step="0.1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button id="add-training-btn" class="btn btn-primary">Add Training Data</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="view-training-tab" class="tab-content">
|
||||||
|
<div class="training-filters">
|
||||||
|
<div class="form-group">
|
||||||
|
<select id="training-filter-type">
|
||||||
|
<option value="">All Content Types</option>
|
||||||
|
<option value="email_campaign">Email Campaign</option>
|
||||||
|
<option value="social_media">Social Media</option>
|
||||||
|
<option value="blog_post">Blog Post</option>
|
||||||
|
<option value="website_copy">Website Copy</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" placeholder="Search training data..." id="training-search">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="training-list">
|
||||||
|
<div class="training-item">
|
||||||
|
<div class="training-item-type email_campaign">Email</div>
|
||||||
|
<div class="training-item-content">
|
||||||
|
<h4>Transformation Masterclass Promotion</h4>
|
||||||
|
<p>Added on: Apr 15, 2025</p>
|
||||||
|
<div class="metrics">
|
||||||
|
<span class="metric">Open Rate: 42%</span>
|
||||||
|
<span class="metric">Click Rate: 18%</span>
|
||||||
|
<span class="metric">Conversion: 8%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="training-item-actions">
|
||||||
|
<button class="btn btn-icon" title="View content"><i class="fas fa-eye"></i></button>
|
||||||
|
<button class="btn btn-icon" title="Delete content"><i class="fas fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="training-item">
|
||||||
|
<div class="training-item-type social_media">Social</div>
|
||||||
|
<div class="training-item-content">
|
||||||
|
<h4>Breakthrough Framework</h4>
|
||||||
|
<p>Added on: Apr 10, 2025</p>
|
||||||
|
<div class="metrics">
|
||||||
|
<span class="metric">Engagement: 6.4%</span>
|
||||||
|
<span class="metric">Saves: 178</span>
|
||||||
|
<span class="metric">Shares: 92</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="training-item-actions">
|
||||||
|
<button class="btn btn-icon" title="View content"><i class="fas fa-eye"></i></button>
|
||||||
|
<button class="btn btn-icon" title="Delete content"><i class="fas fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,964 @@
|
|||||||
|
/* Global Variables */
|
||||||
|
:root {
|
||||||
|
--primary-color: #6236FF;
|
||||||
|
--primary-light: #8E6FFF;
|
||||||
|
--primary-dark: #4B2AD8;
|
||||||
|
--secondary-color: #FFB800;
|
||||||
|
--success-color: #1AC888;
|
||||||
|
--danger-color: #FF4757;
|
||||||
|
--warning-color: #FFBA00;
|
||||||
|
--dark-color: #161925;
|
||||||
|
--grey-100: #F5F7FA;
|
||||||
|
--grey-200: #E4E8F0;
|
||||||
|
--grey-300: #CBD2E0;
|
||||||
|
--grey-400: #9AA5B9;
|
||||||
|
--grey-500: #6B7A99;
|
||||||
|
--grey-600: #4A5568;
|
||||||
|
--grey-700: #2D3748;
|
||||||
|
--grey-800: #1A202C;
|
||||||
|
--radius-sm: 4px;
|
||||||
|
--radius-md: 8px;
|
||||||
|
--radius-lg: 12px;
|
||||||
|
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-lg: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
--font-family: 'Poppins', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset & Base Styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--grey-700);
|
||||||
|
background-color: var(--grey-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--grey-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background-color: var(--grey-800);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: white;
|
||||||
|
padding: 20px 0;
|
||||||
|
position: fixed;
|
||||||
|
height: 100vh;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 240px;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: calc(100vw - 240px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo */
|
||||||
|
.logo {
|
||||||
|
padding: 10px 20px 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h2 {
|
||||||
|
color: white;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menu */
|
||||||
|
.menu {
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu li {
|
||||||
|
padding: 12px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
color: var(--grey-300);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu li i {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu li:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu li.active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
border-left: 3px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Info */
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid var(--grey-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page */
|
||||||
|
.page {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p {
|
||||||
|
color: var(--grey-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: none;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: var(--grey-200);
|
||||||
|
color: var(--grey-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: var(--grey-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--grey-600);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
background-color: var(--grey-200);
|
||||||
|
color: var(--grey-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon i {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--grey-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="number"],
|
||||||
|
.form-group input[type="email"],
|
||||||
|
.form-group input[type="password"],
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--grey-300);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 15px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(98, 54, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row .form-group {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--grey-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: normal;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid var(--grey-300);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:hover input ~ .checkmark {
|
||||||
|
border-color: var(--grey-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox input:checked ~ .checkmark {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox input:checked ~ .checkmark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox .checkmark:after {
|
||||||
|
left: 7px;
|
||||||
|
top: 3px;
|
||||||
|
width: 5px;
|
||||||
|
height: 10px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generation Form */
|
||||||
|
.generation-form {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Result Container */
|
||||||
|
.result-container {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-container.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
padding: 20px 25px;
|
||||||
|
border-bottom: 1px solid var(--grey-200);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header h3 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
padding: 25px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-panel {
|
||||||
|
background-color: var(--grey-100);
|
||||||
|
padding: 20px 25px;
|
||||||
|
border-top: 1px solid var(--grey-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-item {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: var(--grey-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-bar {
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--grey-200);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--success-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-container h4 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--grey-600);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-list li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--grey-200);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-list li:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background-color: rgba(98, 54, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.improvement-panel {
|
||||||
|
padding: 20px 25px;
|
||||||
|
border-top: 1px solid var(--grey-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.improvement-panel.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.improvement-panel h4 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Indicator */
|
||||||
|
.loading-indicator {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid var(--grey-200);
|
||||||
|
border-top: 4px solid var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Templates Grid */
|
||||||
|
.templates-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card h3 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card p {
|
||||||
|
color: var(--grey-500);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card button {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card.template-add {
|
||||||
|
border: 2px dashed var(--grey-300);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card.template-add .template-icon {
|
||||||
|
background-color: var(--grey-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card.template-add:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* History and Training List */
|
||||||
|
.history-filters,
|
||||||
|
.training-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-filters .form-group,
|
||||||
|
.training-filters .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list,
|
||||||
|
.training-list {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item,
|
||||||
|
.training-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid var(--grey-200);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item:last-child,
|
||||||
|
.training-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-type,
|
||||||
|
.training-item-type {
|
||||||
|
width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-type.email_campaign,
|
||||||
|
.training-item-type.email_campaign {
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-type.social_media,
|
||||||
|
.training-item-type.social_media {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-type.blog_post,
|
||||||
|
.training-item-type.blog_post {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-type.website_copy,
|
||||||
|
.training-item-type.website_copy {
|
||||||
|
background-color: var(--warning-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-content,
|
||||||
|
.training-item-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-content h4,
|
||||||
|
.training-item-content h4 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-content p,
|
||||||
|
.training-item-content p {
|
||||||
|
color: var(--grey-500);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-date {
|
||||||
|
color: var(--grey-500);
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 100px;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-actions,
|
||||||
|
.training-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--grey-600);
|
||||||
|
background-color: var(--grey-100);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brand Style */
|
||||||
|
.tag-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: var(--grey-200);
|
||||||
|
color: var(--grey-700);
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag.selected {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag.removable {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag.removable i {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-editor {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input-container input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminology-table {
|
||||||
|
margin-top: 15px;
|
||||||
|
border: 1px solid var(--grey-200);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminology-header {
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--grey-100);
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid var(--grey-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminology-row {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid var(--grey-200);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminology-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-avoid {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-use {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-actions {
|
||||||
|
width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminology-row.add-row input {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
padding: 5px 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminology-row.add-row input:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Training Tabs */
|
||||||
|
.training-tabs {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid var(--grey-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 12px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
color: var(--grey-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 80px;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h2 {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu li {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu li i {
|
||||||
|
margin-right: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu li span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-left: 80px;
|
||||||
|
max-width: calc(100vw - 80px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-row {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.templates-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item,
|
||||||
|
.training-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-type,
|
||||||
|
.training-item-type {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-content,
|
||||||
|
.training-item-content {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-date {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
+460
@@ -0,0 +1,460 @@
|
|||||||
|
2025-04-17 07:08:51.543 | WARNING | vector_store:search:159 - Empty vector store, no results to return
|
||||||
|
2025-04-17 07:08:51.543 | WARNING | vector_store:search:159 - Empty vector store, no results to return
|
||||||
|
2025-04-17 07:08:51.551 | INFO | copywriter:generate_copy:118 - Generated content with 159 characters
|
||||||
|
2025-04-17 07:08:51.551 | INFO | copywriter:generate_copy:118 - Generated content with 159 characters
|
||||||
|
2025-04-17 07:08:52.803 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:08:52.803 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:12:18.934 | INFO | vector_store:search:212 - Found 1 matching documents for query
|
||||||
|
2025-04-17 07:12:18.934 | INFO | vector_store:search:212 - Found 1 matching documents for query
|
||||||
|
2025-04-17 07:12:18.936 | INFO | copywriter:generate_copy:118 - Generated content with 159 characters
|
||||||
|
2025-04-17 07:12:18.936 | INFO | copywriter:generate_copy:118 - Generated content with 159 characters
|
||||||
|
2025-04-17 07:12:19.677 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:12:19.677 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:15:03.309 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:15:03.309 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:15:05.643 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:05.643 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:05.644 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:05.644 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:10.452 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:10.452 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:10.455 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:10.455 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:15.166 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:15.166 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:15.168 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:15.168 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:15.170 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x114e4da60 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:15:15.170 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x114e4da60 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:15:20.280 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:15:20.280 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:15:21.317 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:21.317 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:21.369 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:21.369 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:26.051 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:26.051 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:26.052 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:26.052 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:30.842 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:30.842 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:30.847 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:30.847 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:30.859 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x114d3b440 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:15:30.859 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x114d3b440 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:15:36.115 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:15:36.115 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:15:36.882 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:36.882 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:36.885 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:36.885 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:41.549 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:41.549 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:41.551 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:41.551 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:46.258 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:46.258 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 404, {
|
||||||
|
"error": {
|
||||||
|
"message": "The model `gpt-4` does not exist or you do not have access to it.",
|
||||||
|
"type": "invalid_request_error",
|
||||||
|
"param": null,
|
||||||
|
"code": "model_not_found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:15:46.266 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:46.266 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 404
|
||||||
|
2025-04-17 07:15:46.269 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x114b5a690 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:15:46.269 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x114b5a690 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:15:46.274 | ERROR | main:generate_copy:157 - Error generating copy: RetryError[<Future at 0x114de2630 state=finished raised RetryError>]
|
||||||
|
2025-04-17 07:15:46.274 | ERROR | main:generate_copy:157 - Error generating copy: RetryError[<Future at 0x114de2630 state=finished raised RetryError>]
|
||||||
|
2025-04-17 07:18:54.993 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:18:54.993 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:18:57.991 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:18:57.991 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:18:57.993 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:18:57.993 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:02.717 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:02.717 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:02.719 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:02.719 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:07.525 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:07.525 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:07.526 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:07.526 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:07.527 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x11d4918b0 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:19:07.527 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x11d4918b0 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:19:12.302 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:19:12.302 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:19:13.063 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:13.063 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:13.064 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:13.064 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:21.192 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:21.192 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:21.199 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:21.199 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:26.353 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:26.353 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:26.360 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:26.360 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:26.364 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x11d44aff0 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:19:26.364 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x11d44aff0 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:19:31.480 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:19:31.480 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:19:32.593 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:32.593 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:32.597 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:32.597 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:37.418 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:37.418 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:37.425 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:37.425 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:42.179 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:42.179 | ERROR | copywriter:_call_llm_api:161 - OpenAI API error: 429, {
|
||||||
|
"error": {
|
||||||
|
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
|
||||||
|
"type": "insufficient_quota",
|
||||||
|
"param": null,
|
||||||
|
"code": "insufficient_quota"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
2025-04-17 07:19:42.180 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:42.180 | ERROR | copywriter:_call_llm_api:165 - Error calling OpenAI API: OpenAI API error: 429
|
||||||
|
2025-04-17 07:19:42.181 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x11d4aa0f0 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:19:42.181 | ERROR | copywriter:generate_copy:122 - Error generating copy: RetryError[<Future at 0x11d4aa0f0 state=finished raised Exception>]
|
||||||
|
2025-04-17 07:19:42.182 | ERROR | main:generate_copy:157 - Error generating copy: RetryError[<Future at 0x11d426360 state=finished raised RetryError>]
|
||||||
|
2025-04-17 07:19:42.182 | ERROR | main:generate_copy:157 - Error generating copy: RetryError[<Future at 0x11d426360 state=finished raised RetryError>]
|
||||||
|
2025-04-17 07:23:26.426 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:23:26.426 | INFO | vector_store:search:212 - Found 2 matching documents for query
|
||||||
|
2025-04-17 07:23:41.296 | INFO | copywriter:generate_copy:118 - Generated content with 1092 characters
|
||||||
|
2025-04-17 07:23:41.296 | INFO | copywriter:generate_copy:118 - Generated content with 1092 characters
|
||||||
|
2025-04-17 07:23:41.800 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:23:41.800 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:24:54.053 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 07:24:54.053 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 07:25:04.622 | INFO | copywriter:generate_copy:118 - Generated content with 1528 characters
|
||||||
|
2025-04-17 07:25:04.622 | INFO | copywriter:generate_copy:118 - Generated content with 1528 characters
|
||||||
|
2025-04-17 07:25:05.154 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:25:05.154 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:36:21.399 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 07:36:21.399 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 07:36:38.021 | INFO | copywriter:generate_copy:118 - Generated content with 1506 characters
|
||||||
|
2025-04-17 07:36:38.021 | INFO | copywriter:generate_copy:118 - Generated content with 1506 characters
|
||||||
|
2025-04-17 07:36:38.691 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:36:38.691 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:52:38.745 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 07:52:38.745 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 07:52:43.989 | INFO | copywriter:generate_copy:118 - Generated content with 735 characters
|
||||||
|
2025-04-17 07:52:43.989 | INFO | copywriter:generate_copy:118 - Generated content with 735 characters
|
||||||
|
2025-04-17 07:52:44.389 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:52:44.389 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:53:48.816 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines
|
||||||
|
2025-04-17 07:53:48.816 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines
|
||||||
|
2025-04-17 07:53:53.715 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines
|
||||||
|
2025-04-17 07:53:53.715 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines
|
||||||
|
2025-04-17 07:57:41.845 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 07:57:41.845 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 07:57:49.623 | INFO | copywriter:generate_copy:118 - Generated content with 1037 characters
|
||||||
|
2025-04-17 07:57:49.623 | INFO | copywriter:generate_copy:118 - Generated content with 1037 characters
|
||||||
|
2025-04-17 07:57:49.997 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:57:49.997 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:58:37.795 | INFO | copywriter:generate_copy:118 - Generated content with 1229 characters
|
||||||
|
2025-04-17 07:58:37.795 | INFO | copywriter:generate_copy:118 - Generated content with 1229 characters
|
||||||
|
2025-04-17 07:58:38.334 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 07:58:38.334 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 08:00:19.501 | INFO | copywriter:improve_copy:221 - Improved content based on feedback
|
||||||
|
2025-04-17 08:00:19.501 | INFO | copywriter:improve_copy:221 - Improved content based on feedback
|
||||||
|
2025-04-17 08:02:10.367 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines
|
||||||
|
2025-04-17 08:02:10.367 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines
|
||||||
|
2025-04-17 08:03:00.533 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 08:03:00.533 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 08:03:15.382 | INFO | copywriter:generate_copy:118 - Generated content with 2057 characters
|
||||||
|
2025-04-17 08:03:15.382 | INFO | copywriter:generate_copy:118 - Generated content with 2057 characters
|
||||||
|
2025-04-17 08:03:15.964 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 08:03:15.964 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 08:04:49.387 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 08:04:49.387 | INFO | vector_store:search:212 - Found 3 matching documents for query
|
||||||
|
2025-04-17 08:05:19.792 | ERROR | copywriter:_call_llm_api:167 - Error calling Cohere API:
|
||||||
|
2025-04-17 08:05:19.792 | ERROR | copywriter:_call_llm_api:167 - Error calling Cohere API:
|
||||||
|
2025-04-17 08:05:33.019 | INFO | copywriter:generate_copy:118 - Generated content with 938 characters
|
||||||
|
2025-04-17 08:05:33.019 | INFO | copywriter:generate_copy:118 - Generated content with 938 characters
|
||||||
|
2025-04-17 08:05:33.540 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 08:05:33.540 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 08:08:22.724 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines
|
||||||
|
2025-04-17 08:08:22.724 | INFO | brand_style:update_style_guidelines:80 - Updated brand style guidelines
|
||||||
|
2025-04-17 08:10:36.577 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
|
2025-04-17 08:10:36.577 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store
|
||||||
Reference in New Issue
Block a user