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",