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:
Michael Ikehi
2025-04-17 08:50:12 +01:00
commit 6fd7213076
21 changed files with 4497 additions and 0 deletions
+44
View File
@@ -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
+21
View File
@@ -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.
+247
View File
@@ -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
+175
View File
@@ -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()
+87
View File
@@ -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)
+278
View File
@@ -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.
+138
View File
@@ -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
View File
@@ -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
)
+15
View File
@@ -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
+347
View File
@@ -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()
+81
View File
@@ -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
View File
+36
View 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"
}
}
View File
+187
View File
@@ -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
View File
@@ -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';
});
+498
View File
@@ -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>
+964
View File
@@ -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
View File
@@ -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