diff --git a/lib/ai_writers/ai_blog_rewriter.py b/lib/ai_writers/ai_blog_rewriter.py
index 683fcf70..48faf139 100644
--- a/lib/ai_writers/ai_blog_rewriter.py
+++ b/lib/ai_writers/ai_blog_rewriter.py
@@ -1,100 +1,1139 @@
-"""AI-powered blog rewriter tool."""
+"""
+AI Blog Rewriter Module
+
+This module provides functionality to rewrite and update existing blog content
+with improved quality, factual accuracy, and SEO optimization.
+"""
import streamlit as st
-from bs4 import BeautifulSoup
import requests
-from transformers import pipeline
+from bs4 import BeautifulSoup
+import re
import time
-from exa_py import Exa
+import logging
+from typing import Dict, List, Tuple, Optional, Any
+import json
+import os
+from datetime import datetime
-# Load the LLM
-generator = pipeline('text-generation', model='gpt-3') # Example, adjust based on your model
+# Import required modules from the project
+from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
+from ..gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
+from ..web_research.exa_search import exa_search
+from ..web_research.tavily_search import tavily_search
-def main():
- st.markdown("
AI Blog Content Refresher
", unsafe_allow_html=True)
- st.markdown("Keep your blog fresh and engaging with AI!
", unsafe_allow_html=True)
+# Configure logging
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
- # User Inputs
- with st.form("content_refresh_form"):
- url = st.text_input("Enter Blog Post URL", placeholder="https://www.example.com/blog-post")
- keywords = st.text_area("Enter Relevant Keywords", placeholder="Example: 'SEO best practices', 'digital marketing tips'")
- tone = st.selectbox("Choose Desired Tone", ["Formal", "Informal", "Engaging", "Informative"])
- target_audience = st.text_input("Target Audience", placeholder="e.g., tech enthusiasts, business owners")
- desired_length = st.slider("Desired Content Length (words)", min_value=300, max_value=1500, value=600, step=100)
+# Define constants
+MAX_TITLE_LENGTH = 70
+MAX_META_DESCRIPTION_LENGTH = 160
+REWRITE_MODES = {
+ "standard": "Standard rewrite with improved clarity and flow",
+ "seo_optimization": "Optimize for search engines with targeted keywords",
+ "simplification": "Simplify complex content for broader audience",
+ "expansion": "Expand with additional details and examples",
+ "fact_check": "Focus on fact-checking and updating information",
+ "tone_shift": "Change the tone while preserving content",
+ "modernization": "Update outdated content with current information"
+}
- submitted = st.form_submit_button("Refresh Content")
+# Define tone options
+TONE_OPTIONS = [
+ "Professional", "Conversational", "Academic", "Enthusiastic",
+ "Authoritative", "Friendly", "Technical", "Inspirational"
+]
- if submitted:
- st.markdown("Content Refresh for: "+url+"
", unsafe_allow_html=True)
- st.info(f"Refreshing your blog post...")
-
- # Fetch the existing content
- website_data = collect_website_data(url)
-
- # Get additional context from web research (using Metaphor API)
- web_research_context = get_web_research_context(keywords)
-
- # Generate the updated content
- updated_content = generate_updated_content(
- website_data, keywords, tone, target_audience, desired_length, web_research_context
+class BlogRewriter:
+ """Class to handle blog rewriting functionality."""
+
+ def __init__(self):
+ """Initialize the BlogRewriter class."""
+ self.original_content = {}
+ self.rewritten_content = {}
+ self.research_results = {}
+ self.content_analysis = {}
+ self.image_suggestions = []
+
+ def extract_content_from_url(self, url: str) -> Dict[str, Any]:
+ """
+ Extract content from a given URL.
+
+ Args:
+ url: The URL to extract content from
+
+ Returns:
+ Dictionary containing extracted content
+ """
+ logger.info(f"Extracting content from URL: {url}")
+
+ try:
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+ }
+ response = requests.get(url, headers=headers, timeout=15)
+ response.raise_for_status()
+
+ soup = BeautifulSoup(response.text, 'html.parser')
+
+ # Extract title
+ title = soup.title.string if soup.title else ""
+
+ # Extract meta description
+ meta_desc = ""
+ meta_tag = soup.find("meta", attrs={"name": "description"})
+ if meta_tag and "content" in meta_tag.attrs:
+ meta_desc = meta_tag["content"]
+
+ # Extract main content - this is a simplified approach
+ # In a real implementation, you'd want more sophisticated content extraction
+ content = ""
+ article_tag = soup.find("article")
+ if article_tag:
+ content = article_tag.get_text(separator="\\n\\n")
+ else:
+ # Try to find main content by looking for common content containers
+ main_content = soup.find(["main", "div", "section"], class_=re.compile(r"content|article|post|entry"))
+ if main_content:
+ # Remove navigation, sidebars, comments, etc.
+ for elem in main_content.find_all(["nav", "aside", "footer", "comments", "script", "style"]):
+ elem.decompose()
+ content = main_content.get_text(separator="\\n\\n")
+ else:
+ # Fallback to body content
+ body = soup.find("body")
+ if body:
+ content = body.get_text(separator="\\n\\n")
+
+ # Clean up the content
+ content = re.sub(r'\\n{3,}', '\\n\\n', content) # Remove excessive newlines
+ content = re.sub(r'\s{2,}', ' ', content) # Remove excessive spaces
+
+ # Extract headings for structure analysis
+ headings = []
+ for h in soup.find_all(["h1", "h2", "h3", "h4", "h5", "h6"]):
+ headings.append({
+ "level": int(h.name[1]),
+ "text": h.get_text().strip()
+ })
+
+ # Extract images
+ images = []
+ for img in soup.find_all("img"):
+ if img.get("src") and not img.get("src").startswith("data:"):
+ image_url = img.get("src")
+ if not image_url.startswith(("http://", "https://")):
+ # Convert relative URL to absolute
+ base_url = "/".join(url.split("/")[:3]) # Get domain
+ image_url = f"{base_url}/{image_url.lstrip('/')}"
+
+ alt_text = img.get("alt", "")
+ images.append({
+ "url": image_url,
+ "alt_text": alt_text
+ })
+
+ # Extract publish date if available
+ publish_date = None
+ date_meta = soup.find("meta", attrs={"property": "article:published_time"})
+ if date_meta and "content" in date_meta.attrs:
+ publish_date = date_meta["content"]
+ else:
+ # Try common date patterns in the HTML
+ date_elem = soup.find(["time", "span", "div"], class_=re.compile(r"date|time|publish"))
+ if date_elem and date_elem.get_text():
+ publish_date = date_elem.get_text().strip()
+
+ # Extract author if available
+ author = None
+ author_meta = soup.find("meta", attrs={"name": "author"})
+ if author_meta and "content" in author_meta.attrs:
+ author = author_meta["content"]
+ else:
+ # Try common author patterns in the HTML
+ author_elem = soup.find(["a", "span", "div"], class_=re.compile(r"author|byline"))
+ if author_elem and author_elem.get_text():
+ author = author_elem.get_text().strip()
+
+ return {
+ "title": title,
+ "meta_description": meta_desc,
+ "content": content,
+ "headings": headings,
+ "images": images,
+ "publish_date": publish_date,
+ "author": author,
+ "url": url
+ }
+
+ except Exception as e:
+ logger.error(f"Error extracting content from URL: {e}")
+ return {
+ "title": "",
+ "meta_description": "",
+ "content": "",
+ "headings": [],
+ "images": [],
+ "publish_date": None,
+ "author": None,
+ "url": url,
+ "error": str(e)
+ }
+
+ def analyze_content(self, content: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ Analyze the extracted content to provide insights.
+
+ Args:
+ content: Dictionary containing extracted content
+
+ Returns:
+ Dictionary containing content analysis
+ """
+ logger.info("Analyzing content")
+
+ analysis = {}
+
+ # Basic metrics
+ text_content = content.get("content", "")
+ word_count = len(text_content.split())
+ sentence_count = len(re.split(r'[.!?]+', text_content))
+ paragraph_count = len(re.split(r'\\n\\n+', text_content))
+
+ analysis["metrics"] = {
+ "word_count": word_count,
+ "sentence_count": sentence_count,
+ "paragraph_count": paragraph_count,
+ "avg_words_per_sentence": round(word_count / max(sentence_count, 1), 1),
+ "avg_sentences_per_paragraph": round(sentence_count / max(paragraph_count, 1), 1)
+ }
+
+ # Heading structure analysis
+ headings = content.get("headings", [])
+ heading_structure = {}
+ for h in headings:
+ level = h["level"]
+ if level not in heading_structure:
+ heading_structure[level] = 0
+ heading_structure[level] += 1
+
+ analysis["heading_structure"] = heading_structure
+
+ # Content age analysis
+ publish_date = content.get("publish_date")
+ if publish_date:
+ try:
+ # Try to parse the date in various formats
+ if "T" in publish_date:
+ # ISO format
+ pub_date = datetime.fromisoformat(publish_date.replace("Z", "+00:00"))
+ else:
+ # Try common date formats
+ date_formats = [
+ "%Y-%m-%d", "%d-%m-%Y", "%B %d, %Y", "%b %d, %Y",
+ "%d %B %Y", "%d %b %Y", "%Y/%m/%d", "%d/%m/%Y"
+ ]
+ for fmt in date_formats:
+ try:
+ pub_date = datetime.strptime(publish_date, fmt)
+ break
+ except ValueError:
+ continue
+
+ # Calculate content age
+ now = datetime.now()
+ age_days = (now - pub_date).days
+ analysis["content_age"] = {
+ "days": age_days,
+ "months": round(age_days / 30, 1),
+ "years": round(age_days / 365, 1)
+ }
+ except Exception as e:
+ logger.warning(f"Could not parse publish date: {e}")
+ analysis["content_age"] = {"error": "Could not determine content age"}
+ else:
+ analysis["content_age"] = {"error": "No publish date found"}
+
+ # Image analysis
+ images = content.get("images", [])
+ analysis["images"] = {
+ "count": len(images),
+ "with_alt_text": sum(1 for img in images if img.get("alt_text"))
+ }
+
+ return analysis
+
+ def conduct_research(self, title: str, content: str, research_depth: str = "medium") -> Dict[str, Any]:
+ """
+ Conduct web research to find updated information related to the blog content.
+
+ Args:
+ title: Blog title
+ content: Blog content
+ research_depth: Depth of research (low, medium, high)
+
+ Returns:
+ Dictionary containing research results
+ """
+ logger.info(f"Conducting research with depth: {research_depth}")
+
+ # Extract key topics from the content
+ prompt = f"""
+ Extract 3-5 key topics or claims from this blog content that might need fact-checking or updating.
+ For each topic, provide a concise search query that would help find the most recent information.
+
+ Blog title: {title}
+
+ First 1000 characters of content:
+ {content[:1000]}...
+
+ Format your response as a JSON array of objects with 'topic' and 'query' fields.
+ """
+
+ try:
+ topics_json = llm_text_gen(prompt)
+ # Extract JSON from the response
+ topics_json = re.search(r'\[.*\]', topics_json, re.DOTALL)
+ if topics_json:
+ topics = json.loads(topics_json.group(0))
+ else:
+ # Fallback if JSON extraction fails
+ topics = [
+ {"topic": title, "query": title + " latest information"},
+ {"topic": "Updates on " + title, "query": title + " recent developments"}
+ ]
+ except Exception as e:
+ logger.error(f"Error extracting topics: {e}")
+ topics = [
+ {"topic": title, "query": title + " latest information"},
+ {"topic": "Updates on " + title, "query": title + " recent developments"}
+ ]
+
+ # Determine number of results based on research depth
+ num_results = {"low": 2, "medium": 3, "high": 5}.get(research_depth, 3)
+
+ research_results = {"topics": []}
+
+ # Conduct research for each topic
+ for topic in topics[:3]: # Limit to 3 topics to avoid excessive API calls
+ topic_results = {"topic": topic["topic"], "sources": []}
+
+ # Try Exa search first
+ try:
+ exa_results = exa_search(topic["query"], num_results=num_results)
+ if exa_results:
+ topic_results["sources"].extend(exa_results)
+ except Exception as e:
+ logger.warning(f"Exa search failed: {e}")
+
+ # If Exa didn't return enough results, try Tavily
+ if len(topic_results["sources"]) < num_results:
+ try:
+ tavily_results = tavily_search(topic["query"], num_results=num_results)
+ if tavily_results:
+ # Avoid duplicates
+ existing_urls = [s["url"] for s in topic_results["sources"]]
+ for result in tavily_results:
+ if result["url"] not in existing_urls:
+ topic_results["sources"].append(result)
+ existing_urls.append(result["url"])
+ except Exception as e:
+ logger.warning(f"Tavily search failed: {e}")
+
+ research_results["topics"].append(topic_results)
+
+ return research_results
+
+ def generate_rewrite_prompt(self, original_content: Dict[str, Any],
+ user_preferences: Dict[str, Any],
+ research_results: Dict[str, Any],
+ content_analysis: Dict[str, Any]) -> str:
+ """
+ Generate a prompt for the LLM to rewrite the blog.
+
+ Args:
+ original_content: Original blog content
+ user_preferences: User preferences for rewriting
+ research_results: Research results for updating content
+ content_analysis: Analysis of the original content
+
+ Returns:
+ Prompt string for the LLM
+ """
+ logger.info("Generating rewrite prompt")
+
+ # Extract key information
+ title = original_content.get("title", "")
+ content = original_content.get("content", "")
+
+ # Truncate content if it's too long
+ max_content_length = 6000 # Adjust based on your LLM's context window
+ if len(content) > max_content_length:
+ content_preview = content[:max_content_length] + "...\\n[Content truncated due to length]"
+ else:
+ content_preview = content
+
+ # Format research results
+ research_summary = ""
+ for topic in research_results.get("topics", []):
+ research_summary += f"\\n## {topic['topic']}\\n"
+ for i, source in enumerate(topic.get("sources", [])[:3]): # Limit to 3 sources per topic
+ research_summary += f"Source {i+1}: {source.get('title', 'Untitled')}\\n"
+ research_summary += f"URL: {source.get('url', 'No URL')}\\n"
+ research_summary += f"Content: {source.get('content', 'No content')[:300]}...\\n\\n"
+
+ # Build the prompt
+ prompt = f"""
+ # Blog Rewriting Task
+
+ ## Original Blog Information
+ Title: {title}
+ Word Count: {content_analysis.get('metrics', {}).get('word_count', 'Unknown')}
+ Estimated Age: {content_analysis.get('content_age', {}).get('months', 'Unknown')} months
+
+ ## Rewriting Instructions
+ Mode: {user_preferences.get('rewrite_mode', 'standard')}
+ Target Tone: {user_preferences.get('tone', 'Professional')}
+ Target Word Count: {user_preferences.get('target_word_count', 'Same as original')}
+ Focus Keywords: {', '.join(user_preferences.get('keywords', []))}
+
+ ## Special Instructions
+ {user_preferences.get('special_instructions', 'No special instructions')}
+
+ ## Recent Research Findings
+ {research_summary if research_summary else "No research results available."}
+
+ ## Original Content
+ {content_preview}
+
+ ## Your Task
+ Please rewrite this blog post according to the instructions above. The rewritten blog should:
+
+ 1. Maintain the core message and value of the original content
+ 2. Update any outdated information based on the research findings
+ 3. Adopt the requested tone and style
+ 4. Incorporate the focus keywords naturally
+ 5. Improve readability and engagement
+ 6. Maintain a logical structure with appropriate headings
+ 7. Include a compelling introduction and conclusion
+
+ ## Output Format
+ Please provide your response in the following JSON format:
+ ```json
+ {{
+ "title": "Rewritten title",
+ "meta_description": "SEO-optimized meta description (max 160 characters)",
+ "content": "Full rewritten content with proper markdown formatting",
+ "suggested_images": [
+ {{
+ "description": "Brief description of a suggested image",
+ "caption": "Suggested caption for the image",
+ "placement": "Where this image should be placed (e.g., 'After introduction', 'Before conclusion')"
+ }}
+ ]
+ }}
+ ```
+
+ Ensure the JSON is properly formatted and valid.
+ """
+
+ return prompt
+
+ def rewrite_blog(self, original_content: Dict[str, Any],
+ user_preferences: Dict[str, Any],
+ research_results: Dict[str, Any],
+ content_analysis: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ Rewrite the blog based on original content, user preferences, and research.
+
+ Args:
+ original_content: Original blog content
+ user_preferences: User preferences for rewriting
+ research_results: Research results for updating content
+ content_analysis: Analysis of the original content
+
+ Returns:
+ Dictionary containing rewritten content
+ """
+ logger.info("Rewriting blog content")
+
+ # Generate the prompt
+ prompt = self.generate_rewrite_prompt(
+ original_content, user_preferences, research_results, content_analysis
)
+
+ # Call the LLM to rewrite the content
+ try:
+ response = llm_text_gen(prompt)
+
+ # Extract JSON from the response
+ json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL)
+ if json_match:
+ json_str = json_match.group(1)
+ else:
+ json_str = response
+
+ # Clean up the JSON string
+ json_str = re.sub(r'```(json)?', '', json_str).strip()
+
+ # Parse the JSON
+ rewritten_content = json.loads(json_str)
+
+ # Validate the response structure
+ required_fields = ["title", "meta_description", "content"]
+ for field in required_fields:
+ if field not in rewritten_content:
+ rewritten_content[field] = original_content.get(field, "")
+
+ # Ensure suggested_images exists
+ if "suggested_images" not in rewritten_content:
+ rewritten_content["suggested_images"] = []
+
+ return rewritten_content
+
+ except Exception as e:
+ logger.error(f"Error rewriting blog: {e}")
+ return {
+ "title": original_content.get("title", ""),
+ "meta_description": original_content.get("meta_description", ""),
+ "content": original_content.get("content", ""),
+ "suggested_images": [],
+ "error": str(e)
+ }
+
+ def generate_image(self, image_prompt: str, style: str = "realistic") -> str:
+ """
+ Generate an image based on the prompt.
+
+ Args:
+ image_prompt: Prompt for image generation
+ style: Style of the image
+
+ Returns:
+ Path to the generated image
+ """
+ logger.info(f"Generating image with prompt: {image_prompt}")
+
+ try:
+ image_path = generate_image(image_prompt, style=style)
+ return image_path
+ except Exception as e:
+ logger.error(f"Error generating image: {e}")
+ return ""
- # Display Results
- st.subheader("Updated Blog Content")
- st.write(updated_content)
-def collect_website_data(url):
- # ... (Your web scraping function remains the same)
-
-def get_web_research_context(keywords):
- """Fetches web research context using Metaphor API."""
- METAPHOR_API_KEY = os.getenv('METAPHOR_API_KEY')
- if not METAPHOR_API_KEY:
- st.error("METAPHOR_API_KEY environment variable not set!")
- return None
-
- metaphor = Exa(METAPHOR_API_KEY)
- try:
- search_response = metaphor.search_and_contents(
- keywords,
- use_autoprompt=True,
- num_results=5
+def write_blog_rewriter():
+ """Main function to display the blog rewriter UI."""
+ st.title("AI Blog Rewriter & Updater")
+
+ # Create a container for the header section
+ with st.container():
+ st.markdown("""
+
+
Revitalize Your Content
+
Update, fact-check, and enhance your existing blog posts with AI assistance.
+ Our tool analyzes your content, researches the latest information, and rewrites your blog
+ to be more engaging, accurate, and SEO-friendly.
+
+ """, unsafe_allow_html=True)
+
+ # Initialize the BlogRewriter class
+ if "blog_rewriter" not in st.session_state:
+ st.session_state.blog_rewriter = BlogRewriter()
+
+ # Initialize session state variables
+ if "original_content" not in st.session_state:
+ st.session_state.original_content = {}
+ if "content_analysis" not in st.session_state:
+ st.session_state.content_analysis = {}
+ if "research_results" not in st.session_state:
+ st.session_state.research_results = {}
+ if "rewritten_content" not in st.session_state:
+ st.session_state.rewritten_content = {}
+ if "generated_images" not in st.session_state:
+ st.session_state.generated_images = {}
+ if "current_step" not in st.session_state:
+ st.session_state.current_step = 1
+
+ # Create tabs for the workflow
+ tab1, tab2, tab3, tab4 = st.tabs([
+ "1️⃣ Import Content",
+ "2️⃣ Analyze & Research",
+ "3️⃣ Rewrite Settings",
+ "4️⃣ Results & Export"
+ ])
+
+ # Tab 1: Import Content
+ with tab1:
+ st.header("Import Your Blog Content")
+
+ import_method = st.radio(
+ "Choose import method:",
+ ["Import from URL", "Paste content manually"],
+ horizontal=True
)
- return search_response.results
- except Exception as err:
- st.error(f"Error fetching web research context: {err}")
- return None
-
-def generate_updated_content(website_data, keywords, tone, target_audience, desired_length, web_research_context):
- prompt = f"""
- You are an expert blog content writer.
- Analyze the following existing blog post content:
-
- ```
- {website_data['content']}
- ```
-
- Here is some additional context from web research:
-
- ```
- {web_research_context}
- ```
-
- Generate an updated version of this content, keeping the core message but making it more engaging and relevant for a {target_audience} audience.
-
- Consider the following:
-
- * Use the provided keywords: {keywords}
- * Adopt a {tone} writing style.
- * Keep the content around {desired_length} words.
- * Make sure the content is fresh, up-to-date, and provides value to the reader.
- * Incorporate insights from the web research context to make the content more comprehensive and insightful.
-
- Format your response as Markdown.
- """
-
- response = generator(prompt, max_length=2000, num_return_sequences=1, do_sample=True)
- return response[0]['generated_text']
+
+ if import_method == "Import from URL":
+ url = st.text_input(
+ "Enter blog URL:",
+ placeholder="https://example.com/blog-post",
+ help="Enter the full URL of the blog post you want to rewrite"
+ )
+
+ if st.button("Import Content", type="primary"):
+ if not url:
+ st.error("Please enter a valid URL")
+ else:
+ with st.spinner("Extracting content from URL..."):
+ # Extract content from URL
+ st.session_state.original_content = st.session_state.blog_rewriter.extract_content_from_url(url)
+
+ if "error" in st.session_state.original_content:
+ st.error(f"Error extracting content: {st.session_state.original_content['error']}")
+ else:
+ st.success("Content extracted successfully!")
+ st.session_state.current_step = 2
+ # Auto-click the next tab
+ st.experimental_rerun()
+ else:
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ title = st.text_input(
+ "Blog Title:",
+ placeholder="Enter the title of your blog post"
+ )
+
+ with col2:
+ author = st.text_input(
+ "Author (optional):",
+ placeholder="Author name"
+ )
+
+ meta_description = st.text_area(
+ "Meta Description (optional):",
+ placeholder="Enter the meta description of your blog post",
+ max_chars=MAX_META_DESCRIPTION_LENGTH,
+ height=80
+ )
+
+ content = st.text_area(
+ "Blog Content:",
+ placeholder="Paste your blog content here...",
+ height=300
+ )
+
+ if st.button("Import Content", type="primary"):
+ if not title or not content:
+ st.error("Please enter both title and content")
+ else:
+ # Store the manually entered content
+ st.session_state.original_content = {
+ "title": title,
+ "meta_description": meta_description,
+ "content": content,
+ "author": author,
+ "headings": [],
+ "images": [],
+ "publish_date": None,
+ "url": None
+ }
+
+ st.success("Content imported successfully!")
+ st.session_state.current_step = 2
+ # Auto-click the next tab
+ st.experimental_rerun()
+
+ # Display the imported content if available
+ if st.session_state.original_content and "title" in st.session_state.original_content:
+ with st.expander("View Imported Content", expanded=False):
+ st.subheader(st.session_state.original_content["title"])
+
+ if st.session_state.original_content.get("meta_description"):
+ st.markdown(f"**Meta Description:** {st.session_state.original_content['meta_description']}")
+
+ if st.session_state.original_content.get("author"):
+ st.markdown(f"**Author:** {st.session_state.original_content['author']}")
+
+ if st.session_state.original_content.get("publish_date"):
+ st.markdown(f"**Published:** {st.session_state.original_content['publish_date']}")
+
+ st.markdown("**Content Preview:**")
+ content_preview = st.session_state.original_content["content"]
+ if len(content_preview) > 1000:
+ content_preview = content_preview[:1000] + "..."
+ st.text_area("", content_preview, height=200, disabled=True)
+
+ # Display images if available
+ if st.session_state.original_content.get("images"):
+ st.markdown(f"**Images:** {len(st.session_state.original_content['images'])} images found")
+
+ # Tab 2: Analyze & Research
+ with tab2:
+ st.header("Analyze & Research")
+
+ if not st.session_state.original_content or "title" not in st.session_state.original_content:
+ st.info("Please import your blog content first")
+ else:
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if st.button("Analyze Content", type="primary"):
+ with st.spinner("Analyzing content..."):
+ # Analyze the content
+ st.session_state.content_analysis = st.session_state.blog_rewriter.analyze_content(
+ st.session_state.original_content
+ )
+ st.success("Content analysis complete!")
+
+ with col2:
+ research_depth = st.selectbox(
+ "Research Depth:",
+ ["low", "medium", "high"],
+ index=1,
+ format_func=lambda x: {"low": "Basic", "medium": "Standard", "high": "Comprehensive"}[x],
+ help="Choose the depth of research to update your content"
+ )
+
+ if st.button("Conduct Research", type="primary"):
+ with st.spinner("Researching latest information..."):
+ # Conduct research
+ st.session_state.research_results = st.session_state.blog_rewriter.conduct_research(
+ st.session_state.original_content["title"],
+ st.session_state.original_content["content"],
+ research_depth
+ )
+ st.success("Research complete!")
+
+ # Display content analysis if available
+ if st.session_state.content_analysis:
+ st.subheader("Content Analysis")
+
+ metrics = st.session_state.content_analysis.get("metrics", {})
+
+ # Create metrics display
+ col1, col2, col3, col4 = st.columns(4)
+ with col1:
+ st.metric("Word Count", metrics.get("word_count", 0))
+ with col2:
+ st.metric("Paragraphs", metrics.get("paragraph_count", 0))
+ with col3:
+ st.metric("Sentences", metrics.get("sentence_count", 0))
+ with col4:
+ content_age = st.session_state.content_analysis.get("content_age", {})
+ if "months" in content_age:
+ st.metric("Content Age", f"{content_age['months']} months")
+ elif "error" in content_age:
+ st.metric("Content Age", "Unknown")
+
+ # Heading structure
+ heading_structure = st.session_state.content_analysis.get("heading_structure", {})
+ if heading_structure:
+ st.markdown("**Heading Structure:**")
+ for level, count in sorted(heading_structure.items()):
+ st.markdown(f"H{level}: {count} headings")
+
+ # Image analysis
+ images = st.session_state.content_analysis.get("images", {})
+ if images:
+ st.markdown(f"**Images:** {images.get('count', 0)} images found, {images.get('with_alt_text', 0)} with alt text")
+
+ # Display research results if available
+ if st.session_state.research_results:
+ st.subheader("Research Results")
+
+ topics = st.session_state.research_results.get("topics", [])
+ if topics:
+ for topic in topics:
+ with st.expander(f"Topic: {topic['topic']}", expanded=False):
+ for i, source in enumerate(topic.get("sources", [])):
+ st.markdown(f"**Source {i+1}:** {source.get('title', 'Untitled')}")
+ st.markdown(f"**URL:** {source.get('url', 'No URL')}")
+ st.markdown(f"**Content Preview:** {source.get('content', 'No content')[:200]}...")
+ st.markdown("---")
+ else:
+ st.info("No research results available")
+
+ # Enable proceeding to the next step if both analysis and research are done
+ if st.session_state.content_analysis and st.session_state.research_results:
+ if st.button("Proceed to Rewrite Settings", type="primary"):
+ st.session_state.current_step = 3
+ st.experimental_rerun()
+
+ # Tab 3: Rewrite Settings
+ with tab3:
+ st.header("Rewrite Settings")
+
+ if not st.session_state.original_content or "title" not in st.session_state.original_content:
+ st.info("Please import your blog content first")
+ elif not st.session_state.content_analysis or not st.session_state.research_results:
+ st.info("Please complete content analysis and research first")
+ else:
+ # Create a form for rewrite settings
+ with st.form("rewrite_settings_form"):
+ st.subheader("Content Transformation")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ rewrite_mode = st.selectbox(
+ "Rewrite Mode:",
+ list(REWRITE_MODES.keys()),
+ format_func=lambda x: x.replace("_", " ").title(),
+ help="Choose how you want to transform your content"
+ )
+
+ st.info(REWRITE_MODES[rewrite_mode])
+
+ with col2:
+ tone = st.selectbox(
+ "Target Tone:",
+ TONE_OPTIONS,
+ index=0,
+ help="Choose the tone for your rewritten content"
+ )
+
+ st.subheader("Content Length")
+
+ original_word_count = st.session_state.content_analysis.get("metrics", {}).get("word_count", 0)
+
+ length_option = st.radio(
+ "Target Length:",
+ ["same", "shorter", "longer", "custom"],
+ format_func=lambda x: {
+ "same": f"Same as original ({original_word_count} words)",
+ "shorter": f"Shorter (about {int(original_word_count * 0.7)} words)",
+ "longer": f"Longer (about {int(original_word_count * 1.3)} words)",
+ "custom": "Custom word count"
+ }[x],
+ horizontal=True
+ )
+
+ if length_option == "custom":
+ target_word_count = st.number_input(
+ "Custom Word Count:",
+ min_value=100,
+ max_value=10000,
+ value=original_word_count,
+ step=100
+ )
+ else:
+ target_word_count = {
+ "same": original_word_count,
+ "shorter": int(original_word_count * 0.7),
+ "longer": int(original_word_count * 1.3)
+ }[length_option]
+
+ st.subheader("SEO Optimization")
+
+ keywords = st.text_input(
+ "Focus Keywords (comma-separated):",
+ placeholder="e.g., digital marketing, SEO, content strategy",
+ help="Enter keywords to optimize your content for"
+ )
+
+ st.subheader("Additional Instructions")
+
+ special_instructions = st.text_area(
+ "Special Instructions (optional):",
+ placeholder="Add any specific instructions for rewriting your content...",
+ help="Provide any additional instructions for the AI"
+ )
+
+ # Submit button
+ submitted = st.form_submit_button("Rewrite Blog", type="primary")
+
+ if submitted:
+ # Process the form data
+ user_preferences = {
+ "rewrite_mode": rewrite_mode,
+ "tone": tone,
+ "target_word_count": target_word_count,
+ "keywords": [k.strip() for k in keywords.split(",")] if keywords else [],
+ "special_instructions": special_instructions
+ }
+
+ with st.spinner("Rewriting your blog..."):
+ # Rewrite the blog
+ st.session_state.rewritten_content = st.session_state.blog_rewriter.rewrite_blog(
+ st.session_state.original_content,
+ user_preferences,
+ st.session_state.research_results,
+ st.session_state.content_analysis
+ )
+
+ if "error" in st.session_state.rewritten_content:
+ st.error(f"Error rewriting blog: {st.session_state.rewritten_content['error']}")
+ else:
+ st.success("Blog rewritten successfully!")
+ st.session_state.current_step = 4
+ st.experimental_rerun()
+
+ # Tab 4: Results & Export
+ with tab4:
+ st.header("Results & Export")
+
+ if not st.session_state.rewritten_content or "title" not in st.session_state.rewritten_content:
+ st.info("Please complete the rewriting process first")
+ else:
+ # Display the rewritten content
+ st.subheader("Rewritten Blog")
+
+ # Title and meta description
+ st.markdown(f"## {st.session_state.rewritten_content['title']}")
+
+ if st.session_state.rewritten_content.get("meta_description"):
+ with st.expander("Meta Description", expanded=True):
+ st.text_area(
+ "",
+ st.session_state.rewritten_content["meta_description"],
+ height=80,
+ disabled=True
+ )
+
+ # Create tabs for different views
+ content_tab1, content_tab2 = st.tabs(["Preview", "Markdown"])
+
+ with content_tab1:
+ st.markdown(st.session_state.rewritten_content["content"])
+
+ with content_tab2:
+ st.text_area(
+ "",
+ st.session_state.rewritten_content["content"],
+ height=400
+ )
+
+ # Image generation section
+ st.subheader("Generate Images")
+
+ suggested_images = st.session_state.rewritten_content.get("suggested_images", [])
+ if suggested_images:
+ st.markdown("**Suggested Images:**")
+
+ for i, img in enumerate(suggested_images):
+ with st.expander(f"Image {i+1}: {img.get('description', 'No description')}", expanded=False):
+ st.markdown(f"**Description:** {img.get('description', 'No description')}")
+ st.markdown(f"**Caption:** {img.get('caption', 'No caption')}")
+ st.markdown(f"**Placement:** {img.get('placement', 'No placement specified')}")
+
+ # Generate image button
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ image_prompt = st.text_area(
+ "Image Prompt:",
+ value=img.get('description', ''),
+ key=f"image_prompt_{i}"
+ )
+
+ with col2:
+ style = st.selectbox(
+ "Style:",
+ ["realistic", "artistic", "cartoon", "3d_render"],
+ key=f"style_{i}"
+ )
+
+ if st.button("Generate Image", key=f"gen_img_{i}"):
+ with st.spinner("Generating image..."):
+ image_path = st.session_state.blog_rewriter.generate_image(image_prompt, style)
+
+ if image_path:
+ # Store the generated image
+ if "generated_images" not in st.session_state:
+ st.session_state.generated_images = {}
+
+ st.session_state.generated_images[f"image_{i}"] = {
+ "path": image_path,
+ "caption": img.get('caption', ''),
+ "placement": img.get('placement', '')
+ }
+
+ st.success("Image generated successfully!")
+ st.experimental_rerun()
+
+ # Display the generated image if available
+ if f"image_{i}" in st.session_state.generated_images:
+ st.image(
+ st.session_state.generated_images[f"image_{i}"]["path"],
+ caption=st.session_state.generated_images[f"image_{i}"]["caption"],
+ use_column_width=True
+ )
+ else:
+ st.info("No image suggestions available")
+
+ # Custom image generation
+ with st.expander("Generate Custom Image", expanded=True):
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ custom_image_prompt = st.text_area(
+ "Image Prompt:",
+ placeholder="Describe the image you want to generate..."
+ )
+
+ with col2:
+ custom_style = st.selectbox(
+ "Style:",
+ ["realistic", "artistic", "cartoon", "3d_render"]
+ )
+
+ if st.button("Generate Custom Image"):
+ if not custom_image_prompt:
+ st.error("Please enter an image prompt")
+ else:
+ with st.spinner("Generating image..."):
+ image_path = st.session_state.blog_rewriter.generate_image(custom_image_prompt, custom_style)
+
+ if image_path:
+ # Store the generated image
+ if "generated_images" not in st.session_state:
+ st.session_state.generated_images = {}
+
+ st.session_state.generated_images["custom_image"] = {
+ "path": image_path,
+ "caption": "Custom generated image",
+ "placement": "Custom placement"
+ }
+
+ st.success("Image generated successfully!")
+ st.experimental_rerun()
+
+ # Display the generated custom image if available
+ if "custom_image" in st.session_state.generated_images:
+ st.image(
+ st.session_state.generated_images["custom_image"]["path"],
+ caption=st.session_state.generated_images["custom_image"]["caption"],
+ use_column_width=True
+ )
+
+ # Export options
+ st.subheader("Export Options")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.download_button(
+ "Download as Markdown",
+ data=st.session_state.rewritten_content["content"],
+ file_name=f"{st.session_state.rewritten_content['title'].replace(' ', '_')}.md",
+ mime="text/markdown"
+ )
+
+ with col2:
+ # Create HTML version
+ html_content = f"""
+
+
+
+ {st.session_state.rewritten_content['title']}
+
+
+
+
+ {st.session_state.rewritten_content['title']}
+ {st.session_state.rewritten_content['content']}
+
+
+ """
+
+ st.download_button(
+ "Download as HTML",
+ data=html_content,
+ file_name=f"{st.session_state.rewritten_content['title'].replace(' ', '_')}.html",
+ mime="text/html"
+ )
+
+ with col3:
+ # Create JSON version with all content and metadata
+ json_content = {
+ "title": st.session_state.rewritten_content["title"],
+ "meta_description": st.session_state.rewritten_content.get("meta_description", ""),
+ "content": st.session_state.rewritten_content["content"],
+ "suggested_images": st.session_state.rewritten_content.get("suggested_images", []),
+ "generated_images": [
+ {
+ "caption": img_data["caption"],
+ "placement": img_data["placement"],
+ "path": img_data["path"]
+ }
+ for img_key, img_data in st.session_state.generated_images.items()
+ ] if hasattr(st.session_state, "generated_images") else [],
+ "original_title": st.session_state.original_content.get("title", ""),
+ "original_url": st.session_state.original_content.get("url", ""),
+ "rewrite_date": datetime.now().isoformat()
+ }
+
+ st.download_button(
+ "Download as JSON",
+ data=json.dumps(json_content, indent=2),
+ file_name=f"{st.session_state.rewritten_content['title'].replace(' ', '_')}.json",
+ mime="application/json"
+ )
+
+ # Copy to clipboard buttons
+ st.subheader("Quick Copy")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if st.button("Copy Title", key="copy_title"):
+ st.code(st.session_state.rewritten_content["title"])
+ st.success("Title copied to clipboard!")
+
+ with col2:
+ if st.button("Copy Meta Description", key="copy_meta"):
+ st.code(st.session_state.rewritten_content.get("meta_description", ""))
+ st.success("Meta description copied to clipboard!")
+
+ with col3:
+ if st.button("Copy Full Content", key="copy_content"):
+ st.success("Content copied to clipboard!")
+
+ # Comparison with original
+ with st.expander("Compare with Original", expanded=False):
+ comp_col1, comp_col2 = st.columns(2)
+
+ with comp_col1:
+ st.subheader("Original")
+ st.markdown(f"**Title:** {st.session_state.original_content.get('title', '')}")
+ if st.session_state.original_content.get("meta_description"):
+ st.markdown(f"**Meta Description:** {st.session_state.original_content['meta_description']}")
+ st.text_area(
+ "Original Content",
+ st.session_state.original_content.get("content", ""),
+ height=300,
+ disabled=True
+ )
+
+ with comp_col2:
+ st.subheader("Rewritten")
+ st.markdown(f"**Title:** {st.session_state.rewritten_content['title']}")
+ if st.session_state.rewritten_content.get("meta_description"):
+ st.markdown(f"**Meta Description:** {st.session_state.rewritten_content['meta_description']}")
+ st.text_area(
+ "Rewritten Content",
+ st.session_state.rewritten_content["content"],
+ height=300,
+ disabled=True
+ )
+
+ # Start over button
+ if st.button("Start Over", type="primary"):
+ # Reset session state
+ for key in ["original_content", "content_analysis", "research_results",
+ "rewritten_content", "generated_images", "current_step"]:
+ if key in st.session_state:
+ del st.session_state[key]
+
+ st.experimental_rerun()
if __name__ == "__main__":
- main()
+ write_blog_rewriter()
\ No newline at end of file
diff --git a/lib/ai_writers/ai_copywriter/copywriter_dashboard.py b/lib/ai_writers/ai_copywriter/copywriter_dashboard.py
index 24bdf95c..e04571dc 100644
--- a/lib/ai_writers/ai_copywriter/copywriter_dashboard.py
+++ b/lib/ai_writers/ai_copywriter/copywriter_dashboard.py
@@ -3,6 +3,9 @@ import importlib
import sys
import os
from pathlib import Path
+import time
+import json
+from typing import Dict, List, Callable, Optional, Tuple
# Add the parent directory to the path to allow importing from lib
current_dir = Path(__file__).parent
@@ -28,132 +31,449 @@ copywriter_modules = [
"4r_copywriter"
]
-# Dynamically import all copywriter modules
-for module_name in copywriter_modules:
+# Define formula categories for better organization
+formula_categories = {
+ "Emotional Appeal": ["ai_emotional_copywriter", "oath_copywriter"],
+ "Structured Framework": ["acca_copywriter", "app_copywriter", "star_copywriter", "quest_copywriter"],
+ "Sales Funnel": ["aidppc_copywriter", "aida_copywriter"],
+ "Problem-Solution": ["pas_copywriter"],
+ "Feature-Benefit": ["fab_copywriter"],
+ "Messaging Framework": ["4c_copywriter", "4r_copywriter"]
+}
+
+# Define formula metadata for better display and filtering
+formula_metadata = {
+ "ai_emotional_copywriter": {
+ "name": "Emotional Copywriter",
+ "icon": "🎭",
+ "description": "Create copy that resonates with your audience's emotions and drives action.",
+ "color": "#FF6B6B",
+ "difficulty": "Intermediate",
+ "best_for": ["Landing Pages", "Email", "Social Media"],
+ "tags": ["emotional", "persuasive", "engagement"]
+ },
+ "acca_copywriter": {
+ "name": "ACCA Copywriter",
+ "icon": "🎯",
+ "description": "Use the ACCA (Attention, Context, Content, Action) framework to create compelling copy.",
+ "color": "#4ECDC4",
+ "difficulty": "Beginner",
+ "best_for": ["Ads", "Email", "Landing Pages"],
+ "tags": ["structured", "conversion", "clear"]
+ },
+ "app_copywriter": {
+ "name": "APP Copywriter",
+ "icon": "🤝",
+ "description": "Implement the APP (Agree, Promise, Preview) formula to create persuasive copy.",
+ "color": "#45B7D1",
+ "difficulty": "Beginner",
+ "best_for": ["Blog Posts", "Sales Pages", "Email"],
+ "tags": ["persuasive", "agreement", "preview"]
+ },
+ "star_copywriter": {
+ "name": "STAR Copywriter",
+ "icon": "⭐",
+ "description": "Use the STAR (Situation, Task, Action, Result) framework to tell compelling stories.",
+ "color": "#FFD166",
+ "difficulty": "Intermediate",
+ "best_for": ["Case Studies", "Testimonials", "About Pages"],
+ "tags": ["storytelling", "results", "case-study"]
+ },
+ "oath_copywriter": {
+ "name": "OATH Copywriter",
+ "icon": "📜",
+ "description": "Apply the OATH (Oblivious, Apathetic, Thinking, Hurting) framework to target specific audience mindsets.",
+ "color": "#06D6A0",
+ "difficulty": "Advanced",
+ "best_for": ["Ads", "Landing Pages", "Email Sequences"],
+ "tags": ["audience", "mindset", "targeting"]
+ },
+ "quest_copywriter": {
+ "name": "QUEST Copywriter",
+ "icon": "🔍",
+ "description": "Use the QUEST (Question, Unpack, Emphasize, Solution, Transform) framework for narrative-driven copy.",
+ "color": "#118AB2",
+ "difficulty": "Intermediate",
+ "best_for": ["Long-form Content", "Sales Pages", "Video Scripts"],
+ "tags": ["narrative", "transformation", "solution"]
+ },
+ "aidppc_copywriter": {
+ "name": "AIDPPC Copywriter",
+ "icon": "💰",
+ "description": "Implement the AIDPPC (Attention, Interest, Desire, Proof, Persuasion, Call to Action) framework for PPC ads.",
+ "color": "#073B4C",
+ "difficulty": "Advanced",
+ "best_for": ["PPC Ads", "Social Ads", "Display Ads"],
+ "tags": ["advertising", "ppc", "conversion"]
+ },
+ "aida_copywriter": {
+ "name": "AIDA Copywriter",
+ "icon": "🎬",
+ "description": "Use the AIDA (Attention, Interest, Desire, Action) framework to guide customers through the sales funnel.",
+ "color": "#EF476F",
+ "difficulty": "Beginner",
+ "best_for": ["Sales Pages", "Email", "Product Descriptions"],
+ "tags": ["sales", "funnel", "conversion"]
+ },
+ "pas_copywriter": {
+ "name": "PAS Copywriter",
+ "icon": "🔧",
+ "description": "Apply the PAS (Problem, Agitate, Solution) formula to address pain points and offer solutions.",
+ "color": "#7209B7",
+ "difficulty": "Beginner",
+ "best_for": ["Ads", "Email", "Landing Pages"],
+ "tags": ["problem-solving", "pain-points", "solutions"]
+ },
+ "fab_copywriter": {
+ "name": "FAB Copywriter",
+ "icon": "💎",
+ "description": "Use the FAB (Features, Advantages, Benefits) framework to highlight product value.",
+ "color": "#3A0CA3",
+ "difficulty": "Beginner",
+ "best_for": ["Product Descriptions", "Sales Pages", "Brochures"],
+ "tags": ["product", "features", "benefits"]
+ },
+ "4c_copywriter": {
+ "name": "4C Copywriter",
+ "icon": "📝",
+ "description": "Implement the 4C (Clear, Concise, Credible, Compelling) framework for effective messaging.",
+ "color": "#4361EE",
+ "difficulty": "Intermediate",
+ "best_for": ["Brand Messaging", "Mission Statements", "Value Propositions"],
+ "tags": ["clarity", "concise", "credibility"]
+ },
+ "4r_copywriter": {
+ "name": "4R Copywriter",
+ "icon": "🔄",
+ "description": "Use the 4R (Relevance, Resonance, Response, Results) framework to connect with your audience.",
+ "color": "#F72585",
+ "difficulty": "Intermediate",
+ "best_for": ["Content Marketing", "Email", "Social Media"],
+ "tags": ["relevance", "resonance", "results"]
+ }
+}
+
+def load_user_preferences() -> Dict:
+ """Load user preferences from session state or initialize if not present."""
+ if "copywriter_preferences" not in st.session_state:
+ st.session_state.copywriter_preferences = {
+ "recent_formulas": [],
+ "favorite_formulas": [],
+ "comparison_formulas": [],
+ "view_mode": "grid" # or "list"
+ }
+ return st.session_state.copywriter_preferences
+
+def save_user_preferences(preferences: Dict) -> None:
+ """Save user preferences to session state."""
+ st.session_state.copywriter_preferences = preferences
+
+def add_recent_formula(module_name: str) -> None:
+ """Add a formula to the recent formulas list."""
+ preferences = load_user_preferences()
+
+ # Remove if already exists
+ if module_name in preferences["recent_formulas"]:
+ preferences["recent_formulas"].remove(module_name)
+
+ # Add to the beginning of the list
+ preferences["recent_formulas"].insert(0, module_name)
+
+ # Keep only the 5 most recent
+ preferences["recent_formulas"] = preferences["recent_formulas"][:5]
+
+ save_user_preferences(preferences)
+
+def toggle_favorite_formula(module_name: str) -> bool:
+ """Toggle a formula as favorite and return the new state."""
+ preferences = load_user_preferences()
+
+ if module_name in preferences["favorite_formulas"]:
+ preferences["favorite_formulas"].remove(module_name)
+ is_favorite = False
+ else:
+ preferences["favorite_formulas"].append(module_name)
+ is_favorite = True
+
+ save_user_preferences(preferences)
+ return is_favorite
+
+def is_favorite_formula(module_name: str) -> bool:
+ """Check if a formula is in the favorites list."""
+ preferences = load_user_preferences()
+ return module_name in preferences["favorite_formulas"]
+
+def add_to_comparison(module_name: str) -> None:
+ """Add a formula to the comparison list."""
+ preferences = load_user_preferences()
+
+ if module_name not in preferences["comparison_formulas"]:
+ preferences["comparison_formulas"].append(module_name)
+
+ # Keep only up to 3 formulas for comparison
+ preferences["comparison_formulas"] = preferences["comparison_formulas"][:3]
+
+ save_user_preferences(preferences)
+
+def remove_from_comparison(module_name: str) -> None:
+ """Remove a formula from the comparison list."""
+ preferences = load_user_preferences()
+
+ if module_name in preferences["comparison_formulas"]:
+ preferences["comparison_formulas"].remove(module_name)
+
+ save_user_preferences(preferences)
+
+def clear_comparison() -> None:
+ """Clear the comparison list."""
+ preferences = load_user_preferences()
+ preferences["comparison_formulas"] = []
+ save_user_preferences(preferences)
+
+def lazy_load_module(module_name: str) -> Optional[Callable]:
+ """Lazily load a module and return its input_section function."""
+ if module_name in input_sections:
+ return input_sections[module_name]
+
try:
module_path = f"lib.ai_writers.ai_copywriter.{module_name}"
module = importlib.import_module(module_path)
if hasattr(module, "input_section"):
input_sections[module_name] = module.input_section
+ return module.input_section
+ else:
+ st.warning(f"Module {module_name} does not have an input_section function.")
+ return None
except Exception as e:
- st.write(f"Debug: Error importing {module_name}: {str(e)}")
+ st.error(f"Error loading module {module_name}: {str(e)}")
+ return None
+
+def render_formula_card(module_name: str, index: int, view_mode: str = "grid") -> None:
+ """Render a formula card with its details."""
+ metadata = formula_metadata.get(module_name, {})
+
+ if not metadata:
+ return
+
+ is_favorite = is_favorite_formula(module_name)
+ favorite_icon = "★" if is_favorite else "☆"
+ favorite_tooltip = "Remove from favorites" if is_favorite else "Add to favorites"
+
+ if view_mode == "grid":
+ with st.container():
+ st.markdown(f"""
+
+
{favorite_icon}
+
{metadata["icon"]} {metadata["name"]}
+
{metadata["description"]}
+
+
+ {metadata["difficulty"]}
+
+
+
+ """, unsafe_allow_html=True)
+
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ if st.button(f"Use {metadata['name']}", key=f"use_btn_{index}", use_container_width=True):
+ add_recent_formula(module_name)
+ st.session_state.selected_formula = {
+ "module": module_name,
+ "name": metadata["name"],
+ "icon": metadata["icon"],
+ "function": lazy_load_module(module_name)
+ }
+ st.rerun()
+
+ with col2:
+ if st.button(f"{favorite_icon} Favorite", key=f"fav_btn_{index}", help=favorite_tooltip, use_container_width=True):
+ toggle_favorite_formula(module_name)
+ st.rerun()
+
+ with col3:
+ if module_name in load_user_preferences()["comparison_formulas"]:
+ if st.button("Remove from Compare", key=f"comp_btn_{index}", use_container_width=True):
+ remove_from_comparison(module_name)
+ st.rerun()
+ else:
+ if st.button("Add to Compare", key=f"comp_btn_{index}", use_container_width=True):
+ add_to_comparison(module_name)
+ st.rerun()
+ else: # list view
+ with st.container():
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown(f"""
+
+
{metadata["icon"]} {metadata["name"]} {favorite_icon}
+
{metadata["description"]}
+
+
+ {metadata["difficulty"]}
+
+ Best for: {", ".join(metadata["best_for"][:2])}
+
+
+ """, unsafe_allow_html=True)
+
+ with col2:
+ if st.button(f"Use", key=f"use_list_btn_{index}", use_container_width=True):
+ add_recent_formula(module_name)
+ st.session_state.selected_formula = {
+ "module": module_name,
+ "name": metadata["name"],
+ "icon": metadata["icon"],
+ "function": lazy_load_module(module_name)
+ }
+ st.rerun()
+
+ if st.button(f"{favorite_icon}", key=f"fav_list_btn_{index}", help=favorite_tooltip):
+ toggle_favorite_formula(module_name)
+ st.rerun()
+
+ if module_name in load_user_preferences()["comparison_formulas"]:
+ if st.button("- Compare", key=f"comp_list_btn_{index}"):
+ remove_from_comparison(module_name)
+ st.rerun()
+ else:
+ if st.button("+ Compare", key=f"comp_list_btn_{index}"):
+ add_to_comparison(module_name)
+ st.rerun()
+
+def render_formula_comparison() -> None:
+ """Render a comparison of selected formulas."""
+ preferences = load_user_preferences()
+ comparison_formulas = preferences["comparison_formulas"]
+
+ if not comparison_formulas:
+ st.info("Add formulas to compare them side by side.")
+ return
+
+ # Create a table for comparison
+ comparison_data = []
+ for module_name in comparison_formulas:
+ metadata = formula_metadata.get(module_name, {})
+ if metadata:
+ comparison_data.append({
+ "Name": f"{metadata['icon']} {metadata['name']}",
+ "Description": metadata["description"],
+ "Difficulty": metadata["difficulty"],
+ "Best For": ", ".join(metadata["best_for"][:3]),
+ "Tags": ", ".join(metadata["tags"])
+ })
+
+ # Display the comparison table
+ st.markdown("### Formula Comparison")
+
+ # Create columns for each formula
+ cols = st.columns(len(comparison_data))
+
+ # Display headers
+ for i, col in enumerate(cols):
+ with col:
+ st.markdown(f"#### {comparison_data[i]['Name']}")
+
+ # Display description
+ st.markdown("##### Description")
+ for i, col in enumerate(cols):
+ with col:
+ st.write(comparison_data[i]["Description"])
+
+ # Display difficulty
+ st.markdown("##### Difficulty")
+ for i, col in enumerate(cols):
+ with col:
+ st.write(comparison_data[i]["Difficulty"])
+
+ # Display best for
+ st.markdown("##### Best For")
+ for i, col in enumerate(cols):
+ with col:
+ st.write(comparison_data[i]["Best For"])
+
+ # Display tags
+ st.markdown("##### Tags")
+ for i, col in enumerate(cols):
+ with col:
+ st.write(comparison_data[i]["Tags"])
+
+ # Add buttons to use each formula
+ st.markdown("##### Actions")
+ for i, col in enumerate(cols):
+ with col:
+ module_name = comparison_formulas[i]
+ if st.button(f"Use {formula_metadata[module_name]['name']}", key=f"use_comp_btn_{i}"):
+ add_recent_formula(module_name)
+ st.session_state.selected_formula = {
+ "module": module_name,
+ "name": formula_metadata[module_name]["name"],
+ "icon": formula_metadata[module_name]["icon"],
+ "function": lazy_load_module(module_name)
+ }
+ st.rerun()
+
+ # Add a button to clear the comparison
+ if st.button("Clear Comparison", key="clear_comparison"):
+ clear_comparison()
+ st.rerun()
+
+def filter_formulas(formulas: List[str], search_term: str, category: str, difficulty: str) -> List[str]:
+ """Filter formulas based on search term, category, and difficulty."""
+ filtered_formulas = []
+
+ for module_name in formulas:
+ metadata = formula_metadata.get(module_name, {})
+ if not metadata:
+ continue
+
+ # Check if the formula matches the search term
+ name_match = search_term.lower() in metadata["name"].lower()
+ desc_match = search_term.lower() in metadata["description"].lower()
+ tags_match = any(search_term.lower() in tag.lower() for tag in metadata.get("tags", []))
+
+ # Check if the formula matches the category
+ category_match = True
+ if category != "All Categories":
+ category_match = module_name in formula_categories.get(category, [])
+
+ # Check if the formula matches the difficulty
+ difficulty_match = True
+ if difficulty != "All Difficulties":
+ difficulty_match = metadata.get("difficulty", "") == difficulty
+
+ # Add the formula if it matches all criteria
+ if (name_match or desc_match or tags_match) and category_match and difficulty_match:
+ filtered_formulas.append(module_name)
+
+ return filtered_formulas
def copywriter_dashboard():
"""
Main function to display the copywriting dashboard.
This function can be called from content_generator.py when the user selects "AI Copywriter".
"""
-
- # Define the copywriting formulas with their details
- copywriting_formulas = [
- {
- "name": "Emotional Copywriter",
- "icon": "🎭",
- "description": "Create copy that resonates with your audience's emotions and drives action.",
- "color": "#FF6B6B",
- "module": "ai_emotional_copywriter",
- "function": input_sections.get("ai_emotional_copywriter")
- },
- {
- "name": "ACCA Copywriter",
- "icon": "🎯",
- "description": "Use the ACCA (Attention, Context, Content, Action) framework to create compelling copy.",
- "color": "#4ECDC4",
- "module": "acca_copywriter",
- "function": input_sections.get("acca_copywriter")
- },
- {
- "name": "APP Copywriter",
- "icon": "🤝",
- "description": "Implement the APP (Agree, Promise, Preview) formula to create persuasive copy.",
- "color": "#45B7D1",
- "module": "app_copywriter",
- "function": input_sections.get("app_copywriter")
- },
- {
- "name": "STAR Copywriter",
- "icon": "⭐",
- "description": "Use the STAR (Situation, Task, Action, Result) framework to tell compelling stories.",
- "color": "#FFD166",
- "module": "star_copywriter",
- "function": input_sections.get("star_copywriter")
- },
- {
- "name": "OATH Copywriter",
- "icon": "📜",
- "description": "Apply the OATH (Oblivious, Apathetic, Thinking, Hurting) framework to target specific audience mindsets.",
- "color": "#06D6A0",
- "module": "oath_copywriter",
- "function": input_sections.get("oath_copywriter")
- },
- {
- "name": "QUEST Copywriter",
- "icon": "🔍",
- "description": "Use the QUEST (Question, Unpack, Emphasize, Solution, Transform) framework for narrative-driven copy.",
- "color": "#118AB2",
- "module": "quest_copywriter",
- "function": input_sections.get("quest_copywriter")
- },
- {
- "name": "AIDPPC Copywriter",
- "icon": "💰",
- "description": "Implement the AIDPPC (Attention, Interest, Desire, Proof, Persuasion, Call to Action) framework for PPC ads.",
- "color": "#073B4C",
- "module": "aidppc_copywriter",
- "function": input_sections.get("aidppc_copywriter")
- },
- {
- "name": "AIDA Copywriter",
- "icon": "🎬",
- "description": "Use the AIDA (Attention, Interest, Desire, Action) framework to guide customers through the sales funnel.",
- "color": "#EF476F",
- "module": "aida_copywriter",
- "function": input_sections.get("aida_copywriter")
- },
- {
- "name": "PAS Copywriter",
- "icon": "🔧",
- "description": "Apply the PAS (Problem, Agitate, Solution) formula to address pain points and offer solutions.",
- "color": "#7209B7",
- "module": "pas_copywriter",
- "function": input_sections.get("pas_copywriter")
- },
- {
- "name": "FAB Copywriter",
- "icon": "💎",
- "description": "Use the FAB (Features, Advantages, Benefits) framework to highlight product value.",
- "color": "#3A0CA3",
- "module": "fab_copywriter",
- "function": input_sections.get("fab_copywriter")
- },
- {
- "name": "4C Copywriter",
- "icon": "📝",
- "description": "Implement the 4C (Clear, Concise, Credible, Compelling) framework for effective messaging.",
- "color": "#4361EE",
- "module": "four_c_copywriter",
- "function": input_sections.get("four_c_copywriter")
- },
- {
- "name": "4R Copywriter",
- "icon": "🔄",
- "description": "Use the 4R (Relevance, Resonance, Response, Results) framework to connect with your audience.",
- "color": "#F72585",
- "module": "four_r_copywriter",
- "function": input_sections.get("four_r_copywriter")
- }
- ]
-
- # Create a container for the dashboard
- dashboard_container = st.container()
-
- # Create a container for the formula input section
- formula_container = st.container()
+ # Load user preferences
+ preferences = load_user_preferences()
# Initialize session state for selected formula if it doesn't exist
if "selected_formula" not in st.session_state:
st.session_state.selected_formula = None
+ # Initialize session state for search and filter options
+ if "search_term" not in st.session_state:
+ st.session_state.search_term = ""
+ if "selected_category" not in st.session_state:
+ st.session_state.selected_category = "All Categories"
+ if "selected_difficulty" not in st.session_state:
+ st.session_state.selected_difficulty = "All Difficulties"
+ if "view_mode" not in st.session_state:
+ st.session_state.view_mode = preferences["view_mode"]
+
+ # Create a container for the formula input section
+ formula_container = st.container()
+
# If a formula is selected, show its input section
if st.session_state.selected_formula is not None:
with formula_container:
@@ -173,6 +493,9 @@ def copywriter_dashboard():
else:
st.error(f"The {st.session_state.selected_formula['name']} module is not available.")
else:
+ # Create a container for the dashboard
+ dashboard_container = st.container()
+
with dashboard_container:
# Display the dashboard
# Header
@@ -183,47 +506,162 @@ def copywriter_dashboard():
""", unsafe_allow_html=True)
- # Introduction
- st.markdown("""
- ## Welcome to the AI Copywriting Suite
+ # Create tabs for different sections
+ tab1, tab2, tab3, tab4 = st.tabs(["All Formulas", "Recent & Favorites", "Compare Formulas", "Help & Guide"])
- This dashboard provides access to a variety of copywriting formulas, each designed for specific marketing needs.
- Select a formula below to get started with creating compelling copy for your brand.
-
- ### How to Use This Dashboard
-
- 1. Browse the available copywriting formulas below
- 2. Click on a formula card to access its specific tool
- 3. Fill in the required information
- 4. Generate high-quality copy tailored to your needs
- """)
-
- # Create a 3-column layout for the formula cards
- col1, col2, col3 = st.columns(3)
-
- # Display the formula cards
- for i, formula in enumerate(copywriting_formulas):
- # Skip formulas that don't have a function
- if formula["function"] is None:
- continue
-
- # Determine which column to use
- col = col1 if i % 3 == 0 else col2 if i % 3 == 1 else col3
+ with tab1:
+ # Search and filter options
+ col1, col2, col3, col4 = st.columns([3, 2, 2, 1])
- with col:
- # Create a card for each formula
- st.markdown(f"""
-
-
{formula["icon"]} {formula["name"]}
-
{formula["description"]}
-
- """, unsafe_allow_html=True)
+ with col1:
+ search_term = st.text_input("🔍 Search formulas", value=st.session_state.search_term)
+ if search_term != st.session_state.search_term:
+ st.session_state.search_term = search_term
+
+ with col2:
+ categories = ["All Categories"] + list(formula_categories.keys())
+ selected_category = st.selectbox("Category", categories, index=categories.index(st.session_state.selected_category))
+ if selected_category != st.session_state.selected_category:
+ st.session_state.selected_category = selected_category
+
+ with col3:
+ difficulties = ["All Difficulties", "Beginner", "Intermediate", "Advanced"]
+ selected_difficulty = st.selectbox("Difficulty", difficulties, index=difficulties.index(st.session_state.selected_difficulty))
+ if selected_difficulty != st.session_state.selected_difficulty:
+ st.session_state.selected_difficulty = selected_difficulty
+
+ with col4:
+ view_options = {"Grid": "grid", "List": "list"}
+ view_mode = st.selectbox("View", list(view_options.keys()), index=list(view_options.values()).index(st.session_state.view_mode))
+ st.session_state.view_mode = view_options[view_mode]
+ preferences["view_mode"] = st.session_state.view_mode
+ save_user_preferences(preferences)
+
+ # Filter formulas based on search and filter options
+ filtered_formulas = filter_formulas(
+ copywriter_modules,
+ st.session_state.search_term,
+ st.session_state.selected_category,
+ st.session_state.selected_difficulty
+ )
+
+ if not filtered_formulas:
+ st.info("No formulas match your search criteria. Try adjusting your filters.")
+ else:
+ # Display the formula cards
+ if st.session_state.view_mode == "grid":
+ # Create a 3-column layout for the formula cards
+ col1, col2, col3 = st.columns(3)
+
+ # Display the formula cards
+ for i, module_name in enumerate(filtered_formulas):
+ # Determine which column to use
+ col = col1 if i % 3 == 0 else col2 if i % 3 == 1 else col3
+
+ with col:
+ render_formula_card(module_name, i, st.session_state.view_mode)
+ else: # list view
+ for i, module_name in enumerate(filtered_formulas):
+ render_formula_card(module_name, i, st.session_state.view_mode)
+
+ with tab2:
+ # Recent formulas
+ st.subheader("Recently Used Formulas")
+ recent_formulas = preferences["recent_formulas"]
+
+ if not recent_formulas:
+ st.info("You haven't used any formulas yet. Start by selecting a formula from the 'All Formulas' tab.")
+ else:
+ # Create a 3-column layout for the recent formula cards
+ col1, col2, col3 = st.columns(3)
- # Add a button to access the formula
- if st.button(f"Use {formula['name']}", key=f"btn_{i}"):
- # Store the selected formula in session state
- st.session_state.selected_formula = formula
- st.rerun()
+ # Display the recent formula cards
+ for i, module_name in enumerate(recent_formulas):
+ # Determine which column to use
+ col = col1 if i % 3 == 0 else col2 if i % 3 == 1 else col3
+
+ with col:
+ render_formula_card(module_name, i + 100, "grid") # Use a different index to avoid key conflicts
+
+ # Favorite formulas
+ st.subheader("Favorite Formulas")
+ favorite_formulas = preferences["favorite_formulas"]
+
+ if not favorite_formulas:
+ st.info("You haven't added any formulas to your favorites yet. Click the star icon on a formula card to add it to your favorites.")
+ else:
+ # Create a 3-column layout for the favorite formula cards
+ col1, col2, col3 = st.columns(3)
+
+ # Display the favorite formula cards
+ for i, module_name in enumerate(favorite_formulas):
+ # Determine which column to use
+ col = col1 if i % 3 == 0 else col2 if i % 3 == 1 else col3
+
+ with col:
+ render_formula_card(module_name, i + 200, "grid") # Use a different index to avoid key conflicts
+
+ with tab3:
+ # Formula comparison
+ render_formula_comparison()
+
+ with tab4:
+ # Help and guide
+ st.subheader("Copywriting Formula Guide")
+ st.write("""
+ This dashboard provides access to a variety of copywriting formulas, each designed for specific marketing needs.
+ Here's how to make the most of these powerful tools:
+ """)
+
+ st.markdown("""
+ #### How to Use This Dashboard
+
+ 1. **Browse Formulas**: Explore the available copywriting formulas in the "All Formulas" tab
+ 2. **Search & Filter**: Use the search box and filters to find the perfect formula for your needs
+ 3. **Compare Formulas**: Add up to 3 formulas to the comparison tab to see them side by side
+ 4. **Save Favorites**: Click the star icon to save formulas you use frequently
+ 5. **Access Recent**: Quickly access your recently used formulas in the "Recent & Favorites" tab
+
+ #### Choosing the Right Formula
+
+ Different formulas work best for different marketing goals:
+
+ - **Emotional Appeal**: Use when you want to connect with your audience on an emotional level
+ - **Structured Framework**: Great for organizing complex information in a compelling way
+ - **Sales Funnel**: Designed to guide prospects through the buying journey
+ - **Problem-Solution**: Effective for highlighting pain points and positioning your solution
+ - **Feature-Benefit**: Perfect for product descriptions and technical offerings
+ - **Messaging Framework**: Helps create clear, consistent messaging across channels
+
+ #### Formula Difficulty Levels
+
+ - **Beginner**: Easy to use with minimal copywriting experience
+ - **Intermediate**: Requires some understanding of copywriting principles
+ - **Advanced**: Most effective when used by experienced copywriters
+ """)
+
+ # Add a section about how to use the generated copy
+ st.subheader("Using Your Generated Copy")
+ st.write("""
+ After generating copy with your chosen formula:
+
+ 1. **Review & Edit**: Always review and personalize the generated content
+ 2. **Test Different Versions**: Try multiple formulas for the same product/service
+ 3. **A/B Test**: Use different versions in your marketing to see which performs best
+ 4. **Adapt for Channels**: Modify the copy as needed for different marketing channels
+ """)
+
+ # Add a feedback section
+ st.subheader("Feedback & Suggestions")
+ st.write("We're constantly improving our copywriting tools. If you have feedback or suggestions, please let us know!")
+
+ feedback = st.text_area("Your feedback", placeholder="Share your thoughts, suggestions, or report any issues...")
+ if st.button("Submit Feedback"):
+ if feedback:
+ st.success("Thank you for your feedback! We'll use it to improve our tools.")
+ # In a real implementation, you would save this feedback somewhere
+ else:
+ st.warning("Please enter your feedback before submitting.")
# For standalone execution
if __name__ == "__main__":
diff --git a/lib/blog_metadata/get_blog_metadata.py b/lib/blog_metadata/get_blog_metadata.py
index 1fe59044..d2751c72 100644
--- a/lib/blog_metadata/get_blog_metadata.py
+++ b/lib/blog_metadata/get_blog_metadata.py
@@ -6,6 +6,7 @@ import streamlit as st
from loguru import logger
import random
import asyncio
+import re
logger.remove()
logger.add(sys.stdout,
@@ -17,110 +18,367 @@ from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
async def blog_metadata(blog_article):
- """ Common function to get blog metadata """
- logger.info(f"Generating Content MetaData\n")
+ """
+ Generate comprehensive SEO metadata for a blog article.
+
+ Args:
+ blog_article (str): The content of the blog article
+
+ Returns:
+ tuple: (blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug)
+ """
+ logger.info("Generating comprehensive blog metadata")
progress_bar = st.progress(0)
- total_steps = 4
+ total_steps = 6 # Increased steps for new metadata types
+ status_container = st.empty()
- # Step 1: Generate blog title
- await asyncio.sleep(random.uniform(1, 3))
- blog_title = generate_blog_title(blog_article)
- progress_bar.progress(1 / total_steps)
+ try:
+ # Step 1: Generate blog title
+ status_container.info("Generating SEO-optimized blog title...")
+ await asyncio.sleep(random.uniform(0.5, 1.5))
+ blog_title = generate_blog_title(blog_article)
+ progress_bar.progress(1 / total_steps)
- # Step 2: Generate blog meta description
- await asyncio.sleep(random.uniform(1, 3))
- blog_meta_desc = generate_blog_description(blog_article)
- progress_bar.progress(2 / total_steps)
+ # Step 2: Generate blog meta description
+ status_container.info("Creating compelling meta description...")
+ await asyncio.sleep(random.uniform(0.5, 1.5))
+ blog_meta_desc = generate_blog_description(blog_article)
+ progress_bar.progress(2 / total_steps)
- # Step 3: Generate blog tags
- await asyncio.sleep(random.uniform(1, 3))
- blog_tags = get_blog_tags(blog_article)
- progress_bar.progress(3 / total_steps)
+ # Step 3: Generate blog tags
+ status_container.info("Extracting relevant blog tags...")
+ await asyncio.sleep(random.uniform(0.5, 1.5))
+ blog_tags = get_blog_tags(blog_article)
+ progress_bar.progress(3 / total_steps)
- # Step 4: Generate blog categories
- await asyncio.sleep(random.uniform(1, 3))
- blog_categories = get_blog_categories(blog_article)
- progress_bar.progress(4 / total_steps)
+ # Step 4: Generate blog categories
+ status_container.info("Identifying primary blog categories...")
+ await asyncio.sleep(random.uniform(0.5, 1.5))
+ blog_categories = get_blog_categories(blog_article)
+ progress_bar.progress(4 / total_steps)
+
+ # Step 5: Generate social media hashtags
+ status_container.info("Creating social media hashtags...")
+ await asyncio.sleep(random.uniform(0.5, 1.5))
+ blog_hashtags = generate_blog_hashtags(blog_article)
+ progress_bar.progress(5 / total_steps)
+
+ # Step 6: Generate SEO URL slug
+ status_container.info("Generating SEO-friendly URL slug...")
+ await asyncio.sleep(random.uniform(0.5, 1.5))
+ blog_slug = generate_blog_slug(blog_title)
+ progress_bar.progress(6 / total_steps)
- # Present the result in a table format
- st.table({
- "Metadata": ["Blog Title", "Meta Description", "Tags", "Categories"],
- "Value": [blog_title, blog_meta_desc, blog_tags, blog_categories]
- })
+ # Present the result in a table format
+ status_container.success("✅ Metadata generation complete")
+ st.table({
+ "Metadata": ["Blog Title", "Meta Description", "Tags", "Categories", "Social Hashtags", "URL Slug"],
+ "Value": [blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug]
+ })
- return blog_title, blog_meta_desc, blog_tags, blog_categories
+ return blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug
+
+ except Exception as e:
+ status_container.error(f"Error generating metadata: {str(e)}")
+ logger.error(f"Failed to generate metadata: {str(e)}")
+ # Return default values to ensure the blog generation process can continue
+ return f"Blog Article", "An informative blog post", "content, blog", "General, Information", "#content #blog", "blog-article"
def generate_blog_title(blog_article):
"""
- Given a blog title generate an outline for it
+ Generate an SEO-optimized and engaging title for a blog article.
+
+ Args:
+ blog_article (str): The content of the blog article
+
+ Returns:
+ str: An SEO-optimized title
"""
- logger.info("Generating blog title.")
- prompt = f"""As a SEO expert, I will provide you with a blog content.
- Your task is write a SEO optimized and call to action, blog title for given blog content.
- Follow SEO best practises to suggest the blog title.
- Please keep the titles concise, not exceeding 60 words.
- Respond with only the title and no explanations.
- Negative Keywords: Unvieling, unleash, power of. Dont use such words in your title.
-
- \nGenerate blog title for this given blog content:\n '{blog_article}' """
+ logger.info("Generating SEO-optimized blog title")
+
+ # Extract the first 3000 characters for title generation
+ snippet = blog_article[:3000] if len(blog_article) > 3000 else blog_article
+
+ prompt = f"""As an expert SEO copywriter, create the perfect blog title based on this content.
+
+REQUIREMENTS:
+1. Make it compelling, specific, and actionable
+2. Include primary keywords naturally near the beginning
+3. Keep it between 50-60 characters (10-12 words maximum)
+4. Make it promise clear value to the reader
+5. Use power words that evoke emotion where appropriate
+
+AVOID:
+- Clickbait tactics or false promises
+- Generic titles that could apply to any article
+- Using words like "unveiling", "unleash", "power of", "ultimate guide", or "complete"
+- ALL CAPS or excessive punctuation!!!!
+
+EXAMPLES OF GREAT TITLES:
+- "7 Proven Strategies to Improve Your Email Marketing ROI"
+- "Why Remote Work Improves Productivity: New Research Findings"
+- "How to Build a Personal Budget That Actually Works"
+
+CONTENT TO ANALYZE:
+"{snippet}"
+
+Reply with ONLY the title and no other text or explanation.
+"""
try:
- response = llm_text_gen(prompt)
- return response
+ title = llm_text_gen(prompt)
+ # Clean up any quotes or extra spaces
+ title = title.strip('"\'').strip()
+ logger.info(f"Generated title: {title}")
+ return title
except Exception as err:
- logger.error(f"Failed to get response from LLM: {err}")
- raise err
+ logger.error(f"Failed to generate blog title: {err}")
+ return "Blog Article" # Fallback title
+
def generate_blog_description(blog_content):
"""
- Prompt designed to give SEO optimized blog descripton
+ Generate an SEO-optimized meta description for the blog.
+
+ Args:
+ blog_content (str): The content of the blog article
+
+ Returns:
+ str: An SEO-optimized meta description
"""
- logger.info("Generating Blog Meta Description for the given blog.")
- prompt = f"""As an expert SEO and blog writer, Compose a compelling meta description for the given blog content,
- adhering to SEO best practices. Keep it between 150-160 characters.
- Provide a glimpse of the content's value to entice readers.
- Respond with only one of your best effort and do not include your explanations.
- Blog Content: '{blog_content}'"""
+ logger.info("Generating SEO-optimized meta description")
+
+ # Extract the first 2000 characters for description generation
+ snippet = blog_content[:2000] if len(blog_content) > 2000 else blog_content
+
+ prompt = f"""As an SEO expert, write the perfect meta description for this blog content.
- try:
- response = llm_text_gen(prompt)
- return response
- except Exception as err:
- logger.error(f"Failed to get response from LLM:{err}")
- raise err
+REQUIREMENTS:
+1. Exactly 150-160 characters (this is critical for SEO)
+2. Include primary keywords naturally
+3. Compelling value proposition that makes readers want to click
+4. Clear indication of what the reader will learn/gain
+5. End with an implicit call-to-action when possible
-def get_blog_categories(blog_article):
- """
- Function to generate blog categories for given blog content.
- """
- prompt = f"""As an expert SEO and content writer, I will provide you with blog content.
- Suggest only 2 blog categories which are most relevant to provided blog content,
- by identifying the main topic. Also consider the target audience and the
- blog's category taxonomy. Only reply with comma separated values.
- The blog content is: '{blog_article}'"
- """
- logger.info("Generating blog categories for the given blog.")
+EXAMPLES OF EXCELLENT META DESCRIPTIONS:
+- "Learn how to increase email open rates by 43% with these 5 proven strategies from industry experts. Implement today for immediate results."
+- "Discover why 67% of professionals struggle with work-life balance and explore research-backed techniques to reclaim your time and energy."
+
+CONTENT TO SUMMARIZE:
+"{snippet}"
+
+Reply with ONLY the meta description and no other text. Keep it between 150-160 characters exactly.
+"""
try:
- response = llm_text_gen(prompt)
- return response
+ description = llm_text_gen(prompt)
+ # Clean up any quotes or extra spaces
+ description = description.strip('"\'').strip()
+ logger.info(f"Generated meta description: {description}")
+ return description
except Exception as err:
- logger.error(f"get_blog_categories:Failed to get response from LLM: {err}")
+ logger.error(f"Failed to generate blog description: {err}")
+ return "An informative blog post about this topic." # Fallback description
+
def get_blog_tags(blog_article):
"""
- Function to suggest tags for the given blog content
+ Generate relevant SEO tags for a blog article.
+
+ Args:
+ blog_article (str): The content of the blog article
+
+ Returns:
+ str: Comma-separated list of relevant tags
"""
- prompt = f"""As an expert SEO and blog writer, suggest only 2 relevant and specific blog tags
- for the given blog content. Only reply with comma separated values.
- Blog content: {blog_article}."""
- logger.info("Generating Blog tags for the given blog post.")
+ logger.info("Generating SEO-optimized blog tags")
+
+ # Extract the first 3000 characters for tag generation
+ snippet = blog_article[:3000] if len(blog_article) > 3000 else blog_article
+
+ prompt = f"""As an SEO specialist, extract the 4-6 most relevant tags for this blog post.
+
+REQUIREMENTS:
+1. Choose specific, targeted keywords that accurately represent the content
+2. Include a mix of broad and specific tags
+3. Focus on terms users would actually search for
+4. Include at least one long-tail keyword phrase
+5. Ensure all tags are directly addressed in the content
+
+CONTENT TO ANALYZE:
+"{snippet}"
+
+Reply with ONLY the tags as a comma-separated list (e.g., "keyword1, keyword2, keyword3, keyword phrase"). Provide 4-6 tags total.
+"""
try:
- response = llm_text_gen(prompt)
- return response
+ tags = llm_text_gen(prompt)
+ # Clean up any quotes or extra commas
+ tags = tags.strip('"\'').strip()
+ if tags.endswith(','):
+ tags = tags[:-1]
+ logger.info(f"Generated tags: {tags}")
+ return tags
except Exception as err:
- logger.error(f"Failed to get response from LLM: {err}")
- raise err
+ logger.error(f"Failed to generate blog tags: {err}")
+ return "content, blog" # Fallback tags
+
+
+def get_blog_categories(blog_article):
+ """
+ Identify the most appropriate blog categories for the article.
+
+ Args:
+ blog_article (str): The content of the blog article
+
+ Returns:
+ str: Comma-separated list of relevant categories
+ """
+ logger.info("Generating blog categories")
+
+ # Extract the first 2000 characters for category generation
+ snippet = blog_article[:2000] if len(blog_article) > 2000 else blog_article
+
+ prompt = f"""As a content strategist, identify the 2-3 most appropriate high-level categories for this blog.
+
+REQUIREMENTS:
+1. Choose broad, established categories used in content organization
+2. Select categories that best represent the main themes of the article
+3. Consider the target audience and their interests
+4. Focus on categories that would help with site navigation
+5. Aim for a primary category and 1-2 supporting categories
+
+EXAMPLES OF GOOD CATEGORIES:
+- Marketing, Social Media, Strategy
+- Finance, Personal Budgeting, Money Management
+- Productivity, Remote Work, Business
+
+CONTENT TO ANALYZE:
+"{snippet}"
+
+Reply with ONLY the categories as a comma-separated list (e.g., "Category1, Category2, Category3"). Provide 2-3 categories total.
+"""
+ try:
+ categories = llm_text_gen(prompt)
+ # Clean up any quotes or extra commas
+ categories = categories.strip('"\'').strip()
+ if categories.endswith(','):
+ categories = categories[:-1]
+ logger.info(f"Generated categories: {categories}")
+ return categories
+ except Exception as err:
+ logger.error(f"Failed to generate blog categories: {err}")
+ return "General, Information" # Fallback categories
+
+
+def generate_blog_hashtags(blog_article):
+ """
+ Generate social media hashtags for promoting the blog article.
+
+ Args:
+ blog_article (str): The content of the blog article
+
+ Returns:
+ str: Space-separated list of hashtags starting with #
+ """
+ logger.info("Generating social media hashtags")
+
+ # Extract the first 2000 characters for hashtag generation
+ snippet = blog_article[:2000] if len(blog_article) > 2000 else blog_article
+
+ prompt = f"""As a social media strategist, create 5-7 effective hashtags for this blog content.
+
+REQUIREMENTS:
+1. Mix of popular and niche hashtags for better visibility
+2. Include industry-specific and trending hashtags where relevant
+3. Avoid overly generic hashtags (like #content or #blog)
+4. Format each hashtag with # symbol and camelCase or separate words
+5. Include at least one branded or campaign-style hashtag
+
+EXAMPLES OF EFFECTIVE HASHTAG SETS:
+- #EmailMarketing #ROITips #DigitalStrategy #MarketingTips #GrowthHacking #EmailROI
+- #RemoteWork #ProductivityTips #FutureOfWork #WorkFromHome #RemoteProductivity #HRInsights
+
+CONTENT TO ANALYZE:
+"{snippet}"
+
+Reply with ONLY the hashtags, each starting with # and separated by spaces. Provide 5-7 hashtags total.
+"""
+ try:
+ hashtags = llm_text_gen(prompt)
+ # Clean up any quotes or extra spaces
+ hashtags = hashtags.strip('"\'').strip()
+ # Ensure all hashtags start with #
+ if not hashtags.startswith('#'):
+ hashtags = ' '.join([f"#{tag.strip('#')}" for tag in hashtags.split()])
+ logger.info(f"Generated hashtags: {hashtags}")
+ return hashtags
+ except Exception as err:
+ logger.error(f"Failed to generate blog hashtags: {err}")
+ return "#content #blog" # Fallback hashtags
+
+
+def generate_blog_slug(blog_title):
+ """
+ Generate an SEO-friendly URL slug from the blog title.
+
+ Args:
+ blog_title (str): The title of the blog article
+
+ Returns:
+ str: An SEO-friendly URL slug
+ """
+ logger.info("Generating SEO-friendly URL slug")
+
+ try:
+ # Use a prompt to generate a customized slug
+ prompt = f"""As an SEO specialist, create an SEO-friendly URL slug for this blog title: "{blog_title}"
+
+REQUIREMENTS:
+1. Keep it under 60 characters
+2. Use only lowercase letters, numbers, and hyphens
+3. Include primary keywords near the beginning
+4. Remove all unnecessary words (a, the, and, or, but, etc.)
+5. Ensure it's human-readable and descriptive
+
+EXAMPLES:
+- Title: "10 Effective Ways to Improve Your Email Marketing ROI This Quarter"
+ Slug: "improve-email-marketing-roi"
+
+- Title: "Why Most Remote Workers Are More Productive According to New Research"
+ Slug: "remote-workers-productivity-research"
+
+Reply with ONLY the slug and no other text or explanation.
+"""
+ slug = llm_text_gen(prompt)
+
+ # Clean up and normalize the slug
+ slug = slug.strip('"\'').strip()
+
+ # If the LLM didn't create a proper slug, do it programmatically
+ if not re.match(r'^[a-z0-9-]+$', slug):
+ # Fallback to simple programmatic slug creation
+ slug = blog_title.lower()
+ # Remove special characters
+ slug = re.sub(r'[^a-z0-9\s-]', '', slug)
+ # Replace spaces with hyphens
+ slug = re.sub(r'\s+', '-', slug)
+ # Remove redundant hyphens
+ slug = re.sub(r'-+', '-', slug)
+ # Limit length to 60 characters
+ slug = slug[:60].strip('-')
+
+ logger.info(f"Generated slug: {slug}")
+ return slug
+ except Exception as err:
+ logger.error(f"Failed to generate blog slug: {err}")
+ # Create a simple slug programmatically as fallback
+ slug = blog_title.lower()
+ slug = re.sub(r'[^a-z0-9\s-]', '', slug)
+ slug = re.sub(r'\s+', '-', slug)
+ slug = re.sub(r'-+', '-', slug)
+ slug = slug[:60].strip('-')
+ return slug
+
# Helper function to run the asyncio event loop within Streamlit
def run_async(coro):
diff --git a/lib/gpt_providers/text_generation/main_text_generation.py b/lib/gpt_providers/text_generation/main_text_generation.py
index 4fdef448..59bbd0c9 100644
--- a/lib/gpt_providers/text_generation/main_text_generation.py
+++ b/lib/gpt_providers/text_generation/main_text_generation.py
@@ -42,6 +42,8 @@ def llm_text_gen(prompt, system_prompt=None, json_struct=None):
top_p = 0.9
n = 1
fp = 16
+ frequency_penalty = 0.0
+ presence_penalty = 0.0
# Default blog characteristics
blog_tone = "Professional"
@@ -60,7 +62,16 @@ def llm_text_gen(prompt, system_prompt=None, json_struct=None):
model = llm_config[1] if llm_config[1] else model
temperature = llm_config[2] if llm_config[2] else temperature
max_tokens = llm_config[3] if llm_config[3] else max_tokens
- # Use default values for top_p, n, fp if they're not in the config
+
+ # Handle additional parameters with defaults if they're missing
+ if len(llm_config) > 4:
+ top_p = llm_config[4] if llm_config[4] else top_p
+ if len(llm_config) > 5:
+ # Try to get n parameter (could be either 'N' or 'n' in config)
+ n = llm_config[5] if llm_config[5] else n
+ if len(llm_config) > 6:
+ frequency_penalty = llm_config[6] if llm_config[6] else frequency_penalty
+
logger.debug(f"[llm_text_gen] LLM Config loaded: Provider={gpt_provider}, Model={model}, Temp={temperature}")
except Exception as err:
logger.warning(f"[llm_text_gen] Couldn't load LLM config completely, using defaults where needed: {err}")
diff --git a/lib/utils/api_key_manager/manager.py b/lib/utils/api_key_manager/manager.py
index 3f66fbaa..d429642f 100644
--- a/lib/utils/api_key_manager/manager.py
+++ b/lib/utils/api_key_manager/manager.py
@@ -144,7 +144,7 @@ class APIKeyManager:
'ANTHROPIC_API_KEY',
'MISTRAL_API_KEY',
# Research Providers
- 'SERPAPI_KEY',
+ 'SERPER_API_KEY',
'TAVILY_API_KEY',
'METAPHOR_API_KEY',
'FIRECRAWL_API_KEY'
diff --git a/lib/workspace/alwrity_config/main_config.json b/lib/workspace/alwrity_config/main_config.json
index 02d9f392..698c9020 100644
--- a/lib/workspace/alwrity_config/main_config.json
+++ b/lib/workspace/alwrity_config/main_config.json
@@ -8,18 +8,21 @@
"Blog Output Format": "markdown"
},
"Blog Images Details": {
- "Image Generation Model": "stable-diffusion",
+ "Image Generation Model": "Gemini-AI",
"Number of Blog Images": 1,
"Image Style": "Realistic"
},
"LLM Options": {
"GPT Provider": "google",
- "Model": "gemini-1.5-flash-latest",
+ "Model": "gemini-2.0-flash",
"Temperature": 0.7,
"Max Tokens": 4000,
"Top-p": 0.9,
+ "N": 1,
"n": 1,
- "fp": 16
+ "fp": 16,
+ "Frequency Penalty": 0.0,
+ "Presence Penalty": 0.0
},
"Search Engine Parameters": {
"Geographic Location": "us",