Alpha Subscription Implementation Plan
This commit is contained in:
@@ -1,639 +0,0 @@
|
||||
import os
|
||||
import streamlit as st
|
||||
from loguru import logger
|
||||
|
||||
from lib.utils.voice_processing import record_voice
|
||||
from lib.ai_writers.ai_blog_writer.blog_writer_styles import apply_blog_writer_styles
|
||||
from lib.ai_writers.ai_blog_writer.ai_blog_generator_utils import (
|
||||
CONFIG_PATH,
|
||||
load_config,
|
||||
get_search_params_from_config,
|
||||
get_blog_characteristics_from_config,
|
||||
get_blog_images_from_config,
|
||||
get_llm_options_from_config,
|
||||
process_input,
|
||||
handle_content_generation
|
||||
)
|
||||
|
||||
apply_blog_writer_styles()
|
||||
|
||||
def display_input_section():
|
||||
"""Display the input section with text area, file upload, and voice recording options."""
|
||||
# Main container with columns for better organization
|
||||
col1, col2, col3 = st.columns([2, 1.5, 0.5])
|
||||
|
||||
# First column: Keywords input
|
||||
with col1:
|
||||
st.markdown("### 📌 Content Source")
|
||||
user_input = st.text_area(
|
||||
'Power your content with keywords or a website URL',
|
||||
help='Provide keywords, a blog title, YouTube link, or web URL to generate targeted content.',
|
||||
placeholder="Examples:\n- Keywords: AI tools, digital marketing\n- Blog Title: The Future of AI in Marketing\n- YouTube Link: https://youtube.com/...\n- Web URL: https://example.com/...",
|
||||
height=150
|
||||
)
|
||||
|
||||
# Second column: File uploader
|
||||
with col2:
|
||||
st.markdown("### 📁 File Upload")
|
||||
uploaded_file = st.file_uploader(
|
||||
"Add files to enhance your content",
|
||||
type=["txt", "pdf", "docx", "jpg", "jpeg", "png", "mp3", "wav", "mp4", "mkv", "avi"],
|
||||
help='Upload documents, images, or media files to incorporate additional information in your blog.'
|
||||
)
|
||||
|
||||
# Third column: Voice input
|
||||
with col3:
|
||||
st.markdown("### 🎤 Voice")
|
||||
audio_input = record_voice()
|
||||
if audio_input:
|
||||
st.success("Voice recorded!")
|
||||
|
||||
return user_input, uploaded_file, audio_input
|
||||
|
||||
|
||||
def display_content_type_selection(inside_expander=False):
|
||||
"""Display the content type selection section and return the selected type.
|
||||
|
||||
Args:
|
||||
inside_expander (bool): If True, adjust heading levels for display inside an expander.
|
||||
"""
|
||||
# Content options in a cleaner layout
|
||||
if not inside_expander:
|
||||
st.markdown("### 🔧 Content Configuration")
|
||||
st.markdown("#### Select Content Type")
|
||||
else:
|
||||
st.markdown("#### Content Type")
|
||||
|
||||
# Content type selection with better UI
|
||||
content_type = st.radio(
|
||||
"Choose the format and length of your blog content",
|
||||
["Standard Blog Post", "Comprehensive Long-form", "AI Agent Team (Beta)"],
|
||||
horizontal=True,
|
||||
help="Standard: 800-1200 words | Long-form: 1500+ words | AI Agent: Experimental multi-perspective content"
|
||||
)
|
||||
|
||||
# Map the friendly content type names to the original options
|
||||
content_type_map = {
|
||||
"Standard Blog Post": "Normal-length content",
|
||||
"Comprehensive Long-form": "Long-form content",
|
||||
"AI Agent Team (Beta)": "Experimental - AI Agents team"
|
||||
}
|
||||
|
||||
return content_type, content_type_map[content_type]
|
||||
|
||||
|
||||
def display_content_characteristics_tab():
|
||||
"""Display the Content Characteristics tab and return the selected options."""
|
||||
st.markdown("#### Blog Content Characteristics")
|
||||
|
||||
# Load default values from configuration
|
||||
config_blog_chars = get_blog_characteristics_from_config()
|
||||
|
||||
# Blog length
|
||||
blog_length = st.number_input(
|
||||
"Blog Length (words)",
|
||||
min_value=500,
|
||||
max_value=5000,
|
||||
value=int(config_blog_chars.get("blog_length", 2000)),
|
||||
step=100,
|
||||
help="Target word count for your blog post"
|
||||
)
|
||||
|
||||
# Blog tone
|
||||
tone_options = ["Professional", "Casual", "Formal", "Conversational", "Authoritative", "Friendly"]
|
||||
default_tone = config_blog_chars.get("blog_tone", "Professional")
|
||||
default_tone_index = tone_options.index(default_tone) if default_tone in tone_options else 0
|
||||
|
||||
blog_tone = st.selectbox(
|
||||
"Blog Tone",
|
||||
options=tone_options,
|
||||
index=default_tone_index,
|
||||
help="The overall tone and style of your blog content"
|
||||
)
|
||||
|
||||
# Blog demographic
|
||||
demographic_options = ["Professional", "General", "Technical", "Beginner", "Expert", "Student"]
|
||||
default_demo = config_blog_chars.get("blog_demographic", "Professional")
|
||||
default_demo_index = demographic_options.index(default_demo) if default_demo in demographic_options else 0
|
||||
|
||||
blog_demographic = st.selectbox(
|
||||
"Target Audience",
|
||||
options=demographic_options,
|
||||
index=default_demo_index,
|
||||
help="Who your blog content is primarily written for"
|
||||
)
|
||||
|
||||
# Blog type
|
||||
type_options = ["Informational", "How-to", "List", "Review", "Tutorial", "Opinion"]
|
||||
default_type = config_blog_chars.get("blog_type", "Informational")
|
||||
default_type_index = type_options.index(default_type) if default_type in type_options else 0
|
||||
|
||||
blog_type = st.selectbox(
|
||||
"Blog Type",
|
||||
options=type_options,
|
||||
index=default_type_index,
|
||||
help="The format and purpose of your blog content"
|
||||
)
|
||||
|
||||
# Blog language
|
||||
language_options = ["English", "Spanish", "French", "German", "Italian", "Portuguese"]
|
||||
default_lang = config_blog_chars.get("blog_language", "English")
|
||||
default_lang_index = language_options.index(default_lang) if default_lang in language_options else 0
|
||||
|
||||
blog_language = st.selectbox(
|
||||
"Blog Language",
|
||||
options=language_options,
|
||||
index=default_lang_index,
|
||||
help="The language your blog will be written in"
|
||||
)
|
||||
|
||||
# Blog output format
|
||||
format_options = ["markdown", "html", "plain text"]
|
||||
default_format = config_blog_chars.get("blog_output_format", "markdown").lower()
|
||||
default_format_index = format_options.index(default_format) if default_format in format_options else 0
|
||||
|
||||
blog_output_format = st.selectbox(
|
||||
"Output Format",
|
||||
options=format_options,
|
||||
index=default_format_index,
|
||||
help="The format in which the blog content will be generated"
|
||||
)
|
||||
|
||||
# Show current configuration source
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
st.success(f"✅ Using blog characteristics from configuration file")
|
||||
else:
|
||||
st.info("ℹ️ Using default blog characteristics (no configuration file found)")
|
||||
|
||||
return {
|
||||
"blog_length": blog_length,
|
||||
"blog_tone": blog_tone,
|
||||
"blog_demographic": blog_demographic,
|
||||
"blog_type": blog_type,
|
||||
"blog_language": blog_language,
|
||||
"blog_output_format": blog_output_format
|
||||
}
|
||||
|
||||
|
||||
def display_content_analysis_tab():
|
||||
"""Display the Content & Analysis Options tab and return the selected options."""
|
||||
st.markdown("#### Content & Analysis Options")
|
||||
|
||||
# Create two columns for better organization
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("**Content Enhancements**")
|
||||
create_seo_tags = st.checkbox(
|
||||
'✅ Generate SEO metadata',
|
||||
value=True,
|
||||
help='Create schema markup, meta tags, and social media metadata'
|
||||
)
|
||||
generate_social_media = st.checkbox(
|
||||
'✅ Create social media posts',
|
||||
value=False,
|
||||
help="Generate matching social content for Facebook, Twitter, and LinkedIn"
|
||||
)
|
||||
add_table_of_contents = st.checkbox(
|
||||
'✅ Add table of contents',
|
||||
value=True,
|
||||
help="Include an auto-generated table of contents at the beginning of the blog"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("**Analysis & Improvement**")
|
||||
content_analysis = st.checkbox(
|
||||
'✅ Perform content analysis',
|
||||
value=False,
|
||||
help="Include proofreading, readability score, and improvement suggestions"
|
||||
)
|
||||
enhance_readability = st.checkbox(
|
||||
'✅ Enhance readability',
|
||||
value=True,
|
||||
help="Optimize sentence structure and vocabulary for better readability"
|
||||
)
|
||||
fact_checking = st.checkbox(
|
||||
'✅ Basic fact verification',
|
||||
value=False,
|
||||
help="Verify key facts from multiple sources when possible"
|
||||
)
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown("**Formatting Options**")
|
||||
|
||||
# Create two columns for formatting options
|
||||
fmt_col1, fmt_col2 = st.columns(2)
|
||||
|
||||
with fmt_col1:
|
||||
section_headings = st.checkbox(
|
||||
'✅ Use section headings',
|
||||
value=True,
|
||||
help="Include clear section headings throughout the blog"
|
||||
)
|
||||
include_lists = st.checkbox(
|
||||
'✅ Use bullet points and lists',
|
||||
value=True,
|
||||
help="Format appropriate content as bullet points or numbered lists"
|
||||
)
|
||||
|
||||
with fmt_col2:
|
||||
include_quotes = st.checkbox(
|
||||
'✅ Include relevant quotes',
|
||||
value=False,
|
||||
help="Add expert quotes or important statements as blockquotes"
|
||||
)
|
||||
use_subheadings = st.checkbox(
|
||||
'✅ Use subheadings',
|
||||
value=True,
|
||||
help="Break down sections with descriptive subheadings"
|
||||
)
|
||||
|
||||
return {
|
||||
"create_seo_tags": create_seo_tags,
|
||||
"generate_social_media": generate_social_media,
|
||||
"add_table_of_contents": add_table_of_contents,
|
||||
"content_analysis": content_analysis,
|
||||
"enhance_readability": enhance_readability,
|
||||
"fact_checking": fact_checking,
|
||||
"section_headings": section_headings,
|
||||
"include_lists": include_lists,
|
||||
"include_quotes": include_quotes,
|
||||
"use_subheadings": use_subheadings
|
||||
}
|
||||
|
||||
|
||||
def display_blog_images_tab():
|
||||
"""Display the Blog Images Details tab and return the selected options."""
|
||||
st.markdown("#### Blog Images Settings")
|
||||
|
||||
# Load default values from configuration
|
||||
config_images = get_blog_images_from_config()
|
||||
|
||||
# Image generation model selection
|
||||
model_options = ["stable-diffusion", "dall-e", "midjourney", "imagen"]
|
||||
default_model = config_images.get("image_model", "stable-diffusion")
|
||||
default_model_index = model_options.index(default_model) if default_model in model_options else 0
|
||||
|
||||
image_model = st.selectbox(
|
||||
"Image Generation Model",
|
||||
options=model_options,
|
||||
index=default_model_index,
|
||||
help="AI model used to generate blog images"
|
||||
)
|
||||
|
||||
# Number of blog images
|
||||
num_images = st.number_input(
|
||||
"Number of Blog Images",
|
||||
min_value=0,
|
||||
max_value=10,
|
||||
value=config_images.get("num_images", 1),
|
||||
step=1,
|
||||
help="Number of images to generate for the blog"
|
||||
)
|
||||
|
||||
# Image style
|
||||
style_options = ["Realistic", "Artistic", "Cartoon", "Minimalist", "Corporate", "Vibrant"]
|
||||
default_style = config_images.get("image_style", "Realistic")
|
||||
default_style_index = style_options.index(default_style) if default_style in style_options else 0
|
||||
|
||||
image_style = st.selectbox(
|
||||
"Image Style",
|
||||
options=style_options,
|
||||
index=default_style_index,
|
||||
help="Visual style of the generated images"
|
||||
)
|
||||
|
||||
# Additional image options
|
||||
st.markdown("**Additional Image Options**")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
generate_featured = st.checkbox(
|
||||
'✅ Generate featured image',
|
||||
value=True,
|
||||
help="Create a featured header image for the blog"
|
||||
)
|
||||
add_captions = st.checkbox(
|
||||
'✅ Add image captions',
|
||||
value=True,
|
||||
help="Generate descriptive captions for each image"
|
||||
)
|
||||
|
||||
with col2:
|
||||
use_alt_text = st.checkbox(
|
||||
'✅ Generate alt text',
|
||||
value=True,
|
||||
help="Create accessibility alt text for all images"
|
||||
)
|
||||
optimize_images = st.checkbox(
|
||||
'✅ Optimize image placement',
|
||||
value=True,
|
||||
help="Intelligently place images throughout the content"
|
||||
)
|
||||
|
||||
# Show current configuration source
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
st.success(f"✅ Using image settings from configuration file")
|
||||
else:
|
||||
st.info("ℹ️ Using default image settings (no configuration file found)")
|
||||
|
||||
return {
|
||||
"image_model": image_model,
|
||||
"num_images": num_images,
|
||||
"image_style": image_style,
|
||||
"generate_featured": generate_featured,
|
||||
"add_captions": add_captions,
|
||||
"use_alt_text": use_alt_text,
|
||||
"optimize_placement": optimize_images
|
||||
}
|
||||
|
||||
|
||||
def display_llm_options_tab():
|
||||
"""Display the LLM Options tab and return the selected options."""
|
||||
st.markdown("#### Language Model Settings")
|
||||
|
||||
# Load default values from configuration
|
||||
config_llm = get_llm_options_from_config()
|
||||
|
||||
# LLM provider selection
|
||||
provider_options = ["google", "openai", "anthropic", "local"]
|
||||
default_provider = config_llm.get("provider", "google")
|
||||
default_provider_index = provider_options.index(default_provider) if default_provider in provider_options else 0
|
||||
|
||||
llm_provider = st.selectbox(
|
||||
"AI Provider",
|
||||
options=provider_options,
|
||||
index=default_provider_index,
|
||||
help="The AI provider to use for content generation"
|
||||
)
|
||||
|
||||
# Model selection (dynamic based on provider)
|
||||
if llm_provider == "google":
|
||||
model_options = ["gemini-1.5-flash-latest", "gemini-1.5-pro-latest", "gemini-pro"]
|
||||
elif llm_provider == "openai":
|
||||
model_options = ["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"]
|
||||
elif llm_provider == "anthropic":
|
||||
model_options = ["claude-3-opus", "claude-3-sonnet", "claude-3-haiku"]
|
||||
else:
|
||||
model_options = ["llama-3-70b", "mistral-large", "local-model"]
|
||||
|
||||
default_model = config_llm.get("model", "gemini-1.5-flash-latest")
|
||||
default_model_index = 0
|
||||
if default_model in model_options:
|
||||
default_model_index = model_options.index(default_model)
|
||||
|
||||
llm_model = st.selectbox(
|
||||
"AI Model",
|
||||
options=model_options,
|
||||
index=default_model_index,
|
||||
help="The specific AI model to use for content generation"
|
||||
)
|
||||
|
||||
# Create two columns for temperature and max tokens
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
# Temperature setting
|
||||
temperature = st.slider(
|
||||
"Temperature",
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
value=config_llm.get("temperature", 0.7),
|
||||
step=0.1,
|
||||
help="Controls randomness: lower values are more deterministic, higher values more creative"
|
||||
)
|
||||
|
||||
with col2:
|
||||
# Max tokens
|
||||
max_tokens = st.number_input(
|
||||
"Max Tokens",
|
||||
min_value=1000,
|
||||
max_value=32000,
|
||||
value=config_llm.get("max_tokens", 4000),
|
||||
step=1000,
|
||||
help="Maximum length of generated content (in tokens)"
|
||||
)
|
||||
|
||||
# Advanced LLM options
|
||||
st.markdown("---")
|
||||
st.markdown("**Advanced LLM Options**")
|
||||
show_advanced_llm = st.checkbox("Show advanced LLM parameters", value=False)
|
||||
|
||||
advanced_params = {}
|
||||
if show_advanced_llm:
|
||||
# Top-p (nucleus sampling)
|
||||
top_p = st.slider(
|
||||
"Top-p (Nucleus Sampling)",
|
||||
min_value=0.1,
|
||||
max_value=1.0,
|
||||
value=0.9,
|
||||
step=0.1,
|
||||
help="Controls diversity via nucleus sampling: 1.0 considers all tokens, lower values restrict to more likely tokens"
|
||||
)
|
||||
|
||||
# Top-k
|
||||
top_k = st.slider(
|
||||
"Top-k",
|
||||
min_value=1,
|
||||
max_value=100,
|
||||
value=40,
|
||||
step=1,
|
||||
help="Controls diversity by limiting to top k tokens: higher values allow more diversity"
|
||||
)
|
||||
|
||||
# Presence penalty
|
||||
presence_penalty = st.slider(
|
||||
"Presence Penalty",
|
||||
min_value=-2.0,
|
||||
max_value=2.0,
|
||||
value=0.0,
|
||||
step=0.1,
|
||||
help="Penalizes repeated tokens: positive values discourage repetition"
|
||||
)
|
||||
|
||||
advanced_params = {
|
||||
"top_p": top_p,
|
||||
"top_k": top_k,
|
||||
"presence_penalty": presence_penalty
|
||||
}
|
||||
|
||||
# Show current configuration source
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
st.success(f"✅ Using LLM settings from configuration file")
|
||||
else:
|
||||
st.info("ℹ️ Using default LLM settings (no configuration file found)")
|
||||
|
||||
return {
|
||||
"provider": llm_provider,
|
||||
"model": llm_model,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
**advanced_params
|
||||
}
|
||||
|
||||
|
||||
def display_search_settings_tab():
|
||||
"""Display the Search Settings tab and return the selected options."""
|
||||
st.markdown("#### AI Search Configuration")
|
||||
st.markdown("Control how the AI researches your topic")
|
||||
|
||||
# Load default values from configuration
|
||||
config_search_params = get_search_params_from_config()
|
||||
|
||||
# Number of search results
|
||||
max_results = st.slider(
|
||||
"Maximum Results",
|
||||
min_value=5,
|
||||
max_value=30,
|
||||
value=config_search_params.get("max_results", 10),
|
||||
step=5,
|
||||
help="Maximum number of search results to use for research"
|
||||
)
|
||||
|
||||
# Search depth
|
||||
search_depth = st.radio(
|
||||
"Search Depth",
|
||||
options=["basic", "advanced"],
|
||||
index=0,
|
||||
horizontal=True,
|
||||
help="Basic: Faster but less comprehensive. Advanced: More thorough but slower."
|
||||
)
|
||||
|
||||
# Include domains
|
||||
include_domains = st.text_input(
|
||||
"Include Domains (Optional)",
|
||||
value="",
|
||||
help="Comma-separated list of domains to prioritize in search (e.g., wikipedia.org,nih.gov)"
|
||||
)
|
||||
|
||||
# Time range - use value from config
|
||||
time_options = ["day", "week", "month", "year", "all"]
|
||||
default_time_index = time_options.index(config_search_params.get("time_range", "year")) if config_search_params.get("time_range", "year") in time_options else 3 # Default to "year" (index 3)
|
||||
|
||||
time_range = st.select_slider(
|
||||
"Time Range",
|
||||
options=time_options,
|
||||
value=time_options[default_time_index],
|
||||
help="Limit search results to a specific time period"
|
||||
)
|
||||
|
||||
# Show current configuration source
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
st.success(f"✅ Using search defaults from configuration file")
|
||||
else:
|
||||
st.info("ℹ️ Using default search settings (no configuration file found)")
|
||||
|
||||
# Replace expander with checkbox for configuration display
|
||||
show_config = st.checkbox("Show configuration details", value=False)
|
||||
if show_config:
|
||||
st.markdown("""
|
||||
**Configuration File Location**
|
||||
Search parameters are loaded from the main configuration file at:
|
||||
`lib/workspace/alwrity_config/main_config.json`
|
||||
|
||||
You can modify this file to change the default search settings.
|
||||
""")
|
||||
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
try:
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
config_content = f.read()
|
||||
st.code(config_content, language="json")
|
||||
except:
|
||||
st.warning("Could not read configuration file")
|
||||
|
||||
st.info("These settings control how the AI performs web research for your content. More thorough searches may take longer but produce better results.")
|
||||
|
||||
# Process include_domains from string to list if provided
|
||||
domains_list = []
|
||||
if include_domains:
|
||||
domains_list = [domain.strip() for domain in include_domains.split(",") if domain.strip()]
|
||||
|
||||
return {
|
||||
"max_results": max_results,
|
||||
"search_depth": search_depth,
|
||||
"time_range": time_range,
|
||||
"include_domains": domains_list
|
||||
}
|
||||
|
||||
|
||||
def display_advanced_options():
|
||||
"""Display all advanced options tabs and return the selected configurations."""
|
||||
|
||||
with st.expander("⚙️ Advanced Options for Personalization, Analysis, Images, LLM, and Search", expanded=False):
|
||||
content_type, selected_content_type = display_content_type_selection(inside_expander=True)
|
||||
|
||||
tabs = st.tabs(["Personalization", "Analysis Options", "Blog Images Details", "LLM Options", "Search Settings"])
|
||||
|
||||
with tabs[0]: # Content Characteristics
|
||||
blog_params = display_content_characteristics_tab()
|
||||
|
||||
with tabs[1]: # Combined Content & Analysis Options
|
||||
content_analysis_params = display_content_analysis_tab()
|
||||
|
||||
with tabs[2]: # Blog Images Details
|
||||
image_params = display_blog_images_tab()
|
||||
|
||||
with tabs[3]: # LLM Options
|
||||
llm_params = display_llm_options_tab()
|
||||
|
||||
with tabs[4]: # Search Settings
|
||||
search_params = display_search_settings_tab()
|
||||
|
||||
return content_type, selected_content_type, blog_params, content_analysis_params, image_params, llm_params, search_params
|
||||
|
||||
|
||||
def blog_from_keyword():
|
||||
"""Input blog keywords, research and write a factual blog with enhanced UI."""
|
||||
|
||||
# Get user inputs
|
||||
user_input, uploaded_file, audio_input = display_input_section()
|
||||
|
||||
# Display advanced options and get configurations
|
||||
content_type, selected_content_type, blog_params, content_analysis_params, image_params, llm_params, search_params = display_advanced_options()
|
||||
|
||||
# Generate button with icon and clearer purpose
|
||||
st.markdown("") # Add spacing
|
||||
generate_pressed = st.button("✨ Generate Blog Content", use_container_width=True)
|
||||
|
||||
# Processing logic
|
||||
if generate_pressed:
|
||||
st.empty()
|
||||
|
||||
if not uploaded_file and not user_input and not audio_input:
|
||||
st.error("Please provide at least one input source (keywords, file, or voice recording)")
|
||||
st.stop()
|
||||
|
||||
input_type = process_input(user_input, uploaded_file)
|
||||
|
||||
# Use the utility function to handle content generation
|
||||
handle_content_generation(input_type, user_input, uploaded_file, search_params, blog_params, selected_content_type)
|
||||
|
||||
|
||||
def ai_blog_writer_page():
|
||||
"""Render the AI Blog Writer page with enhanced styling."""
|
||||
logger.info("Rendering AI Blog Writer page")
|
||||
|
||||
# Apply shared blog writer styles
|
||||
apply_blog_writer_styles()
|
||||
|
||||
# Back button with icon
|
||||
if st.button("← Back to Dashboard", key="back_to_dashboard"):
|
||||
logger.info("User clicked back button, returning to ai writer dashboard")
|
||||
st.query_params.clear()
|
||||
st.rerun()
|
||||
|
||||
# Enhanced header with icon
|
||||
st.markdown("""
|
||||
<div class="page-header">
|
||||
<h1>✍️ AI Blog Writer</h1>
|
||||
<p>Create engaging, SEO-optimized blog content with AI assistance. Our advanced algorithms help you generate high-quality, relevant articles for any topic or niche.</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Call the blog generator function with enhanced UI
|
||||
logger.info("Calling blog_from_keyword function")
|
||||
blog_from_keyword()
|
||||
|
||||
logger.info("Finished rendering AI Blog Writer page")
|
||||
@@ -1,867 +0,0 @@
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
from loguru import logger
|
||||
import PyPDF2
|
||||
import streamlit as st
|
||||
import tiktoken
|
||||
import openai
|
||||
from datetime import datetime
|
||||
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
# Remove the circular import
|
||||
# from lib.ai_writers.ai_blog_writer.keywords_to_blog_streamlit import write_blog_from_keywords
|
||||
from lib.ai_writers.speech_to_blog.main_audio_to_blog import generate_audio_blog
|
||||
from lib.ai_writers.long_form_ai_writer import long_form_generator
|
||||
from lib.ai_writers.web_url_ai_writer import blog_from_url
|
||||
from lib.ai_writers.image_ai_writer import blog_from_image
|
||||
from .blog_from_google_serp import write_blog_google_serp
|
||||
from lib.blog_metadata.get_blog_metadata import blog_metadata
|
||||
from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||
|
||||
# Constants
|
||||
CONFIG_PATH = os.path.join("lib", "workspace", "alwrity_config", "main_config.json")
|
||||
DEFAULT_CONFIG = {
|
||||
"Search Engine Parameters": {
|
||||
"Geographic Location": "us",
|
||||
"Search Language": "en",
|
||||
"Number of Results": 10,
|
||||
"Time Range": "year"
|
||||
}
|
||||
}
|
||||
|
||||
# Function to load configuration from JSON file
|
||||
def load_config():
|
||||
"""Load configuration from the main config JSON file."""
|
||||
try:
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
config = json.load(f)
|
||||
logger.info(f"Loaded configuration from {CONFIG_PATH}")
|
||||
return config
|
||||
else:
|
||||
logger.warning(f"Configuration file not found at {CONFIG_PATH}, using defaults")
|
||||
return DEFAULT_CONFIG
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading configuration: {str(e)}")
|
||||
return DEFAULT_CONFIG
|
||||
|
||||
# Function to get search parameters from config
|
||||
def get_search_params_from_config():
|
||||
"""Extract search parameters from the main configuration."""
|
||||
config = load_config()
|
||||
search_params = config.get("Search Engine Parameters", {})
|
||||
|
||||
# Map config values to expected parameter names
|
||||
result = {
|
||||
"max_results": search_params.get("Number of Results", 10),
|
||||
"time_range": search_params.get("Time Range", "year").lower(),
|
||||
"geo": search_params.get("Geographic Location", "us"),
|
||||
"language": search_params.get("Search Language", "en")
|
||||
}
|
||||
|
||||
# Normalize time_range to match our options
|
||||
time_map = {
|
||||
"day": "day",
|
||||
"week": "week",
|
||||
"month": "month",
|
||||
"year": "year",
|
||||
"anytime": "all",
|
||||
"all": "all"
|
||||
}
|
||||
result["time_range"] = time_map.get(result["time_range"].lower(), "year")
|
||||
|
||||
logger.info(f"Using search parameters from config: {result}")
|
||||
return result
|
||||
|
||||
# Function to get blog content characteristics from config
|
||||
def get_blog_characteristics_from_config():
|
||||
"""Extract blog content characteristics from the main configuration."""
|
||||
config = load_config()
|
||||
blog_characteristics = config.get("Blog Content Characteristics", {})
|
||||
|
||||
# Map config values to expected parameter names
|
||||
result = {
|
||||
"blog_length": blog_characteristics.get("Blog Length", "2000"),
|
||||
"blog_tone": blog_characteristics.get("Blog Tone", "Professional"),
|
||||
"blog_demographic": blog_characteristics.get("Blog Demographic", "Professional"),
|
||||
"blog_type": blog_characteristics.get("Blog Type", "Informational"),
|
||||
"blog_language": blog_characteristics.get("Blog Language", "English"),
|
||||
"blog_output_format": blog_characteristics.get("Blog Output Format", "markdown")
|
||||
}
|
||||
|
||||
logger.info(f"Using blog characteristics from config: {result}")
|
||||
return result
|
||||
|
||||
# Function to get blog image details from config
|
||||
def get_blog_images_from_config():
|
||||
"""Extract blog image details from the main configuration."""
|
||||
config = load_config()
|
||||
blog_images = config.get("Blog Images Details", {})
|
||||
|
||||
# Map config values to expected parameter names
|
||||
result = {
|
||||
"image_model": blog_images.get("Image Generation Model", "stable-diffusion"),
|
||||
"num_images": int(blog_images.get("Number of Blog Images", 1)),
|
||||
"image_style": blog_images.get("Image Style", "Realistic")
|
||||
}
|
||||
|
||||
logger.info(f"Using blog image details from config: {result}")
|
||||
return result
|
||||
|
||||
# Function to get LLM options from config
|
||||
def get_llm_options_from_config():
|
||||
"""Extract LLM options from the main configuration."""
|
||||
config = load_config()
|
||||
llm_options = config.get("LLM Options", {})
|
||||
|
||||
# Map config values to expected parameter names
|
||||
result = {
|
||||
"provider": llm_options.get("GPT Provider", "google"),
|
||||
"model": llm_options.get("Model", "gemini-1.5-flash-latest"),
|
||||
"temperature": float(llm_options.get("Temperature", 0.7)),
|
||||
"max_tokens": int(llm_options.get("Max Tokens", 4000))
|
||||
}
|
||||
|
||||
logger.info(f"Using LLM options from config: {result}")
|
||||
return result
|
||||
|
||||
# Split a text into smaller chunks of size n, preferably ending at the end of a sentence
|
||||
def create_chunks(text, n, tokenizer):
|
||||
tokens = tokenizer.encode(text)
|
||||
"""Yield successive n-sized chunks from text."""
|
||||
i = 0
|
||||
while i < len(tokens):
|
||||
# Find the nearest end of sentence within a range of 0.5 * n and 1.5 * n tokens
|
||||
j = min(i + int(1.5 * n), len(tokens))
|
||||
while j > i + int(0.5 * n):
|
||||
# Decode the tokens and check for full stop or newline
|
||||
chunk = tokenizer.decode(tokens[i:j])
|
||||
if chunk.endswith(".") or chunk.endswith("\n"):
|
||||
break
|
||||
j -= 1
|
||||
# If no end of sentence found, use n tokens as the chunk size
|
||||
if j == i + int(0.5 * n):
|
||||
j = min(i + n, len(tokens))
|
||||
yield tokens[i:j]
|
||||
i = j
|
||||
|
||||
|
||||
def extract_chunk(document, template_prompt):
|
||||
""" Chunking for large documents, exceed context window"""
|
||||
prompt = template_prompt.replace('<document>', document)
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to get response from LLM: {err}")
|
||||
raise
|
||||
|
||||
|
||||
def blog_from_pdf(pdf_text):
|
||||
"""
|
||||
Load in a long PDF and extract key information.
|
||||
Chunk up document and process each chunk, then combine them.
|
||||
"""
|
||||
template_prompt=f'''Extract key pieces of information from the given document.
|
||||
|
||||
When you extract a key piece of information, include the closest page number.
|
||||
Ex: Extracted Information (Page number)
|
||||
\n\nDocument: \"\"\"<document>\"\"\"\n\n'''
|
||||
|
||||
# Initialize tokenizer
|
||||
tokenizer = tiktoken.get_encoding("cl100k_base")
|
||||
results = []
|
||||
|
||||
chunks = create_chunks(pdf_text, 1000, tokenizer)
|
||||
text_chunks = [tokenizer.decode(chunk) for chunk in chunks]
|
||||
|
||||
for chunk in text_chunks:
|
||||
try:
|
||||
results.append(extract_chunk(chunk, template_prompt))
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing chunk: {e}")
|
||||
# Continue with other chunks even if one fails
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# Input validation functions
|
||||
def is_youtube_link(text):
|
||||
"""Check if text is a valid YouTube link."""
|
||||
if text is not None:
|
||||
youtube_regex = re.compile(r'(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})')
|
||||
return youtube_regex.match(text)
|
||||
return False
|
||||
|
||||
|
||||
def is_web_link(text):
|
||||
"""Check if text is a valid web link."""
|
||||
if text is not None:
|
||||
web_regex = re.compile(r'(https?://)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)')
|
||||
return web_regex.match(text)
|
||||
return False
|
||||
|
||||
|
||||
def process_input(input_text, uploaded_file):
|
||||
"""
|
||||
Determine the type of input provided by the user.
|
||||
|
||||
Args:
|
||||
input_text (str): The text input from the user
|
||||
uploaded_file: The file uploaded by the user
|
||||
|
||||
Returns:
|
||||
str: The determined input type ("youtube_url", "web_url", "keywords", "PDF_file", "image_file", "audio_file", "video_file", or None)
|
||||
"""
|
||||
# Process text input
|
||||
if input_text:
|
||||
if is_youtube_link(input_text):
|
||||
if input_text.startswith("https://www.youtube.com/") or input_text.startswith("http://www.youtube.com/"):
|
||||
return "youtube_url"
|
||||
else:
|
||||
st.error("Invalid YouTube URL. Please enter a valid URL.")
|
||||
return None
|
||||
elif is_web_link(input_text):
|
||||
return "web_url"
|
||||
else:
|
||||
return "keywords"
|
||||
|
||||
# Process file input
|
||||
if uploaded_file is not None:
|
||||
file_details = {"filename": uploaded_file.name, "filetype": uploaded_file.type}
|
||||
st.write(file_details)
|
||||
|
||||
# Handle different file types
|
||||
if uploaded_file.type.startswith("text/"):
|
||||
content = uploaded_file.read().decode("utf-8")
|
||||
st.text(content)
|
||||
return "text_file"
|
||||
elif uploaded_file.type == "application/pdf":
|
||||
return "PDF_file"
|
||||
elif uploaded_file.type in ["application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/msword"]:
|
||||
st.write("Word document uploaded. Add your DOCX processing logic here.")
|
||||
return "word_file"
|
||||
elif uploaded_file.type.startswith("image/"):
|
||||
st.image(uploaded_file)
|
||||
return "image_file"
|
||||
elif uploaded_file.type.startswith("audio/"):
|
||||
st.audio(uploaded_file)
|
||||
return "audio_file"
|
||||
elif uploaded_file.type.startswith("video/"):
|
||||
st.video(uploaded_file)
|
||||
return "video_file"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Content processing functions
|
||||
def process_keywords_input(user_input, search_params, blog_params, selected_content_type):
|
||||
"""Process keywords input and generate content based on the selected options."""
|
||||
if not user_input or len(user_input.split()) < 2:
|
||||
st.error('Please provide at least two keywords for best results')
|
||||
return False
|
||||
|
||||
# Check for dialog states and handle them directly
|
||||
if st.session_state.get("show_title_dialog", False):
|
||||
st.warning("Please use the main function to handle title refinement dialog")
|
||||
# Clear the dialog state to avoid getting stuck
|
||||
st.session_state.show_title_dialog = False
|
||||
return False
|
||||
|
||||
if st.session_state.get("show_meta_dialog", False):
|
||||
st.warning("Please use the main function to handle meta description refinement dialog")
|
||||
# Clear the dialog state to avoid getting stuck
|
||||
st.session_state.show_meta_dialog = False
|
||||
return False
|
||||
|
||||
if st.session_state.get("show_snippet_dialog", False):
|
||||
st.warning("Please use the main function to handle structured data dialog")
|
||||
# Clear the dialog state to avoid getting stuck
|
||||
st.session_state.show_snippet_dialog = False
|
||||
return False
|
||||
|
||||
try:
|
||||
if selected_content_type == "Normal-length content":
|
||||
st.subheader("Your Generated Blog Post")
|
||||
logger.info(f"Generating standard blog post with parameters: {blog_params}")
|
||||
|
||||
# Use a direct approach to generate blog content to avoid nested expanders
|
||||
# Instead of importing write_blog_from_keywords which contains many expanders
|
||||
try:
|
||||
# Show simplified progress UI
|
||||
progress_container = st.container()
|
||||
with progress_container:
|
||||
progress_bar = st.progress(0)
|
||||
status_text = st.empty()
|
||||
|
||||
# Step 1: Initialize and show progress
|
||||
status_text.info("Initializing blog generation...")
|
||||
progress_bar.progress(0.1)
|
||||
|
||||
# Initialize parameters
|
||||
from .blog_ai_research_utils import initialize_parameters
|
||||
search_params, blog_params = initialize_parameters(search_params, blog_params)
|
||||
|
||||
# Step 2: Research phase
|
||||
status_text.info("Researching your topic...")
|
||||
progress_bar.progress(0.2)
|
||||
|
||||
# Perform research using direct function calls
|
||||
from .blog_ai_research_utils import do_google_serp_search, do_tavily_ai_search
|
||||
|
||||
# Do Google search
|
||||
status_text.info("Searching Google for relevant information...")
|
||||
google_result = do_google_serp_search(user_input, max_results=search_params.get("max_results", 10))
|
||||
google_success = google_result and 'results' in google_result and google_result['results']
|
||||
progress_bar.progress(0.4)
|
||||
|
||||
# Do Tavily search if needed
|
||||
tavily_result = None
|
||||
tavily_success = False
|
||||
if not google_success:
|
||||
status_text.info("Performing additional research with Tavily...")
|
||||
tavily_result, _, _ = do_tavily_ai_search(
|
||||
user_input,
|
||||
max_results=search_params.get("max_results", 10),
|
||||
search_depth=search_params.get("search_depth", "basic")
|
||||
)
|
||||
tavily_success = tavily_result is not None
|
||||
progress_bar.progress(0.5)
|
||||
|
||||
# Step 3: Generate content
|
||||
status_text.info("Generating blog content...")
|
||||
progress_bar.progress(0.6)
|
||||
|
||||
# Generate content based on search results
|
||||
from .blog_from_google_serp import write_blog_google_serp
|
||||
|
||||
if google_success:
|
||||
blog_content = write_blog_google_serp(user_input, google_result['results'], blog_params=blog_params)
|
||||
elif tavily_success:
|
||||
blog_content = write_blog_google_serp(user_input, tavily_result, blog_params=blog_params)
|
||||
else:
|
||||
status_text.error("Failed to gather research data. Please try again.")
|
||||
return False
|
||||
|
||||
# Step 4: Generate metadata and image
|
||||
status_text.info("Adding metadata and final touches...")
|
||||
progress_bar.progress(0.8)
|
||||
|
||||
# Import functions from keywords_to_blog_streamlit
|
||||
from .keywords_to_blog_streamlit import generate_audio_version
|
||||
|
||||
# Define a simple update_progress function for compatibility
|
||||
def simple_update_progress(step, total, message):
|
||||
status_text.info(message)
|
||||
progress_bar.progress(step / total)
|
||||
|
||||
# Generate metadata and image
|
||||
# Import only essential functions needed for core processing
|
||||
from .ai_blog_generator_utils import generate_blog_metadata, generate_blog_image
|
||||
try:
|
||||
# Create a proper status object
|
||||
with st.status("Generating metadata and image...", expanded=True) as status:
|
||||
# Generate metadata
|
||||
blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = generate_blog_metadata(
|
||||
blog_content, user_input, status)
|
||||
|
||||
# Generate featured image if metadata is available
|
||||
generated_image_filepath = None
|
||||
if blog_title and blog_meta_desc:
|
||||
generated_image_filepath = generate_blog_image(
|
||||
blog_title, blog_meta_desc, blog_content, status, blog_tags)
|
||||
|
||||
# Save blog content to file
|
||||
saved_blog_to_file = None
|
||||
from ...blog_postprocessing.save_blog_to_file import save_blog_to_file
|
||||
if blog_title and blog_meta_desc:
|
||||
saved_blog_to_file = save_blog_to_file(
|
||||
blog_content, blog_title, blog_meta_desc, blog_tags,
|
||||
blog_categories, generated_image_filepath)
|
||||
|
||||
# Create metadata dictionary with string conversions for table display
|
||||
metadata = {
|
||||
"blog_title": blog_title or "",
|
||||
"blog_meta_desc": blog_meta_desc or "",
|
||||
"blog_tags": ", ".join(blog_tags) if isinstance(blog_tags, list) else str(blog_tags or ""),
|
||||
"blog_categories": ", ".join(blog_categories) if isinstance(blog_categories, list) else str(blog_categories or ""),
|
||||
"blog_hashtags": blog_hashtags or "",
|
||||
"blog_slug": blog_slug or ""
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating metadata or image: {e}")
|
||||
metadata = {
|
||||
"blog_title": "Generated Blog",
|
||||
"blog_meta_desc": "",
|
||||
"blog_tags": "",
|
||||
"blog_categories": "",
|
||||
"blog_hashtags": "",
|
||||
"blog_slug": ""
|
||||
}
|
||||
generated_image_filepath = None
|
||||
saved_blog_to_file = None
|
||||
|
||||
# Clear progress indicators
|
||||
progress_bar.empty()
|
||||
status_text.empty()
|
||||
|
||||
# Final message
|
||||
final_message = st.empty()
|
||||
final_message.success("Blog generation complete!")
|
||||
|
||||
# Display blog content first (without using expanders)
|
||||
st.markdown("## Content")
|
||||
st.markdown(blog_content)
|
||||
|
||||
# Show file save information if available
|
||||
if saved_blog_to_file:
|
||||
st.success(f"✅ Blog saved to: {saved_blog_to_file}")
|
||||
|
||||
# Add the audio generation button
|
||||
st.markdown("---")
|
||||
audio_col1, audio_col2 = st.columns([1, 3])
|
||||
with audio_col1:
|
||||
generate_audio_button = st.button("🔊 Generate Audio Version", use_container_width=True)
|
||||
|
||||
with audio_col2:
|
||||
if generate_audio_button:
|
||||
generate_audio_version(blog_content)
|
||||
|
||||
# Display metadata success message
|
||||
if metadata["blog_title"]:
|
||||
st.success(f"✅ Generated metadata for: {metadata['blog_title']}")
|
||||
|
||||
# Display metadata table (without nesting expanders)
|
||||
st.markdown("---")
|
||||
st.subheader("🏷️ Blog SEO Metadata")
|
||||
st.table({
|
||||
"Metadata": ["Blog Title", "Meta Description", "Tags", "Categories", "Hashtags", "Slug"],
|
||||
"Value": [
|
||||
metadata["blog_title"],
|
||||
metadata["blog_meta_desc"],
|
||||
metadata["blog_tags"],
|
||||
metadata["blog_categories"],
|
||||
metadata["blog_hashtags"],
|
||||
metadata["blog_slug"]
|
||||
]
|
||||
})
|
||||
|
||||
# Display image if available
|
||||
if generated_image_filepath:
|
||||
st.subheader("🖼️ Featured Image")
|
||||
st.image(generated_image_filepath, caption=metadata["blog_title"] or "Featured Image", use_column_width=True)
|
||||
|
||||
# Add regenerate button
|
||||
if st.button("🔄 Regenerate Image", key="regenerate_image_simplified"):
|
||||
# Use the function directly to avoid any nested expanders
|
||||
new_image_path = regenerate_blog_image(
|
||||
metadata["blog_title"],
|
||||
metadata["blog_meta_desc"],
|
||||
blog_content,
|
||||
metadata["blog_tags"]
|
||||
)
|
||||
if new_image_path:
|
||||
st.success("✅ Image regenerated successfully!")
|
||||
st.image(new_image_path, caption=metadata["blog_title"], use_column_width=True)
|
||||
else:
|
||||
st.subheader("🖼️ Featured Image")
|
||||
st.info("No image was generated. Try regenerating the blog.")
|
||||
|
||||
# Add refinement buttons directly, without using helper functions
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("🔄 Refine Blog Title", key="refine_title_simplified", use_container_width=True):
|
||||
st.session_state.show_title_dialog = True
|
||||
st.rerun()
|
||||
with col2:
|
||||
if st.button("🔄 Refine Meta Description", key="refine_meta_simplified", use_container_width=True):
|
||||
st.session_state.show_meta_dialog = True
|
||||
st.rerun()
|
||||
|
||||
# Add structured data section directly, without using helper functions
|
||||
st.markdown("---")
|
||||
st.markdown("### Get Structured Data")
|
||||
|
||||
structured_data_col1, structured_data_col2 = st.columns([3, 1])
|
||||
with structured_data_col1:
|
||||
st.info("Rich snippets boost visibility and click-through rates in search results.")
|
||||
with structured_data_col2:
|
||||
if st.button("📊 Generate Rich Snippet", key="snippet_simplified", use_container_width=True):
|
||||
st.session_state.show_snippet_dialog = True
|
||||
st.rerun()
|
||||
|
||||
# Clear the success message after a delay
|
||||
import time
|
||||
time.sleep(3)
|
||||
final_message.empty()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as inner_err:
|
||||
logger.error(f"Error in simplified blog generation: {inner_err}")
|
||||
st.error(f"Failed to generate blog content: {inner_err}")
|
||||
return False
|
||||
|
||||
elif selected_content_type == "Long-form content":
|
||||
logger.info(f"Generating long-form content with parameters: {blog_params}")
|
||||
|
||||
# Ensure all blog parameters are properly passed to long-form generator
|
||||
long_form_generator(
|
||||
user_input,
|
||||
search_params=search_params,
|
||||
blog_params=blog_params
|
||||
)
|
||||
|
||||
# Show success message briefly then clear it
|
||||
success_msg = st.empty()
|
||||
success_msg.success(f"Successfully generated long-form content for: {user_input}")
|
||||
# Clear the message after 3 seconds
|
||||
import time
|
||||
time.sleep(3)
|
||||
success_msg.empty()
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
info_msg = st.empty()
|
||||
info_msg.info("AI Agent Team feature is coming soon! This will provide multi-perspective content with different AI experts collaborating on your blog.")
|
||||
return False
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"An error occurred while generating content: {err}")
|
||||
st.error(f"An error occurred while generating content: {err}")
|
||||
return False
|
||||
|
||||
|
||||
def process_pdf_input(uploaded_file):
|
||||
"""Process a PDF file and generate content."""
|
||||
# Replace expander with a container to avoid nested expanders
|
||||
pdf_container = st.container()
|
||||
with pdf_container:
|
||||
st.subheader("Processing PDF Document")
|
||||
pdf_reader = PyPDF2.PdfReader(uploaded_file)
|
||||
text = ""
|
||||
combined_result = ""
|
||||
|
||||
# Show progress with better UI
|
||||
progress_text = st.empty()
|
||||
progress_bar = st.progress(0)
|
||||
|
||||
total_pages = len(pdf_reader.pages)
|
||||
for page_num, page in enumerate(pdf_reader.pages):
|
||||
progress_text.text(f"Processing page {page_num+1}/{total_pages}")
|
||||
text += page.extract_text()
|
||||
text = text.replace("\n", " ")
|
||||
text = re.sub(r"(\w)([A-Z])", r"\1 \2", text)
|
||||
|
||||
results = blog_from_pdf(text)
|
||||
progress_percent = (page_num + 1) / total_pages
|
||||
progress_bar.progress(progress_percent)
|
||||
combined_result += str(results[-1])
|
||||
|
||||
progress_text.empty()
|
||||
progress_bar.empty()
|
||||
|
||||
st.subheader("Generated Content from PDF")
|
||||
st.markdown(combined_result)
|
||||
return True
|
||||
|
||||
|
||||
def process_youtube_or_audio(user_input):
|
||||
"""Process a YouTube URL or audio file and generate content."""
|
||||
if not generate_audio_blog(user_input):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def process_web_url(user_input):
|
||||
"""Process a web URL and generate content."""
|
||||
blog_from_url(user_input)
|
||||
return True
|
||||
|
||||
|
||||
def process_image_input(user_input, uploaded_file):
|
||||
"""Process an image file and generate content."""
|
||||
blog_from_image(user_input, uploaded_file)
|
||||
return True
|
||||
|
||||
|
||||
def handle_content_generation(input_type, user_input, uploaded_file, search_params, blog_params, selected_content_type):
|
||||
"""
|
||||
Handle content generation based on the input type.
|
||||
|
||||
Args:
|
||||
input_type: The type of input ("youtube_url", "web_url", etc.)
|
||||
user_input: The text input from the user
|
||||
uploaded_file: The uploaded file (if any)
|
||||
search_params: Search parameters
|
||||
blog_params: Blog content parameters
|
||||
selected_content_type: The selected content type
|
||||
|
||||
Returns:
|
||||
bool: True if content generation was successful, False otherwise
|
||||
"""
|
||||
# Create a status placeholder instead of a permanent message
|
||||
status_message = st.empty()
|
||||
status_message.info("Crafting your blog content... Please wait.")
|
||||
|
||||
try:
|
||||
if input_type == "keywords":
|
||||
result = process_keywords_input(user_input, search_params, blog_params, selected_content_type)
|
||||
# Clear the status message when done
|
||||
status_message.empty()
|
||||
return result
|
||||
|
||||
elif input_type == "youtube_url" or input_type == "audio_file":
|
||||
result = process_youtube_or_audio(user_input)
|
||||
status_message.empty()
|
||||
return result
|
||||
|
||||
elif input_type == "web_url":
|
||||
result = process_web_url(user_input)
|
||||
status_message.empty()
|
||||
return result
|
||||
|
||||
elif input_type == "image_file":
|
||||
result = process_image_input(user_input, uploaded_file)
|
||||
status_message.empty()
|
||||
return result
|
||||
|
||||
elif input_type == "PDF_file":
|
||||
result = process_pdf_input(uploaded_file)
|
||||
status_message.empty()
|
||||
return result
|
||||
|
||||
else:
|
||||
status_message.empty()
|
||||
st.error(f"Unsupported input type: {input_type}")
|
||||
return False
|
||||
except Exception as e:
|
||||
status_message.empty()
|
||||
st.error(f"An error occurred during content generation: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def generate_blog_content(search_keywords, google_search_result, tavily_search_result,
|
||||
google_search_success, tavily_search_success, blog_params, status):
|
||||
"""
|
||||
Generate blog content using either Google or Tavily search results.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Search keywords
|
||||
google_search_result: Results from Google search
|
||||
tavily_search_result: Results from Tavily search
|
||||
google_search_success (bool): Whether Google search was successful
|
||||
tavily_search_success (bool): Whether Tavily search was successful
|
||||
blog_params (dict): Blog parameters
|
||||
status: Streamlit status object
|
||||
|
||||
Returns:
|
||||
str: Generated blog content or None if generation failed
|
||||
"""
|
||||
# Check if both searches failed - if so, stop the process
|
||||
if not google_search_success and not tavily_search_success:
|
||||
st.error("⛔ Both Google SERP and Tavily AI searches failed. Unable to generate blog content.")
|
||||
st.warning("Please check your API keys in the environment settings and try again.")
|
||||
return None
|
||||
|
||||
# Try Google results first if available
|
||||
if google_search_success and 'results' in google_search_result:
|
||||
try:
|
||||
status.update(label=f"✏️ Writing blog from Google Search results...")
|
||||
# Pass blog parameters to the blog writing function
|
||||
blog_style_info = f"""
|
||||
Length: {blog_params.get('blog_length')} words
|
||||
Tone: {blog_params.get('blog_tone')}
|
||||
Target Audience: {blog_params.get('blog_demographic')}
|
||||
Blog Type: {blog_params.get('blog_type')}
|
||||
Language: {blog_params.get('blog_language')}
|
||||
"""
|
||||
status.update(label=f"✏️ Writing {blog_params.get('blog_tone')} {blog_params.get('blog_type')} blog for {blog_params.get('blog_demographic')} audience...")
|
||||
blog_markdown_str = write_blog_google_serp(search_keywords, google_search_result['results'], blog_params=blog_params)
|
||||
status.update(label="✅ Generated content from Google search results", state="complete")
|
||||
return blog_markdown_str
|
||||
except Exception as err:
|
||||
status.update(label=f"❌ Failed to generate content from Google results: {str(err)}", state="error")
|
||||
st.error(f"Failed to generate content from Google results: {err}")
|
||||
logger.error(f"Failed to process Google search results: {err}")
|
||||
|
||||
# If Google failed or had no results, try Tavily
|
||||
if tavily_search_success and tavily_search_result:
|
||||
try:
|
||||
status.update(label=f"✏️ Writing blog from Tavily search results...")
|
||||
status.update(label=f"✏️ Writing {blog_params.get('blog_tone')} {blog_params.get('blog_type')} blog for {blog_params.get('blog_demographic')} audience...")
|
||||
blog_markdown_str = write_blog_google_serp(search_keywords, tavily_search_result, blog_params=blog_params)
|
||||
status.update(label="✅ Generated content from Tavily search results", state="complete")
|
||||
return blog_markdown_str
|
||||
except Exception as err:
|
||||
status.update(label=f"❌ Failed to generate content from Tavily results: {str(err)}", state="error")
|
||||
st.error(f"Failed to generate content from Tavily results: {err}")
|
||||
logger.error(f"Failed to process Tavily search results: {err}")
|
||||
|
||||
# If we still don't have content, show error
|
||||
st.error("⛔ Failed to generate any blog content from the research results.")
|
||||
return None
|
||||
|
||||
|
||||
def generate_blog_metadata(blog_markdown_str, search_keywords, status):
|
||||
"""
|
||||
Generate metadata for the blog content.
|
||||
|
||||
Args:
|
||||
blog_markdown_str (str): Blog content
|
||||
search_keywords (str): Original search keywords
|
||||
status: Streamlit status object
|
||||
|
||||
Returns:
|
||||
tuple: (blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug)
|
||||
"""
|
||||
status.update(label="🔍 Generating title, meta description, tags, categories, hashtags, and slug...")
|
||||
try:
|
||||
# Get all 6 metadata values from blog_metadata
|
||||
blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = asyncio.run(blog_metadata(blog_markdown_str))
|
||||
status.update(label="✅ Generated blog metadata successfully")
|
||||
return blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug
|
||||
except Exception as err:
|
||||
st.error(f"Failed to get blog metadata: {err}")
|
||||
logger.error(f"Failed to get blog metadata: {err}")
|
||||
status.update(label="❌ Failed to get blog metadata", state="error")
|
||||
return None, None, None, None, None, None
|
||||
|
||||
|
||||
def generate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, status, blog_tags=None):
|
||||
"""
|
||||
Generate a featured image for the blog.
|
||||
|
||||
Args:
|
||||
blog_title (str): Blog title
|
||||
blog_meta_desc (str): Blog meta description
|
||||
blog_markdown_str (str): Blog content
|
||||
status: Streamlit status object
|
||||
blog_tags (list, optional): Blog tags to use for image prompt enhancement
|
||||
|
||||
Returns:
|
||||
str: Path to the generated image or None if generation failed
|
||||
"""
|
||||
try:
|
||||
status.update(label="🖼️ Generating featured image for blog...")
|
||||
|
||||
# Create a better prompt for image generation
|
||||
if blog_title and blog_meta_desc:
|
||||
# If we have both title and description, use them
|
||||
text_to_image = f"{blog_title}: {blog_meta_desc}"
|
||||
elif blog_title:
|
||||
# If we only have title, use it
|
||||
text_to_image = blog_title
|
||||
elif blog_meta_desc:
|
||||
# If we only have description, use it
|
||||
text_to_image = blog_meta_desc
|
||||
else:
|
||||
# Fallback to first 200 chars of content
|
||||
text_to_image = blog_markdown_str[:200]
|
||||
|
||||
# Ensure the prompt is of reasonable length
|
||||
if len(text_to_image) > 300:
|
||||
text_to_image = text_to_image[:300]
|
||||
|
||||
# Log the prompt being used
|
||||
logger.info(f"Generating image with prompt: {text_to_image}")
|
||||
status.update(label=f"🖼️ Creating image with prompt: \"{text_to_image[:50]}...\"")
|
||||
|
||||
# Extract blog tags if available
|
||||
blog_tags_list = blog_tags if isinstance(blog_tags, list) else []
|
||||
|
||||
# Attempt image generation with all available parameters
|
||||
generated_image_filepath = generate_image(
|
||||
user_prompt=text_to_image,
|
||||
title=blog_title,
|
||||
description=blog_meta_desc,
|
||||
tags=blog_tags_list,
|
||||
content=blog_markdown_str[:2000] # Limit content length to avoid too large payloads
|
||||
)
|
||||
|
||||
# If first attempt failed, try with a simplified prompt
|
||||
if not generated_image_filepath:
|
||||
logger.warning("First image generation attempt failed, trying with simplified prompt")
|
||||
status.update(label="⚠️ First image attempt failed, trying again with simplified prompt...")
|
||||
|
||||
# Create a simpler prompt
|
||||
simplified_prompt = " ".join(text_to_image.split()[:10])
|
||||
generated_image_filepath = generate_image(
|
||||
user_prompt=simplified_prompt,
|
||||
title=blog_title,
|
||||
description=blog_meta_desc,
|
||||
tags=blog_tags_list,
|
||||
content=blog_markdown_str[:1000] # Use even shorter content for the retry
|
||||
)
|
||||
|
||||
if generated_image_filepath:
|
||||
status.update(label="✅ Successfully generated featured image")
|
||||
return generated_image_filepath
|
||||
else:
|
||||
status.update(label="❌ Image generation failed - no image created", state="error")
|
||||
return None
|
||||
|
||||
except Exception as err:
|
||||
st.warning(f"Failed in Image generation: {err}")
|
||||
logger.error(f"Failed in Image generation: {err}")
|
||||
status.update(label="❌ Image generation failed - no image created", state="error")
|
||||
return None
|
||||
|
||||
|
||||
def regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags=None):
|
||||
"""
|
||||
Regenerate a blog image on demand.
|
||||
|
||||
Args:
|
||||
blog_title (str): Blog title
|
||||
blog_meta_desc (str): Blog meta description
|
||||
blog_markdown_str (str): Blog content
|
||||
blog_tags (list, optional): Blog tags to use for image prompt enhancement
|
||||
|
||||
Returns:
|
||||
str: Path to the generated image or None if generation failed
|
||||
"""
|
||||
with st.status("Regenerating image...", expanded=True) as status:
|
||||
try:
|
||||
# Use keywords from title or description
|
||||
if blog_title:
|
||||
keywords = " ".join(blog_title.split()[:6])
|
||||
prompt = f"Blog illustration for: {keywords}"
|
||||
elif blog_meta_desc:
|
||||
keywords = " ".join(blog_meta_desc.split()[:6])
|
||||
prompt = f"Blog illustration for: {keywords}"
|
||||
else:
|
||||
keywords = blog_markdown_str.split()[:50]
|
||||
prompt = f"Blog illustration based on: {' '.join(keywords[:6])}"
|
||||
|
||||
status.update(label=f"🖼️ Generating new image with prompt: \"{prompt}\"")
|
||||
|
||||
# Extract any tags if available - will be passed as empty list otherwise
|
||||
blog_tags_list = blog_tags if isinstance(blog_tags, list) else []
|
||||
|
||||
# Generate the image with all parameters
|
||||
generated_image_filepath = generate_image(
|
||||
user_prompt=prompt,
|
||||
title=blog_title,
|
||||
description=blog_meta_desc,
|
||||
tags=blog_tags_list,
|
||||
content=blog_markdown_str[:2000] # Limit content length to avoid too large payloads
|
||||
)
|
||||
|
||||
if generated_image_filepath:
|
||||
status.update(label="✅ Successfully generated new image", state="complete")
|
||||
return generated_image_filepath
|
||||
else:
|
||||
status.update(label="❌ Image regeneration failed", state="error")
|
||||
return None
|
||||
|
||||
except Exception as err:
|
||||
st.error(f"Failed to regenerate image: {err}")
|
||||
logger.error(f"Image regeneration error: {err}")
|
||||
status.update(label="❌ Image regeneration failed", state="error")
|
||||
return None
|
||||
@@ -1,420 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import streamlit as st
|
||||
from loguru import logger
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv(Path('../../../.env'))
|
||||
|
||||
# Import necessary modules
|
||||
from ...ai_web_researcher.gpt_online_researcher import (
|
||||
do_google_serp_search as gpt_do_google_serp_search,
|
||||
do_tavily_ai_search as gpt_do_tavily_ai_search
|
||||
)
|
||||
from ...ai_web_researcher.tavily_ai_search import do_tavily_ai_search as tavily_direct_search
|
||||
|
||||
|
||||
def initialize_parameters(search_params=None, blog_params=None):
|
||||
"""
|
||||
Initialize and validate search and blog parameters with defaults.
|
||||
|
||||
Args:
|
||||
search_params (dict, optional): Search parameters
|
||||
blog_params (dict, optional): Blog parameters
|
||||
|
||||
Returns:
|
||||
tuple: (search_params, blog_params) with defaults applied
|
||||
"""
|
||||
# Initialize search params if not provided
|
||||
if search_params is None:
|
||||
search_params = {}
|
||||
|
||||
# Initialize blog params if not provided
|
||||
if blog_params is None:
|
||||
blog_params = {}
|
||||
|
||||
# Provide default values only for missing keys
|
||||
# This ensures we don't override values that were intentionally set to 0 or other falsy values
|
||||
if "max_results" not in search_params:
|
||||
search_params["max_results"] = 10
|
||||
if "search_depth" not in search_params:
|
||||
search_params["search_depth"] = "basic"
|
||||
if "time_range" not in search_params:
|
||||
search_params["time_range"] = "year"
|
||||
if "include_domains" not in search_params:
|
||||
search_params["include_domains"] = []
|
||||
|
||||
# Provide default values only for missing blog parameter keys
|
||||
if "blog_length" not in blog_params:
|
||||
blog_params["blog_length"] = 2000
|
||||
if "blog_tone" not in blog_params:
|
||||
blog_params["blog_tone"] = "Professional"
|
||||
if "blog_demographic" not in blog_params:
|
||||
blog_params["blog_demographic"] = "Professional"
|
||||
if "blog_type" not in blog_params:
|
||||
blog_params["blog_type"] = "Informational"
|
||||
if "blog_language" not in blog_params:
|
||||
blog_params["blog_language"] = "English"
|
||||
if "blog_output_format" not in blog_params:
|
||||
blog_params["blog_output_format"] = "markdown"
|
||||
|
||||
# Log the parameters for debugging
|
||||
logger.info(f"Using search parameters: {search_params}")
|
||||
logger.info(f"Using blog parameters: {blog_params}")
|
||||
|
||||
return search_params, blog_params
|
||||
|
||||
|
||||
def perform_google_search(search_keywords, search_params, status, status_container, progress_bar):
|
||||
"""
|
||||
Perform Google SERP search for the given keywords.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Keywords to search for
|
||||
search_params (dict): Search parameters
|
||||
status: Streamlit status object
|
||||
status_container: Streamlit container for status messages
|
||||
progress_bar: Streamlit progress bar
|
||||
|
||||
Returns:
|
||||
tuple: (google_search_result, g_titles, success_flag)
|
||||
"""
|
||||
def update_progress(message, progress=None, level="info"):
|
||||
"""Helper function to update progress in Streamlit UI"""
|
||||
if progress is not None:
|
||||
progress_bar.progress(progress)
|
||||
|
||||
if level == "error":
|
||||
status_container.error(f"🚫 {message}")
|
||||
elif level == "warning":
|
||||
status_container.warning(f"⚠️ {message}")
|
||||
elif level == "success":
|
||||
status_container.success(f"✅ {message}")
|
||||
else:
|
||||
status_container.info(f"🔄 {message}")
|
||||
logger.debug(f"Progress update [{level}]: {message}")
|
||||
|
||||
try:
|
||||
# Update the function call to include the required parameters and search_params
|
||||
status.update(label=f"Starting Google SERP search for: {search_keywords}")
|
||||
|
||||
# Add search params to the Google SERP search
|
||||
google_search_params = {
|
||||
"max_results": search_params.get("max_results", 10)
|
||||
}
|
||||
|
||||
# Include domains if provided
|
||||
if search_params.get("include_domains"):
|
||||
google_search_params["include_domains"] = search_params.get("include_domains")
|
||||
|
||||
google_search_result = do_google_serp_search(
|
||||
search_keywords,
|
||||
status_container=status_container,
|
||||
update_progress=update_progress,
|
||||
**google_search_params
|
||||
)
|
||||
|
||||
if google_search_result and google_search_result.get('titles') and len(google_search_result.get('titles', [])) > 0:
|
||||
status.update(label=f"✅ Finished with Google web for Search: {search_keywords}")
|
||||
g_titles = google_search_result.get('titles', [])
|
||||
return google_search_result, g_titles, True
|
||||
else:
|
||||
# Check if there's an error message in the result
|
||||
if google_search_result and 'summary' in google_search_result and 'Error' in google_search_result['summary']:
|
||||
error_msg = google_search_result['summary']
|
||||
status.update(label=f"❌ Google search failed: {error_msg}", state="error")
|
||||
st.error(f"Google SERP search failed: {error_msg}")
|
||||
else:
|
||||
status.update(label="❌ Failed to get Google SERP results. No valid data returned.", state="error")
|
||||
st.error("Google SERP search failed to return valid results.")
|
||||
return google_search_result, [], False
|
||||
except Exception as err:
|
||||
status.update(label=f"❌ Google search error: {str(err)}", state="error")
|
||||
st.error(f"Google web research failed: {err}")
|
||||
logger.error(f"Failed in Google web research: {err}")
|
||||
return None, [], False
|
||||
|
||||
|
||||
def perform_tavily_search(search_keywords, search_params, status):
|
||||
"""
|
||||
Perform Tavily AI search for the given keywords.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Keywords to search for
|
||||
search_params (dict): Search parameters
|
||||
status: Streamlit status object
|
||||
|
||||
Returns:
|
||||
tuple: (tavily_search_result, success_flag)
|
||||
"""
|
||||
try:
|
||||
status.update(label=f"🔍 Starting Tavily AI research: {search_keywords}")
|
||||
|
||||
# Pass the search parameters to Tavily
|
||||
tavily_result_tuple = do_tavily_ai_search(
|
||||
search_keywords,
|
||||
max_results=search_params.get("max_results", 10),
|
||||
search_depth=search_params.get("search_depth", "basic"),
|
||||
include_domains=search_params.get("include_domains", []),
|
||||
time_range=search_params.get("time_range", "year")
|
||||
)
|
||||
|
||||
if tavily_result_tuple and len(tavily_result_tuple) == 3:
|
||||
tavily_search_result, t_titles, t_answer = tavily_result_tuple
|
||||
# If we have either titles or an answer, consider it a success
|
||||
if (t_titles and len(t_titles) > 0) or (t_answer and len(t_answer) > 10):
|
||||
status.update(label=f"✅ Finished Tavily AI Search on: {search_keywords}", state="complete")
|
||||
return tavily_search_result, True
|
||||
else:
|
||||
status.update(label="❌ Tavily search returned empty results", state="error")
|
||||
st.warning("Tavily search didn't find relevant information.")
|
||||
return tavily_search_result, False
|
||||
else:
|
||||
status.update(label="❌ Tavily search returned incomplete results", state="error")
|
||||
st.error("Tavily search failed to return valid results.")
|
||||
return None, False
|
||||
|
||||
except Exception as err:
|
||||
status.update(label=f"❌ Tavily search error: {str(err)}", state="error")
|
||||
st.error(f"Failed in Tavily web research: {err}")
|
||||
logger.error(f"Failed in Tavily web research: {err}")
|
||||
return None, False
|
||||
|
||||
|
||||
def do_google_serp_search(search_keywords, status_container=None, update_progress=None, **kwargs):
|
||||
"""
|
||||
Wrapper function to handle the parameter mismatch with the original function.
|
||||
"""
|
||||
try:
|
||||
if status_container is None:
|
||||
status_container = st.empty()
|
||||
|
||||
if update_progress is None:
|
||||
def update_progress(message, progress=None, level="info"):
|
||||
if level == "error":
|
||||
status_container.error(message)
|
||||
elif level == "warning":
|
||||
status_container.warning(message)
|
||||
else:
|
||||
status_container.info(message)
|
||||
|
||||
# Create a fixed update_progress function that handles any progress type
|
||||
def safe_update_progress(message, progress=None, level="info"):
|
||||
try:
|
||||
# Handle progress value of different types
|
||||
if progress is not None:
|
||||
if isinstance(progress, str):
|
||||
# Try to convert string to float if it represents a number
|
||||
try:
|
||||
progress = float(progress)
|
||||
except ValueError:
|
||||
# If conversion fails, just log the message without updating progress
|
||||
progress = None
|
||||
|
||||
# Call the original update_progress with sanitized values
|
||||
update_progress(message, progress, level)
|
||||
except Exception as err:
|
||||
# If there's an error in the progress function, just log to console
|
||||
logger.error(f"Error in progress update: {err}")
|
||||
# Try one more time with just the message
|
||||
try:
|
||||
update_progress(message, None, level)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Set default search parameters - fix the parameter to use 'max_results' not 'num_results'
|
||||
search_params = {
|
||||
"max_results": kwargs.get("max_results", 10),
|
||||
"include_domains": kwargs.get("include_domains", []),
|
||||
"search_depth": kwargs.get("search_depth", "basic")
|
||||
}
|
||||
|
||||
# Update status to indicate we're checking API keys
|
||||
status_container.info("🔑 Checking required API keys...")
|
||||
|
||||
# Call the original function with the required parameters
|
||||
result = gpt_do_google_serp_search(search_keywords, status_container, safe_update_progress, **search_params)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Error in do_google_serp_search wrapper: {error_msg}")
|
||||
|
||||
# Check for common error patterns and display user-friendly messages
|
||||
if "SERPER_API_KEY is missing" in error_msg:
|
||||
status_container.error("🔑 Google search API key (SERPER_API_KEY) is missing. Please check your environment settings.")
|
||||
st.error("Google SERP search failed: API key is missing. Using alternative methods.")
|
||||
elif "Progress Value has invalid type" in error_msg:
|
||||
# This is an internal error, log it but show a more user-friendly message
|
||||
status_container.warning("⚠️ Internal progress tracking error. Continuing with search.")
|
||||
else:
|
||||
# For unknown errors, show the full error message
|
||||
status_container.error(f"🚫 Google search error: {error_msg}")
|
||||
st.error(f"Google SERP search failed: {error_msg}")
|
||||
|
||||
# Return a minimal result structure to prevent downstream errors
|
||||
return {
|
||||
'results': {},
|
||||
'titles': [],
|
||||
'summary': f"Error occurred during search: {error_msg}",
|
||||
'stats': {
|
||||
'organic_count': 0,
|
||||
'questions_count': 0,
|
||||
'related_count': 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def do_tavily_ai_search(keywords, max_results=10, search_depth="basic", include_domains=None, time_range="year"):
|
||||
"""
|
||||
Wrapper function for Tavily search to handle parameter differences.
|
||||
|
||||
Args:
|
||||
keywords (str): Keywords to search for
|
||||
max_results (int): Maximum number of search results to return
|
||||
search_depth (str): "basic" or "advanced" search depth
|
||||
include_domains (list): List of domains to prioritize in search
|
||||
time_range (str): Time range for results ("day", "week", "month", "year", "all")
|
||||
"""
|
||||
status_container = st.empty()
|
||||
|
||||
if include_domains is None:
|
||||
include_domains = []
|
||||
|
||||
try:
|
||||
# Show status message
|
||||
status_container.info(f"🔍 Preparing Tavily AI search with {search_depth} depth...")
|
||||
|
||||
# FIXED: Ensure all parameters have correct types to prevent comparison errors
|
||||
tavily_params = {
|
||||
'max_results': int(max_results), # Explicitly convert to int
|
||||
'search_depth': str(search_depth), # Ensure this is a string
|
||||
'include_domains': include_domains,
|
||||
'time_range': str(time_range)
|
||||
}
|
||||
|
||||
# Log the parameters for debugging
|
||||
logger.info(f"Tavily search parameters: {tavily_params}")
|
||||
|
||||
# Check for API key before making the request
|
||||
tavily_api_key = os.environ.get("TAVILY_API_KEY")
|
||||
if not tavily_api_key:
|
||||
status_container.error("🔑 Tavily API key (TAVILY_API_KEY) is missing. Please check your environment settings.")
|
||||
st.error("Tavily search failed: API key is missing. Using alternative methods.")
|
||||
return None, [], "API key missing"
|
||||
|
||||
status_container.info(f"🔍 Searching with Tavily AI using {search_depth} depth for: {keywords}")
|
||||
|
||||
# Direct implementation without calling gpt_do_tavily_ai_search to avoid type issues
|
||||
try:
|
||||
# Call the function directly with correct parameter types
|
||||
tavily_raw_results = tavily_direct_search(
|
||||
keywords,
|
||||
max_results=tavily_params['max_results'],
|
||||
search_depth=tavily_params['search_depth'],
|
||||
include_domains=tavily_params['include_domains'],
|
||||
time_range=tavily_params['time_range']
|
||||
)
|
||||
|
||||
# Extract the needed information
|
||||
if isinstance(tavily_raw_results, tuple) and len(tavily_raw_results) == 3:
|
||||
# If already in the right format, use it directly
|
||||
return tavily_raw_results
|
||||
|
||||
# Process the results to extract titles and answer
|
||||
t_results = tavily_raw_results
|
||||
t_titles = []
|
||||
t_answer = ""
|
||||
|
||||
# Extract titles from results if available
|
||||
if isinstance(t_results, dict):
|
||||
if 'results' in t_results and isinstance(t_results['results'], list):
|
||||
t_titles = [r.get('title', '') for r in t_results['results']]
|
||||
status_container.success(f"✅ Found {len(t_titles)} relevant articles")
|
||||
if 'answer' in t_results:
|
||||
t_answer = t_results['answer']
|
||||
status_container.success("✅ Generated a summary answer")
|
||||
|
||||
return t_results, t_titles, t_answer
|
||||
|
||||
except ImportError:
|
||||
# Fall back to the original function if direct import fails
|
||||
status_container.warning("⚠️ Using fallback Tavily search method...")
|
||||
logger.warning("Using fallback Tavily search method")
|
||||
|
||||
# FIXED: Alternative approach - wrap the call in try/except to handle type errors
|
||||
try:
|
||||
tavily_result = gpt_do_tavily_ai_search(keywords, **tavily_params)
|
||||
|
||||
# Format the result to match what the blog writer expects
|
||||
if isinstance(tavily_result, tuple) and len(tavily_result) == 3:
|
||||
status_container.success("✅ Tavily search completed successfully")
|
||||
return tavily_result
|
||||
|
||||
# If not a tuple with expected values, try to extract what we need
|
||||
t_results = tavily_result
|
||||
|
||||
# Extract titles and answer if available
|
||||
t_titles = []
|
||||
t_answer = ""
|
||||
|
||||
if isinstance(t_results, dict):
|
||||
if 'results' in t_results and isinstance(t_results['results'], list):
|
||||
t_titles = [r.get('title', '') for r in t_results['results']]
|
||||
status_container.success(f"✅ Found {len(t_titles)} relevant articles")
|
||||
if 'answer' in t_results:
|
||||
t_answer = t_results['answer']
|
||||
status_container.success("✅ Generated a summary answer")
|
||||
|
||||
return t_results, t_titles, t_answer
|
||||
|
||||
except TypeError as type_err:
|
||||
# Handle the specific type error more gracefully
|
||||
error_msg = str(type_err)
|
||||
logger.error(f"Type error in Tavily search: {error_msg}")
|
||||
|
||||
if "'>' not supported" in error_msg:
|
||||
status_container.error("🚫 Tavily search parameter type error. Trying alternative approach...")
|
||||
|
||||
# Try a simpler approach with minimal parameters
|
||||
try:
|
||||
# Call with only the keyword and fixed max_results
|
||||
tavily_result = gpt_do_tavily_ai_search(keywords, max_results=10)
|
||||
|
||||
# Minimal processing to extract titles and answer
|
||||
t_results = tavily_result
|
||||
t_titles = []
|
||||
t_answer = ""
|
||||
|
||||
if isinstance(t_results, dict):
|
||||
if 'results' in t_results and isinstance(t_results['results'], list):
|
||||
t_titles = [r.get('title', '') for r in t_results['results']]
|
||||
if 'answer' in t_results:
|
||||
t_answer = t_results['answer']
|
||||
|
||||
return t_results, t_titles, t_answer
|
||||
except Exception as inner_err:
|
||||
logger.error(f"Alternative Tavily approach also failed: {inner_err}")
|
||||
raise
|
||||
else:
|
||||
# Re-raise other type errors
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Error in do_tavily_ai_search wrapper: {error_msg}")
|
||||
|
||||
# Display user-friendly error message
|
||||
status_container.error(f"🚫 Tavily search error: {error_msg}")
|
||||
st.error(f"Tavily AI search failed: {error_msg}")
|
||||
|
||||
# Return empty results to prevent downstream errors
|
||||
return None, [], f"Error: {error_msg}"
|
||||
|
||||
finally:
|
||||
# Clear the status container after a delay
|
||||
time.sleep(2)
|
||||
status_container.empty()
|
||||
@@ -1,199 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
from ...gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
def write_blog_google_serp(keywords, search_results, blog_params=None):
|
||||
"""
|
||||
Write a blog post using search results from Google SERP.
|
||||
|
||||
Args:
|
||||
keywords (str): The keywords or topic for the blog
|
||||
search_results (dict): Results from Google SERP search
|
||||
blog_params (dict, optional): Blog content characteristics:
|
||||
- blog_length: Target word count
|
||||
- blog_tone: Content tone
|
||||
- blog_demographic: Target audience
|
||||
- blog_type: Type of blog post
|
||||
- blog_language: Language for the blog
|
||||
|
||||
Returns:
|
||||
str: The generated blog content in markdown format
|
||||
"""
|
||||
# If no blog parameters are provided, use defaults
|
||||
if blog_params is None:
|
||||
blog_params = {
|
||||
"blog_length": 2000,
|
||||
"blog_tone": "Professional",
|
||||
"blog_demographic": "Professional",
|
||||
"blog_type": "Informational",
|
||||
"blog_language": "English"
|
||||
}
|
||||
|
||||
# Ensure all parameters have default values
|
||||
blog_length = blog_params.get("blog_length", 2000)
|
||||
blog_tone = blog_params.get("blog_tone", "Professional")
|
||||
blog_demographic = blog_params.get("blog_demographic", "Professional")
|
||||
blog_type = blog_params.get("blog_type", "Informational")
|
||||
blog_language = blog_params.get("blog_language", "English")
|
||||
|
||||
logger.info(f"Generating {blog_tone} {blog_type} blog of {blog_length} words for {blog_demographic} audience in {blog_language}")
|
||||
|
||||
try:
|
||||
# Build a prompt based on search results
|
||||
prompt_parts = [
|
||||
f"You are a specialized blog writer who writes in a {blog_tone} tone for a {blog_demographic} audience. "
|
||||
f"Create a {blog_type} blog post that is approximately {blog_length} words in {blog_language}.",
|
||||
f"The blog should be about: {keywords}",
|
||||
"Use the following search results to create an informative, accurate, and well-structured blog post:"
|
||||
]
|
||||
|
||||
# Add organic search results
|
||||
if 'organic' in search_results:
|
||||
prompt_parts.append("\nSearch results:")
|
||||
for i, result in enumerate(search_results['organic'][:5], 1):
|
||||
title = result.get('title', 'No title')
|
||||
snippet = result.get('snippet', 'No snippet')
|
||||
prompt_parts.append(f"{i}. {title}: {snippet}")
|
||||
|
||||
# Add people also ask questions if available
|
||||
if 'peopleAlsoAsk' in search_results and search_results['peopleAlsoAsk']:
|
||||
prompt_parts.append("\nPeople also ask:")
|
||||
for i, question in enumerate(search_results['peopleAlsoAsk'][:3], 1):
|
||||
q_text = question.get('question', 'No question')
|
||||
q_answer = question.get('answer', {}).get('snippet', 'No answer')
|
||||
prompt_parts.append(f"{i}. Q: {q_text}\n A: {q_answer}")
|
||||
|
||||
# Add related searches if available
|
||||
if 'relatedSearches' in search_results and search_results['relatedSearches']:
|
||||
related = [item.get('query', '') for item in search_results['relatedSearches'][:5]]
|
||||
if related:
|
||||
prompt_parts.append("\nRelated topics to consider including:")
|
||||
prompt_parts.append(", ".join(related))
|
||||
|
||||
# Add specific instructions based on blog_type
|
||||
type_instructions = {
|
||||
"Informational": "Focus on providing factual information and educating the reader about the topic.",
|
||||
"How-to": "Include clear step-by-step instructions with actionable advice.",
|
||||
"List": "Organize content into a numbered or bulleted list of points, tips, or examples.",
|
||||
"Review": "Provide balanced analysis with pros and cons, and a clear conclusion or recommendation.",
|
||||
"Tutorial": "Include detailed instructions with examples and explanations for each step.",
|
||||
"Opinion": "Present a clear perspective supported by evidence, while acknowledging other viewpoints."
|
||||
}
|
||||
|
||||
prompt_parts.append(f"\nSpecific instructions: {type_instructions.get(blog_type, '')}")
|
||||
|
||||
# Add formatting instructions
|
||||
prompt_parts.append("""
|
||||
Format the blog post in markdown with:
|
||||
- A compelling title (# Title)
|
||||
- An introduction that hooks the reader
|
||||
- Well-structured sections with appropriate headings (## Headings)
|
||||
- Bullet points or numbered lists where appropriate
|
||||
- A conclusion summarizing key points
|
||||
- Make sure all content is accurate, informative, and adds value to the reader.
|
||||
- Include 2-3 subheadings to organize the content well.
|
||||
- Be concise and to the point.
|
||||
- Write in an engaging, reader-friendly style.
|
||||
- Avoid using phrases like "According to the search results" or "Based on the information provided."
|
||||
- Present information as direct knowledge.
|
||||
""")
|
||||
|
||||
# Combine all prompt parts
|
||||
full_prompt = "\n".join(prompt_parts)
|
||||
|
||||
# Generate the blog content using the prompt
|
||||
response = llm_text_gen(full_prompt)
|
||||
|
||||
# Return the generated content
|
||||
return response
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Error generating blog from search results: {err}")
|
||||
raise
|
||||
|
||||
|
||||
def improve_blog_intro(blog_content, blog_intro):
|
||||
"""Combine the given online research and gpt blog content"""
|
||||
prompt = f"""
|
||||
You are a skilled content editor, tasked with creating an engaging peek into the blog post provided.
|
||||
This peek should entice readers to delve into the full content.
|
||||
|
||||
Here's what you need to do:
|
||||
1. **Replace the old blog introduction with the new one provided.**
|
||||
2. **Craft a short and captivating summary of the key points and interesting takeaways from the blog.**
|
||||
- Highlight what makes the blog unique and worth reading.
|
||||
- This peek should be placed directly before the new introduction.
|
||||
3. **Include the complete blog content, with the new introduction and the added peek.**
|
||||
|
||||
Do not provide explanations for your actions, simply present the edited blog content.
|
||||
|
||||
Blog Content: \"\"\"{blog_content}\"\"\"
|
||||
Blog Introduction: \"\"\"{blog_intro}\"\"\"
|
||||
"""
|
||||
logger.info("Generating blog introduction from tavily answer.")
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Exit: Failed to get response from LLM: {err}")
|
||||
exit(1)
|
||||
|
||||
|
||||
def blog_with_keywords(blog, keywords):
|
||||
"""Combine the given online research and gpt blog content"""
|
||||
prompt = f"""
|
||||
You are Sarah, the Creative Content writer, writing up fresh ideas and crafts them with care.
|
||||
She makes complex topics easy to understand and writes in a friendly tone that connects with everyone.
|
||||
She excels at simplifying complex topics and communicates with charisma, making technical jargon come alive for her audience.
|
||||
|
||||
As an expert digital content writer, specializing in content optimization and SEO.
|
||||
I will provide you with my 'blog content' and 'list of keywords' on the same topic.
|
||||
Your task is to write an original blog, utilizing given keywords and blog content.
|
||||
Your blog should be highly detailed and well formatted.
|
||||
|
||||
Blog content: '{blog}'
|
||||
list of keywords: '{keywords}'
|
||||
"""
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"blog_with_keywords: Failed to get response from LLM: {err}")
|
||||
raise err
|
||||
|
||||
|
||||
def blog_with_research(report, blog):
|
||||
"""Combine the given online research and gpt blog content"""
|
||||
prompt = f"""
|
||||
As expert Creative Content writer, Your task is to update a blog post using the latest research.
|
||||
|
||||
Here's what you need to do:
|
||||
|
||||
1. **Read the outdated blog content and the new research report carefully.**
|
||||
2. **Identify key insights and updates from the research report that should be incorporated into the blog post.**
|
||||
3. **Rewrite sections of the blog post to reflect the new information, ensuring a smooth and natural flow.**
|
||||
4. **Maintain the blog's original friendly and conversational tone throughout.**
|
||||
|
||||
Remember, your goal is to seamlessly blend the new information into the existing blog post, making it accurate and engaging for readers.
|
||||
\n\n
|
||||
Research Report: \"\"\"{report}\"\"\"
|
||||
|
||||
Blog Content: \"\"\"{blog}\"\"\"
|
||||
"""
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"blog_with_research: Failed to get response from LLM: {err}")
|
||||
raise err
|
||||
@@ -1,252 +0,0 @@
|
||||
import streamlit as st
|
||||
|
||||
def apply_blog_writer_styles():
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Base UI improvements */
|
||||
body, .main .block-container {
|
||||
background: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%) !important;
|
||||
min-height: 100vh;
|
||||
color: #2c3e50;
|
||||
font-family: 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
/* Main layout improvements */
|
||||
.main .block-container {
|
||||
padding: 1rem 2rem 2rem 2rem !important;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Back button styling */
|
||||
[data-testid="stButton"] > button:first-of-type {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 10px rgba(25, 118, 210, 0.2);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
[data-testid="stButton"] > button:first-of-type:hover {
|
||||
background: #1565c0;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 15px rgba(25, 118, 210, 0.3);
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.blog-header, .page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 2rem 1.5rem;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f5f7fa 100%);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.blog-header h1, .page-header h1 {
|
||||
font-size: 2.5em;
|
||||
font-family: 'Helvetica Neue', sans-serif;
|
||||
font-weight: 700;
|
||||
color: #1976d2;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.blog-header p, .page-header p {
|
||||
font-size: 1.1em;
|
||||
color: #546e7a;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Input section styling */
|
||||
.stTextArea textarea, .stTextInput input {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #2c3e50;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.stTextArea textarea:focus, .stTextInput input:focus {
|
||||
border: 1.5px solid #1976d2;
|
||||
box-shadow: 0 2px 10px rgba(25, 118, 210, 0.12);
|
||||
}
|
||||
|
||||
/* File uploader styling */
|
||||
.stFileUploader > div {
|
||||
background: #ffffff;
|
||||
border: 2px dashed #cfd8dc;
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.stFileUploader > div:hover {
|
||||
border-color: #1976d2;
|
||||
background: rgba(25, 118, 210, 0.03);
|
||||
}
|
||||
|
||||
/* Options expander styling */
|
||||
.stExpander {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 1.5rem 0;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.stExpander > details {
|
||||
background: #ffffff;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.stExpander > details > summary {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.stExpander > details > summary:hover {
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
/* Checkbox styling */
|
||||
.stCheckbox > div {
|
||||
background: #ffffff;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stCheckbox > div:hover {
|
||||
background: rgba(25, 118, 210, 0.03);
|
||||
}
|
||||
|
||||
.stCheckbox label {
|
||||
font-weight: 500;
|
||||
color: #455a64;
|
||||
}
|
||||
|
||||
/* Radio button styling */
|
||||
.stRadio > div {
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stRadio > div > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stRadio > div > div > label {
|
||||
background: #f5f7fa;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 30px;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: #546e7a;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.stRadio > div > div > label:hover {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
border-color: #bbdefb;
|
||||
}
|
||||
|
||||
.stRadio > div > div > label[data-baseweb="radio"] input:checked + div {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
/* Generate button styling */
|
||||
button[data-testid="baseButton-secondary"],
|
||||
button[data-testid="baseButton-primary"] {
|
||||
background: linear-gradient(45deg, #1976d2, #2196f3);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
padding: 0.85rem 1.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(25, 118, 210, 0.25);
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
button[data-testid="baseButton-secondary"]:hover,
|
||||
button[data-testid="baseButton-primary"]:hover {
|
||||
background: linear-gradient(45deg, #1565c0, #1976d2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.35);
|
||||
}
|
||||
|
||||
/* Input labels */
|
||||
.stTextArea label, .stTextInput label, .stFileUploader label {
|
||||
font-weight: 600;
|
||||
color: #455a64;
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Section headers */
|
||||
.stMarkdown h3 {
|
||||
color: #1976d2;
|
||||
font-weight: 600;
|
||||
font-size: 1.3rem;
|
||||
margin: 1.5rem 0 0.75rem 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
|
||||
.stMarkdown h4 {
|
||||
color: #455a64;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin: 1rem 0 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Column layout improvements */
|
||||
[data-testid="column"] {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 1.2rem;
|
||||
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
/* Success and error messages */
|
||||
.stSuccess, .stInfo, .stError {
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
@@ -1,864 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
from textwrap import dedent
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import streamlit as st
|
||||
from gtts import gTTS
|
||||
import base64
|
||||
from dotenv import load_dotenv
|
||||
import time
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv(Path('../../.env'))
|
||||
# Logger setup
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}")
|
||||
|
||||
# Import other necessary modules
|
||||
from ...ai_web_researcher.gpt_online_researcher import (
|
||||
do_metaphor_ai_research, do_google_pytrends_analysis)
|
||||
from .blog_from_google_serp import write_blog_google_serp, blog_with_research
|
||||
from ...blog_metadata.get_blog_metadata import blog_metadata
|
||||
from ...blog_postprocessing.save_blog_to_file import save_blog_to_file
|
||||
from ...gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||
from ...ai_seo_tools.content_title_generator import generate_blog_titles
|
||||
from ...ai_seo_tools.meta_desc_generator import generate_blog_metadesc
|
||||
from ...ai_seo_tools.seo_structured_data import ai_structured_data
|
||||
|
||||
# Import search functions from the research utils module
|
||||
from .blog_ai_research_utils import (
|
||||
initialize_parameters,
|
||||
perform_google_search,
|
||||
perform_tavily_search,
|
||||
do_google_serp_search,
|
||||
do_tavily_ai_search
|
||||
)
|
||||
|
||||
|
||||
def save_blog_content(blog_markdown_str, blog_title, blog_meta_desc, blog_tags, blog_categories, generated_image_filepath, status, blog_hashtags=None, blog_slug=None):
|
||||
"""
|
||||
Save the blog content to a file.
|
||||
|
||||
Args:
|
||||
blog_markdown_str (str): Blog content
|
||||
blog_title (str): Blog title
|
||||
blog_meta_desc (str): Blog meta description
|
||||
blog_tags (list): Blog tags
|
||||
blog_categories (list): Blog categories
|
||||
generated_image_filepath (str): Path to the generated image
|
||||
status: Streamlit status object
|
||||
blog_hashtags (str, optional): Social media hashtags
|
||||
blog_slug (str, optional): SEO-friendly URL slug
|
||||
|
||||
Returns:
|
||||
str: Path to the saved file or None if saving failed
|
||||
"""
|
||||
try:
|
||||
status.update(label="💾 Saving blog content to file...")
|
||||
saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc,
|
||||
blog_tags, blog_categories, generated_image_filepath)
|
||||
status.update(label=f"✅ Saved the content to: {saved_blog_to_file}")
|
||||
return saved_blog_to_file
|
||||
except Exception as err:
|
||||
st.error(f"Failed to save blog to file: {err}")
|
||||
logger.error(f"Failed to save blog to file: {err}")
|
||||
status.update(label="❌ Failed to save blog to file", state="error")
|
||||
return None
|
||||
|
||||
|
||||
def generate_audio_version(blog_markdown_str, status=None):
|
||||
"""
|
||||
Generate an audio version of the blog content.
|
||||
|
||||
Args:
|
||||
blog_markdown_str (str): Blog content
|
||||
status: Streamlit status object (optional)
|
||||
|
||||
Returns:
|
||||
bool: True if audio generation was successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
if status:
|
||||
status.update(label="🔊 Generating audio version of the blog...")
|
||||
else:
|
||||
st.info("🔊 Generating audio version...")
|
||||
|
||||
# Only generate audio for reasonable-sized blogs (to avoid errors with very large text)
|
||||
if blog_markdown_str and len(blog_markdown_str) < 50000: # Max ~50KB of text
|
||||
tts = gTTS(text=blog_markdown_str[:40000], lang='en', slow=False) # Use first 40K chars to be safe
|
||||
tts.save("delete_me.mp3")
|
||||
st.audio("delete_me.mp3")
|
||||
st.download_button(
|
||||
label="📥 Download Audio File",
|
||||
data=open("delete_me.mp3", "rb").read(),
|
||||
file_name="blog_audio.mp3",
|
||||
mime="audio/mp3"
|
||||
)
|
||||
if status:
|
||||
status.update(label="✅ Audio version generated successfully", state="complete")
|
||||
else:
|
||||
st.success("✅ Audio version generated successfully")
|
||||
return True
|
||||
else:
|
||||
st.warning("Blog content too large for audio generation")
|
||||
if status:
|
||||
status.update(label="⚠️ Blog content too large for audio generation", state="complete")
|
||||
return False
|
||||
except Exception as err:
|
||||
st.warning(f"Failed to generate audio version: {err}")
|
||||
logger.error(f"Failed to generate audio version: {err}")
|
||||
if status:
|
||||
status.update(label="❌ Failed to generate audio version", state="error")
|
||||
return False
|
||||
|
||||
|
||||
# Helper functions for write_blog_from_keywords
|
||||
def setup_progress_tracking():
|
||||
"""Set up progress tracking elements for blog generation."""
|
||||
# Create a placeholder for the final blog content
|
||||
final_content_placeholder = st.empty()
|
||||
|
||||
# Create progress tracking
|
||||
progress_placeholder = st.empty()
|
||||
with progress_placeholder.container():
|
||||
progress_bar = st.progress(0)
|
||||
status_text = st.empty()
|
||||
|
||||
def update_progress(step, total_steps, message):
|
||||
"""Update the progress bar and status message"""
|
||||
progress_value = min(step / total_steps, 1.0)
|
||||
progress_bar.progress(progress_value)
|
||||
status_text.info(f"Step {step}/{total_steps}: {message}")
|
||||
|
||||
# When process is complete, clear the progress info
|
||||
if step == total_steps:
|
||||
import time
|
||||
time.sleep(3) # Show the complete message for 3 seconds
|
||||
progress_bar.empty()
|
||||
status_text.empty()
|
||||
|
||||
return final_content_placeholder, progress_placeholder, progress_bar, status_text, update_progress
|
||||
|
||||
|
||||
def perform_research_phase(search_keywords, search_params, update_progress):
|
||||
"""
|
||||
Perform the research phase of blog generation.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Keywords to research
|
||||
search_params (dict): Search parameters
|
||||
update_progress (function): Function to update progress
|
||||
|
||||
Returns:
|
||||
tuple: Google search results, Tavily search results, success flags, and blog titles
|
||||
"""
|
||||
update_progress(1, 5, f"Starting web research on '{search_keywords}'")
|
||||
logger.info(f"Researching and Writing Blog on keywords: {search_keywords}")
|
||||
|
||||
# Create a section header for the research phase
|
||||
st.subheader("🔍 Web Research Progress")
|
||||
|
||||
# Use a collapsible expander for research details
|
||||
with st.expander("Research Details", expanded=True):
|
||||
example_blog_titles = []
|
||||
|
||||
# Create a status element for research updates
|
||||
with st.status("Web research in progress...", expanded=True) as status:
|
||||
status.update(label=f"📊 Performing web research on: {search_keywords}")
|
||||
|
||||
# Create status container and progress tracking for Google SERP
|
||||
status_container = st.empty()
|
||||
research_progress = st.progress(0)
|
||||
|
||||
# Google Search
|
||||
status.update(label="🔍 Performing Google search...")
|
||||
google_search_result, g_titles, google_search_success = perform_google_search(
|
||||
search_keywords, search_params, status, status_container, research_progress
|
||||
)
|
||||
if g_titles:
|
||||
example_blog_titles.append(g_titles)
|
||||
status.update(label=f"✅ Google search complete - found {len(g_titles)} relevant resources")
|
||||
else:
|
||||
status.update(label="⚠️ Google search yielded limited results")
|
||||
|
||||
# Tavily Search
|
||||
status.update(label="🔍 Performing Tavily AI search...")
|
||||
tavily_search_result, tavily_search_success = perform_tavily_search(
|
||||
search_keywords, search_params, status
|
||||
)
|
||||
|
||||
if tavily_search_success:
|
||||
status.update(label="✅ Tavily AI search complete", state="complete")
|
||||
elif google_search_success:
|
||||
status.update(label="⚠️ Tavily search had issues, but Google search was successful")
|
||||
else:
|
||||
status.update(label="❌ Both search methods encountered issues", state="error")
|
||||
|
||||
# Clear the progress indicators
|
||||
status_container.empty()
|
||||
research_progress.empty()
|
||||
|
||||
return google_search_result, tavily_search_result, google_search_success, tavily_search_success, example_blog_titles
|
||||
|
||||
|
||||
def generate_content_phase(search_keywords, google_search_result, tavily_search_result,
|
||||
google_search_success, tavily_search_success, blog_params, update_progress):
|
||||
"""
|
||||
Generate blog content from research results.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Keywords to research
|
||||
google_search_result: Results from Google search
|
||||
tavily_search_result: Results from Tavily search
|
||||
google_search_success (bool): Whether Google search was successful
|
||||
tavily_search_success (bool): Whether Tavily search was successful
|
||||
blog_params (dict): Blog parameters
|
||||
update_progress (function): Function to update progress
|
||||
|
||||
Returns:
|
||||
str: Generated blog content or None if generation failed
|
||||
"""
|
||||
# Import content generation function here to avoid circular import
|
||||
from .ai_blog_generator_utils import generate_blog_content
|
||||
|
||||
update_progress(2, 5, "Generating blog content from research")
|
||||
|
||||
# Create a section header for the content generation phase
|
||||
st.subheader("✍️ Content Generation Progress")
|
||||
|
||||
# Use a collapsible expander for content generation details
|
||||
with st.expander("Content Generation Details", expanded=True):
|
||||
# Create a status element for content generation updates
|
||||
with st.status("Content generation in progress...", expanded=True) as status:
|
||||
if google_search_success:
|
||||
source = "Google search results"
|
||||
else:
|
||||
source = "Tavily AI research"
|
||||
|
||||
status.update(label=f"📝 Creating {blog_params.get('blog_tone')} {blog_params.get('blog_type')} content for {blog_params.get('blog_demographic')} audience...")
|
||||
|
||||
blog_markdown_str = generate_blog_content(
|
||||
search_keywords, google_search_result, tavily_search_result,
|
||||
google_search_success, tavily_search_success, blog_params, status
|
||||
)
|
||||
|
||||
if blog_markdown_str:
|
||||
status.update(label=f"✅ Successfully generated ~{len(blog_markdown_str.split())} words of content using {source}", state="complete")
|
||||
else:
|
||||
status.update(label="❌ Content generation failed", state="error")
|
||||
|
||||
return blog_markdown_str
|
||||
|
||||
|
||||
def generate_metadata_and_image(blog_markdown_str, search_keywords, blog_tags, update_progress):
|
||||
"""
|
||||
Generate metadata and featured image for the blog.
|
||||
|
||||
Args:
|
||||
blog_markdown_str (str): Blog content
|
||||
search_keywords (str): Keywords used for research
|
||||
blog_tags (list): Blog tags
|
||||
update_progress (function): Function to update progress
|
||||
|
||||
Returns:
|
||||
tuple: Blog metadata and image filepath
|
||||
"""
|
||||
# Import metadata and image generation functions here to avoid circular import
|
||||
from .ai_blog_generator_utils import generate_blog_metadata, generate_blog_image
|
||||
|
||||
update_progress(3, 5, "Generating SEO metadata and enhancements")
|
||||
|
||||
# Create a section header for the enhancement phase
|
||||
st.subheader("🔍 SEO & Enhancement Progress")
|
||||
|
||||
# Use a collapsible expander for enhancement details
|
||||
with st.expander("Enhancement Details", expanded=True):
|
||||
blog_title = None
|
||||
blog_meta_desc = None
|
||||
blog_categories = None
|
||||
blog_hashtags = None
|
||||
blog_slug = None
|
||||
generated_image_filepath = None
|
||||
saved_blog_to_file = None
|
||||
|
||||
# Create a status element for enhancement updates
|
||||
with st.status("Enhancing content...", expanded=True) as status:
|
||||
# Generate metadata
|
||||
status.update(label="🏷️ Generating SEO metadata (title, description, tags)...")
|
||||
blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = generate_blog_metadata(
|
||||
blog_markdown_str, search_keywords, status
|
||||
)
|
||||
|
||||
# Check if there are updated values in session state
|
||||
if 'blog_title' in st.session_state:
|
||||
blog_title = st.session_state.blog_title
|
||||
status.update(label=f"✅ Using refined title: \"{blog_title}\"")
|
||||
|
||||
if 'blog_meta_desc' in st.session_state:
|
||||
blog_meta_desc = st.session_state.blog_meta_desc
|
||||
status.update(label=f"✅ Using refined meta description")
|
||||
|
||||
if blog_title and blog_meta_desc:
|
||||
status.update(label=f"✅ Generated metadata: \"{blog_title}\"")
|
||||
|
||||
# Generate featured image
|
||||
status.update(label="🖼️ Creating featured image...")
|
||||
generated_image_filepath = generate_blog_image(
|
||||
blog_title, blog_meta_desc, blog_markdown_str, status, blog_tags
|
||||
)
|
||||
|
||||
# Save blog content to file
|
||||
status.update(label="💾 Saving blog content...")
|
||||
saved_blog_to_file = save_blog_content(
|
||||
blog_markdown_str, blog_title, blog_meta_desc, blog_tags,
|
||||
blog_categories, generated_image_filepath, status, blog_hashtags, blog_slug
|
||||
)
|
||||
|
||||
status.update(label="✅ Content enhancement complete", state="complete")
|
||||
else:
|
||||
status.update(label="⚠️ Metadata generation had issues, using simplified format", state="warning")
|
||||
|
||||
# Add buttons for metadata refinement
|
||||
create_metadata_refinement_ui()
|
||||
|
||||
# Add rich snippet section
|
||||
create_structured_data_ui()
|
||||
|
||||
metadata = {
|
||||
"blog_title": blog_title,
|
||||
"blog_meta_desc": blog_meta_desc,
|
||||
"blog_tags": blog_tags,
|
||||
"blog_categories": blog_categories,
|
||||
"blog_hashtags": blog_hashtags,
|
||||
"blog_slug": blog_slug
|
||||
}
|
||||
|
||||
return metadata, generated_image_filepath, saved_blog_to_file
|
||||
|
||||
|
||||
def create_metadata_refinement_ui():
|
||||
"""Create UI elements for refining blog metadata (title and meta description)."""
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("🔄 Refine Blog Title", key="refine_title_main", use_container_width=True):
|
||||
st.session_state.show_title_dialog = True
|
||||
st.rerun()
|
||||
with col2:
|
||||
if st.button("🔄 Refine Meta Description", key="refine_meta_main", use_container_width=True):
|
||||
st.session_state.show_meta_dialog = True
|
||||
st.rerun()
|
||||
|
||||
|
||||
def create_structured_data_ui():
|
||||
"""Create UI elements for generating structured data."""
|
||||
st.markdown("---")
|
||||
structured_data_col1, structured_data_col2 = st.columns([3, 1])
|
||||
|
||||
with structured_data_col1:
|
||||
# Educational popover explaining why rich snippets are important
|
||||
with st.expander("ℹ️ Why Rich Snippets Are Important for SEO"):
|
||||
st.markdown("""
|
||||
### Rich Snippets: Boosting Your SEO and Click-Through Rates
|
||||
|
||||
**What are Rich Snippets?**
|
||||
|
||||
Rich snippets are enhanced search results that display additional information directly in search engine results pages (SERPs). They're created using structured data markup (JSON-LD) that helps search engines understand your content better.
|
||||
|
||||
**Why are they important?**
|
||||
|
||||
1. **Increased Visibility**: Rich snippets stand out in search results with stars, images, and additional information
|
||||
|
||||
2. **Higher Click-Through Rates (CTR)**: Studies show rich snippets can increase CTR by 30-150%
|
||||
|
||||
3. **Improved SEO**: They help search engines understand your content better, potentially improving rankings
|
||||
|
||||
4. **Enhanced User Experience**: Users can see key information before clicking, leading to more qualified traffic
|
||||
|
||||
5. **Mobile-Friendly**: Rich snippets are especially effective on mobile searches
|
||||
|
||||
**Common types of rich snippets include:**
|
||||
- Articles/Blogs (with author, date, image)
|
||||
- Products (with ratings, price, availability)
|
||||
- Recipes (with cooking time, ratings, calories)
|
||||
- Events (with date, location, ticket info)
|
||||
- Local Business (with address, hours, ratings)
|
||||
|
||||
Adding structured data to your content is a powerful SEO technique that requires minimal effort but provides significant benefits.
|
||||
""")
|
||||
|
||||
with structured_data_col2:
|
||||
# Button to generate rich snippet
|
||||
if st.button("📊 Generate Rich Snippet", key="snippet_main", use_container_width=True):
|
||||
st.session_state.show_snippet_dialog = True
|
||||
st.rerun()
|
||||
|
||||
|
||||
def display_featured_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags, generated_image_filepath):
|
||||
"""
|
||||
Display the featured image with regeneration options.
|
||||
|
||||
Args:
|
||||
blog_title (str): Blog title
|
||||
blog_meta_desc (str): Blog meta description
|
||||
blog_markdown_str (str): Blog content
|
||||
blog_tags (list): Blog tags
|
||||
generated_image_filepath (str): Path to the generated image
|
||||
|
||||
Returns:
|
||||
str: Updated image filepath if regenerated, otherwise original filepath
|
||||
"""
|
||||
# Import image regeneration function here to avoid circular import
|
||||
from .ai_blog_generator_utils import regenerate_blog_image
|
||||
|
||||
st.subheader("🖼️ Featured Image")
|
||||
image_container = st.container()
|
||||
|
||||
# Display featured image
|
||||
with image_container:
|
||||
if generated_image_filepath:
|
||||
st.image(generated_image_filepath, caption=blog_title or "Featured Image", use_column_width=True)
|
||||
|
||||
# Add regenerate button
|
||||
if st.button("🔄 Regenerate Image", key="regenerate_image"):
|
||||
new_image_path = regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags)
|
||||
if new_image_path:
|
||||
return new_image_path
|
||||
else:
|
||||
st.info("No featured image was generated. Click below to generate one.")
|
||||
if st.button("🖼️ Generate Image", key="generate_image"):
|
||||
new_image_path = regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags)
|
||||
if new_image_path:
|
||||
return new_image_path
|
||||
|
||||
return generated_image_filepath
|
||||
|
||||
|
||||
def display_blog_content_and_audio(blog_markdown_str, saved_blog_to_file):
|
||||
"""
|
||||
Display the blog content and audio generation option.
|
||||
|
||||
Args:
|
||||
blog_markdown_str (str): Blog content
|
||||
saved_blog_to_file (str): Path to the saved blog file
|
||||
"""
|
||||
# Display blog content
|
||||
st.markdown("## Content")
|
||||
st.markdown(blog_markdown_str)
|
||||
|
||||
# Show file save information if available
|
||||
if saved_blog_to_file:
|
||||
st.success(f"✅ Blog saved to: {saved_blog_to_file}")
|
||||
|
||||
# Add the audio generation button
|
||||
st.markdown("---")
|
||||
audio_col1, audio_col2 = st.columns([1, 3])
|
||||
with audio_col1:
|
||||
generate_audio_button = st.button("🔊 Generate Audio Version", use_container_width=True)
|
||||
|
||||
with audio_col2:
|
||||
if generate_audio_button:
|
||||
generate_audio_version(blog_markdown_str)
|
||||
|
||||
|
||||
def display_final_metadata_table(metadata, update_progress):
|
||||
"""
|
||||
Display the final metadata table and options.
|
||||
|
||||
Args:
|
||||
metadata (dict): Blog metadata
|
||||
update_progress (function): Function to update progress
|
||||
"""
|
||||
update_progress(4, 5, "Preparing final blog presentation")
|
||||
|
||||
st.markdown("---")
|
||||
# Display metadata in a collapsible expander to save space
|
||||
with st.expander("🏷️ Metadata", expanded=True):
|
||||
st.table({
|
||||
"Metadata": ["Blog Title", "Meta Description", "Tags", "Categories", "Hashtags", "Slug"],
|
||||
"Value": [
|
||||
metadata["blog_title"],
|
||||
metadata["blog_meta_desc"],
|
||||
metadata["blog_tags"],
|
||||
metadata["blog_categories"],
|
||||
metadata["blog_hashtags"],
|
||||
metadata["blog_slug"]
|
||||
]
|
||||
})
|
||||
|
||||
# Add buttons in columns for refining metadata
|
||||
create_metadata_refinement_ui()
|
||||
|
||||
# Add a row for structured data with a "Generate Rich Snippet" button
|
||||
st.markdown("---")
|
||||
st.markdown("### Get Structured Data")
|
||||
|
||||
# Add structured data UI
|
||||
create_structured_data_ui()
|
||||
|
||||
# Create snippet generation dialog if button is clicked
|
||||
if st.session_state.get("show_snippet_dialog", False):
|
||||
display_structured_data_dialog(metadata["blog_title"], metadata["blog_tags"])
|
||||
|
||||
|
||||
def display_structured_data_dialog(blog_title, blog_tags):
|
||||
"""
|
||||
Display the structured data generation dialog.
|
||||
|
||||
Args:
|
||||
blog_title (str): Blog title
|
||||
blog_tags (list): Blog tags
|
||||
"""
|
||||
with st.expander("Structured Data Generation Tool", expanded=True):
|
||||
st.subheader("Generate Structured Data (Rich Snippets)")
|
||||
|
||||
# Close button at the top
|
||||
if st.button("Close", key="close_structured_data"):
|
||||
st.session_state.show_snippet_dialog = False
|
||||
st.rerun()
|
||||
|
||||
# Simplified blog URL input
|
||||
blog_url = st.text_input(
|
||||
"Blog URL:",
|
||||
placeholder="https://yourblog.com/your-article",
|
||||
help="Enter the URL where this blog will be published"
|
||||
)
|
||||
|
||||
# Auto-fill content type to "Article" since we're working with a blog
|
||||
content_type = "Article"
|
||||
st.info(f"Content Type: {content_type} (Auto-selected for blog content)")
|
||||
|
||||
# Form for additional article details
|
||||
with st.form(key="structured_data_form"):
|
||||
st.markdown("#### Article Details")
|
||||
|
||||
# Pre-fill with blog title and other metadata
|
||||
article_title = st.text_input("Headline:", value=blog_title if blog_title else "")
|
||||
article_author = st.text_input("Author:", value="")
|
||||
article_date = st.date_input("Date Published:", value=datetime.now())
|
||||
article_keywords = st.text_input("Keywords:", value=blog_tags if blog_tags else "")
|
||||
|
||||
submit_structured_data = st.form_submit_button("Generate JSON-LD")
|
||||
|
||||
if submit_structured_data:
|
||||
if not blog_url:
|
||||
st.error("Please enter a blog URL to generate structured data.")
|
||||
else:
|
||||
# Create details dictionary
|
||||
details = {
|
||||
"Headline": article_title,
|
||||
"Author": article_author,
|
||||
"Date Published": article_date,
|
||||
"Keywords": article_keywords
|
||||
}
|
||||
|
||||
# Call the imported ai_structured_data function or recreate its functionality
|
||||
with st.spinner("Generating structured data..."):
|
||||
# Import and use the function from the module directly
|
||||
from ...ai_seo_tools.seo_structured_data import generate_json_data
|
||||
|
||||
# Generate the structured data
|
||||
structured_data = generate_json_data(content_type, details, blog_url)
|
||||
|
||||
if structured_data:
|
||||
st.success("✅ Structured data generated successfully!")
|
||||
st.markdown("### Generated JSON-LD Code")
|
||||
st.code(structured_data, language="json")
|
||||
|
||||
# Download button
|
||||
st.download_button(
|
||||
label="📥 Download JSON-LD",
|
||||
data=structured_data,
|
||||
file_name=f"{content_type}_structured_data.json",
|
||||
mime="application/json",
|
||||
)
|
||||
|
||||
# Implementation instructions
|
||||
with st.expander("How to Implement This Code"):
|
||||
st.markdown("""
|
||||
### Adding this JSON-LD to your website:
|
||||
|
||||
1. **Copy the generated JSON-LD code** above
|
||||
|
||||
2. **Add it to the `<head>` section of your HTML** like this:
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
[PASTE YOUR JSON-LD CODE HERE]
|
||||
</script>
|
||||
```
|
||||
|
||||
3. **Verify the implementation** using Google's Rich Results Test tool:
|
||||
[https://search.google.com/test/rich-results](https://search.google.com/test/rich-results)
|
||||
|
||||
4. **Monitor your search appearance** in Google Search Console
|
||||
""")
|
||||
else:
|
||||
st.error("Failed to generate structured data. Please check your inputs and try again.")
|
||||
|
||||
|
||||
def display_title_refinement_dialog(blog_title, blog_tags):
|
||||
"""
|
||||
Display a dialog for refining the blog title.
|
||||
|
||||
Args:
|
||||
blog_title (str): Current blog title
|
||||
blog_tags (list): Blog tags for context
|
||||
"""
|
||||
with st.expander("Blog Title Refinement Tool", expanded=True):
|
||||
st.subheader("Generate Better Blog Titles")
|
||||
|
||||
# Form for title generation
|
||||
with st.form(key="title_generation_form"):
|
||||
st.markdown("#### Title Generation Parameters")
|
||||
|
||||
# Pre-fill with blog tags if available
|
||||
keywords = st.text_input("Target Keywords:",
|
||||
value=blog_tags if blog_tags else "",
|
||||
help="Enter primary keywords to target in the title")
|
||||
|
||||
blog_type = st.selectbox(
|
||||
"Blog Type:",
|
||||
["How-to Guide", "Tutorial", "List Post", "Informational", "Case Study", "Opinion Piece", "Review"],
|
||||
index=0,
|
||||
help="Select the type of blog you're creating"
|
||||
)
|
||||
|
||||
search_intent = st.selectbox(
|
||||
"Search Intent:",
|
||||
["Informational", "Commercial", "Navigational", "Transactional"],
|
||||
index=0,
|
||||
help="Select the primary search intent your title should address"
|
||||
)
|
||||
|
||||
language = st.selectbox(
|
||||
"Language:",
|
||||
["English", "Spanish", "French", "German", "Italian"],
|
||||
index=0
|
||||
)
|
||||
|
||||
submit_title = st.form_submit_button("Generate Title Suggestions")
|
||||
|
||||
if submit_title:
|
||||
with st.spinner("Generating title suggestions..."):
|
||||
# Import and use the function from the module
|
||||
from ...ai_seo_tools.content_title_generator import generate_blog_titles
|
||||
|
||||
# Generate the titles
|
||||
title_suggestions = generate_blog_titles(
|
||||
target_keywords=keywords,
|
||||
blog_type=blog_type,
|
||||
search_intent=search_intent,
|
||||
language=language
|
||||
)
|
||||
|
||||
if title_suggestions:
|
||||
st.success("✅ Generated title suggestions!")
|
||||
|
||||
# Display each title with an option to select it
|
||||
st.markdown("### Select a Title or Modify")
|
||||
|
||||
selected_title = st.text_input(
|
||||
"Selected or Modified Title:",
|
||||
value=blog_title if blog_title else (title_suggestions[0] if title_suggestions else ""),
|
||||
help="Select one of the suggested titles or modify it to your preference"
|
||||
)
|
||||
|
||||
if st.button("Confirm Title"):
|
||||
st.session_state.blog_title = selected_title
|
||||
st.session_state.show_title_dialog = False
|
||||
st.success(f"Title updated to: {selected_title}")
|
||||
st.rerun()
|
||||
|
||||
# Display all suggestions
|
||||
for i, title in enumerate(title_suggestions):
|
||||
st.markdown(f"**Option {i+1}:** {title}")
|
||||
else:
|
||||
st.error("Failed to generate title suggestions. Please try different parameters.")
|
||||
|
||||
|
||||
def display_meta_description_dialog(blog_meta_desc, blog_tags):
|
||||
"""
|
||||
Display a dialog for refining the meta description.
|
||||
|
||||
Args:
|
||||
blog_meta_desc (str): Current meta description
|
||||
blog_tags (list): Blog tags for context
|
||||
"""
|
||||
with st.expander("Meta Description Refinement Tool", expanded=True):
|
||||
st.subheader("Generate Optimized Meta Descriptions")
|
||||
|
||||
# Form for meta description generation
|
||||
with st.form(key="meta_desc_generation_form"):
|
||||
st.markdown("#### Meta Description Parameters")
|
||||
|
||||
# Pre-fill with blog tags if available
|
||||
keywords = st.text_input("Target Keywords:",
|
||||
value=blog_tags if blog_tags else "",
|
||||
help="Enter primary keywords to target in the meta description")
|
||||
|
||||
tone = st.selectbox(
|
||||
"Tone:",
|
||||
["Informative", "Engaging", "Professional", "Conversational", "Humorous", "Urgent"],
|
||||
index=0,
|
||||
help="Select the tone for your meta description"
|
||||
)
|
||||
|
||||
search_intent = st.selectbox(
|
||||
"Search Intent:",
|
||||
["Informational", "Commercial", "Navigational", "Transactional"],
|
||||
index=0,
|
||||
help="Select the primary search intent your meta description should address"
|
||||
)
|
||||
|
||||
language = st.selectbox(
|
||||
"Language:",
|
||||
["English", "Spanish", "French", "German", "Italian"],
|
||||
index=0
|
||||
)
|
||||
|
||||
submit_meta = st.form_submit_button("Generate Meta Description Suggestions")
|
||||
|
||||
if submit_meta:
|
||||
with st.spinner("Generating meta description suggestions..."):
|
||||
# Import and use the function from the module
|
||||
from ...ai_seo_tools.meta_desc_generator import generate_blog_metadesc
|
||||
|
||||
# Generate the meta descriptions
|
||||
meta_suggestions = generate_blog_metadesc(
|
||||
target_keywords=keywords,
|
||||
tone=tone,
|
||||
search_intent=search_intent,
|
||||
language=language
|
||||
)
|
||||
|
||||
if meta_suggestions:
|
||||
st.success("✅ Generated meta description suggestions!")
|
||||
|
||||
# Display each meta description with an option to select it
|
||||
st.markdown("### Select a Meta Description or Modify")
|
||||
|
||||
selected_meta = st.text_area(
|
||||
"Selected or Modified Meta Description:",
|
||||
value=blog_meta_desc if blog_meta_desc else (meta_suggestions[0] if meta_suggestions else ""),
|
||||
height=100,
|
||||
help="Select one of the suggested meta descriptions or modify it to your preference"
|
||||
)
|
||||
|
||||
if st.button("Confirm Meta Description"):
|
||||
st.session_state.blog_meta_desc = selected_meta
|
||||
st.session_state.show_meta_dialog = False
|
||||
st.success(f"Meta description updated!")
|
||||
st.rerun()
|
||||
|
||||
# Display all suggestions
|
||||
for i, meta in enumerate(meta_suggestions):
|
||||
st.markdown(f"**Option {i+1}:** {meta}")
|
||||
else:
|
||||
st.error("Failed to generate meta description suggestions. Please try different parameters.")
|
||||
|
||||
|
||||
def write_blog_from_keywords(search_keywords, url=None, search_params=None, blog_params=None):
|
||||
"""
|
||||
This function will take a blog Topic to first generate sections for it
|
||||
and then generate content for each section.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Keywords to research and write about
|
||||
url (str, optional): Optional URL to use as a source
|
||||
search_params (dict, optional): Dictionary of search parameters including:
|
||||
- max_results: Maximum number of search results (default: 10)
|
||||
- search_depth: "basic" or "advanced" search depth (default: "basic")
|
||||
- include_domains: List of domains to prioritize in search
|
||||
- time_range: Time range for results (default: "year")
|
||||
blog_params (dict, optional): Dictionary of blog content characteristics including:
|
||||
- blog_length: Target word count (default: 2000)
|
||||
- blog_tone: Tone of the content (default: "Professional")
|
||||
- blog_demographic: Target audience (default: "Professional")
|
||||
- blog_type: Type of blog post (default: "Informational")
|
||||
- blog_language: Language for the blog (default: "English")
|
||||
- blog_output_format: Format for the blog (default: "markdown")
|
||||
"""
|
||||
# Check if we need to display any dialog boxes first
|
||||
if st.session_state.get("show_title_dialog") and "blog_title" in st.session_state:
|
||||
display_title_refinement_dialog(st.session_state.blog_title, None)
|
||||
return None
|
||||
|
||||
if st.session_state.get("show_meta_dialog") and "blog_meta_desc" in st.session_state:
|
||||
display_meta_description_dialog(st.session_state.blog_meta_desc, None)
|
||||
return None
|
||||
|
||||
if st.session_state.get("show_snippet_dialog"):
|
||||
# Get blog title and tags to pass to the dialog
|
||||
blog_title = st.session_state.get("blog_title", "")
|
||||
blog_tags = st.session_state.get("blog_tags", "")
|
||||
display_structured_data_dialog(blog_title, blog_tags)
|
||||
return None
|
||||
|
||||
# Initialize parameters with defaults
|
||||
search_params, blog_params = initialize_parameters(search_params, blog_params)
|
||||
|
||||
# Set up progress tracking
|
||||
final_content_placeholder, progress_placeholder, progress_bar, status_text, update_progress = setup_progress_tracking()
|
||||
|
||||
# STEP 1: Research phase
|
||||
google_search_result, tavily_search_result, google_search_success, tavily_search_success, example_blog_titles = perform_research_phase(
|
||||
search_keywords, search_params, update_progress
|
||||
)
|
||||
|
||||
# Check if both searches failed - if so, stop the process
|
||||
if not google_search_success and not tavily_search_success:
|
||||
update_progress(5, 5, "Research failed")
|
||||
progress_placeholder.error("⛔ Both Google SERP and Tavily AI searches failed. Unable to generate blog content.")
|
||||
st.warning("Please check your API keys in the environment settings and try again.")
|
||||
st.stop()
|
||||
return None
|
||||
|
||||
# STEP 2: Content generation phase
|
||||
blog_markdown_str = generate_content_phase(
|
||||
search_keywords, google_search_result, tavily_search_result,
|
||||
google_search_success, tavily_search_success, blog_params, update_progress
|
||||
)
|
||||
|
||||
if not blog_markdown_str:
|
||||
update_progress(5, 5, "Content generation failed")
|
||||
progress_placeholder.error("⛔ Failed to generate blog content from research data.")
|
||||
st.stop()
|
||||
return None
|
||||
|
||||
# STEP 3: Metadata & enhancement phase
|
||||
metadata, generated_image_filepath, saved_blog_to_file = generate_metadata_and_image(
|
||||
blog_markdown_str, search_keywords, None, update_progress
|
||||
)
|
||||
|
||||
# Display image with regeneration option
|
||||
updated_image_filepath = display_featured_image(
|
||||
metadata["blog_title"], metadata["blog_meta_desc"],
|
||||
blog_markdown_str, metadata["blog_tags"], generated_image_filepath
|
||||
)
|
||||
|
||||
if updated_image_filepath != generated_image_filepath:
|
||||
generated_image_filepath = updated_image_filepath
|
||||
st.rerun() # Refresh the page to show the new image
|
||||
|
||||
# Display blog content and audio option
|
||||
display_blog_content_and_audio(blog_markdown_str, saved_blog_to_file)
|
||||
|
||||
# STEP 4: Final presentation
|
||||
with final_content_placeholder.container():
|
||||
display_final_metadata_table(metadata, update_progress)
|
||||
|
||||
# If there's a button click to generate a structured data snippet, handle it
|
||||
if st.session_state.get("show_snippet_dialog", False):
|
||||
display_structured_data_dialog(metadata["blog_title"], metadata["blog_tags"])
|
||||
|
||||
# Final progress update
|
||||
update_progress(5, 5, "Blog generation complete!")
|
||||
|
||||
# Replace progress bar with success message
|
||||
progress_placeholder.success("✅ Blog generation process completed successfully!")
|
||||
|
||||
return blog_markdown_str
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,758 +0,0 @@
|
||||
"""
|
||||
Letter Templates Module
|
||||
|
||||
This module provides structured templates and guidance for generating
|
||||
different types and subtypes of letters.
|
||||
Templates are defined as a nested dictionary containing 'structure' (list of sections)
|
||||
and 'guidance' (a string) for each letter type and subtype.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
|
||||
# Define letter templates using a nested dictionary structure for better organization and lookup.
|
||||
# The structure is {letter_type: {subtype: {template_details}}}
|
||||
# 'default' subtype is used as a fallback if a specific subtype isn't found for a given type.
|
||||
TEMPLATES: Dict[str, Dict[str, Dict[str, Any]]] = {
|
||||
"personal": {
|
||||
"congratulations": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express congratulations",
|
||||
"Acknowledge the achievement",
|
||||
"Share personal thoughts/memory (optional)",
|
||||
"Look to the future/well wishes",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, sincere, and specific about the achievement. Express genuine happiness for the recipient. Keep the tone personal and friendly."
|
||||
},
|
||||
"thank_you": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express gratitude clearly",
|
||||
"Specify what you are thankful for",
|
||||
"Explain the impact or how you used it (optional)",
|
||||
"Share a personal thought or memory (optional)",
|
||||
"Offer reciprocation or look to the future",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be specific about what you're thankful for and how it affected you. Express sincere appreciation. Personalize the message."
|
||||
},
|
||||
"sympathy": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express sympathy for the loss",
|
||||
"Acknowledge the significance of the person/situation",
|
||||
"Share a positive memory or quality (optional)",
|
||||
"Offer specific support (optional)",
|
||||
"Closing with comforting words"
|
||||
],
|
||||
"guidance": "Be gentle, compassionate, and sincere. Avoid clichés. Focus on offering genuine comfort and acknowledging the recipient's feelings."
|
||||
},
|
||||
"apology": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Clearly state your apology",
|
||||
"Acknowledge the specific mistake or action",
|
||||
"Express understanding of the impact on the other person",
|
||||
"Explain (briefly, without making excuses) what happened (optional)",
|
||||
"Offer amends or suggest how to make things right",
|
||||
"Assure it won't happen again",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be sincere, take full responsibility for your actions, and focus on making things right. Avoid making excuses or blaming others."
|
||||
},
|
||||
"invitation": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Clearly state the invitation",
|
||||
"Provide full event details (What, When, Where)",
|
||||
"Explain the significance or purpose (optional)",
|
||||
"Mention who else might be there (optional)",
|
||||
"Request RSVP (date and contact method)",
|
||||
"Express anticipation",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be clear and specific about the details (what, when, where, why). Make it easy for the person to respond."
|
||||
},
|
||||
"friendship": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express appreciation for the friendship",
|
||||
"Share a recent memory or anecdote",
|
||||
"Acknowledge the value of the relationship",
|
||||
"Check in on them or share updates",
|
||||
"Look to the future (getting together, etc.)",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, personal, and specific about what you value in the friendship. Share updates and show genuine interest."
|
||||
},
|
||||
"love": {
|
||||
"structure": [
|
||||
"Greeting (Terms of endearment)",
|
||||
"Express depth of feelings",
|
||||
"Share a cherished memory or moment",
|
||||
"Describe specific qualities you love and appreciate",
|
||||
"Reaffirm commitment or future hopes",
|
||||
"Closing (Terms of endearment)"
|
||||
],
|
||||
"guidance": "Be sincere, personal, and specific about your feelings. Use sensory details and emotional language appropriate for your relationship."
|
||||
},
|
||||
"encouragement": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Acknowledge the situation or challenge they face",
|
||||
"Express belief in their abilities/strength",
|
||||
"Offer specific words of encouragement or support",
|
||||
"Remind them of past successes (optional)",
|
||||
"Offer practical help (optional)",
|
||||
"Look to the future with hope",
|
||||
"Closing with support"
|
||||
],
|
||||
"guidance": "Be positive, supportive, and specific about the person's strengths and abilities. Offer genuine encouragement and belief in them."
|
||||
},
|
||||
"farewell": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"State the purpose (saying goodbye)",
|
||||
"Express feelings about their departure (sadness, happiness for them)",
|
||||
"Share a positive memory or highlight their contribution",
|
||||
"Express good wishes for their future endeavors",
|
||||
"Look to staying in touch (optional)",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, reflective, and forward-looking. Focus on positive memories and express genuine good wishes for their next steps."
|
||||
},
|
||||
# Default personal letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Introduction",
|
||||
"Main content paragraphs",
|
||||
"Closing thoughts",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be personal, authentic, and appropriate for your relationship with the recipient. The tone is typically informal to semi-formal."
|
||||
}
|
||||
},
|
||||
"formal": {
|
||||
"application": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (if known)",
|
||||
"Subject line (Clear and concise)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position applied for and where you saw it)",
|
||||
"Body paragraphs (Highlight relevant skills and experience)",
|
||||
"Closing paragraph (Reiterate interest, mention enclosed resume, call to action)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)",
|
||||
"Enclosures (Mention if attaching resume/portfolio)"
|
||||
],
|
||||
"guidance": "Be professional, concise, and specific about your qualifications and genuine interest in the position. Tailor it to the specific job description."
|
||||
},
|
||||
"complaint": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state it's a complaint)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the purpose: complaint about X service/product)",
|
||||
"Problem description (Provide specific details: date, time, location, product details, names if applicable)",
|
||||
"Impact statement (Explain how the problem affected you)",
|
||||
"Requested resolution (Clearly state what you want: refund, replacement, action)",
|
||||
"Closing paragraph (Reference attached documents, state expectation for response)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the issue and your desired resolution. Maintain a respectful but firm tone. Include all relevant details."
|
||||
},
|
||||
"request": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state the request)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the purpose: making a request)",
|
||||
"Request details (Clearly explain what you are requesting)",
|
||||
"Justification (Explain why the request is necessary or beneficial)",
|
||||
"Provide supporting information (optional)",
|
||||
"Closing paragraph (Express gratitude for consideration, reiterate call to action)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and courteous about your request. Explain why it's important or beneficial to the recipient or organization."
|
||||
},
|
||||
"recommendation": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Letter of Recommendation for [Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your name, title, relationship to the recommendee, and for what purpose the letter is written)",
|
||||
"Body paragraphs (Describe the recommendee's qualifications, skills, and achievements with specific examples)",
|
||||
"Highlight relevant experiences and contributions",
|
||||
"Closing recommendation (Summarize endorsement, strongly recommend the person)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be specific, positive, and credible. Use concrete examples and anecdotes to support your recommendation. Tailor it to the specific role/opportunity."
|
||||
},
|
||||
"resignation": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Immediate supervisor/HR)",
|
||||
"Subject line (Letter of Resignation - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Statement of resignation (Clearly state you are resigning)",
|
||||
"Last day of employment (Specify the date)",
|
||||
"Gratitude and reflection (Optional: Express thanks for the opportunity/experience)",
|
||||
"Transition plan/Offer of assistance (Optional: Suggest how to ensure a smooth handover)",
|
||||
"Closing paragraph (Express good wishes for the company's future)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, positive (if possible), and clear about your departure and last day. Maintain a good relationship."
|
||||
},
|
||||
"inquiry": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state the nature of the inquiry)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your purpose for writing - making an inquiry)",
|
||||
"Inquiry details (Provide necessary context or background)",
|
||||
"Specific questions (List your questions clearly, perhaps numbered)",
|
||||
"Closing paragraph (Express gratitude for assistance, indicate when you need a response)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and courteous about your inquiry. Organize your questions logically for easy answering."
|
||||
},
|
||||
"authorization": {
|
||||
"structure": [
|
||||
"Sender's contact information (The grantor of authority)",
|
||||
"Date",
|
||||
"Recipient's contact information (The person/entity receiving the letter)",
|
||||
"Subject line (Letter of Authorization)",
|
||||
"Salutation (Formal)",
|
||||
"Statement of authorization (Clearly state who is authorized)",
|
||||
"Authorized person's details (Full name, ID if applicable)",
|
||||
"Scope of authority (Precisely define what they are authorized to do)",
|
||||
"Limitations (Specify any restrictions or conditions)",
|
||||
"Duration of authorization (Start and end dates, if applicable)",
|
||||
"Closing paragraph (State responsibility, express confidence)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and precise about who is authorized, what they can do, for how long, and under what conditions. This is a legal document."
|
||||
},
|
||||
"appeal": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Appeals committee/relevant authority)",
|
||||
"Subject line (Letter of Appeal - [Your Name] - [Subject of Appeal])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your name, the decision being appealed, and the date of the decision)",
|
||||
"Grounds for appeal (Clearly state the reasons why you believe the decision is incorrect)",
|
||||
"Provide supporting evidence (Reference attached documents: records, photos, etc.)",
|
||||
"Explain mitigating circumstances (Optional)",
|
||||
"Requested outcome (Clearly state what resolution you seek)",
|
||||
"Closing paragraph (Express hope for reconsideration, gratitude for time)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be respectful, factual, and persuasive. Focus on valid grounds for appeal and provide clear, supporting evidence. Maintain a formal tone."
|
||||
},
|
||||
"introduction": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Introduction - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (Introduce yourself and the purpose of the letter)",
|
||||
"Background information (Briefly describe your relevant background or expertise)",
|
||||
"Reason for reaching out (Explain why you are introducing yourself to this specific person/entity)",
|
||||
"Potential areas of collaboration or shared interest (Optional)",
|
||||
"Call to action (Suggest a meeting, call, or further communication)",
|
||||
"Closing paragraph (Express enthusiasm for potential connection)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, informative, and engaging. Clearly explain who you are, your expertise, and why you're reaching out to them specifically."
|
||||
},
|
||||
# Default formal letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Sender's address",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, clear, and concise. Use formal language and structure. The tone is typically formal."
|
||||
}
|
||||
},
|
||||
"business": {
|
||||
"sales": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Benefit-oriented)",
|
||||
"Salutation",
|
||||
"Attention-grabbing opening (Address a pain point or introduce a benefit)",
|
||||
"Problem statement (Briefly describe the challenge the recipient faces)",
|
||||
"Solution presentation (Introduce your product/service as the solution)",
|
||||
"Benefits and features (Explain how your solution helps, focusing on benefits)",
|
||||
"Social proof (Optional: Testimonials, case studies, data)",
|
||||
"Call to action (Clearly state what you want them to do next)",
|
||||
"Closing paragraph (Reiterate benefit, create urgency/incentive)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)",
|
||||
"Enclosures (Optional: Brochure, pricing)"
|
||||
],
|
||||
"guidance": "Be persuasive, customer-focused, and clear about the value proposition. Focus on benefits, not just features. Make the call to action obvious."
|
||||
},
|
||||
"proposal": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Clear and descriptive)",
|
||||
"Salutation",
|
||||
"Introduction (State purpose: submitting a proposal)",
|
||||
"Problem statement/Needs assessment (Demonstrate understanding of client's needs)",
|
||||
"Proposed solution (Describe your solution in detail)",
|
||||
"Implementation plan (Outline steps and timeline)",
|
||||
"Costs and investment (Clearly state pricing and payment terms)",
|
||||
"Benefits and ROI (Explain the value the client will receive)",
|
||||
"Call to action (Suggest next steps: meeting, discussion)",
|
||||
"Closing paragraph (Express enthusiasm, availability for questions)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)",
|
||||
"Enclosures (Proposal document, appendix)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and persuasive about your solution. Focus on the client's needs and the value you provide. Structure it logically."
|
||||
},
|
||||
"order": {
|
||||
"structure": [
|
||||
"Letterhead (Your company)",
|
||||
"Date",
|
||||
"Recipient's address (Supplier)",
|
||||
"Subject line (Purchase Order - [PO Number])",
|
||||
"Salutation",
|
||||
"Introduction (Reference quote/agreement, state purpose: placing an order)",
|
||||
"Order details (Item list with quantities, descriptions, unit prices, total)",
|
||||
"Delivery requirements (Shipping address, requested delivery date, shipping method)",
|
||||
"Payment terms (Reference agreed terms)",
|
||||
"Closing paragraph (Express expectation for timely delivery)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and detailed about what you're ordering, quantities, delivery requirements, and payment terms. Include a purchase order number."
|
||||
},
|
||||
"quotation": {
|
||||
"structure": [
|
||||
"Letterhead (Your company)",
|
||||
"Date",
|
||||
"Recipient's address (Customer)",
|
||||
"Subject line (Quotation for [Product/Service])",
|
||||
"Salutation",
|
||||
"Introduction (Reference inquiry, state purpose: providing a quotation)",
|
||||
"Quotation details (List items/services, descriptions, unit prices, quantities, line totals)",
|
||||
"Pricing breakdown (Mention taxes, discounts, fees separately)",
|
||||
"Terms and conditions (Payment terms, delivery terms, warranty)",
|
||||
"Validity period (State how long the quote is valid)",
|
||||
"Next steps (How they can place an order)",
|
||||
"Closing paragraph (Express hope to do business, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and transparent about pricing, terms, and what's included or excluded. Make it easy for the customer to understand and accept."
|
||||
},
|
||||
"acknowledgment": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Acknowledgment of [Received Item/Request])",
|
||||
"Salutation",
|
||||
"Acknowledgment statement (Clearly state what you have received or are acknowledging)",
|
||||
"Details of what's being acknowledged (Reference number, date, brief description)",
|
||||
"Confirm understanding (Optional: Briefly restate the request/issue to show understanding)",
|
||||
"Next steps (Outline what will happen next, e.g., processing order, investigating issue)",
|
||||
"Timeline (Provide an estimated timeframe if possible)",
|
||||
"Closing paragraph (Express gratitude, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be prompt, clear, and specific about what you're acknowledging. Set clear expectations for next steps and timelines."
|
||||
},
|
||||
"collection": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Invoice [Invoice Number] - Payment Due)",
|
||||
"Salutation",
|
||||
"Introduction (Reference invoice number and due date)",
|
||||
"Account status (Clearly state the outstanding amount)",
|
||||
"Payment request (Politely request payment)",
|
||||
"Payment options (Remind them how to pay)",
|
||||
"Consequences of non-payment (Optional: Briefly mention late fees or further action, depending on letter stage)",
|
||||
"Call to action (Request payment by a specific date)",
|
||||
"Closing paragraph (Express hope for prompt payment, offer to discuss)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be firm but professional. Clearly state the amount due, due date, and payment options. The tone may vary depending on how overdue the payment is."
|
||||
},
|
||||
"adjustment": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address (Customer who made a complaint)",
|
||||
"Subject line (Response to your inquiry - [Reference Number])",
|
||||
"Salutation",
|
||||
"Acknowledgment of complaint (Reference their communication and the issue)",
|
||||
"Investigation findings (Explain the outcome of your investigation)",
|
||||
"Adjustment offered (Clearly state the resolution: refund, replacement, credit, etc.)",
|
||||
"Apology (Optional: Express regret for the inconvenience)",
|
||||
"Preventive measures (Optional: Explain steps taken to prevent recurrence)",
|
||||
"Closing paragraph (Express hope for continued business, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be responsive, empathetic, and solution-oriented. Clearly explain the adjustment and any preventive measures taken."
|
||||
},
|
||||
"credit": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address (Applicant)",
|
||||
"Subject line (Credit Application Status - [Applicant Name])",
|
||||
"Salutation",
|
||||
"Introduction (Reference their credit application and the purpose of the letter)",
|
||||
"Credit decision (Clearly state if credit is approved or denied)",
|
||||
"If approved: Credit terms (Credit limit, payment terms, interest rates)",
|
||||
"If denied: Reason for decision (Provide specific, compliant reasons)",
|
||||
"Requirements (If approved: any further steps or documents needed)",
|
||||
"Closing paragraph (If approved: Express welcome; If denied: Offer alternative options or appeals process)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and transparent about the credit decision, terms, limits, or reasons for denial. Ensure compliance with regulations if denying credit."
|
||||
},
|
||||
"follow_up": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Following up on [Previous Communication/Meeting])",
|
||||
"Salutation",
|
||||
"Reference to previous communication (Mention date, topic, or meeting)",
|
||||
"Purpose of follow-up (Clearly state why you are writing again)",
|
||||
"Action items/Next steps (Remind of agreed-upon actions or propose next steps)",
|
||||
"Provide additional information (Optional)",
|
||||
"Call to action (If applicable, e.g., request a response, schedule a meeting)",
|
||||
"Closing paragraph (Reiterate interest, express anticipation)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and action-oriented. Reference previous communication and clearly state the purpose of your follow-up and desired outcome."
|
||||
},
|
||||
# Default business letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, clear, and concise. Focus on the business purpose of your letter. The tone is typically formal to semi-formal."
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
"standard": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information (if known)",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the position you are applying for, where you saw the advertisement, and a brief statement of enthusiasm)",
|
||||
"Body paragraph 1 (Highlight skills and experience directly relevant to the job description - often 1-2 key qualifications)",
|
||||
"Body paragraph 2 (Provide a specific example or anecdote demonstrating your abilities)",
|
||||
"Body paragraph 3 (Connect your passion/goals to the company's mission/values - optional but effective)",
|
||||
"Closing paragraph (Reiterate interest, mention enclosed resume, call to action)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, specific about your most relevant qualifications, and clear about your interest in the position. Tailor every cover letter to the specific job and company."
|
||||
},
|
||||
"career_change": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the position and acknowledge your career transition)",
|
||||
"Body paragraph 1 (Highlight transferable skills from previous roles)",
|
||||
"Body paragraph 2 (Explain your motivation for the career change and how your skills apply)",
|
||||
"Body paragraph 3 (Demonstrate understanding of the new industry/role)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention enclosed resume, call to action)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Focus on transferable skills and explain your career transition. Connect your past experience and new skills directly to the requirements of the target role."
|
||||
},
|
||||
"entry_level": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the position and your enthusiasm for the opportunity as a recent graduate/entrant)",
|
||||
"Body paragraph 1 (Highlight relevant education, coursework, GPA if strong)",
|
||||
"Body paragraph 2 (Describe relevant internships, projects, or volunteer experience)",
|
||||
"Body paragraph 3 (Showcase soft skills: teamwork, communication, eagerness to learn)",
|
||||
"Closing paragraph (Reiterate interest, mention attached resume, express availability for interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize education, relevant internships/projects, and transferable skills gained through academic or extracurricular activities. Show strong potential and enthusiasm."
|
||||
},
|
||||
"executive": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Senior Executive/Board Member)",
|
||||
"Subject line (Executive Application - [Your Name] - [Position])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position applying for, brief summary of executive profile)",
|
||||
"Body paragraph 1 (Highlight strategic leadership experience and key achievements)",
|
||||
"Body paragraph 2 (Discuss relevant industry expertise and market insights)",
|
||||
"Body paragraph 3 (Describe experience in driving growth, managing teams, achieving results)",
|
||||
"Closing paragraph (Reiterate interest, express desire to discuss contribution to the organization)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize strategic leadership experience, significant achievements with measurable results, and industry expertise. Use a confident, authoritative, and forward-looking tone."
|
||||
},
|
||||
"creative": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Application - [Your Name] - [Creative Role])",
|
||||
"Salutation",
|
||||
"Creative introduction (Engaging hook related to the role or your passion)",
|
||||
"Body paragraph 1 (Highlight relevant creative experience and skills)",
|
||||
"Body paragraph 2 (Reference specific portfolio pieces or projects that showcase your style/abilities)",
|
||||
"Body paragraph 3 (Describe your creative process or approach)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention attached resume/portfolio link, call to action)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Use a more engaging and expressive style appropriate for a creative role while maintaining professionalism. Highlight specific creative achievements and link to your portfolio."
|
||||
},
|
||||
"technical": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Application - [Your Name] - [Technical Role])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position, source, and brief technical interest)",
|
||||
"Body paragraph 1 (Highlight specific technical skills and proficiencies relevant to the job description)",
|
||||
"Body paragraph 2 (Describe relevant technical projects or challenges you've solved)",
|
||||
"Body paragraph 3 (Discuss problem-solving abilities and experience with relevant technologies)",
|
||||
"Closing paragraph (Reiterate interest, mention attached resume, express availability for technical discussion/interview)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Focus on technical skills, relevant projects, and problem-solving abilities. Use appropriate technical terminology accurately."
|
||||
},
|
||||
"academic": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Search Committee Chair)",
|
||||
"Subject line (Application for [Position] - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the position, the department, and express your strong interest)",
|
||||
"Body paragraph 1 (Discuss your research experience, focus on key projects and contributions)",
|
||||
"Body paragraph 2 (Describe your teaching philosophy and relevant teaching experience)",
|
||||
"Body paragraph 3 (Mention publications, presentations, grants, and other scholarly contributions)",
|
||||
"Closing paragraph (Reiterate enthusiasm for joining the faculty, express availability for interview/presentation)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Focus on research experience, teaching philosophy, publications, and contributions to the field. Use a scholarly and professional tone suitable for academia."
|
||||
},
|
||||
"remote": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Remote Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the remote position, source, and enthusiasm for remote work)",
|
||||
"Body paragraph 1 (Highlight experience working remotely or independently)",
|
||||
"Body paragraph 2 (Emphasize self-management, time management, and organizational skills required for remote work)",
|
||||
"Body paragraph 3 (Describe strong written and verbal communication skills, essential for remote collaboration)",
|
||||
"Closing paragraph (Reiterate interest in the remote role, mention attached resume, express availability for video interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize self-motivation, excellent communication skills (especially written), time management, and any prior experience working independently or in remote teams."
|
||||
},
|
||||
"referral": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Referral Application - [Your Name] - [Job Title] - Referred by [Referrer's Name])",
|
||||
"Salutation",
|
||||
"Referral introduction (Immediately state who referred you and for what position)",
|
||||
"Body paragraph 1 (Briefly explain your connection to the referrer and how you learned about the role)",
|
||||
"Body paragraph 2 (Highlight key qualifications relevant to the job description)",
|
||||
"Body paragraph 3 (Express strong interest in the position and the company)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention attached resume, express availability for interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Mention the referral prominently and early. Explain your connection to the referrer and how it aligns with your interest in the role. Still, ensure you highlight your own qualifications."
|
||||
},
|
||||
# Default cover letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Contact information",
|
||||
"Date",
|
||||
"Recipient's information",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, specific about your qualifications, and clear about your interest in the position. Tailor your letter to the specific job and company."
|
||||
}
|
||||
},
|
||||
# Overall default template if letter type is not recognized
|
||||
"default": {
|
||||
"structure": [
|
||||
"Introduction",
|
||||
"Body",
|
||||
"Conclusion"
|
||||
],
|
||||
"guidance": "Be clear, concise, and appropriate for your audience and purpose. This is a generic structure."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_template_by_type(letter_type: str, subtype: str = "default") -> Dict[str, Any]:
|
||||
"""
|
||||
Get a template for a specific letter type and subtype using a dictionary lookup.
|
||||
|
||||
Args:
|
||||
letter_type: Type of letter (e.g., "personal", "formal", "business", "cover").
|
||||
subtype: Subtype of letter (e.g., "congratulations", "application", "sales").
|
||||
Defaults to "default" if no subtype is specified.
|
||||
|
||||
Returns:
|
||||
Template dictionary with 'structure' (List[str]) and 'guidance' (str).
|
||||
Returns the default template if the letter type or subtype is not found,
|
||||
ensuring the return structure is always consistent.
|
||||
"""
|
||||
# Get templates for the specific letter type, or the overall default templates
|
||||
# .get() method is used for safe dictionary access with a default fallback
|
||||
type_templates = TEMPLATES.get(letter_type, TEMPLATES["default"])
|
||||
|
||||
# Get the template for the specific subtype, or the default for that letter type
|
||||
# Chain .get() calls to handle cases where subtype or the type's default is missing
|
||||
template = type_templates.get(subtype, type_templates.get("default", TEMPLATES["default"]))
|
||||
|
||||
# Ensure the returned template always has 'structure' (as a list) and 'guidance' (as a string) keys.
|
||||
# This adds robustness in case a template definition is incomplete.
|
||||
if "structure" not in template or not isinstance(template["structure"], list):
|
||||
# Fallback structure if missing or incorrect type
|
||||
template["structure"] = ["Introduction", "Body", "Conclusion"]
|
||||
# Update guidance to reflect that the structure was defaulted
|
||||
template["guidance"] = "Generic template structure applied due to missing or invalid definition."
|
||||
|
||||
if "guidance" not in template or not isinstance(template["guidance"], str):
|
||||
# Fallback guidance if missing or incorrect type
|
||||
template["guidance"] = "Generic guidance applied due to missing or invalid definition."
|
||||
|
||||
|
||||
return template
|
||||
|
||||
# Example usage (for testing purposes)
|
||||
if __name__ == '__main__':
|
||||
# Test cases to demonstrate functionality and default handling
|
||||
print("--- Testing Letter Templates Module ---")
|
||||
|
||||
# Test a known personal letter subtype
|
||||
personal_congrats = get_template_by_type("personal", "congratulations")
|
||||
print("\nPersonal Congratulations Template:")
|
||||
print(f"Structure: {personal_congrats['structure']}")
|
||||
print(f"Guidance: {personal_congrats['guidance']}")
|
||||
|
||||
# Test a known formal letter subtype
|
||||
formal_complaint = get_template_by_type("formal", "complaint")
|
||||
print("\nFormal Complaint Template:")
|
||||
print(f"Structure: {formal_complaint['structure']}")
|
||||
print(f"Guidance: {formal_complaint['guidance']}")
|
||||
|
||||
# Test a known business letter subtype
|
||||
business_sales = get_template_by_type("business", "sales")
|
||||
print("\nBusiness Sales Template:")
|
||||
print(f"Structure: {business_sales['structure']}")
|
||||
print(f"Guidance: {business_sales['guidance']}")
|
||||
|
||||
# Test a known cover letter subtype
|
||||
cover_entry_level = get_template_by_type("cover", "entry_level")
|
||||
print("\nCover Entry Level Template:")
|
||||
print(f"Structure: {cover_entry_level['structure']}")
|
||||
print(f"Guidance: {cover_entry_level['guidance']}")
|
||||
|
||||
# Test an unknown letter type (should fallback to overall default)
|
||||
unknown_type = get_template_by_type("unknown_type", "some_subtype")
|
||||
print("\nUnknown Type Template (Should be Overall Default):")
|
||||
print(f"Structure: {unknown_type['structure']}")
|
||||
print(f"Guidance: {unknown_type['guidance']}")
|
||||
|
||||
# Test a known letter type but unknown subtype (should fallback to type's default)
|
||||
personal_unknown_subtype = get_template_by_type("personal", "unknown_subtype")
|
||||
print("\nPersonal Unknown Subtype Template (Should be Personal Default):")
|
||||
print(f"Structure: {personal_unknown_subtype['structure']}")
|
||||
print(f"Guidance: {personal_unknown_subtype['guidance']}")
|
||||
|
||||
# Test with only letter type (should use type's default)
|
||||
formal_default = get_template_by_type("formal")
|
||||
print("\nFormal Default Template (No Subtype Specified):")
|
||||
print(f"Structure: {formal_default['structure']}")
|
||||
print(f"Guidance: {formal_default['guidance']}")
|
||||
@@ -1,236 +0,0 @@
|
||||
"""
|
||||
AI Letter Writer - Main Module
|
||||
|
||||
This module provides a comprehensive interface for generating various types of letters
|
||||
using AI assistance. It supports multiple letter formats, styles, and use cases.
|
||||
It uses Streamlit for the user interface.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
# Assuming these modules exist in a package structure
|
||||
from .letter_types import (
|
||||
business_letters,
|
||||
personal_letters,
|
||||
formal_letters,
|
||||
cover_letters,
|
||||
recommendation_letters,
|
||||
complaint_letters,
|
||||
thank_you_letters,
|
||||
invitation_letters
|
||||
)
|
||||
# Assuming these utility functions exist
|
||||
from .utils.letter_formatter import format_letter
|
||||
from .utils.letter_analyzer import analyze_letter_tone, check_formality
|
||||
from .utils.letter_templates import get_template_by_type
|
||||
|
||||
# Define the letter types and their properties
|
||||
LETTER_TYPES_CONFIG = [
|
||||
{
|
||||
"id": "business",
|
||||
"name": "Business Letters",
|
||||
"icon": "💼",
|
||||
"description": "Professional correspondence for business contexts.",
|
||||
"color": "#1E88E5", # Blue 600
|
||||
"module": business_letters
|
||||
},
|
||||
{
|
||||
"id": "personal",
|
||||
"name": "Personal Letters",
|
||||
"icon": "💌",
|
||||
"description": "Heartfelt messages for friends and family.",
|
||||
"color": "#43A047", # Green 600
|
||||
"module": personal_letters
|
||||
},
|
||||
{
|
||||
"id": "formal",
|
||||
"name": "Formal Letters",
|
||||
"icon": "📜",
|
||||
"description": "Official correspondence for institutions and authorities.",
|
||||
"color": "#5E35B1", # Deep Purple 600
|
||||
"module": formal_letters
|
||||
},
|
||||
{
|
||||
"id": "cover",
|
||||
"name": "Cover Letters",
|
||||
"icon": "📋",
|
||||
"description": "Job application letters to showcase your qualifications.",
|
||||
"color": "#FB8C00", # Orange 600
|
||||
"module": cover_letters
|
||||
},
|
||||
{
|
||||
"id": "recommendation",
|
||||
"name": "Recommendation Letters",
|
||||
"icon": "👍",
|
||||
"description": "Endorse colleagues, students, or employees.",
|
||||
"color": "#00ACC1", # Cyan 600
|
||||
"module": recommendation_letters
|
||||
},
|
||||
{
|
||||
"id": "complaint",
|
||||
"name": "Complaint Letters",
|
||||
"icon": "⚠️",
|
||||
"description": "Address issues with products, services, or situations.",
|
||||
"color": "#E53935", # Red 600
|
||||
"module": complaint_letters
|
||||
},
|
||||
{
|
||||
"id": "thank_you",
|
||||
"name": "Thank You Letters",
|
||||
"icon": "🙏",
|
||||
"description": "Express gratitude for various occasions.",
|
||||
"color": "#8E24AA", # Purple 600
|
||||
"module": thank_you_letters
|
||||
},
|
||||
{
|
||||
"id": "invitation",
|
||||
"name": "Invitation Letters",
|
||||
"icon": "🎉",
|
||||
"description": "Invite people to events, interviews, or gatherings.",
|
||||
"color": "#FFB300", # Amber 600
|
||||
"module": invitation_letters
|
||||
}
|
||||
]
|
||||
|
||||
# Map letter type IDs to their modules for easy access
|
||||
LETTER_MODULES_MAP = {config["id"]: config["module"] for config in LETTER_TYPES_CONFIG}
|
||||
|
||||
|
||||
def initialize_session_state() -> None:
|
||||
"""Initializes necessary Streamlit session state variables."""
|
||||
if "letter_type" not in st.session_state:
|
||||
st.session_state.letter_type = None
|
||||
if "letter_subtype" not in st.session_state:
|
||||
st.session_state.letter_subtype = None # Useful if a letter type has subtypes
|
||||
if "generated_letter" not in st.session_state:
|
||||
st.session_state.generated_letter = None
|
||||
if "letter_metadata" not in st.session_state:
|
||||
# Store information like sender, recipient, date, subject, tone, etc.
|
||||
st.session_state.letter_metadata = {}
|
||||
if "letter_input_data" not in st.session_state:
|
||||
# Store user inputs for letter generation
|
||||
st.session_state.letter_input_data = {}
|
||||
|
||||
|
||||
def display_letter_type_selection() -> None:
|
||||
"""Displays the letter type selection interface using a grid of styled containers with buttons."""
|
||||
|
||||
st.markdown("## Select Letter Type")
|
||||
|
||||
# Create a grid layout for the cards (3 columns)
|
||||
cols = st.columns(3)
|
||||
|
||||
# Display each letter type as a card with a button below it
|
||||
for i, letter_type_config in enumerate(LETTER_TYPES_CONFIG):
|
||||
with cols[i % 3]:
|
||||
# Use markdown to create a styled container for the card appearance
|
||||
st.markdown(
|
||||
f"""
|
||||
<div style="
|
||||
background-color: {letter_type_config['color']};
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px; /* Space between card content and button */
|
||||
color: white;
|
||||
min-height: 180px; /* Ensure consistent minimum height */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between; /* Distribute space within the card */
|
||||
">
|
||||
<h3 style="margin-top: 0; color: white;">{letter_type_config['icon']} {letter_type_config['name']}</h3>
|
||||
<p style="color: white;">{letter_type_config['description']}</p>
|
||||
</div>
|
||||
""",
|
||||
unsafe_allow_html=True
|
||||
)
|
||||
|
||||
# Place the Streamlit button below the styled container
|
||||
# Make the button expand to the width of the column for better alignment with the card
|
||||
if st.button(
|
||||
f"Select {letter_type_config['name']}",
|
||||
key=f"btn_select_{letter_type_config['id']}", # Unique key for each button
|
||||
use_container_width=True
|
||||
):
|
||||
st.session_state.letter_type = letter_type_config['id']
|
||||
# Clear previous state data when selecting a new type
|
||||
st.session_state.letter_subtype = None
|
||||
st.session_state.generated_letter = None
|
||||
st.session_state.letter_metadata = {}
|
||||
st.session_state.letter_input_data = {}
|
||||
st.rerun()
|
||||
|
||||
|
||||
def display_letter_interface(letter_type_id: str) -> None:
|
||||
"""
|
||||
Displays the interface for the selected letter type by calling the
|
||||
appropriate module's write function.
|
||||
|
||||
Args:
|
||||
letter_type_id: The ID string of the selected letter type.
|
||||
"""
|
||||
module = LETTER_MODULES_MAP.get(letter_type_id)
|
||||
|
||||
if module:
|
||||
try:
|
||||
# Call the main function (e.g., write_letter or main) from the selected module
|
||||
# Assuming the module has a function that renders its UI and handles generation
|
||||
module.write_letter() # Assuming the function is named 'write_letter'
|
||||
except AttributeError:
|
||||
st.error(f"Module for '{letter_type_id}' does not have a 'write_letter' function.")
|
||||
except Exception as e:
|
||||
st.error(f"An error occurred while loading the interface for '{letter_type_id}': {e}")
|
||||
else:
|
||||
st.error(f"Letter type module '{letter_type_id}' not found in map.")
|
||||
|
||||
|
||||
def write_letter() -> None:
|
||||
"""Main function for the AI Letter Writer interface."""
|
||||
|
||||
# Page title and description
|
||||
st.title("✉️ AI Letter Writer")
|
||||
st.markdown("""
|
||||
Create professional, personalized letters for any occasion. Select a letter type below to get started.
|
||||
Our AI will help you craft the perfect letter with the right tone, structure, and content.
|
||||
""")
|
||||
|
||||
# Initialize session state on first run
|
||||
initialize_session_state()
|
||||
|
||||
# Back button logic - only show if a letter type is selected
|
||||
if st.session_state.letter_type is not None:
|
||||
if st.button("← Back to Letter Types"):
|
||||
# Reset session state to return to selection
|
||||
st.session_state.letter_type = None
|
||||
st.session_state.letter_subtype = None
|
||||
st.session_state.generated_letter = None
|
||||
st.session_state.letter_metadata = {}
|
||||
st.session_state.letter_input_data = {}
|
||||
st.rerun() # Rerun to show the selection page
|
||||
|
||||
# Main navigation logic
|
||||
if st.session_state.letter_type is None:
|
||||
# Display letter type selection if no type is selected
|
||||
display_letter_type_selection()
|
||||
else:
|
||||
# Display the interface for the selected letter type
|
||||
display_letter_interface(st.session_state.letter_type)
|
||||
|
||||
# --- Placeholder for displaying generated letter and actions ---
|
||||
# This part would typically be handled within the specific letter type modules
|
||||
# after the letter is generated. However, if a common display is needed
|
||||
# after returning from the module function, it would go here, but this
|
||||
# requires the module function to somehow signal completion or store
|
||||
# the generated letter in session state. The current structure expects
|
||||
# the module's write_letter() to handle its entire lifecycle.
|
||||
|
||||
# Example of potentially displaying a generated letter after returning
|
||||
# (This assumes the module updates st.session_state.generated_letter)
|
||||
# if st.session_state.generated_letter:
|
||||
# st.subheader("Generated Letter Preview")
|
||||
# st.text_area("Your Letter", st.session_state.generated_letter, height=400)
|
||||
# # Add options like copy, download, analyze, edit, etc.
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the main letter writing function when the script is executed
|
||||
write_letter()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,493 +0,0 @@
|
||||
"""
|
||||
Letter Analyzer Utility
|
||||
|
||||
This module provides functions for analyzing letter content, including tone,
|
||||
formality, readability, and offering basic suggestions for improvement.
|
||||
Note: The analysis methods provided here are simplified rule-based and
|
||||
keyword-based approaches. For more sophisticated analysis in a production
|
||||
environment, consider using advanced Natural Language Processing (NLP)
|
||||
libraries and models.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Dict, Any, Tuple, List
|
||||
|
||||
def analyze_letter_tone(content: str) -> Dict[str, float]:
|
||||
"""
|
||||
Analyze the tone of a letter based on the presence of specific keywords
|
||||
and phrases.
|
||||
|
||||
Args:
|
||||
content: The letter content to analyze.
|
||||
|
||||
Returns:
|
||||
Dictionary with tone scores (formal, friendly, assertive, etc.).
|
||||
Scores are based on the frequency of matching patterns and capped at 1.0.
|
||||
"""
|
||||
# This is a simplified version using keyword matching.
|
||||
# A more sophisticated approach would involve NLP libraries for sentiment and tone analysis.
|
||||
|
||||
# Initialize tone scores
|
||||
# Scores are arbitrary counts normalized in a simple way
|
||||
tone_scores = {
|
||||
"formal": 0.0,
|
||||
"friendly": 0.0,
|
||||
"assertive": 0.0,
|
||||
"respectful": 0.0,
|
||||
"urgent": 0.0,
|
||||
"apologetic": 0.0
|
||||
}
|
||||
|
||||
# Define patterns for different tones (case-insensitive)
|
||||
formal_patterns = [
|
||||
r"\bI am writing to\b",
|
||||
r"\bI would like to\b",
|
||||
r"\bplease find\b",
|
||||
r"\bregarding\b",
|
||||
r"\bpursuant to\b",
|
||||
r"\bhereby\b",
|
||||
r"\bthus\b",
|
||||
r"\btherefore\b",
|
||||
r"\bfurthermore\b",
|
||||
r"\bconsequently\b",
|
||||
r"\bnevertheless\b",
|
||||
r"\bmoreover\b",
|
||||
r"\benclosed\b", # Added common formal word
|
||||
r"\bherewith\b" # Added common formal word
|
||||
]
|
||||
|
||||
friendly_patterns = [
|
||||
r"\bhope you're well\b",
|
||||
r"\bhope this finds you well\b",
|
||||
r"\bgreat to hear\b",
|
||||
r"\blooking forward\b",
|
||||
r"\bthanks\b",
|
||||
r"\bappreciate\b",
|
||||
r"!", # Exclamation points often indicate friendly or excited tone
|
||||
r"\bexcited\b",
|
||||
r"\bgreat\b", # Common friendly adjective
|
||||
r"\bnice\b" # Common friendly adjective
|
||||
]
|
||||
|
||||
assertive_patterns = [
|
||||
r"\brequire\b",
|
||||
r"\bmust\b",
|
||||
r"\bneed\b",
|
||||
r"\bexpect\b",
|
||||
r"\bdemand\b",
|
||||
r"\binsist\b",
|
||||
r"\bimmediately\b",
|
||||
r"\baction\b", # Often used in assertive contexts
|
||||
r"\bresolution\b" # Can imply assertion
|
||||
]
|
||||
|
||||
respectful_patterns = [
|
||||
r"\brespectfully\b",
|
||||
r"\bhonored\b",
|
||||
r"\bplease\b",
|
||||
r"\bkindly\b",
|
||||
r"\bgrateful\b",
|
||||
r"\bthank you\b",
|
||||
r"\bappreciate\b",
|
||||
r"\bhumbly\b", # Added respectful word
|
||||
r"\bapologies\b" # Can show respect for impact
|
||||
]
|
||||
|
||||
urgent_patterns = [
|
||||
r"\burgent\b",
|
||||
r"\bas soon as possible\b",
|
||||
r"\bASAP\b",
|
||||
r"\bimmediately\b",
|
||||
r"\bpressing\b",
|
||||
r"\bcritical\b",
|
||||
r"\bdeadline\b",
|
||||
r"\bexpedite\b", # Added urgent word
|
||||
r"\bpromptly\b" # Added urgent word
|
||||
]
|
||||
|
||||
apologetic_patterns = [
|
||||
r"\bapologize\b",
|
||||
r"\bsorry\b",
|
||||
r"\bregret\b",
|
||||
r"\bmistake\b",
|
||||
r"\berror\b",
|
||||
r"\binconvenience\b",
|
||||
r"\bfault\b", # Added apologetic word
|
||||
r"\boversight\b" # Added apologetic word
|
||||
]
|
||||
|
||||
# Count pattern matches and update scores (arbitrary weighting)
|
||||
# A simple count multiplied by a factor acts as a basic indicator
|
||||
for pattern in formal_patterns:
|
||||
tone_scores["formal"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
for pattern in friendly_patterns:
|
||||
tone_scores["friendly"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
for pattern in assertive_patterns:
|
||||
tone_scores["assertive"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
for pattern in respectful_patterns:
|
||||
tone_scores["respectful"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
for pattern in urgent_patterns:
|
||||
tone_scores["urgent"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
for pattern in apologetic_patterns:
|
||||
tone_scores["apologetic"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
# Cap scores at 1.0 (arbitrary capping)
|
||||
# A more meaningful score might be relative frequency or use a proper model
|
||||
for tone in tone_scores:
|
||||
tone_scores[tone] = min(tone_scores[tone], 1.0)
|
||||
|
||||
return tone_scores
|
||||
|
||||
def check_formality(content: str) -> float:
|
||||
"""
|
||||
Check the formality level of a letter based on the presence of formal
|
||||
vs. informal indicators and contractions.
|
||||
|
||||
Args:
|
||||
content: The letter content to analyze.
|
||||
|
||||
Returns:
|
||||
Formality score between 0.0 (very informal) and 1.0 (very formal).
|
||||
Calculated as formal_count / (formal_count + informal_count).
|
||||
"""
|
||||
# This is a simplified version based on keyword counting.
|
||||
# More accurate formality analysis would require advanced NLP techniques.
|
||||
|
||||
# Define formal and informal indicators (case-insensitive)
|
||||
formal_indicators = [
|
||||
r"\bDear\b",
|
||||
r"\bSincerely\b",
|
||||
r"\bRegards\b",
|
||||
r"\bRespectfully\b",
|
||||
r"\bI am writing to\b",
|
||||
r"\bI would like to\b",
|
||||
r"\bplease find\b",
|
||||
r"\bregarding\b",
|
||||
r"\bpursuant to\b",
|
||||
r"\bhereby\b",
|
||||
r"\bthus\b",
|
||||
r"\btherefore\b",
|
||||
r"\bfurthermore\b",
|
||||
r"\bconsequently\b",
|
||||
r"\bnevertheless\b",
|
||||
r"\bmoreover\b",
|
||||
r"\benclosed\b",
|
||||
r"\bherewith\b",
|
||||
r"\bsincerely yours\b", # Added
|
||||
r"\bto whom it may concern\b" # Added
|
||||
]
|
||||
|
||||
informal_indicators = [
|
||||
r"\bHey\b",
|
||||
r"\bHi\b",
|
||||
r"\bWhat's up\b",
|
||||
r"\bCheers\b",
|
||||
r"\bThanks\b", # 'Thank you' is formal, 'Thanks' is informal
|
||||
r"\bTake care\b",
|
||||
r"\bSee you\b",
|
||||
r"\bLater\b",
|
||||
r"\bBye\b",
|
||||
r"\bLove\b", # As a closing
|
||||
r"\bXO\b",
|
||||
r"!+", # Multiple exclamation points
|
||||
r"\bawesome\b",
|
||||
r"\bcool\b",
|
||||
r"\bgreat\b",
|
||||
r"\bnice\b",
|
||||
r"\bbtw\b", # By the way
|
||||
r"\bimo\b", # In my opinion
|
||||
r"\blol\b" # Laugh out loud
|
||||
]
|
||||
|
||||
# Define common contractions (case-insensitive)
|
||||
contractions = [
|
||||
r"\bdon't\b", r"\bcan't\b", r"\bwon't\b", r"\bshouldn't\b",
|
||||
r"\bcouldn't\b", r"\bwouldn't\b", r"\bhasn't\b", r"\bhaven't\b",
|
||||
r"\bisn't\b", r"\baren't\b", r"\bwasn't\b", r"\bweren't\b",
|
||||
r"\bi'm\b", r"\byou're\b", r"\bhe's\b", r"\bshe's\b", r"\bit's\b",
|
||||
r"\bwe're\b", r"\bthey're\b", r"\bi've\b", r"\byou've\b",
|
||||
r"\bwe've\b", r"\bthey've\b", r"\bi'd\b", r"\byou'd\b",
|
||||
r"\bhe'd\b", r"\bshe'd\b", r"\bit'd\b", r"\bwe'd\b", r"\bthey'd\b",
|
||||
r"\bi'll\b", r"\byou'll\b", r"\bhe'll\b", r"\bshe'll\b", r"\bit'll\b",
|
||||
r"\bwe'll\b", r"\bthey'll\b"
|
||||
]
|
||||
|
||||
formal_count = 0
|
||||
for pattern in formal_indicators:
|
||||
formal_count += len(re.findall(pattern, content, re.IGNORECASE))
|
||||
|
||||
informal_count = 0
|
||||
for pattern in informal_indicators:
|
||||
informal_count += len(re.findall(pattern, content, re.IGNORECASE))
|
||||
|
||||
# Count contractions as informal indicators
|
||||
for pattern in contractions:
|
||||
informal_count += len(re.findall(pattern, content, re.IGNORECASE))
|
||||
|
||||
# Calculate formality score
|
||||
total_indicators = formal_count + informal_count
|
||||
if total_indicators == 0:
|
||||
# If no indicators found, return a neutral score
|
||||
return 0.5
|
||||
|
||||
# Score is the proportion of formal indicators
|
||||
formality_score = formal_count / total_indicators
|
||||
return formality_score
|
||||
|
||||
def count_syllables_simple(word: str) -> int:
|
||||
"""
|
||||
Counts syllables in a word using a simplified heuristic.
|
||||
This method is not linguistically perfect but provides a basic estimate
|
||||
for readability formulas.
|
||||
|
||||
Args:
|
||||
word: The word string.
|
||||
|
||||
Returns:
|
||||
Estimated syllable count.
|
||||
"""
|
||||
word = word.lower()
|
||||
if len(word) <= 3:
|
||||
# Assume short words have one syllable
|
||||
return 1
|
||||
|
||||
# Remove common silent endings like 'e', 'es', 'ed'
|
||||
if word.endswith(('es', 'ed')):
|
||||
word = word[:-2]
|
||||
elif word.endswith('e'):
|
||||
word = word[:-1]
|
||||
|
||||
# Count vowel groups (consecutive vowels count as one syllable)
|
||||
vowels = 'aeiouy'
|
||||
count = 0
|
||||
prev_is_vowel = False
|
||||
|
||||
for char in word:
|
||||
is_vowel = char in vowels
|
||||
if is_vowel and not prev_is_vowel:
|
||||
count += 1
|
||||
prev_is_vowel = is_vowel
|
||||
|
||||
# Ensure at least one syllable is counted
|
||||
return max(1, count)
|
||||
|
||||
|
||||
def get_readability_metrics(content: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate readability metrics for a letter using simplified methods
|
||||
like Flesch Reading Ease.
|
||||
|
||||
Args:
|
||||
content: The letter content to analyze.
|
||||
|
||||
Returns:
|
||||
Dictionary with readability metrics: word_count, sentence_count,
|
||||
avg_words_per_sentence, flesch_reading_ease, reading_level.
|
||||
"""
|
||||
# Split content into words and sentences using simple regex
|
||||
words = re.findall(r'\b\w+\b', content)
|
||||
# Split by common sentence terminators, handling potential multiple marks
|
||||
sentences = re.split(r'[.!?]+\s*', content)
|
||||
# Filter out empty strings resulting from the split (e.g., trailing punctuation)
|
||||
sentences = [s for s in sentences if s.strip()]
|
||||
|
||||
word_count = len(words)
|
||||
sentence_count = len(sentences)
|
||||
syllable_count = sum(count_syllables_simple(word) for word in words)
|
||||
|
||||
if word_count == 0 or sentence_count == 0:
|
||||
return {
|
||||
"word_count": word_count,
|
||||
"sentence_count": sentence_count,
|
||||
"avg_words_per_sentence": 0.0,
|
||||
"flesch_reading_ease": 0.0,
|
||||
"reading_level": "N/A"
|
||||
}
|
||||
|
||||
# Calculate average words per sentence
|
||||
avg_words_per_sentence = word_count / sentence_count
|
||||
|
||||
# Calculate Flesch Reading Ease Score
|
||||
# Formula: 206.835 - (1.015 * AvgWordsPerSentence) - (84.6 * AvgSyllablesPerWord)
|
||||
# AvgSyllablesPerWord = syllable_count / word_count
|
||||
avg_syllables_per_word = syllable_count / word_count if word_count > 0 else 0
|
||||
|
||||
flesch = 206.835 - (1.015 * avg_words_per_sentence) - (84.6 * avg_syllables_per_word)
|
||||
# Clamp score between 0 and 100
|
||||
flesch = max(0.0, min(100.0, flesch))
|
||||
|
||||
# Determine reading level based on Flesch score ranges
|
||||
if flesch >= 90:
|
||||
reading_level = "Very Easy (5th grade)"
|
||||
elif flesch >= 80:
|
||||
reading_level = "Easy (6th grade)"
|
||||
elif flesch >= 70:
|
||||
reading_level = "Fairly Easy (7th grade)"
|
||||
elif flesch >= 60:
|
||||
reading_level = "Standard (8th-9th grade)"
|
||||
elif flesch >= 50:
|
||||
reading_level = "Fairly Difficult (10th-12th grade)"
|
||||
elif flesch >= 30:
|
||||
reading_level = "Difficult (College)"
|
||||
else:
|
||||
reading_level = "Very Difficult (Graduate)"
|
||||
|
||||
return {
|
||||
"word_count": word_count,
|
||||
"sentence_count": sentence_count,
|
||||
"avg_words_per_sentence": round(avg_words_per_sentence, 2), # Rounded for display
|
||||
"flesch_reading_ease": round(flesch, 2), # Rounded for display
|
||||
"reading_level": reading_level
|
||||
}
|
||||
|
||||
def suggest_improvements(content: str, letter_type: str) -> List[str]:
|
||||
"""
|
||||
Suggest improvements for a letter based on its content, basic analysis,
|
||||
and target letter type.
|
||||
|
||||
Args:
|
||||
content: The letter content to analyze.
|
||||
letter_type: The type of letter (e.g., "business", "cover", "personal").
|
||||
|
||||
Returns:
|
||||
List of improvement suggestions strings.
|
||||
"""
|
||||
suggestions = []
|
||||
|
||||
words = re.findall(r'\b\w+\b', content)
|
||||
word_count = len(words)
|
||||
|
||||
# Basic length check based on letter type
|
||||
if letter_type in ["business", "formal"]:
|
||||
if word_count < 100 and word_count > 10: # Avoid suggesting for very short placeholders
|
||||
suggestions.append("Consider adding more details to make your letter more comprehensive.")
|
||||
elif word_count > 600: # Increased max length slightly
|
||||
suggestions.append("Your letter is quite long. Consider condensing it for better readability and focus.")
|
||||
elif letter_type == "cover":
|
||||
if word_count < 150 and word_count > 10: # Avoid suggesting for very short placeholders
|
||||
suggestions.append("Your cover letter may be too brief. Consider highlighting more of your relevant qualifications.")
|
||||
elif word_count > 500: # Increased max length slightly
|
||||
suggestions.append("Your cover letter is quite long. Consider focusing on your most relevant qualifications and experiences.")
|
||||
elif letter_type == "recommendation":
|
||||
if word_count < 150 and word_count > 10:
|
||||
suggestions.append("Consider adding more specific examples or anecdotes to strengthen the recommendation.")
|
||||
elif word_count > 600:
|
||||
suggestions.append("Your recommendation letter is quite long. Ensure it remains focused and impactful.")
|
||||
|
||||
|
||||
# Check for overuse of "I" (simple count-based heuristic)
|
||||
# Count "I" as a standalone word
|
||||
i_count = len(re.findall(r"\bI\b", content))
|
||||
# Avoid suggestion for very short content or content with few sentences
|
||||
sentence_count = len(re.split(r'[.!?]+\s*', content.strip()))
|
||||
if sentence_count > 2 and word_count > 50 and i_count > sentence_count * 1.5: # Suggest if 'I' count is significantly higher than sentence count
|
||||
suggestions.append("Your letter contains many uses of 'I'. Consider rephrasing some sentences to focus more on the recipient or the subject matter.")
|
||||
|
||||
|
||||
# Check for expression of gratitude (using common phrases)
|
||||
gratitude_patterns = [r"\bthank you\b", r"\bgrateful\b", r"\bappreciate\b"]
|
||||
has_gratitude = any(re.search(pattern, content, re.IGNORECASE) for pattern in gratitude_patterns)
|
||||
# Suggest adding gratitude, but avoid for letter types where it might be less common (e.g., some complaint letters)
|
||||
if not has_gratitude and letter_type not in ["complaint", "urgent"]:
|
||||
suggestions.append("Consider expressing gratitude or appreciation somewhere in your letter.")
|
||||
|
||||
# Check for clear call to action (using common phrases)
|
||||
# Phrases indicating desired action or next step
|
||||
action_phrases = [
|
||||
"look forward to", "please", "would appreciate", "request",
|
||||
"hope to", "call me", "email me", "contact me", "schedule",
|
||||
"arrange", "require action", "next steps"
|
||||
]
|
||||
has_call_to_action = any(phrase in content.lower() for phrase in action_phrases)
|
||||
# Suggest adding a call to action for relevant letter types
|
||||
if not has_call_to_action and letter_type in ["business", "cover", "complaint", "invitation"]:
|
||||
suggestions.append("Consider adding a clear call to action or outlining the desired next steps.")
|
||||
|
||||
# Check for proper closing (using common phrases)
|
||||
closing_patterns = [
|
||||
r"\bSincerely\b", r"\bRegards\b", r"\bThank you\b", r"\bBest regards\b",
|
||||
r"\bYours sincerely\b", r"\bYours faithfully\b", r"\bRespectfully\b",
|
||||
r"\bBest wishes\b", r"\bKind regards\b"
|
||||
]
|
||||
# Check if any standard closing phrase is present, typically near the end
|
||||
# A more robust check might look specifically at the last paragraph/lines
|
||||
has_proper_closing = any(re.search(pattern, content[-200:], re.IGNORECASE) for pattern in closing_patterns) # Check last 200 chars
|
||||
|
||||
if not has_proper_closing and word_count > 20: # Avoid suggesting for very short snippets
|
||||
suggestions.append("Consider adding a proper closing phrase (e.g., Sincerely, Regards) followed by your name.")
|
||||
|
||||
return suggestions
|
||||
|
||||
# Example usage (for testing purposes, not part of the module's core functionality)
|
||||
if __name__ == '__main__':
|
||||
sample_formal_letter = """
|
||||
Dear Mr. Smith,
|
||||
|
||||
I am writing to follow up regarding the project proposal submitted on October 26, 2023.
|
||||
We believe the proposed solution aligns well with your stated requirements.
|
||||
Please find the revised budget document attached for your review.
|
||||
We look forward to your feedback at your earliest convenience.
|
||||
|
||||
Sincerely,
|
||||
Jane Doe
|
||||
"""
|
||||
|
||||
sample_informal_letter = """
|
||||
Hey John,
|
||||
|
||||
Hope you're doing well! Just wanted to quickly touch base about the party next week.
|
||||
Excited to catch up with everyone! Let me know if you need any help setting up.
|
||||
Thanks!
|
||||
|
||||
Best,
|
||||
Alex
|
||||
"""
|
||||
|
||||
sample_complaint_letter = """
|
||||
To Whom It May Concern,
|
||||
|
||||
I am writing to complain about the faulty product I received on November 1, 2023 (Order #12345).
|
||||
The device stopped working after only two days of use. I require a full refund or replacement immediately.
|
||||
I expect a prompt response regarding this issue.
|
||||
|
||||
Sincerely,
|
||||
Concerned Customer
|
||||
"""
|
||||
|
||||
print("--- Analyzing Formal Letter ---")
|
||||
tone = analyze_letter_tone(sample_formal_letter)
|
||||
formality = check_formality(sample_formal_letter)
|
||||
readability = get_readability_metrics(sample_formal_letter)
|
||||
suggestions = suggest_improvements(sample_formal_letter, "business")
|
||||
|
||||
print(f"Tone: {tone}")
|
||||
print(f"Formality: {formality:.2f}")
|
||||
print(f"Readability: {readability}")
|
||||
print(f"Suggestions: {suggestions}")
|
||||
|
||||
print("\n--- Analyzing Informal Letter ---")
|
||||
tone = analyze_letter_tone(sample_informal_letter)
|
||||
formality = check_formality(sample_informal_letter)
|
||||
readability = get_readability_metrics(sample_informal_letter)
|
||||
suggestions = suggest_improvements(sample_informal_letter, "personal")
|
||||
|
||||
print(f"Tone: {tone}")
|
||||
print(f"Formality: {formality:.2f}")
|
||||
print(f"Readability: {readability}")
|
||||
print(f"Suggestions: {suggestions}")
|
||||
|
||||
print("\n--- Analyzing Complaint Letter ---")
|
||||
tone = analyze_letter_tone(sample_complaint_letter)
|
||||
formality = check_formality(sample_complaint_letter)
|
||||
readability = get_readability_metrics(sample_complaint_letter)
|
||||
suggestions = suggest_improvements(sample_complaint_letter, "complaint")
|
||||
|
||||
print(f"Tone: {tone}")
|
||||
print(f"Formality: {formality:.2f}")
|
||||
print(f"Readability: {readability}")
|
||||
print(f"Suggestions: {suggestions}")
|
||||
@@ -1,545 +0,0 @@
|
||||
"""
|
||||
Letter Formatter Module
|
||||
|
||||
This module provides utilities for formatting letters and generating HTML
|
||||
previews in different styles (Personal, Formal, Business, Cover).
|
||||
The formatting functions here are primarily focused on generating HTML
|
||||
for preview purposes, applying standard layout conventions for each letter type
|
||||
using inline CSS styles.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Dict, Any
|
||||
|
||||
def format_letter(content: str, metadata: Dict[str, Any], letter_type: str = "personal") -> str:
|
||||
"""
|
||||
Format a letter with basic structure (paragraphs).
|
||||
|
||||
Args:
|
||||
content: The raw letter content (string).
|
||||
metadata: Dictionary containing metadata (currently not used for formatting in this placeholder).
|
||||
letter_type: Type of letter (personal, formal, business, cover).
|
||||
|
||||
Returns:
|
||||
Formatted letter content (currently just returns the input content).
|
||||
This is a placeholder and would be expanded to apply specific
|
||||
formatting rules (e.g., indentation, spacing) based on letter type
|
||||
and metadata in a full implementation before generating HTML.
|
||||
For this module, we primarily rely on the HTML generation functions
|
||||
to handle the visual formatting.
|
||||
"""
|
||||
# This is a basic placeholder. In a real implementation, this function
|
||||
# might process the raw text content to add indentation, adjust line breaks,
|
||||
# or handle specific markdown-like syntax before it's passed to the
|
||||
# HTML generation functions.
|
||||
# For now, we assume the input `content` uses double newlines for paragraphs.
|
||||
return content
|
||||
|
||||
def get_letter_preview_html(content: str, metadata: Dict[str, Any], letter_type: str = "personal") -> str:
|
||||
"""
|
||||
Generate HTML for letter preview based on letter type and metadata.
|
||||
This function acts as a dispatcher to the specific HTML generation functions.
|
||||
|
||||
Args:
|
||||
content: The letter content string.
|
||||
metadata: Dictionary containing metadata like sender/recipient info, date, etc.
|
||||
letter_type: Type of letter ("personal", "formal", "business", "cover").
|
||||
Defaults to "personal".
|
||||
|
||||
Returns:
|
||||
HTML string for letter preview, styled appropriately for the type.
|
||||
Includes basic styling for a printable letter appearance.
|
||||
"""
|
||||
# Dispatch to the appropriate HTML generation function based on letter type
|
||||
# Pass the content and metadata to the specific functions
|
||||
if letter_type == "personal":
|
||||
return get_personal_letter_html(content, metadata)
|
||||
elif letter_type == "formal":
|
||||
return get_formal_letter_html(content, metadata)
|
||||
elif letter_type == "business":
|
||||
return get_business_letter_html(content, metadata)
|
||||
elif letter_type == "cover":
|
||||
return get_cover_letter_html(content, metadata)
|
||||
else:
|
||||
# Fallback for unrecognized types, displaying raw content in a styled box
|
||||
return f"""
|
||||
<div style="max-width: 800px; margin: 20px auto; padding: 20px; border: 1px solid #ccc; font-family: sans-serif; line-height: 1.6; background-color: #fff8f8; color: #333; border-radius: 8px;">
|
||||
<h3 style="color: #e53935; margin-top: 0;">Preview Unavailable for Unknown Letter Type</h3>
|
||||
<p>The letter type '{letter_type}' is not recognized. Displaying raw content:</p>
|
||||
<pre style="white-space: pre-wrap; word-wrap: break-word; background-color: #f8f8f8; padding: 15px; border: 1px solid #ddd; border-radius: 4px; overflow-x: auto;">{content}</pre>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def get_personal_letter_html(content: str, metadata: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generate HTML for personal letter preview with basic styling.
|
||||
Uses a more informal layout and font style.
|
||||
|
||||
Args:
|
||||
content: The letter content string.
|
||||
metadata: Dictionary containing personal letter metadata (sender_name, date).
|
||||
|
||||
Returns:
|
||||
HTML string for personal letter preview.
|
||||
"""
|
||||
# Extract metadata with default empty strings for robustness
|
||||
sender_name = metadata.get("sender_name", "")
|
||||
# recipient_name = metadata.get("recipient_name", "") # Less common in personal body, but could be used in greeting
|
||||
date = metadata.get("date", "")
|
||||
|
||||
# Split content into paragraphs based on double newlines
|
||||
# Use list comprehension to strip whitespace and filter out empty strings
|
||||
paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
|
||||
|
||||
# Format paragraphs as HTML <p> tags with bottom margin
|
||||
formatted_paragraphs = "".join(f"<p style='margin-bottom: 1em;'>{paragraph}</p>" for paragraph in paragraphs)
|
||||
|
||||
# Basic HTML structure with inline styles for a personal letter feel
|
||||
# Styles aim for a warm, readable appearance
|
||||
html = f"""
|
||||
<div style="max-width: 700px; margin: 20px auto; padding: 30px; border: 1px solid #e0e0e0; border-radius: 8px; background-color: #ffffff; font-family: 'Georgia', serif; line-height: 1.7; color: #333; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div style="text-align: right; margin-bottom: 30px; font-size: 0.9em; color: #555;">
|
||||
{date if date else "[Date]"}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 30px;">
|
||||
{formatted_paragraphs if formatted_paragraphs else "<p style='color: #888;'>Letter content goes here...</p>"}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px;">
|
||||
<p style="margin-bottom: 0.5em;">Sincerely,</p>
|
||||
<p style="font-weight: bold; margin-top: 0;">{sender_name if sender_name else "[Sender Name]"}</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
|
||||
def get_formal_letter_html(content: str, metadata: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generate HTML for formal letter preview with standard formal structure and styling.
|
||||
Uses a more professional layout and font style (Arial/sans-serif).
|
||||
|
||||
Args:
|
||||
content: The letter content string.
|
||||
metadata: Dictionary containing formal letter metadata.
|
||||
|
||||
Returns:
|
||||
HTML string for formal letter preview.
|
||||
"""
|
||||
# Extract metadata with default empty strings
|
||||
sender_name = metadata.get("sender_name", "")
|
||||
sender_title = metadata.get("sender_title", "")
|
||||
sender_organization = metadata.get("sender_organization", "")
|
||||
# Replace newlines in address for HTML display
|
||||
sender_address = metadata.get("sender_address", "").replace("\n", "<br>")
|
||||
sender_phone = metadata.get("sender_phone", "")
|
||||
sender_email = metadata.get("sender_email", "")
|
||||
|
||||
recipient_name = metadata.get("recipient_name", "")
|
||||
recipient_title = metadata.get("recipient_title", "")
|
||||
recipient_organization = metadata.get("recipient_organization", "")
|
||||
# Replace newlines in address for HTML display
|
||||
recipient_address = metadata.get("recipient_address", "").replace("\n", "<br>")
|
||||
|
||||
date = metadata.get("date", "")
|
||||
subject = metadata.get("subject", "") # Added subject line
|
||||
salutation = metadata.get("salutation", "Dear Sir/Madam,") # Added salutation
|
||||
complimentary_close = metadata.get("complimentary_close", "Sincerely,") # Added close
|
||||
|
||||
# Determine alignment based on letter format (simplified)
|
||||
# Full Block: All aligned left
|
||||
# Modified Block: Sender address block, date, closing, and signature are right-aligned
|
||||
letter_format = metadata.get("letter_format", "Full Block")
|
||||
sender_address_align = "left"
|
||||
date_align = "left"
|
||||
closing_align = "left"
|
||||
|
||||
if letter_format == "Modified Block":
|
||||
sender_address_align = "right"
|
||||
date_align = "right"
|
||||
closing_align = "right"
|
||||
|
||||
# Split content into paragraphs based on double newlines
|
||||
paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
|
||||
|
||||
# Format paragraphs as HTML <p> tags with bottom margin
|
||||
formatted_paragraphs = "".join(f"<p style='margin-bottom: 1em;'>{paragraph}</p>" for paragraph in paragraphs)
|
||||
|
||||
# Basic HTML structure with inline styles for a formal letter
|
||||
html = f"""
|
||||
<div style="max-width: 800px; margin: 20px auto; padding: 30px; border: 1px solid #d0d0d0; border-radius: 8px; background-color: #ffffff; font-family: 'Arial', sans-serif; line-height: 1.6; color: #333; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
|
||||
<div style="text-align: {sender_address_align}; margin-bottom: 20px; font-size: 0.9em;">
|
||||
<p style="margin: 0;">{sender_name if sender_name else "[Sender Name]"}{', ' + sender_title if sender_title else ''}</p>
|
||||
<p style="margin: 0;">{sender_organization if sender_organization else "[Sender Organization]"}</p>
|
||||
<p style="margin: 0;">{sender_address if sender_address else "[Sender Address]"}</p>
|
||||
<p style="margin: 0;">{sender_phone}</p>
|
||||
<p style="margin: 0;">{sender_email}</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: {date_align}; margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{date if date else "[Date]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px; font-size: 0.9em;">
|
||||
<p style="margin: 0;">{recipient_name if recipient_name else "[Recipient Name]"}{', ' + recipient_title if recipient_title else ''}</p>
|
||||
<p style="margin: 0;">{recipient_organization if recipient_organization else "[Recipient Organization]"}</p>
|
||||
<p style="margin: 0;">{recipient_address if recipient_address else "[Recipient Address]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0; font-weight: bold;">Subject: {subject if subject else "[Subject Line]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{salutation}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
{formatted_paragraphs if formatted_paragraphs else "<p style='color: #888;'>Letter content goes here...</p>"}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px; text-align: {closing_align};">
|
||||
<p style="margin-bottom: 0.5em;">{complimentary_close}</p>
|
||||
<p style="font-weight: bold; margin: 0;">{sender_name}</p>
|
||||
<p style="margin: 0; font-size: 0.9em;">{sender_title}</p>
|
||||
<p style="margin: 0; font-size: 0.9em;">{sender_organization}</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
|
||||
def get_business_letter_html(content: str, metadata: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generate HTML for business letter preview with standard business structure and styling.
|
||||
Includes optional letterhead.
|
||||
|
||||
Args:
|
||||
content: The letter content string.
|
||||
metadata: Dictionary containing business letter metadata.
|
||||
|
||||
Returns:
|
||||
HTML string for business letter preview.
|
||||
"""
|
||||
# Extract metadata with default empty strings
|
||||
sender_company = metadata.get("sender_company", "")
|
||||
sender_name = metadata.get("sender_name", "")
|
||||
sender_title = metadata.get("sender_title", "")
|
||||
sender_address = metadata.get("sender_address", "").replace("\n", "<br>")
|
||||
sender_phone = metadata.get("sender_phone", "")
|
||||
sender_email = metadata.get("sender_email", "")
|
||||
sender_website = metadata.get("sender_website", "")
|
||||
|
||||
recipient_company = metadata.get("recipient_company", "")
|
||||
recipient_name = metadata.get("recipient_name", "")
|
||||
recipient_title = metadata.get("recipient_title", "")
|
||||
recipient_address = metadata.get("recipient_address", "").replace("\n", "<br>")
|
||||
|
||||
date = metadata.get("date", "")
|
||||
subject = metadata.get("subject", "") # Added subject line
|
||||
salutation = metadata.get("salutation", "Dear Sir/Madam,") # Added salutation
|
||||
complimentary_close = metadata.get("complimentary_close", "Sincerely,") # Added close
|
||||
|
||||
# Determine alignment based on letter format (simplified)
|
||||
letter_format = metadata.get("letter_format", "Full Block")
|
||||
sender_info_align = "left"
|
||||
date_align = "left"
|
||||
closing_align = "left"
|
||||
|
||||
if letter_format == "Modified Block":
|
||||
sender_info_align = "right"
|
||||
date_align = "right"
|
||||
closing_align = "right"
|
||||
|
||||
# Include letterhead logic
|
||||
include_letterhead = metadata.get("include_letterhead", True)
|
||||
|
||||
# Split content into paragraphs based on double newlines
|
||||
paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
|
||||
|
||||
# Format paragraphs as HTML <p> tags with bottom margin
|
||||
formatted_paragraphs = "".join(f"<p style='margin-bottom: 1em;'>{paragraph}</p>" for paragraph in paragraphs)
|
||||
|
||||
# Create letterhead HTML if included and company name is provided
|
||||
letterhead_html = ""
|
||||
if include_letterhead and sender_company:
|
||||
letterhead_html = f"""
|
||||
<div style="padding-bottom: 15px; margin-bottom: 20px; border-bottom: 1px solid #eee;">
|
||||
<h2 style="margin: 0; color: #333; font-size: 1.5em;">{sender_company}</h2>
|
||||
<p style="margin: 5px 0 0 0; font-size: 0.9em; color: #555;">
|
||||
{sender_address.replace('<br>', ', ') if sender_address else ''}
|
||||
{' | ' + sender_phone if sender_phone else ''}
|
||||
{' | ' + sender_email if sender_email else ''}
|
||||
{' | ' + sender_website if sender_website else ''}
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Basic HTML structure with inline styles for a business letter
|
||||
html = f"""
|
||||
<div style="max-width: 800px; margin: 20px auto; padding: 30px; border: 1px solid #d0d0d0; border-radius: 8px; background-color: #ffffff; font-family: 'Arial', sans-serif; line-height: 1.6; color: #333; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
{letterhead_html}
|
||||
|
||||
<div style="text-align: {date_align}; margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{date if date else "[Date]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px; font-size: 0.9em;">
|
||||
<p style="margin: 0;">{recipient_name if recipient_name else "[Recipient Name]"}{', ' + recipient_title if recipient_title else ''}</p>
|
||||
<p style="margin: 0;">{recipient_company if recipient_company else "[Recipient Company]"}</p>
|
||||
<p style="margin: 0;">{recipient_address if recipient_address else "[Recipient Address]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0; font-weight: bold;">Subject: {subject if subject else "[Subject Line]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{salutation}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
{formatted_paragraphs if formatted_paragraphs else "<p style='color: #888;'>Letter content goes here...</p>"}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px; text-align: {closing_align};">
|
||||
<p style="margin-bottom: 0.5em;">{complimentary_close}</p>
|
||||
<p style="font-weight: bold; margin: 0;">{sender_name if sender_name else "[Sender Name]"}</p>
|
||||
<p style="margin: 0; font-size: 0.9em;">{sender_title}</p>
|
||||
<p style="margin: 0; font-size: 0.9em;">{sender_company}</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
|
||||
def get_cover_letter_html(content: str, metadata: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generate HTML for cover letter preview with standard cover letter structure and styling.
|
||||
Includes sender contact block and optional online links.
|
||||
|
||||
Args:
|
||||
content: The letter content string.
|
||||
metadata: Dictionary containing cover letter metadata.
|
||||
|
||||
Returns:
|
||||
HTML string for cover letter preview.
|
||||
"""
|
||||
# Extract metadata with default empty strings
|
||||
sender_name = metadata.get("sender_name", "")
|
||||
sender_email = metadata.get("sender_email", "")
|
||||
sender_phone = metadata.get("sender_phone", "")
|
||||
sender_location = metadata.get("sender_location", "")
|
||||
sender_linkedin = metadata.get("sender_linkedin", "")
|
||||
sender_portfolio = metadata.get("sender_portfolio", "")
|
||||
|
||||
recipient_name = metadata.get("recipient_name", "")
|
||||
recipient_title = metadata.get("recipient_title", "") # Added recipient title
|
||||
recipient_company = metadata.get("recipient_company", "")
|
||||
recipient_department = metadata.get("recipient_department", "") # Added department
|
||||
recipient_address = metadata.get("recipient_address", "").replace("\n", "<br>") # Added recipient address
|
||||
|
||||
date = metadata.get("date", "")
|
||||
job_title = metadata.get("job_title", "") # Added job title for subject
|
||||
salutation = metadata.get("salutation", "Dear Hiring Manager,") # Added salutation
|
||||
complimentary_close = metadata.get("complimentary_close", "Sincerely,") # Added close
|
||||
|
||||
|
||||
# Split content into paragraphs based on double newlines
|
||||
paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
|
||||
|
||||
# Format paragraphs as HTML <p> tags with bottom margin
|
||||
formatted_paragraphs = "".join(f"<p style='margin-bottom: 1em;'>{paragraph}</p>" for paragraph in paragraphs)
|
||||
|
||||
# Construct sender contact line, only including fields that have values
|
||||
sender_contact_parts = [sender_location, sender_phone, sender_email]
|
||||
sender_contact_line = " | ".join(filter(None, sender_contact_parts))
|
||||
|
||||
# Construct sender online links line, only including fields that have values
|
||||
sender_online_parts = []
|
||||
if sender_linkedin:
|
||||
# Add basic styling for links
|
||||
sender_online_parts.append(f'<a href="{sender_linkedin}" style="color: #0077b5; text-decoration: none;">LinkedIn</a>')
|
||||
if sender_portfolio:
|
||||
# Add basic styling for links
|
||||
sender_online_parts.append(f'<a href="{sender_portfolio}" style="color: #0077b5; text-decoration: none;">Portfolio</a>')
|
||||
|
||||
sender_online_line = " | ".join(filter(None, sender_online_parts))
|
||||
|
||||
|
||||
# Basic HTML structure with inline styles for a cover letter
|
||||
# Styles aim for a clean, professional look
|
||||
html = f"""
|
||||
<div style="max-width: 800px; margin: 20px auto; padding: 30px; border: 1px solid #d0d0d0; border-radius: 8px; background-color: #ffffff; font-family: 'Arial', sans-serif; line-height: 1.6; color: #333; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
|
||||
<div style="text-align: left; margin-bottom: 30px; padding-bottom: 15px; border-bottom: 1px solid #eee;">
|
||||
<h2 style="margin: 0; color: #333; font-size: 1.5em;">{sender_name if sender_name else "[Your Name]"}</h2>
|
||||
{'<p style="margin: 5px 0 0 0; font-size: 0.9em; color: #555;">' + sender_contact_line + '</p>' if sender_contact_line else ''}
|
||||
{'<p style="margin: 2px 0 0 0; font-size: 0.9em;">' + sender_online_line + '</p>' if sender_online_line else ''}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{date if date else "[Date]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px; font-size: 0.9em;">
|
||||
<p style="margin: 0;">{recipient_name if recipient_name else "[Recipient Name]"}{', ' + recipient_title if recipient_title else ''}</p>
|
||||
<p style="margin: 0;">{recipient_department}</p>
|
||||
<p style="margin: 0;">{recipient_company if recipient_company else "[Recipient Company]"}</p>
|
||||
<p style="margin: 0;">{recipient_address if recipient_address else "[Recipient Address]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0; font-weight: bold;">Subject: Application for {job_title if job_title else '[Job Title]'} Position</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{salutation}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
{formatted_paragraphs if formatted_paragraphs else "<p style='color: #888;'>Letter content goes here...</p>"}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px;">
|
||||
<p style="margin-bottom: 0.5em;">{complimentary_close}</p>
|
||||
<p style="font-weight: bold; margin: 0;">{sender_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
|
||||
# Example usage (for testing purposes)
|
||||
if __name__ == '__main__':
|
||||
sample_personal_content = """
|
||||
Hi Sarah,
|
||||
|
||||
Hope you're doing well!
|
||||
|
||||
Just wanted to send a quick note to say how much I enjoyed catching up last week. It was great hearing about your trip to Italy.
|
||||
|
||||
Let's try to do it again soon!
|
||||
|
||||
Best,
|
||||
Emily
|
||||
"""
|
||||
sample_personal_metadata = {
|
||||
"sender_name": "Emily Davis",
|
||||
"recipient_name": "Sarah Johnson",
|
||||
"date": "November 5, 2023"
|
||||
}
|
||||
|
||||
sample_formal_content = """
|
||||
I am writing to formally request a copy of my academic transcript.
|
||||
|
||||
I require this document for a graduate school application. The deadline for submission is December 15, 2023.
|
||||
|
||||
Please let me know if there are any fees associated with this request or if any further information is needed from my end.
|
||||
|
||||
Thank you for your time and assistance.
|
||||
"""
|
||||
sample_formal_metadata_full_block = {
|
||||
"sender_name": "John Smith",
|
||||
"sender_title": "Student",
|
||||
"sender_organization": "University of Example",
|
||||
"sender_address": "123 University Ave\nAnytown, CA 91234",
|
||||
"sender_phone": "(555) 123-4567",
|
||||
"sender_email": "john.smith@example.com",
|
||||
"recipient_name": "Registrar's Office",
|
||||
"recipient_organization": "University of Example",
|
||||
"recipient_address": "456 Admin Building\nAnytown, CA 91234",
|
||||
"date": "November 5, 2023",
|
||||
"subject": "Request for Academic Transcript",
|
||||
"salutation": "To the Registrar's Office,",
|
||||
"complimentary_close": "Sincerely,",
|
||||
"letter_format": "Full Block"
|
||||
}
|
||||
|
||||
sample_formal_metadata_modified_block = sample_formal_metadata_full_block.copy()
|
||||
sample_formal_metadata_modified_block["letter_format"] = "Modified Block"
|
||||
|
||||
|
||||
sample_business_content = """
|
||||
This letter confirms the details of Purchase Order #PO-7890.
|
||||
|
||||
We are ordering 50 units of Model X widgets at the agreed-upon price of $100 per unit, totaling $5,000.
|
||||
|
||||
Please ensure delivery to our warehouse by November 20, 2023. Payment will be made within 30 days of receipt of invoice.
|
||||
|
||||
Thank you for your prompt processing of this order.
|
||||
"""
|
||||
sample_business_metadata_full_block = {
|
||||
"sender_company": "Acme Corp",
|
||||
"sender_name": "Alice Brown",
|
||||
"sender_title": "Procurement Manager",
|
||||
"sender_address": "789 Business Rd\nMetropolis, NY 10001",
|
||||
"sender_phone": "(555) 987-6543",
|
||||
"sender_email": "alice.brown@acmecorp.com",
|
||||
"sender_website": "www.acmecorp.com",
|
||||
"recipient_company": "Supplier Co.",
|
||||
"recipient_name": "Sales Department",
|
||||
"recipient_title": "",
|
||||
"recipient_address": "101 Vendor Lane\nIndustriatown, TX 75001",
|
||||
"date": "November 5, 2023",
|
||||
"subject": "Purchase Order Confirmation - PO-7890",
|
||||
"salutation": "To the Sales Department,",
|
||||
"complimentary_close": "Sincerely,",
|
||||
"letter_format": "Full Block",
|
||||
"include_letterhead": True
|
||||
}
|
||||
|
||||
sample_business_metadata_modified_block = sample_business_metadata_full_block.copy()
|
||||
sample_business_metadata_modified_block["letter_format"] = "Modified Block"
|
||||
sample_business_metadata_no_letterhead = sample_business_metadata_full_block.copy()
|
||||
sample_business_metadata_no_letterhead["include_letterhead"] = False
|
||||
|
||||
|
||||
sample_cover_letter_content = """
|
||||
I am writing to express my enthusiastic interest in the Marketing Specialist position advertised on LinkedIn.
|
||||
|
||||
With three years of experience in digital marketing and a proven track record in content creation and social media management, I am confident in my ability to contribute to your team. My skills in [Specific Skill 1] and [Specific Skill 2] align perfectly with the requirements outlined in the job description.
|
||||
|
||||
In my previous role at [Previous Company], I successfully managed social media campaigns that resulted in a 25% increase in engagement. I am particularly drawn to [Company Name]'s innovative approach to [Industry Trend] and believe my creative problem-solving skills would be a valuable asset.
|
||||
|
||||
Thank you for considering my application. I have attached my resume for your review and welcome the opportunity to discuss how my background and skills can benefit [Company Name].
|
||||
"""
|
||||
sample_cover_letter_metadata = {
|
||||
"sender_name": "Jane Doe",
|
||||
"sender_email": "jane.doe@email.com",
|
||||
"sender_phone": "(123) 456-7890",
|
||||
"sender_location": "San Francisco, CA",
|
||||
"sender_linkedin": "https://linkedin.com/in/janedoe",
|
||||
"sender_portfolio": "https://janedoeportfolio.com",
|
||||
"recipient_name": "Hiring Manager",
|
||||
"recipient_title": "", # Example with no recipient title
|
||||
"recipient_company": "Innovative Solutions Inc.",
|
||||
"recipient_department": "Marketing Department",
|
||||
"recipient_address": "456 Tech Way\nSilicon Valley, CA 95001",
|
||||
"date": "November 5, 2023",
|
||||
"job_title": "Marketing Specialist",
|
||||
"salutation": "Dear Hiring Manager,",
|
||||
"complimentary_close": "Sincerely,"
|
||||
}
|
||||
|
||||
print("--- Personal Letter HTML Preview ---")
|
||||
print(get_letter_preview_html(sample_personal_content, sample_personal_metadata, letter_type="personal"))
|
||||
|
||||
print("\n--- Formal Letter HTML Preview (Full Block) ---")
|
||||
print(get_letter_preview_html(sample_formal_content, sample_formal_metadata_full_block, letter_type="formal"))
|
||||
|
||||
print("\n--- Formal Letter HTML Preview (Modified Block) ---")
|
||||
print(get_letter_preview_html(sample_formal_content, sample_formal_metadata_modified_block, letter_type="formal"))
|
||||
|
||||
print("\n--- Business Letter HTML Preview (Full Block, with Letterhead) ---")
|
||||
print(get_letter_preview_html(sample_business_content, sample_business_metadata_full_block, letter_type="business"))
|
||||
|
||||
print("\n--- Business Letter HTML Preview (Modified Block, with Letterhead) ---")
|
||||
print(get_letter_preview_html(sample_business_content, sample_business_metadata_modified_block, letter_type="business"))
|
||||
|
||||
print("\n--- Business Letter HTML Preview (Full Block, no Letterhead) ---")
|
||||
print(get_letter_preview_html(sample_business_content, sample_business_metadata_no_letterhead, letter_type="business"))
|
||||
|
||||
print("\n--- Cover Letter HTML Preview ---")
|
||||
print(get_letter_preview_html(sample_cover_letter_content, sample_cover_letter_metadata, letter_type="cover"))
|
||||
|
||||
print("\n--- Unknown Type HTML Preview ---")
|
||||
print(get_letter_preview_html("Some random content.", {}, letter_type="unknown"))
|
||||
@@ -1,988 +0,0 @@
|
||||
"""
|
||||
Letter Templates Module
|
||||
|
||||
This module provides structured templates and guidance for generating
|
||||
different types and subtypes of letters.
|
||||
Templates are defined as dictionaries containing a 'structure' (list of sections)
|
||||
and 'guidance' (a string).
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
|
||||
# Define letter templates using a nested dictionary structure for easier management
|
||||
TEMPLATES: Dict[str, Dict[str, Dict[str, Any]]] = {
|
||||
"personal": {
|
||||
"congratulations": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express congratulations",
|
||||
"Acknowledge the achievement",
|
||||
"Share personal thoughts/memory (optional)",
|
||||
"Look to the future/well wishes",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, sincere, and specific about the achievement. Express genuine happiness for the recipient. Keep the tone personal and friendly."
|
||||
},
|
||||
"thank_you": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express gratitude clearly",
|
||||
"Specify what you are thankful for",
|
||||
"Explain the impact or how you used it (optional)",
|
||||
"Share a personal thought or memory (optional)",
|
||||
"Offer reciprocation or look to the future",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be specific about what you're thankful for and how it affected you. Express sincere appreciation. Personalize the message."
|
||||
},
|
||||
"sympathy": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express sympathy for the loss",
|
||||
"Acknowledge the significance of the person/situation",
|
||||
"Share a positive memory or quality (optional)",
|
||||
"Offer specific support (optional)",
|
||||
"Closing with comforting words"
|
||||
],
|
||||
"guidance": "Be gentle, compassionate, and sincere. Avoid clichés. Focus on offering genuine comfort and acknowledging the recipient's feelings."
|
||||
},
|
||||
"apology": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Clearly state your apology",
|
||||
"Acknowledge the specific mistake or action",
|
||||
"Express understanding of the impact on the other person",
|
||||
"Explain (briefly, without making excuses) what happened (optional)",
|
||||
"Offer amends or suggest how to make things right",
|
||||
"Assure it won't happen again",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be sincere, take full responsibility for your actions, and focus on making things right. Avoid making excuses or blaming others."
|
||||
},
|
||||
"invitation": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Clearly state the invitation",
|
||||
"Provide full event details (What, When, Where)",
|
||||
"Explain the significance or purpose (optional)",
|
||||
"Mention who else might be there (optional)",
|
||||
"Request RSVP (date and contact method)",
|
||||
"Express anticipation",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be clear and specific about the details (what, when, where, why). Make it easy for the person to respond."
|
||||
},
|
||||
"friendship": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express appreciation for the friendship",
|
||||
"Share a recent memory or anecdote",
|
||||
"Acknowledge the value of the relationship",
|
||||
"Check in on them or share updates",
|
||||
"Look to the future (getting together, etc.)",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, personal, and specific about what you value in the friendship. Share updates and show genuine interest."
|
||||
},
|
||||
"love": {
|
||||
"structure": [
|
||||
"Greeting (Terms of endearment)",
|
||||
"Express depth of feelings",
|
||||
"Share a cherished memory or moment",
|
||||
"Describe specific qualities you love and appreciate",
|
||||
"Reaffirm commitment or future hopes",
|
||||
"Closing (Terms of endearment)"
|
||||
],
|
||||
"guidance": "Be sincere, personal, and specific about your feelings. Use sensory details and emotional language appropriate for your relationship."
|
||||
},
|
||||
"encouragement": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Acknowledge the situation or challenge they face",
|
||||
"Express belief in their abilities/strength",
|
||||
"Offer specific words of encouragement or support",
|
||||
"Remind them of past successes (optional)",
|
||||
"Offer practical help (optional)",
|
||||
"Look to the future with hope",
|
||||
"Closing with support"
|
||||
],
|
||||
"guidance": "Be positive, supportive, and specific about the person's strengths and abilities. Offer genuine encouragement and belief in them."
|
||||
},
|
||||
"farewell": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"State the purpose (saying goodbye)",
|
||||
"Express feelings about their departure (sadness, happiness for them)",
|
||||
"Share a positive memory or highlight their contribution",
|
||||
"Express good wishes for their future endeavors",
|
||||
"Look to staying in touch (optional)",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, reflective, and forward-looking. Focus on positive memories and express genuine good wishes for their next steps."
|
||||
},
|
||||
# Default personal letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Introduction",
|
||||
"Main content paragraphs",
|
||||
"Closing thoughts",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be personal, authentic, and appropriate for your relationship with the recipient. The tone is typically informal to semi-formal."
|
||||
}
|
||||
},
|
||||
"formal": {
|
||||
"application": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (if known)",
|
||||
"Subject line (Clear and concise)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position applied for and where you saw it)",
|
||||
"Body paragraphs (Highlight relevant skills and experience)",
|
||||
"Closing paragraph (Reiterate interest, mention enclosed resume, call to action)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)",
|
||||
"Enclosures (Mention if attaching resume/portfolio)"
|
||||
],
|
||||
"guidance": "Be professional, concise, and specific about your qualifications and genuine interest in the position. Tailor it to the specific job description."
|
||||
},
|
||||
"complaint": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state it's a complaint)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the purpose: complaint about X service/product)",
|
||||
"Problem description (Provide specific details: date, time, location, product details, names if applicable)",
|
||||
"Impact statement (Explain how the problem affected you)",
|
||||
"Requested resolution (Clearly state what you want: refund, replacement, action)",
|
||||
"Closing paragraph (Reference attached documents, state expectation for response)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the issue and your desired resolution. Maintain a respectful but firm tone. Include all relevant details."
|
||||
},
|
||||
"request": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state the request)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the purpose: making a request)",
|
||||
"Request details (Clearly explain what you are requesting)",
|
||||
"Justification (Explain why the request is necessary or beneficial)",
|
||||
"Provide supporting information (optional)",
|
||||
"Closing paragraph (Express gratitude for consideration, reiterate call to action)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and courteous about your request. Explain why it's important or beneficial to the recipient or organization."
|
||||
},
|
||||
"recommendation": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Letter of Recommendation for [Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your name, title, relationship to the recommendee, and for what purpose the letter is written)",
|
||||
"Body paragraphs (Describe the recommendee's qualifications, skills, and achievements with specific examples)",
|
||||
"Highlight relevant experiences and contributions",
|
||||
"Closing recommendation (Summarize endorsement, strongly recommend the person)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be specific, positive, and credible. Use concrete examples and anecdotes to support your recommendation. Tailor it to the specific role/opportunity."
|
||||
},
|
||||
"resignation": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Immediate supervisor/HR)",
|
||||
"Subject line (Letter of Resignation - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Statement of resignation (Clearly state you are resigning)",
|
||||
"Last day of employment (Specify the date)",
|
||||
"Gratitude and reflection (Optional: Express thanks for the opportunity/experience)",
|
||||
"Transition plan/Offer of assistance (Optional: Suggest how to ensure a smooth handover)",
|
||||
"Closing paragraph (Express good wishes for the company's future)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, positive (if possible), and clear about your departure and last day. Maintain a good relationship."
|
||||
},
|
||||
"inquiry": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state the nature of the inquiry)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your purpose for writing - making an inquiry)",
|
||||
"Inquiry details (Provide necessary context or background)",
|
||||
"Specific questions (List your questions clearly, perhaps numbered)",
|
||||
"Closing paragraph (Express gratitude for assistance, indicate when you need a response)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and courteous about your inquiry. Organize your questions logically for easy answering."
|
||||
},
|
||||
"authorization": {
|
||||
"structure": [
|
||||
"Sender's contact information (The grantor of authority)",
|
||||
"Date",
|
||||
"Recipient's contact information (The person/entity receiving the letter)",
|
||||
"Subject line (Letter of Authorization)",
|
||||
"Salutation (Formal)",
|
||||
"Statement of authorization (Clearly state who is authorized)",
|
||||
"Authorized person's details (Full name, ID if applicable)",
|
||||
"Scope of authority (Precisely define what they are authorized to do)",
|
||||
"Limitations (Specify any restrictions or conditions)",
|
||||
"Duration of authorization (Start and end dates, if applicable)",
|
||||
"Closing paragraph (State responsibility, express confidence)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and precise about who is authorized, what they can do, for how long, and under what conditions. This is a legal document."
|
||||
},
|
||||
"appeal": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Appeals committee/relevant authority)",
|
||||
"Subject line (Letter of Appeal - [Your Name] - [Subject of Appeal])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your name, the decision being appealed, and the date of the decision)",
|
||||
"Grounds for appeal (Clearly state the reasons why you believe the decision is incorrect)",
|
||||
"Provide supporting evidence (Reference attached documents: records, photos, etc.)",
|
||||
"Explain mitigating circumstances (Optional)",
|
||||
"Requested outcome (Clearly state what resolution you seek)",
|
||||
"Closing paragraph (Express hope for reconsideration, gratitude for time)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be respectful, factual, and persuasive. Focus on valid grounds for appeal and provide clear, supporting evidence. Maintain a formal tone."
|
||||
},
|
||||
"introduction": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Introduction - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (Introduce yourself and the purpose of the letter)",
|
||||
"Background information (Briefly describe your relevant background or expertise)",
|
||||
"Reason for reaching out (Explain why you are introducing yourself to this specific person/entity)",
|
||||
"Potential areas of collaboration or shared interest (Optional)",
|
||||
"Call to action (Suggest a meeting, call, or further communication)",
|
||||
"Closing paragraph (Express enthusiasm for potential connection)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, informative, and engaging. Clearly explain who you are, your expertise, and why you're reaching out to them specifically."
|
||||
},
|
||||
# Default formal letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Sender's address",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, clear, and concise. Use formal language and structure. The tone is typically formal."
|
||||
}
|
||||
},
|
||||
"business": {
|
||||
"sales": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Benefit-oriented)",
|
||||
"Salutation",
|
||||
"Attention-grabbing opening (Address a pain point or introduce a benefit)",
|
||||
"Problem statement (Briefly describe the challenge the recipient faces)",
|
||||
"Solution presentation (Introduce your product/service as the solution)",
|
||||
"Benefits and features (Explain how your solution helps, focusing on benefits)",
|
||||
"Social proof (Optional: Testimonials, case studies, data)",
|
||||
"Call to action (Clearly state what you want them to do next)",
|
||||
"Closing paragraph (Reiterate benefit, create urgency/incentive)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)",
|
||||
"Enclosures (Optional: Brochure, pricing)"
|
||||
],
|
||||
"guidance": "Be persuasive, customer-focused, and clear about the value proposition. Focus on benefits, not just features. Make the call to action obvious."
|
||||
},
|
||||
"proposal": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Clear and descriptive)",
|
||||
"Salutation",
|
||||
"Introduction (State purpose: submitting a proposal)",
|
||||
"Problem statement/Needs assessment (Demonstrate understanding of client's needs)",
|
||||
"Proposed solution (Describe your solution in detail)",
|
||||
"Implementation plan (Outline steps and timeline)",
|
||||
"Costs and investment (Clearly state pricing and payment terms)",
|
||||
"Benefits and ROI (Explain the value the client will receive)",
|
||||
"Call to action (Suggest next steps: meeting, discussion)",
|
||||
"Closing paragraph (Express enthusiasm, availability for questions)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)",
|
||||
"Enclosures (Proposal document, appendix)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and persuasive about your solution. Focus on the client's needs and the value you provide. Structure it logically."
|
||||
},
|
||||
"order": {
|
||||
"structure": [
|
||||
"Letterhead (Your company)",
|
||||
"Date",
|
||||
"Recipient's address (Supplier)",
|
||||
"Subject line (Purchase Order - [PO Number])",
|
||||
"Salutation",
|
||||
"Introduction (Reference quote/agreement, state purpose: placing an order)",
|
||||
"Order details (Item list with quantities, descriptions, unit prices, total)",
|
||||
"Delivery requirements (Shipping address, requested delivery date, shipping method)",
|
||||
"Payment terms (Reference agreed terms)",
|
||||
"Closing paragraph (Express expectation for timely delivery)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and detailed about what you're ordering, quantities, delivery requirements, and payment terms. Include a purchase order number."
|
||||
},
|
||||
"quotation": {
|
||||
"structure": [
|
||||
"Letterhead (Your company)",
|
||||
"Date",
|
||||
"Recipient's address (Customer)",
|
||||
"Subject line (Quotation for [Product/Service])",
|
||||
"Salutation",
|
||||
"Introduction (Reference inquiry, state purpose: providing a quotation)",
|
||||
"Quotation details (List items/services, descriptions, unit prices, quantities, line totals)",
|
||||
"Pricing breakdown (Mention taxes, discounts, fees separately)",
|
||||
"Terms and conditions (Payment terms, delivery terms, warranty)",
|
||||
"Validity period (State how long the quote is valid)",
|
||||
"Next steps (How they can place an order)",
|
||||
"Closing paragraph (Express hope to do business, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and transparent about pricing, terms, and what's included or excluded. Make it easy for the customer to understand and accept."
|
||||
},
|
||||
"acknowledgment": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Acknowledgment of [Received Item/Request])",
|
||||
"Salutation",
|
||||
"Acknowledgment statement (Clearly state what you have received or are acknowledging)",
|
||||
"Details of what's being acknowledged (Reference number, date, brief description)",
|
||||
"Confirm understanding (Optional: Briefly restate the request/issue to show understanding)",
|
||||
"Next steps (Outline what will happen next, e.g., processing order, investigating issue)",
|
||||
"Timeline (Provide an estimated timeframe if possible)",
|
||||
"Closing paragraph (Express gratitude, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be prompt, clear, and specific about what you're acknowledging. Set clear expectations for next steps and timelines."
|
||||
},
|
||||
"collection": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Invoice [Invoice Number] - Payment Due)",
|
||||
"Salutation",
|
||||
"Introduction (Reference invoice number and due date)",
|
||||
"Account status (Clearly state the outstanding amount)",
|
||||
"Payment request (Politely request payment)",
|
||||
"Payment options (Remind them how to pay)",
|
||||
"Consequences of non-payment (Optional: Briefly mention late fees or further action, depending on letter stage)",
|
||||
"Call to action (Request payment by a specific date)",
|
||||
"Closing paragraph (Express hope for prompt payment, offer to discuss)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be firm but professional. Clearly state the amount due, due date, and payment options. The tone may vary depending on how overdue the payment is."
|
||||
},
|
||||
"adjustment": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address (Customer who made a complaint)",
|
||||
"Subject line (Response to your inquiry - [Reference Number])",
|
||||
"Salutation",
|
||||
"Acknowledgment of complaint (Reference their communication and the issue)",
|
||||
"Investigation findings (Explain the outcome of your investigation)",
|
||||
"Adjustment offered (Clearly state the resolution: refund, replacement, credit, etc.)",
|
||||
"Apology (Optional: Express regret for the inconvenience)",
|
||||
"Preventive measures (Optional: Explain steps taken to prevent recurrence)",
|
||||
"Closing paragraph (Express hope for continued business, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be responsive, empathetic, and solution-oriented. Clearly explain the adjustment and any preventive measures taken."
|
||||
},
|
||||
"credit": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address (Applicant)",
|
||||
"Subject line (Credit Application Status - [Applicant Name])",
|
||||
"Salutation",
|
||||
"Introduction (Reference their credit application and the purpose of the letter)",
|
||||
"Credit decision (Clearly state if credit is approved or denied)",
|
||||
"If approved: Credit terms (Credit limit, payment terms, interest rates)",
|
||||
"If denied: Reason for decision (Provide specific, compliant reasons)",
|
||||
"Requirements (If approved: any further steps or documents needed)",
|
||||
"Closing paragraph (If approved: Express welcome; If denied: Offer alternative options or appeals process)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and transparent about the credit decision, terms, limits, or reasons for denial. Ensure compliance with regulations if denying credit."
|
||||
},
|
||||
"follow_up": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Following up on [Previous Communication/Meeting])",
|
||||
"Salutation",
|
||||
"Reference to previous communication (Mention date, topic, or meeting)",
|
||||
"Purpose of follow-up (Clearly state why you are writing again)",
|
||||
"Action items/Next steps (Remind of agreed-upon actions or propose next steps)",
|
||||
"Provide additional information (Optional)",
|
||||
"Call to action (If applicable, e.g., request a response, schedule a meeting)",
|
||||
"Closing paragraph (Reiterate interest, express anticipation)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and action-oriented. Reference previous communication and clearly state the purpose of your follow-up and desired outcome."
|
||||
},
|
||||
# Default business letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, clear, and concise. Focus on the business purpose of your letter. The tone is typically formal to semi-formal."
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
"standard": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information (if known)",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the position you are applying for, where you saw the advertisement, and a brief statement of enthusiasm)",
|
||||
"Body paragraph 1 (Highlight skills and experience directly relevant to the job description - often 1-2 key qualifications)",
|
||||
"Body paragraph 2 (Provide a specific example or anecdote demonstrating your abilities)",
|
||||
"Body paragraph 3 (Connect your passion/goals to the company's mission/values - optional but effective)",
|
||||
"Closing paragraph (Reiterate interest, mention attached resume, express availability for interview)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, specific about your most relevant qualifications, and clear about your interest in the position. Tailor every cover letter to the specific job and company."
|
||||
},
|
||||
"career_change": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the position and acknowledge your career transition)",
|
||||
"Body paragraph 1 (Highlight transferable skills from previous roles)",
|
||||
"Body paragraph 2 (Explain your motivation for the career change and how your skills apply)",
|
||||
"Body paragraph 3 (Demonstrate understanding of the new industry/role)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention enclosed resume, call to action)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Focus on transferable skills and explain your career transition. Connect your past experience and new skills directly to the requirements of the target role."
|
||||
},
|
||||
"entry_level": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the position and your enthusiasm for the opportunity as a recent graduate/entrant)",
|
||||
"Body paragraph 1 (Highlight relevant education, coursework, GPA if strong)",
|
||||
"Body paragraph 2 (Describe relevant internships, projects, or volunteer experience)",
|
||||
"Body paragraph 3 (Showcase soft skills: teamwork, communication, eagerness to learn)",
|
||||
"Closing paragraph (Reiterate interest, mention attached resume, express availability for interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize education, relevant internships/projects, and transferable skills gained through academic or extracurricular activities. Show strong potential and enthusiasm."
|
||||
},
|
||||
"executive": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Senior Executive/Board Member)",
|
||||
"Subject line (Executive Application - [Your Name] - [Position])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position applying for, brief summary of executive profile)",
|
||||
"Body paragraph 1 (Highlight strategic leadership experience and key achievements)",
|
||||
"Body paragraph 2 (Discuss relevant industry expertise and market insights)",
|
||||
"Body paragraph 3 (Describe experience in driving growth, managing teams, achieving results)",
|
||||
"Closing paragraph (Reiterate interest, express desire to discuss contribution to the organization)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize strategic leadership experience, significant achievements with measurable results, and industry expertise. Use a confident, authoritative, and forward-looking tone."
|
||||
},
|
||||
"creative": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Application - [Your Name] - [Creative Role])",
|
||||
"Salutation",
|
||||
"Creative introduction (Engaging hook related to the role or your passion)",
|
||||
"Body paragraph 1 (Highlight relevant creative experience and skills)",
|
||||
"Body paragraph 2 (Reference specific portfolio pieces or projects that showcase your style/abilities)",
|
||||
"Body paragraph 3 (Describe your creative process or approach)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention attached resume/portfolio link, call to action)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Use a more engaging and expressive style appropriate for a creative role while maintaining professionalism. Highlight specific creative achievements and link to your portfolio."
|
||||
},
|
||||
"technical": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Application - [Your Name] - [Technical Role])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position, source, and brief technical interest)",
|
||||
"Body paragraph 1 (Highlight specific technical skills and proficiencies relevant to the job description)",
|
||||
"Body paragraph 2 (Describe relevant technical projects or challenges you've solved)",
|
||||
"Body paragraph 3 (Discuss problem-solving abilities and experience with relevant technologies)",
|
||||
"Closing paragraph (Reiterate interest, mention attached resume, express availability for technical discussion/interview)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Focus on technical skills, relevant projects, and problem-solving abilities. Use appropriate technical terminology accurately."
|
||||
},
|
||||
"academic": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Search Committee Chair)",
|
||||
"Subject line (Application for [Position] - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the position, the department, and express your strong interest)",
|
||||
"Body paragraph 1 (Discuss your research experience, focus on key projects and contributions)",
|
||||
"Body paragraph 2 (Describe your teaching philosophy and relevant teaching experience)",
|
||||
"Body paragraph 3 (Mention publications, presentations, grants, and other scholarly contributions)",
|
||||
"Closing paragraph (Reiterate enthusiasm for joining the faculty, express availability for interview/presentation)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Focus on research experience, teaching philosophy, publications, and contributions to the field. Use a scholarly and professional tone suitable for academia."
|
||||
},
|
||||
"remote": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Remote Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the remote position, source, and enthusiasm for remote work)",
|
||||
"Body paragraph 1 (Highlight experience working remotely or independently)",
|
||||
"Body paragraph 2 (Emphasize self-management, time management, and organizational skills required for remote work)",
|
||||
"Body paragraph 3 (Describe strong written and verbal communication skills, essential for remote collaboration)",
|
||||
"Closing paragraph (Reiterate interest in the remote role, mention attached resume, express availability for video interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize self-motivation, excellent communication skills (especially written), time management, and any prior experience working independently or in remote teams."
|
||||
},
|
||||
"referral": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Referral Application - [Your Name] - [Job Title] - Referred by [Referrer's Name])",
|
||||
"Salutation",
|
||||
"Referral introduction (Immediately state who referred you and for what position)",
|
||||
"Body paragraph 1 (Briefly explain your connection to the referrer and how you learned about the role)",
|
||||
"Body paragraph 2 (Highlight key qualifications relevant to the job description)",
|
||||
"Body paragraph 3 (Express strong interest in the position and the company)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention attached resume, express availability for interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Mention the referral prominently and early. Explain your connection to the referrer and how it aligns with your interest in the role. Still, ensure you highlight your own qualifications."
|
||||
},
|
||||
# Default cover letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Contact information",
|
||||
"Date",
|
||||
"Recipient's information",
|
||||
"Subject line",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, specific about your qualifications, and clear about your interest in the position. Tailor your letter to the specific job and company."
|
||||
}
|
||||
},
|
||||
"recommendation": {
|
||||
# Recommendation letters are often considered a subtype of Formal,
|
||||
# but can be a top-level type in some systems. Keeping the structure
|
||||
# consistent with the original request, but noting this potential overlap.
|
||||
"standard": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (e.g., Admissions Committee, Hiring Manager)",
|
||||
"Subject line (Letter of Recommendation for [Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your name, title, relationship to the recommendee, how long you've known them, and for what opportunity the letter is written)",
|
||||
"Body paragraph 1 (Describe their relevant skills and qualities, providing specific examples)",
|
||||
"Body paragraph 2 (Discuss their achievements or contributions, with context and impact)",
|
||||
"Body paragraph 3 (Optional: Mention character traits, teamwork, or specific anecdotes)",
|
||||
"Overall Endorsement (Summarize your strong recommendation and why they are a good fit)",
|
||||
"Closing paragraph (Offer to provide further information)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be specific, positive, and credible. Use concrete examples and anecdotes to support your recommendation. Clearly state your relationship with the person and for what opportunity you are recommending them."
|
||||
},
|
||||
# Default recommendation letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Letter of Recommendation for [Name])",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs describing qualifications and experiences",
|
||||
"Specific examples and anecdotes",
|
||||
"Overall endorsement and recommendation",
|
||||
"Closing and offer for further information",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Provide a strong, positive, and specific endorsement based on your professional or academic relationship with the individual."
|
||||
}
|
||||
},
|
||||
"complaint": {
|
||||
# Complaint letters are often considered a subtype of Formal or Business,
|
||||
# but can be a top-level type. Keeping the structure consistent.
|
||||
"product": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Company contact information",
|
||||
"Subject line (Complaint Regarding [Product Name/Model])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State purpose: complaining about a product, include product name, model, date/place of purchase)",
|
||||
"Problem description (Explain the specific defect or issue with the product in detail)",
|
||||
"History of the problem (Mention if you've tried fixing it, contacted support, etc.)",
|
||||
"Desired resolution (Clearly state if you want a refund, replacement, repair)",
|
||||
"Call to action (State what you expect the company to do and by when)",
|
||||
"Closing paragraph (Reference attached documents like receipt, express expectation for resolution)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the product issue and your desired resolution. Include all relevant details like model number, date of purchase, and copies of receipts. Maintain a firm but professional tone."
|
||||
},
|
||||
"service": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Company/Service Provider contact information",
|
||||
"Subject line (Complaint Regarding [Service Type/Issue])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State purpose: complaining about a service received, include date/time/location of service)",
|
||||
"Problem description (Explain the specific issue with the service provided in detail)",
|
||||
"Impact of the issue (Explain how this problem affected you)",
|
||||
"Desired resolution (Clearly state what you want: refund, re-performance of service, compensation)",
|
||||
"Call to action (State what you expect the company to do and by when)",
|
||||
"Closing paragraph (Reference any relevant documents, express expectation for resolution)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the service issue and your desired resolution. Include details like dates, times, and names of service providers if possible. Maintain a firm but professional tone."
|
||||
},
|
||||
"billing": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Company contact information",
|
||||
"Subject line (Complaint Regarding Billing Error - Account #[Your Account Number])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State purpose: complaining about a billing error, include account number and invoice number)",
|
||||
"Problem description (Explain the specific error on the bill: incorrect charge, double billing, etc.)",
|
||||
"Provide supporting evidence (Reference payments made, attach relevant statements)",
|
||||
"Desired resolution (Clearly state what you want: correction of bill, refund, credit)",
|
||||
"Call to action (State what you expect the company to do and by when)",
|
||||
"Closing paragraph (Reference attached documents, express expectation for resolution)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the billing error. Provide supporting documentation like invoices or payment records. Clearly state the desired correction."
|
||||
},
|
||||
# Default complaint letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Complaint Regarding [Issue Summary])",
|
||||
"Salutation",
|
||||
"Introduction (State the purpose of the letter - to complain)",
|
||||
"Detailed description of the problem",
|
||||
"Explanation of the impact",
|
||||
"Desired resolution",
|
||||
"Call to action",
|
||||
"Closing",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the issue and your desired resolution. Maintain a respectful but firm tone and provide relevant details."
|
||||
}
|
||||
},
|
||||
"thank_you": {
|
||||
# Thank You letters are often considered a subtype of Personal or Business,
|
||||
# but can be a top-level type. Keeping the structure consistent.
|
||||
"personal": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express gratitude clearly and sincerely",
|
||||
"Specify what you are thankful for (gift, favor, support)",
|
||||
"Explain the impact it had on you or how you used it",
|
||||
"Share a personal thought or memory related to it (optional)",
|
||||
"Look to the future or express continued appreciation",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, sincere, and specific about what you are thankful for. Personalize the message and explain the impact of their action or gift."
|
||||
},
|
||||
"professional": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Thank You - [Your Name])",
|
||||
"Salutation (Formal/Semi-formal)",
|
||||
"Express gratitude clearly (e.g., Thank you for the interview, thank you for your help)",
|
||||
"Specify what you are thankful for (Meeting date/topic, specific assistance)",
|
||||
"Reiterate interest or connection (e.g., Reiterate interest in the job, mention something discussed)",
|
||||
"Express appreciation for their time or effort",
|
||||
"Closing paragraph (Optional: look to future interaction)",
|
||||
"Complimentary close (Formal/Semi-formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be prompt, professional, and specific. Reiterate your interest or key points discussed. Send within 24 hours for interviews."
|
||||
},
|
||||
"after_interview": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Interviewer's contact information",
|
||||
"Subject line (Thank You - [Your Name] - [Job Title])",
|
||||
"Salutation (Formal)",
|
||||
"Express sincere thanks for the interview opportunity",
|
||||
"Mention the specific position and date of the interview",
|
||||
"Reiterate your strong interest in the role and the company",
|
||||
"Reference a specific point discussed during the interview to show engagement",
|
||||
"Briefly highlight how your skills/experience align with a need discussed",
|
||||
"Express enthusiasm for next steps",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Send within 24 hours of the interview. Be specific, professional, and reiterate your key strengths and interest. Proofread carefully."
|
||||
},
|
||||
# Default thank you letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express thanks",
|
||||
"Specify reason for thanks",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be sincere and specific about what you are thankful for."
|
||||
}
|
||||
},
|
||||
"invitation": {
|
||||
# Invitation letters are often considered a subtype of Personal or Formal,
|
||||
# but can be a top-level type. Keeping the structure consistent.
|
||||
"event": { # e.g., party, gathering, wedding
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"State the purpose: extending an invitation",
|
||||
"Event details (Type of event, Host)",
|
||||
"Date and Time",
|
||||
"Location (Full address)",
|
||||
"Purpose/Theme (Optional)",
|
||||
"Special instructions (Dress code, what to bring, etc. - optional)",
|
||||
"RSVP information (Date, Contact method)",
|
||||
"Express anticipation",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be clear about all the event details (What, When, Where). Make it easy for guests to RSVP. Tone can be formal or informal depending on the event."
|
||||
},
|
||||
"interview": {
|
||||
"structure": [
|
||||
"Company Letterhead",
|
||||
"Date",
|
||||
"Candidate's contact information",
|
||||
"Subject line (Interview Invitation - [Job Title] - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"State the purpose: inviting them for an interview",
|
||||
"Specify the position applied for",
|
||||
"Propose date(s) and time(s) for the interview",
|
||||
"Provide location details (Address, or link for virtual)",
|
||||
"Mention who they will meet with (Names and titles)",
|
||||
"Explain the interview format/duration (Optional)",
|
||||
"Instructions (What to bring, who to contact with questions)",
|
||||
"Call to action (Request confirmation or scheduling)",
|
||||
"Closing paragraph (Express anticipation)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Interviewer/HR Contact Name and Title)"
|
||||
],
|
||||
"guidance": "Be professional, clear, and provide all necessary details for the candidate. Make the scheduling process straightforward."
|
||||
},
|
||||
"meeting": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Invitation to Meeting - [Meeting Topic])",
|
||||
"Salutation",
|
||||
"State the purpose: inviting them to a meeting",
|
||||
"Meeting details (Date, Time, Location/Virtual link)",
|
||||
"Purpose/Agenda (Clearly state what the meeting is about)",
|
||||
"Expected duration (Optional)",
|
||||
"Preparation required (Optional: Documents to review)",
|
||||
"RSVP information (Optional)",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be clear about the purpose, date, time, and location. Provide an agenda so attendees can prepare. The tone can be formal or informal depending on the context."
|
||||
},
|
||||
# Default invitation letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Invitation statement",
|
||||
"Event/Meeting details (What, When, Where)",
|
||||
"Purpose (Optional)",
|
||||
"RSVP information",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be clear and specific about the details of the event or meeting."
|
||||
}
|
||||
},
|
||||
# Overall default template if letter type is not recognized
|
||||
"default": {
|
||||
"structure": [
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Conclusion"
|
||||
],
|
||||
"guidance": "Be clear, concise, and appropriate for your audience and purpose. This is a generic structure."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_template_by_type(letter_type: str, subtype: str = "default") -> Dict[str, Any]:
|
||||
"""
|
||||
Get a template for a specific letter type and subtype using a dictionary lookup.
|
||||
|
||||
Args:
|
||||
letter_type: Type of letter (e.g., "personal", "formal", "business", "cover").
|
||||
subtype: Subtype of letter (e.g., "congratulations", "application", "sales").
|
||||
Defaults to "default" if no subtype is specified.
|
||||
|
||||
Returns:
|
||||
Template dictionary with 'structure' (List[str]) and 'guidance' (str).
|
||||
Returns the default template if the letter type or subtype is not found.
|
||||
"""
|
||||
# Get templates for the specific letter type, or the overall default templates
|
||||
type_templates = TEMPLATES.get(letter_type, TEMPLATES["default"])
|
||||
|
||||
# Get the template for the specific subtype, or the default for that letter type
|
||||
template = type_templates.get(subtype, type_templates.get("default", TEMPLATES["default"])) # Fallback to overall default
|
||||
|
||||
# Ensure the returned template always has 'structure' and 'guidance' keys
|
||||
# This handles cases where an incomplete template might have been defined (error tolerance)
|
||||
if "structure" not in template or not isinstance(template["structure"], list):
|
||||
template["structure"] = ["Introduction", "Body", "Conclusion"]
|
||||
template["guidance"] = "Generic template: structure or guidance missing."
|
||||
|
||||
if "guidance" not in template or not isinstance(template["guidance"], str):
|
||||
template["guidance"] = "Generic guidance: structure or guidance missing."
|
||||
|
||||
|
||||
return template
|
||||
|
||||
# Example usage (for testing purposes)
|
||||
if __name__ == '__main__':
|
||||
# Test cases
|
||||
print("--- Testing Letter Templates ---")
|
||||
|
||||
personal_congrats = get_template_by_type("personal", "congratulations")
|
||||
print("\nPersonal Congratulations Template:")
|
||||
print(f"Structure: {personal_congrats['structure']}")
|
||||
print(f"Guidance: {personal_congrats['guidance']}")
|
||||
|
||||
formal_complaint = get_template_by_type("formal", "complaint")
|
||||
print("\nFormal Complaint Template:")
|
||||
print(f"Structure: {formal_complaint['structure']}")
|
||||
print(f"Guidance: {formal_complaint['guidance']}")
|
||||
|
||||
business_sales = get_template_by_type("business", "sales")
|
||||
print("\nBusiness Sales Template:")
|
||||
print(f"Structure: {business_sales['structure']}")
|
||||
print(f"Guidance: {business_sales['guidance']}")
|
||||
|
||||
cover_entry_level = get_template_by_type("cover", "entry_level")
|
||||
print("\nCover Entry Level Template:")
|
||||
print(f"Structure: {cover_entry_level['structure']}")
|
||||
print(f"Guidance: {cover_entry_level['guidance']}")
|
||||
|
||||
unknown_type = get_template_by_type("unknown_type", "some_subtype")
|
||||
print("\nUnknown Type Template (Should be Default):")
|
||||
print(f"Structure: {unknown_type['structure']}")
|
||||
print(f"Guidance: {unknown_type['guidance']}")
|
||||
|
||||
personal_unknown_subtype = get_template_by_type("personal", "unknown_subtype")
|
||||
print("\nPersonal Unknown Subtype Template (Should be Personal Default):")
|
||||
print(f"Structure: {personal_unknown_subtype['structure']}")
|
||||
print(f"Guidance: {personal_unknown_subtype['guidance']}")
|
||||
@@ -1,557 +0,0 @@
|
||||
# Blog Outline Generator
|
||||
|
||||
A powerful AI-powered tool for generating comprehensive blog outlines with advanced editing capabilities, content generation, and image integration.
|
||||
|
||||
## 🛠 Technical Architecture
|
||||
|
||||
### Core Components
|
||||
- **Backend**: Python-based implementation using Streamlit for UI
|
||||
- **AI Integration**:
|
||||
- Text Generation: Integration with multiple LLM providers (Gemini, OpenAI, Anthropic)
|
||||
- Image Generation: Support for multiple image generation APIs (Gemini-AI, Dalle3, Stability-AI)
|
||||
- **Data Structures**:
|
||||
```python
|
||||
class OutlineConfig:
|
||||
content_type: ContentType
|
||||
content_depth: ContentDepth
|
||||
outline_style: OutlineStyle
|
||||
target_word_count: int
|
||||
num_main_sections: int
|
||||
num_subsections_per_section: int
|
||||
include_images: bool
|
||||
image_style: str
|
||||
image_engine: str
|
||||
```
|
||||
|
||||
### Key Technologies
|
||||
- **Streamlit**: Web application framework
|
||||
- **Asyncio**: Asynchronous operations for AI calls
|
||||
- **Loguru**: Advanced logging system
|
||||
- **BeautifulSoup**: Web content parsing
|
||||
- **Pydantic**: Data validation
|
||||
- **Markdown**: Content formatting
|
||||
|
||||
## 🌟 Features with Examples
|
||||
|
||||
### 1. Content Generation
|
||||
- **AI-Powered Content Creation**:
|
||||
```python
|
||||
# Example prompt for content generation
|
||||
prompt = f"""
|
||||
Generate content for a {content_type} article about {topic}.
|
||||
Target audience: {target_audience}
|
||||
Word count: {target_word_count}
|
||||
Style: {outline_style}
|
||||
"""
|
||||
content = await llm_text_gen(prompt)
|
||||
```
|
||||
|
||||
- **Multiple Content Types**:
|
||||
```python
|
||||
# Example configuration for different content types
|
||||
config = OutlineConfig(
|
||||
content_type=ContentType.TUTORIAL,
|
||||
content_depth=ContentDepth.INTERMEDIATE,
|
||||
target_word_count=2000
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Outline Structure
|
||||
- **Flexible Section Management**:
|
||||
```python
|
||||
# Example section generation
|
||||
async def generate_sections(self, topic: str) -> List[str]:
|
||||
sections = []
|
||||
for i in range(self.config.num_main_sections):
|
||||
section = await self._generate_section(topic, i)
|
||||
sections.append(section)
|
||||
return sections
|
||||
```
|
||||
|
||||
- **Optional Components**:
|
||||
```python
|
||||
# Example FAQ generation
|
||||
async def generate_faqs(self, topic: str) -> List[str]:
|
||||
prompt = f"""
|
||||
Generate 5 common questions about {topic}
|
||||
Content type: {self.config.content_type}
|
||||
Target audience: {self.config.target_audience}
|
||||
"""
|
||||
return await llm_text_gen(prompt)
|
||||
```
|
||||
|
||||
### 3. Advanced Editing Capabilities
|
||||
- **Section Content Editor**:
|
||||
```python
|
||||
# Example content editing interface
|
||||
def edit_section_content(self, section: str, content: str) -> str:
|
||||
edited_content = st.text_area(
|
||||
"Edit Content",
|
||||
value=content,
|
||||
height=300,
|
||||
key=f"content_edit_{section}"
|
||||
)
|
||||
return edited_content
|
||||
```
|
||||
|
||||
- **Subsection Management**:
|
||||
```python
|
||||
# Example subsection reordering
|
||||
def reorder_subsections(self, section: str, subsections: List[str]) -> List[str]:
|
||||
for i, subsection in enumerate(subsections):
|
||||
if st.button("↑", key=f"move_up_{section}_{i}"):
|
||||
subsections[i], subsections[i-1] = subsections[i-1], subsections[i]
|
||||
return subsections
|
||||
```
|
||||
|
||||
### 4. Image Generation
|
||||
- **AI Image Generation**:
|
||||
```python
|
||||
# Example image generation
|
||||
async def generate_image(self, prompt: str, style: str) -> str:
|
||||
image_prompt = f"""
|
||||
Create a {style} image for: {prompt}
|
||||
Style: {self.config.image_style}
|
||||
"""
|
||||
return await generate_image(image_prompt)
|
||||
```
|
||||
|
||||
### 5. Content Optimization
|
||||
- **SEO Features**:
|
||||
```python
|
||||
# Example SEO optimization
|
||||
def optimize_content(self, content: str, keywords: List[str]) -> str:
|
||||
for keyword in keywords:
|
||||
content = self._naturally_insert_keyword(content, keyword)
|
||||
return content
|
||||
```
|
||||
|
||||
## 📊 Technical Implementation Details
|
||||
|
||||
### 1. Content Generation Pipeline
|
||||
```python
|
||||
async def generate_content(self, topic: str) -> Dict:
|
||||
# 1. Generate outline structure
|
||||
outline = await self.generate_outline(topic)
|
||||
|
||||
# 2. Generate content for each section
|
||||
for section in outline:
|
||||
content = await self.generate_section_content(section)
|
||||
outline[section]['content'] = content
|
||||
|
||||
# 3. Generate images if enabled
|
||||
if self.config.include_images:
|
||||
for section in outline:
|
||||
image = await self.generate_section_image(section)
|
||||
outline[section]['image'] = image
|
||||
|
||||
return outline
|
||||
```
|
||||
|
||||
### 2. AI Integration
|
||||
```python
|
||||
class AIIntegration:
|
||||
def __init__(self, provider: str):
|
||||
self.provider = provider
|
||||
self.model = self._initialize_model()
|
||||
|
||||
async def generate_text(self, prompt: str) -> str:
|
||||
if self.provider == "gemini":
|
||||
return await gemini_text_response(prompt)
|
||||
elif self.provider == "openai":
|
||||
return await openai_chatgpt(prompt)
|
||||
```
|
||||
|
||||
### 3. Image Processing
|
||||
```python
|
||||
class ImageProcessor:
|
||||
def __init__(self, engine: str):
|
||||
self.engine = engine
|
||||
|
||||
async def generate_image(self, prompt: str) -> str:
|
||||
if self.engine == "Gemini-AI":
|
||||
return await generate_gemini_image(prompt)
|
||||
elif self.engine == "Dalle3":
|
||||
return await generate_dalle3_images(prompt)
|
||||
```
|
||||
|
||||
## 🔧 Configuration Examples
|
||||
|
||||
### 1. Basic Configuration
|
||||
```python
|
||||
config = OutlineConfig(
|
||||
content_type=ContentType.GUIDE,
|
||||
content_depth=ContentDepth.INTERMEDIATE,
|
||||
target_word_count=2000,
|
||||
num_main_sections=5,
|
||||
num_subsections_per_section=3
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Advanced Configuration
|
||||
```python
|
||||
config = OutlineConfig(
|
||||
content_type=ContentType.TUTORIAL,
|
||||
content_depth=ContentDepth.ADVANCED,
|
||||
outline_style=OutlineStyle.MODERN,
|
||||
target_word_count=3000,
|
||||
include_images=True,
|
||||
image_style="realistic",
|
||||
image_engine="Gemini-AI",
|
||||
target_audience="developers",
|
||||
language="English",
|
||||
keywords=["python", "tutorial", "advanced"]
|
||||
)
|
||||
```
|
||||
|
||||
## 📝 Usage Examples
|
||||
|
||||
### 1. Basic Usage
|
||||
```python
|
||||
# Initialize generator
|
||||
generator = BlogOutlineGenerator()
|
||||
|
||||
# Generate outline
|
||||
outline = await generator.generate_outline("Python Programming Basics")
|
||||
|
||||
# Export to markdown
|
||||
markdown = generator.to_markdown()
|
||||
```
|
||||
|
||||
### 2. Advanced Usage
|
||||
```python
|
||||
# Custom configuration
|
||||
config = OutlineConfig(
|
||||
content_type=ContentType.TUTORIAL,
|
||||
content_depth=ContentDepth.ADVANCED,
|
||||
include_images=True
|
||||
)
|
||||
|
||||
# Initialize with config
|
||||
generator = BlogOutlineGenerator(config)
|
||||
|
||||
# Generate with custom settings
|
||||
outline = await generator.generate_outline(
|
||||
"Advanced Python Decorators",
|
||||
keywords=["python", "decorators", "advanced"]
|
||||
)
|
||||
|
||||
# Export to multiple formats
|
||||
markdown = generator.to_markdown()
|
||||
json_output = generator.to_json()
|
||||
html_output = generator.to_html()
|
||||
```
|
||||
|
||||
## 🔍 Technical Considerations
|
||||
|
||||
### 1. Performance Optimization
|
||||
- Asynchronous operations for AI calls
|
||||
- Caching of generated content
|
||||
- Batch processing for images
|
||||
- Memory management for large documents
|
||||
|
||||
### 2. Error Handling
|
||||
```python
|
||||
try:
|
||||
content = await llm_text_gen(prompt)
|
||||
except Exception as e:
|
||||
logger.error(f"Content generation failed: {e}")
|
||||
return None
|
||||
```
|
||||
|
||||
### 3. Data Validation
|
||||
```python
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
class SectionContent(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
image_path: Optional[str]
|
||||
|
||||
@validator('content')
|
||||
def validate_content_length(cls, v):
|
||||
if len(v.split()) < 100:
|
||||
raise ValueError("Content too short")
|
||||
return v
|
||||
```
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
### 1. Content Generation
|
||||
- **AI-Powered Content Creation**: Generate high-quality content for each section using advanced language models
|
||||
- **Multiple Content Types**: Support for various content formats including:
|
||||
- How-to guides
|
||||
- Tutorials
|
||||
- Listicles
|
||||
- Comparisons
|
||||
- Case studies
|
||||
- Opinion pieces
|
||||
- News articles
|
||||
- Reviews
|
||||
- General guides
|
||||
- **Customizable Content Depth**:
|
||||
- Basic: Simple, easy-to-understand content
|
||||
- Intermediate: Balanced depth with practical examples
|
||||
- Advanced: Detailed technical content
|
||||
- Expert: In-depth analysis and advanced concepts
|
||||
|
||||
### 2. Outline Structure
|
||||
- **Flexible Section Management**:
|
||||
- Customizable number of main sections
|
||||
- Configurable subsections per section
|
||||
- Dynamic section reordering
|
||||
- Easy addition/removal of sections
|
||||
- **Optional Components**:
|
||||
- Introduction section
|
||||
- Conclusion section
|
||||
- FAQ section
|
||||
- Additional resources section
|
||||
|
||||
### 3. Advanced Editing Capabilities
|
||||
- **Section Content Editor**:
|
||||
- Rich text editing interface
|
||||
- Real-time word count tracking
|
||||
- Formatting options (Bold, Italic, Lists, Code Blocks, Links)
|
||||
- AI-powered content enhancement
|
||||
- **Subsection Management**:
|
||||
- Drag-and-drop reordering
|
||||
- Individual subsection editing
|
||||
- Add/remove subsection functionality
|
||||
- Bulk editing capabilities
|
||||
- **Metadata Editing**:
|
||||
- Section-specific settings
|
||||
- Content depth adjustment
|
||||
- Target word count configuration
|
||||
- Image settings customization
|
||||
|
||||
### 4. Image Generation
|
||||
- **AI Image Generation**:
|
||||
- Multiple image styles (realistic, illustration, minimalist, photographic, artistic)
|
||||
- Support for multiple image engines (Gemini-AI, Dalle3, Stability-AI)
|
||||
- Custom image prompts
|
||||
- Image regeneration capability
|
||||
- **Image Integration**:
|
||||
- Automatic image placement
|
||||
- Image preview and editing
|
||||
- Image prompt viewing and editing
|
||||
- Image style customization
|
||||
|
||||
### 5. Content Optimization
|
||||
- **SEO Features**:
|
||||
- Keyword integration
|
||||
- Content structure optimization
|
||||
- Meta description generation
|
||||
- SEO-friendly formatting
|
||||
- **Audience Targeting**:
|
||||
- Customizable target audience
|
||||
- Language selection
|
||||
- Content tone adjustment
|
||||
- Reading level optimization
|
||||
|
||||
### 6. Export Options
|
||||
- **Multiple Formats**:
|
||||
- Markdown export
|
||||
- JSON export
|
||||
- HTML export
|
||||
- Custom formatting options
|
||||
- **Download Capabilities**:
|
||||
- One-click download
|
||||
- Format-specific styling
|
||||
- Custom file naming
|
||||
- Batch export options
|
||||
|
||||
### 7. User Interface
|
||||
- **Intuitive Design**:
|
||||
- Clean, modern interface
|
||||
- Responsive layout
|
||||
- Easy navigation
|
||||
- Clear visual hierarchy
|
||||
- **Interactive Features**:
|
||||
- Real-time preview
|
||||
- Drag-and-drop functionality
|
||||
- Quick edit options
|
||||
- Contextual help
|
||||
|
||||
### 8. Statistics and Analytics
|
||||
- **Content Metrics**:
|
||||
- Word count tracking
|
||||
- Section statistics
|
||||
- Subsection counts
|
||||
- Content depth analysis
|
||||
- **Progress Tracking**:
|
||||
- Generation progress
|
||||
- Edit history
|
||||
- Version comparison
|
||||
- Performance metrics
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Usage
|
||||
1. Launch the application:
|
||||
```bash
|
||||
streamlit run lib/ai_writers/ai_outline_writer/outline_ui.py
|
||||
```
|
||||
|
||||
2. Configure your outline:
|
||||
- Enter your blog topic
|
||||
- Select content type and depth
|
||||
- Choose outline style
|
||||
- Set target word count
|
||||
- Configure sections and subsections
|
||||
|
||||
3. Generate and edit:
|
||||
- Click "Generate Outline"
|
||||
- Review and edit sections
|
||||
- Customize content and images
|
||||
- Export in your preferred format
|
||||
|
||||
## 🔧 Configuration Options
|
||||
|
||||
### Basic Settings
|
||||
- **Blog Topic**: Main subject of your content
|
||||
- **Content Type**: Type of content to generate
|
||||
- **Content Depth**: Level of detail and complexity
|
||||
- **Outline Style**: Structure and formatting style
|
||||
|
||||
### Advanced Settings
|
||||
- **Target Word Count**: Desired length of the content
|
||||
- **Number of Sections**: Customize main sections
|
||||
- **Subsections**: Configure subsections per section
|
||||
- **Image Settings**: Customize image generation
|
||||
- **Target Audience**: Define your audience
|
||||
- **Language**: Select content language
|
||||
- **Keywords**: Add SEO keywords
|
||||
- **Excluded Topics**: Specify topics to avoid
|
||||
|
||||
## 📊 Output Formats
|
||||
|
||||
### 1. Preview Mode
|
||||
- Interactive preview of the entire outline
|
||||
- Real-time editing capabilities
|
||||
- Image preview and management
|
||||
- Content statistics
|
||||
|
||||
### 2. Markdown Export
|
||||
- Clean markdown formatting
|
||||
- Proper heading hierarchy
|
||||
- Image embedding
|
||||
- Code block formatting
|
||||
|
||||
### 3. JSON Export
|
||||
- Structured data format
|
||||
- Complete outline information
|
||||
- Content and image metadata
|
||||
- Configuration details
|
||||
|
||||
### 4. HTML Export
|
||||
- Styled HTML output
|
||||
- Responsive design
|
||||
- Image integration
|
||||
- Custom CSS support
|
||||
|
||||
## 💡 Best Practices
|
||||
|
||||
### Content Generation
|
||||
1. Start with a clear topic and target audience
|
||||
2. Choose appropriate content type and depth
|
||||
3. Use relevant keywords for SEO
|
||||
4. Review and edit generated content
|
||||
5. Add personal insights and examples
|
||||
|
||||
### Outline Structure
|
||||
1. Maintain logical flow between sections
|
||||
2. Balance section lengths
|
||||
3. Include relevant subsections
|
||||
4. Add appropriate transitions
|
||||
5. Ensure comprehensive coverage
|
||||
|
||||
### Image Usage
|
||||
1. Choose appropriate image styles
|
||||
2. Generate relevant images
|
||||
3. Optimize image placement
|
||||
4. Review image prompts
|
||||
5. Consider image licensing
|
||||
|
||||
## 🔄 Workflow
|
||||
|
||||
1. **Initial Setup**
|
||||
- Configure basic settings
|
||||
- Set content parameters
|
||||
- Define target audience
|
||||
|
||||
2. **Generation**
|
||||
- Generate initial outline
|
||||
- Review structure
|
||||
- Generate content
|
||||
- Create images
|
||||
|
||||
3. **Editing**
|
||||
- Review and edit content
|
||||
- Adjust structure
|
||||
- Customize images
|
||||
- Optimize for SEO
|
||||
|
||||
4. **Export**
|
||||
- Choose export format
|
||||
- Review final output
|
||||
- Download content
|
||||
- Save configuration
|
||||
|
||||
## 📝 Tips and Tricks
|
||||
|
||||
### Content Generation
|
||||
- Use specific keywords for better results
|
||||
- Provide clear context for the AI
|
||||
- Review and refine generated content
|
||||
- Add personal expertise
|
||||
|
||||
### Structure Optimization
|
||||
- Maintain consistent section lengths
|
||||
- Use clear subsection hierarchies
|
||||
- Include relevant examples
|
||||
- Add practical applications
|
||||
|
||||
### Image Enhancement
|
||||
- Use descriptive image prompts
|
||||
- Experiment with different styles
|
||||
- Consider image placement
|
||||
- Review image relevance
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please follow these steps:
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Submit a pull request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For support, please:
|
||||
1. Check the documentation
|
||||
2. Review existing issues
|
||||
3. Create a new issue if needed
|
||||
4. Contact the maintainers
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
Planned features:
|
||||
- Multi-language support
|
||||
- Advanced AI models
|
||||
- More export formats
|
||||
- Enhanced editing tools
|
||||
- Collaboration features
|
||||
- Version control integration
|
||||
- Analytics dashboard
|
||||
- Custom templates
|
||||
- API integration
|
||||
- Mobile optimization
|
||||
@@ -1,317 +0,0 @@
|
||||
"""
|
||||
Enhanced Blog Outline Generator
|
||||
|
||||
This module provides a sophisticated outline generation system that creates detailed,
|
||||
well-structured outlines for blog posts based on user preferences and content requirements.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import Dict, List, Optional
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from loguru import logger
|
||||
import json
|
||||
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}")
|
||||
|
||||
class ContentType(Enum):
|
||||
"""Types of content that can be generated."""
|
||||
HOW_TO = "how-to"
|
||||
TUTORIAL = "tutorial"
|
||||
LISTICLE = "listicle"
|
||||
COMPARISON = "comparison"
|
||||
CASE_STUDY = "case-study"
|
||||
OPINION = "opinion"
|
||||
NEWS = "news"
|
||||
REVIEW = "review"
|
||||
GUIDE = "guide"
|
||||
|
||||
class ContentDepth(Enum):
|
||||
"""Depth levels for content coverage."""
|
||||
BASIC = "basic"
|
||||
INTERMEDIATE = "intermediate"
|
||||
ADVANCED = "advanced"
|
||||
EXPERT = "expert"
|
||||
|
||||
class OutlineStyle(Enum):
|
||||
"""Styles for outline structure."""
|
||||
TRADITIONAL = "traditional"
|
||||
MODERN = "modern"
|
||||
CONVERSATIONAL = "conversational"
|
||||
ACADEMIC = "academic"
|
||||
SEO_OPTIMIZED = "seo-optimized"
|
||||
|
||||
@dataclass
|
||||
class OutlineConfig:
|
||||
"""Configuration for outline generation."""
|
||||
content_type: ContentType = ContentType.GUIDE
|
||||
content_depth: ContentDepth = ContentDepth.INTERMEDIATE
|
||||
outline_style: OutlineStyle = OutlineStyle.MODERN
|
||||
target_word_count: int = 2000
|
||||
num_main_sections: int = 5
|
||||
num_subsections_per_section: int = 3
|
||||
include_introduction: bool = True
|
||||
include_conclusion: bool = True
|
||||
include_faqs: bool = True
|
||||
include_resources: bool = True
|
||||
target_audience: str = "general"
|
||||
language: str = "English"
|
||||
keywords: List[str] = None
|
||||
exclude_topics: List[str] = None
|
||||
include_images: bool = True
|
||||
image_style: str = "realistic"
|
||||
image_engine: str = "Gemini-AI"
|
||||
|
||||
@dataclass
|
||||
class SectionContent:
|
||||
"""Content for a section including text and image."""
|
||||
title: str
|
||||
content: str
|
||||
image_prompt: Optional[str] = None
|
||||
image_path: Optional[str] = None
|
||||
|
||||
class BlogOutlineGenerator:
|
||||
"""Enhanced blog outline generator with comprehensive controls."""
|
||||
|
||||
def __init__(self, config: Optional[OutlineConfig] = None):
|
||||
"""Initialize the outline generator with optional configuration."""
|
||||
self.config = config or OutlineConfig()
|
||||
self.outline = {}
|
||||
self.section_contents = {}
|
||||
|
||||
def generate_outline(self, topic: str) -> Dict[str, List[str]]:
|
||||
"""Generate a blog outline based on the topic and configuration."""
|
||||
try:
|
||||
# Create a focused prompt for outline generation
|
||||
prompt = f"""Generate a blog outline for topic: {topic}
|
||||
|
||||
Content Type: {self.config.content_type.value}
|
||||
Target Audience: {self.config.target_audience}
|
||||
Content Depth: {self.config.content_depth.value}
|
||||
Style: {self.config.outline_style.value}
|
||||
Word Count Target: {self.config.target_word_count}
|
||||
Main Sections: {self.config.num_main_sections}
|
||||
Subsections per Section: {self.config.num_subsections_per_section}
|
||||
|
||||
Requirements:
|
||||
- Create exactly {self.config.num_main_sections} main sections
|
||||
- Each section should have exactly {self.config.num_subsections_per_section} subsections
|
||||
- Focus on {self.config.content_type.value} content style
|
||||
- Target {self.config.target_audience} audience
|
||||
- Maintain {self.config.content_depth.value} depth
|
||||
- Follow {self.config.outline_style.value} style
|
||||
- Optimize for {self.config.target_word_count} words total
|
||||
|
||||
IMPORTANT: You must return a valid JSON object with main sections as keys and lists of subsections as values.
|
||||
Example format: {{"Section 1": ["Subsection 1.1", "Subsection 1.2"], "Section 2": ["Subsection 2.1", "Subsection 2.2"]}}
|
||||
Do not include any additional text or explanations, only the JSON object."""
|
||||
|
||||
# Get outline from LLM
|
||||
outline_json = llm_text_gen(prompt)
|
||||
|
||||
# Clean the response to ensure it's valid JSON
|
||||
outline_json = outline_json.strip()
|
||||
if not outline_json.startswith('{'):
|
||||
outline_json = outline_json[outline_json.find('{'):]
|
||||
if not outline_json.endswith('}'):
|
||||
outline_json = outline_json[:outline_json.rfind('}')+1]
|
||||
|
||||
# Parse the outline
|
||||
try:
|
||||
outline = json.loads(outline_json)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON parsing error: {str(e)}")
|
||||
logger.error(f"Raw response: {outline_json}")
|
||||
# Fallback to a basic outline structure
|
||||
outline = {
|
||||
f"Section {i+1}": [f"Subsection {i+1}.{j+1}" for j in range(self.config.num_subsections_per_section)]
|
||||
for i in range(self.config.num_main_sections)
|
||||
}
|
||||
|
||||
# Add introduction and conclusion if configured
|
||||
if self.config.include_introduction:
|
||||
outline = {"Introduction": ["Overview", "Importance", "What to Expect"]} | outline
|
||||
|
||||
if self.config.include_conclusion:
|
||||
outline["Conclusion"] = ["Summary", "Key Takeaways", "Next Steps"]
|
||||
|
||||
# Add FAQs if configured
|
||||
if self.config.include_faqs:
|
||||
# Generate topic-specific FAQs
|
||||
faq_prompt = f"""Generate 3 specific and relevant FAQ questions for a blog post about: {topic}
|
||||
|
||||
Content Type: {self.config.content_type.value}
|
||||
Target Audience: {self.config.target_audience}
|
||||
Content Depth: {self.config.content_depth.value}
|
||||
|
||||
Requirements:
|
||||
- Questions should be specific to the topic
|
||||
- Cover common concerns and important aspects
|
||||
- Be relevant to the target audience
|
||||
- Include both basic and advanced questions
|
||||
|
||||
Format: Return only a JSON array of 3 questions.
|
||||
Example format: ["Question 1?", "Question 2?", "Question 3?"]"""
|
||||
|
||||
try:
|
||||
faq_json = llm_text_gen(faq_prompt)
|
||||
faq_json = faq_json.strip()
|
||||
if not faq_json.startswith('['):
|
||||
faq_json = faq_json[faq_json.find('['):]
|
||||
if not faq_json.endswith(']'):
|
||||
faq_json = faq_json[:faq_json.rfind(']')+1]
|
||||
|
||||
faqs = json.loads(faq_json)
|
||||
outline["Frequently Asked Questions"] = faqs
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating FAQs: {str(e)}")
|
||||
outline["Frequently Asked Questions"] = [
|
||||
f"Common Question about {topic} 1",
|
||||
f"Common Question about {topic} 2",
|
||||
f"Common Question about {topic} 3"
|
||||
]
|
||||
|
||||
# Add resources if configured
|
||||
if self.config.include_resources:
|
||||
outline["Additional Resources"] = [
|
||||
"Further Reading",
|
||||
"Tools and References",
|
||||
"Related Topics"
|
||||
]
|
||||
|
||||
return outline
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating outline: {str(e)}")
|
||||
return {}
|
||||
|
||||
def generate_section_content(self, section: str, subsections: List[str]) -> Optional[SectionContent]:
|
||||
"""Generate content for a section."""
|
||||
try:
|
||||
# Create a focused prompt for content generation
|
||||
prompt = f"""Generate content for section: {section}
|
||||
|
||||
Subsections: {', '.join(subsections)}
|
||||
Content Type: {self.config.content_type.value}
|
||||
Target Audience: {self.config.target_audience}
|
||||
Content Depth: {self.config.content_depth.value}
|
||||
Style: {self.config.outline_style.value}
|
||||
Word Count Target: {self.config.target_word_count // self.config.num_main_sections}
|
||||
|
||||
Requirements:
|
||||
- Write content for each subsection
|
||||
- Maintain {self.config.content_depth.value} depth
|
||||
- Target {self.config.target_audience} audience
|
||||
- Follow {self.config.outline_style.value} style
|
||||
- Optimize for {self.config.target_word_count // self.config.num_main_sections} words
|
||||
- Include relevant examples and data points
|
||||
- Use clear, engaging language
|
||||
|
||||
Format: Return only a JSON object with 'content' and 'image_prompt' fields.
|
||||
Example format: {{"content": "Section content here...", "image_prompt": "Image description here..."}}"""
|
||||
|
||||
# Get content from LLM
|
||||
content_json = llm_text_gen(prompt)
|
||||
content_data = json.loads(content_json)
|
||||
|
||||
# Generate image if configured
|
||||
image_path = None
|
||||
if self.config.include_images:
|
||||
image_path = self.generate_section_image(section)
|
||||
|
||||
return SectionContent(
|
||||
title=section,
|
||||
content=content_data["content"],
|
||||
image_prompt=content_data.get("image_prompt"),
|
||||
image_path=image_path
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content for section {section}: {str(e)}")
|
||||
return None
|
||||
|
||||
def generate_section_image(self, section: str) -> Optional[str]:
|
||||
"""Generate an image for a section."""
|
||||
try:
|
||||
# Create a focused prompt for image generation
|
||||
prompt = f"""Generate an image prompt for section: {section}
|
||||
|
||||
Style: {self.config.image_style}
|
||||
Engine: {self.config.image_engine}
|
||||
Content Type: {self.config.content_type.value}
|
||||
Target Audience: {self.config.target_audience}
|
||||
|
||||
Requirements:
|
||||
- Create a {self.config.image_style} style image
|
||||
- Optimize for {self.config.image_engine} engine
|
||||
- Match {self.config.content_type.value} content type
|
||||
- Appeal to {self.config.target_audience} audience
|
||||
- Be visually engaging and relevant
|
||||
|
||||
Format: Return only a JSON object with an 'image_prompt' field.
|
||||
Example format: {{"image_prompt": "Detailed image description here..."}}"""
|
||||
|
||||
# Get image prompt from LLM
|
||||
prompt_json = llm_text_gen(prompt)
|
||||
prompt_data = json.loads(prompt_json)
|
||||
|
||||
# Generate image using the specified engine
|
||||
if self.config.image_engine == "Gemini-AI":
|
||||
image_path = generate_gemini_image(prompt_data["image_prompt"])
|
||||
elif self.config.image_engine == "Dalle3":
|
||||
image_path = generate_dalle_image(prompt_data["image_prompt"])
|
||||
else: # Stability-AI
|
||||
image_path = generate_stability_image(prompt_data["image_prompt"])
|
||||
|
||||
return image_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating image for section {section}: {str(e)}")
|
||||
return None
|
||||
|
||||
def to_markdown(self) -> str:
|
||||
"""Convert outline to markdown format with content and images."""
|
||||
markdown = f"# {self.outline.get('Introduction', [''])[0]}\n\n"
|
||||
|
||||
for section, subsections in self.outline.items():
|
||||
if section not in ["Introduction", "Conclusion", "FAQs", "Additional Resources"]:
|
||||
markdown += f"## {section}\n\n"
|
||||
|
||||
# Add section content if available
|
||||
if section in self.section_contents:
|
||||
content = self.section_contents[section]
|
||||
markdown += f"{content.content}\n\n"
|
||||
|
||||
# Add image if available
|
||||
if content.image_path:
|
||||
markdown += f"\n\n"
|
||||
|
||||
# Add subsections
|
||||
for subsection in subsections:
|
||||
markdown += f"- {subsection}\n"
|
||||
markdown += "\n"
|
||||
|
||||
if "Conclusion" in self.outline:
|
||||
markdown += "## Conclusion\n\n"
|
||||
for subsection in self.outline["Conclusion"]:
|
||||
markdown += f"- {subsection}\n"
|
||||
markdown += "\n"
|
||||
|
||||
if "FAQs" in self.outline:
|
||||
markdown += "## Frequently Asked Questions\n\n"
|
||||
for faq in self.outline["FAQs"]:
|
||||
markdown += f"- {faq}\n"
|
||||
markdown += "\n"
|
||||
|
||||
if "Additional Resources" in self.outline:
|
||||
markdown += "## Additional Resources\n\n"
|
||||
for resource in self.outline["Additional Resources"]:
|
||||
markdown += f"- {resource}\n"
|
||||
|
||||
return markdown
|
||||
@@ -1,739 +0,0 @@
|
||||
"""
|
||||
Streamlit UI for Enhanced Blog Outline Generator
|
||||
|
||||
This module provides a user-friendly interface for generating comprehensive blog outlines
|
||||
with AI-powered content and image generation capabilities.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from .get_blog_outline import (
|
||||
BlogOutlineGenerator,
|
||||
OutlineConfig,
|
||||
ContentType,
|
||||
ContentDepth,
|
||||
OutlineStyle
|
||||
)
|
||||
|
||||
# Custom CSS for better styling
|
||||
st.markdown("""
|
||||
<style>
|
||||
.main {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.stButton>button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 24px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
}
|
||||
.stButton>button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
/* Add specific styling for the generate outline button */
|
||||
.generate-outline-button {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.generate-outline-button > button {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.section-card {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
.content-preview {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
.stats-card {
|
||||
background-color: #e8f5e9;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
.edit-section {
|
||||
background-color: #e3f2fd;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
.subsection-list {
|
||||
margin-left: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
/* Main container width */
|
||||
.main .block-container {
|
||||
max-width: 100%;
|
||||
padding: 2rem;
|
||||
}
|
||||
/* Full width for the outline display */
|
||||
.outline-container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
/* Section styling */
|
||||
.section-header {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
.subsection-item {
|
||||
font-size: 1.1rem;
|
||||
color: #34495e;
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
/* Content area styling */
|
||||
.content-area {
|
||||
background-color: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
/* Make sure all Streamlit elements use full width */
|
||||
.stMarkdown, .stText, .stTextArea, .stSelectbox, .stSlider {
|
||||
width: 100% !important;
|
||||
}
|
||||
/* Full width for code blocks */
|
||||
.stCodeBlock {
|
||||
width: 100% !important;
|
||||
}
|
||||
/* Full width for the main content */
|
||||
.main .block-container {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* Adjust the main content area */
|
||||
.main .block-container > div {
|
||||
max-width: 100%;
|
||||
}
|
||||
/* Make sure the outline content uses full width */
|
||||
.outline-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* Adjust the preview section */
|
||||
.preview-section {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def edit_section_content(section: str, content: str) -> str:
|
||||
"""Edit section content with advanced options."""
|
||||
st.markdown('<div class="edit-section">', unsafe_allow_html=True)
|
||||
|
||||
# Content editing
|
||||
edited_content = st.text_area(
|
||||
"Edit Content",
|
||||
value=content,
|
||||
height=300,
|
||||
key=f"content_edit_{section}"
|
||||
)
|
||||
|
||||
# Word count and formatting
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
word_count = len(edited_content.split())
|
||||
st.info(f"Word Count: {word_count}")
|
||||
|
||||
with col2:
|
||||
formatting = st.multiselect(
|
||||
"Formatting Options",
|
||||
["Bold", "Italic", "Lists", "Code Blocks", "Links"],
|
||||
key=f"format_{section}"
|
||||
)
|
||||
|
||||
# AI enhancement options
|
||||
with st.expander("AI Enhancement Options"):
|
||||
enhance_options = st.multiselect(
|
||||
"Select Enhancements",
|
||||
["Improve Clarity", "Add Examples", "Expand Details", "Add Statistics", "Improve SEO"],
|
||||
key=f"enhance_{section}"
|
||||
)
|
||||
|
||||
if st.button("Apply Enhancements", key=f"apply_enhance_{section}"):
|
||||
with st.spinner("Applying enhancements..."):
|
||||
# TODO: Implement AI enhancement logic
|
||||
st.success("Enhancements applied!")
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
return edited_content
|
||||
|
||||
def edit_subsections(section: str, subsections: List[str]) -> List[str]:
|
||||
"""Edit subsections with reordering and editing capabilities."""
|
||||
st.markdown('<div class="edit-section">', unsafe_allow_html=True)
|
||||
|
||||
# Reorder subsections
|
||||
st.markdown("### Reorder Subsections")
|
||||
for i, subsection in enumerate(subsections):
|
||||
col1, col2 = st.columns([4, 1])
|
||||
with col1:
|
||||
subsections[i] = st.text_input(
|
||||
f"Subsection {i+1}",
|
||||
value=subsection,
|
||||
key=f"subsection_{section}_{i}"
|
||||
)
|
||||
with col2:
|
||||
if st.button("↑", key=f"move_up_{section}_{i}") and i > 0:
|
||||
subsections[i], subsections[i-1] = subsections[i-1], subsections[i]
|
||||
st.experimental_rerun()
|
||||
if st.button("↓", key=f"move_down_{section}_{i}") and i < len(subsections)-1:
|
||||
subsections[i], subsections[i+1] = subsections[i+1], subsections[i]
|
||||
st.experimental_rerun()
|
||||
|
||||
# Add/remove subsections
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("Add Subsection", key=f"add_sub_{section}"):
|
||||
subsections.append("New Subsection")
|
||||
st.experimental_rerun()
|
||||
with col2:
|
||||
if st.button("Remove Last Subsection", key=f"remove_sub_{section}"):
|
||||
if subsections:
|
||||
subsections.pop()
|
||||
st.experimental_rerun()
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
return subsections
|
||||
|
||||
def edit_section_metadata(section: str, generator: BlogOutlineGenerator):
|
||||
"""Edit section metadata and settings."""
|
||||
st.markdown('<div class="edit-section">', unsafe_allow_html=True)
|
||||
|
||||
# Section settings
|
||||
st.markdown("### Section Settings")
|
||||
|
||||
# Image settings
|
||||
if generator.config.include_images:
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
new_image_style = st.selectbox(
|
||||
"Image Style",
|
||||
["realistic", "illustration", "minimalist", "photographic", "artistic"],
|
||||
key=f"img_style_{section}"
|
||||
)
|
||||
with col2:
|
||||
new_image_engine = st.selectbox(
|
||||
"Image Engine",
|
||||
["Gemini-AI", "Dalle3", "Stability-AI"],
|
||||
key=f"img_engine_{section}"
|
||||
)
|
||||
|
||||
if st.button("Regenerate Image", key=f"regen_img_{section}"):
|
||||
with st.spinner("Regenerating image..."):
|
||||
# TODO: Implement image regeneration logic
|
||||
st.success("Image regenerated!")
|
||||
|
||||
# Content settings
|
||||
st.markdown("### Content Settings")
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
target_word_count = st.number_input(
|
||||
"Target Word Count",
|
||||
min_value=100,
|
||||
max_value=2000,
|
||||
value=500,
|
||||
step=100,
|
||||
key=f"word_count_{section}"
|
||||
)
|
||||
with col2:
|
||||
content_depth = st.selectbox(
|
||||
"Content Depth",
|
||||
[depth.value for depth in ContentDepth],
|
||||
key=f"depth_{section}"
|
||||
)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def display_section(section: str, subsections: List[str], content: Optional[Dict] = None, generator: Optional[BlogOutlineGenerator] = None):
|
||||
"""Display a section with its content and subsections."""
|
||||
st.markdown(f"""
|
||||
<div class="section-card">
|
||||
<div class="section-header">{section}</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Section editing controls
|
||||
col1, col2 = st.columns([4, 1])
|
||||
with col1:
|
||||
st.markdown(f"### {section}")
|
||||
with col2:
|
||||
edit_mode = st.checkbox("Edit Mode", key=f"edit_mode_{section}")
|
||||
|
||||
if content:
|
||||
# Display content with word count
|
||||
word_count = len(content.content.split())
|
||||
st.markdown(f"""
|
||||
<div class="content-preview">
|
||||
<p><strong>Content Preview</strong> ({word_count} words)</p>
|
||||
{content.content[:500]}...
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Image generation and display - Always show if images are enabled
|
||||
if generator and generator.config.include_images:
|
||||
st.markdown("### Image Generation")
|
||||
col1, col2, col3 = st.columns([2, 2, 1])
|
||||
|
||||
with col1:
|
||||
image_style = st.selectbox(
|
||||
"Image Style",
|
||||
["realistic", "illustration", "minimalist", "photographic", "artistic"],
|
||||
index=["realistic", "illustration", "minimalist", "photographic", "artistic"].index(generator.config.image_style),
|
||||
key=f"img_style_{section}"
|
||||
)
|
||||
|
||||
with col2:
|
||||
image_engine = st.selectbox(
|
||||
"Image Engine",
|
||||
["Gemini-AI", "Dalle3", "Stability-AI"],
|
||||
index=["Gemini-AI", "Dalle3", "Stability-AI"].index(generator.config.image_engine),
|
||||
key=f"img_engine_{section}"
|
||||
)
|
||||
|
||||
with col3:
|
||||
if st.button("Generate Image", key=f"gen_img_{section}"):
|
||||
with st.spinner(f"Generating image for {section}..."):
|
||||
# Update config with selected options
|
||||
generator.config.image_style = image_style
|
||||
generator.config.image_engine = image_engine
|
||||
image_path = generator.generate_section_image(section)
|
||||
if image_path:
|
||||
st.success("Image generated successfully!")
|
||||
st.experimental_rerun()
|
||||
else:
|
||||
st.error("Failed to generate image")
|
||||
|
||||
# Display existing image if available
|
||||
if content.image_path:
|
||||
st.markdown('<div class="image-container">', unsafe_allow_html=True)
|
||||
st.image(content.image_path, caption=section, use_column_width=True)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display image prompt in expander
|
||||
if content.image_prompt:
|
||||
with st.expander("View Image Prompt"):
|
||||
st.code(content.image_prompt, language="text")
|
||||
|
||||
# Edit mode controls
|
||||
if edit_mode:
|
||||
st.markdown("### Edit Content")
|
||||
# Edit content
|
||||
edited_content = edit_section_content(section, content.content)
|
||||
if edited_content != content.content:
|
||||
content.content = edited_content
|
||||
st.experimental_rerun()
|
||||
|
||||
st.markdown("### Edit Subsections")
|
||||
# Edit subsections
|
||||
edited_subsections = edit_subsections(section, subsections)
|
||||
if edited_subsections != subsections:
|
||||
subsections[:] = edited_subsections
|
||||
st.experimental_rerun()
|
||||
|
||||
st.markdown("### Edit Metadata")
|
||||
# Edit metadata
|
||||
if generator:
|
||||
edit_section_metadata(section, generator)
|
||||
else:
|
||||
# Display subsections in view mode
|
||||
st.markdown("### Subsections")
|
||||
st.markdown('<div class="subsection-list">', unsafe_allow_html=True)
|
||||
for subsection in subsections:
|
||||
st.markdown(f'<div class="subsection-item">• {subsection}</div>', unsafe_allow_html=True)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
|
||||
def display_stats(generator, outline):
|
||||
"""Display statistics about the generated outline."""
|
||||
total_sections = len(outline)
|
||||
total_subsections = sum(len(subsections) for subsections in outline.values())
|
||||
total_content = sum(len(content.content.split()) for content in generator.section_contents.values())
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.markdown(f"""
|
||||
<div class="stats-card">
|
||||
<h3>📊 Statistics</h3>
|
||||
<p>Total Sections: {total_sections}</p>
|
||||
<p>Total Subsections: {total_subsections}</p>
|
||||
<p>Estimated Word Count: {total_content}</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col2:
|
||||
st.markdown(f"""
|
||||
<div class="stats-card">
|
||||
<h3>🎯 Target</h3>
|
||||
<p>Target Word Count: {generator.config.target_word_count}</p>
|
||||
<p>Content Depth: {generator.config.content_depth.value}</p>
|
||||
<p>Style: {generator.config.outline_style.value}</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col3:
|
||||
st.markdown(f"""
|
||||
<div class="stats-card">
|
||||
<h3>📝 Content Type</h3>
|
||||
<p>Type: {generator.config.content_type.value}</p>
|
||||
<p>Audience: {generator.config.target_audience}</p>
|
||||
<p>Language: {generator.config.language}</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def main():
|
||||
# Header with description
|
||||
st.title("Blog Outline Generator")
|
||||
st.markdown("""
|
||||
Generate comprehensive blog outlines with AI-powered content and images.
|
||||
Customize your outline with various options and get detailed content for each section.
|
||||
""")
|
||||
|
||||
# Main content area with full width
|
||||
st.markdown('<div class="outline-container">', unsafe_allow_html=True)
|
||||
|
||||
# Move topic input to main area and make it more prominent
|
||||
st.markdown("### Enter Your Blog Topic")
|
||||
topic = st.text_input("", placeholder="Enter your blog topic here for creating outline...", key="blog_topic")
|
||||
|
||||
st.markdown("---") # Add a separator
|
||||
st.markdown("### Configuration Options")
|
||||
|
||||
# Create tabs for different configuration sections
|
||||
tab1, tab2, tab3, tab4 = st.tabs([
|
||||
"📝 Content Type & Target",
|
||||
"📊 Content Structure",
|
||||
"🎨 Style & Sections",
|
||||
"🖼️ Image & Optimization"
|
||||
])
|
||||
|
||||
with tab1:
|
||||
st.markdown("#### Content Type & Target")
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
content_type = st.selectbox(
|
||||
"Content Type",
|
||||
[type.value for type in ContentType],
|
||||
index=[type.value for type in ContentType].index(ContentType.GUIDE.value),
|
||||
help="Select the type of content you want to generate"
|
||||
)
|
||||
|
||||
with col2:
|
||||
target_audience = st.selectbox(
|
||||
"Target Audience",
|
||||
["General", "Technical", "Professional", "Academic", "Business", "Students", "Developers"],
|
||||
index=0,
|
||||
help="Select your target audience"
|
||||
)
|
||||
|
||||
with col3:
|
||||
language = st.selectbox(
|
||||
"Language",
|
||||
["English", "Spanish", "French", "German", "Italian", "Portuguese", "Chinese", "Japanese", "Korean"],
|
||||
index=0,
|
||||
help="Select the language for your content"
|
||||
)
|
||||
|
||||
with tab2:
|
||||
st.markdown("#### Content Structure")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
num_main_sections = st.slider(
|
||||
"Number of Main Sections",
|
||||
min_value=3,
|
||||
max_value=10,
|
||||
value=5,
|
||||
step=1,
|
||||
help="Choose how many main sections your outline should have"
|
||||
)
|
||||
|
||||
num_subsections = st.slider(
|
||||
"Subsections per Section",
|
||||
min_value=2,
|
||||
max_value=5,
|
||||
value=3,
|
||||
step=1,
|
||||
help="Choose how many subsections each main section should have"
|
||||
)
|
||||
|
||||
with col2:
|
||||
target_word_count = st.slider(
|
||||
"Target Word Count",
|
||||
min_value=500,
|
||||
max_value=5000,
|
||||
value=2000,
|
||||
step=100,
|
||||
help="Set your target word count for the entire blog post"
|
||||
)
|
||||
|
||||
# Display content statistics
|
||||
st.markdown("##### Content Statistics")
|
||||
st.markdown(f"""
|
||||
- Estimated Sections: {num_main_sections}
|
||||
- Total Subsections: {num_main_sections * num_subsections}
|
||||
- Target Word Count: {target_word_count}
|
||||
- Average Words per Section: {target_word_count // num_main_sections}
|
||||
""")
|
||||
|
||||
with tab3:
|
||||
st.markdown("#### Style & Sections")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
content_depth = st.selectbox(
|
||||
"Content Depth",
|
||||
[depth.value for depth in ContentDepth],
|
||||
index=[depth.value for depth in ContentDepth].index(ContentDepth.INTERMEDIATE.value),
|
||||
help="Select the depth of content coverage"
|
||||
)
|
||||
|
||||
outline_style = st.selectbox(
|
||||
"Outline Style",
|
||||
[style.value for style in OutlineStyle],
|
||||
index=[style.value for style in OutlineStyle].index(OutlineStyle.MODERN.value),
|
||||
help="Select the style of your outline"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("##### Additional Sections")
|
||||
include_intro = st.checkbox("Include Introduction", value=True, help="Add an introduction section")
|
||||
include_conclusion = st.checkbox("Include Conclusion", value=True, help="Add a conclusion section")
|
||||
include_faqs = st.checkbox("Include FAQs", value=True, help="Add a FAQ section")
|
||||
include_resources = st.checkbox("Include Resources", value=True, help="Add a resources section")
|
||||
|
||||
with tab4:
|
||||
st.markdown("#### Image & Optimization")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("##### Image Settings")
|
||||
include_images = st.checkbox("Enable Image Generation", value=True, help="Enable AI image generation for sections")
|
||||
|
||||
if include_images:
|
||||
image_style = st.selectbox(
|
||||
"Image Style",
|
||||
["realistic", "illustration", "minimalist", "photographic", "artistic"],
|
||||
index=0,
|
||||
help="Select the style for generated images"
|
||||
)
|
||||
|
||||
image_engine = st.selectbox(
|
||||
"Image Engine",
|
||||
["Gemini-AI", "Dalle3", "Stability-AI"],
|
||||
index=0,
|
||||
help="Select the AI engine for image generation"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("##### Content Optimization")
|
||||
keywords = st.text_area(
|
||||
"Keywords (comma-separated)",
|
||||
help="Enter keywords for SEO optimization, separated by commas"
|
||||
)
|
||||
|
||||
exclude_topics = st.text_area(
|
||||
"Topics to Exclude (comma-separated)",
|
||||
help="Enter topics you want to exclude from the content"
|
||||
)
|
||||
|
||||
st.markdown("---") # Add a separator before the generate button
|
||||
|
||||
# Create configuration
|
||||
config = OutlineConfig(
|
||||
content_type=ContentType(content_type),
|
||||
content_depth=ContentDepth(content_depth),
|
||||
outline_style=OutlineStyle(outline_style),
|
||||
target_word_count=target_word_count,
|
||||
num_main_sections=num_main_sections,
|
||||
num_subsections_per_section=num_subsections,
|
||||
include_introduction=include_intro,
|
||||
include_conclusion=include_conclusion,
|
||||
include_faqs=include_faqs,
|
||||
include_resources=include_resources,
|
||||
include_images=include_images,
|
||||
image_style=image_style if include_images else "realistic",
|
||||
image_engine=image_engine if include_images else "Gemini-AI",
|
||||
target_audience=target_audience,
|
||||
language=language,
|
||||
keywords=[k.strip() for k in keywords.split(',')] if keywords else None,
|
||||
exclude_topics=[t.strip() for t in exclude_topics.split(',')] if exclude_topics else None
|
||||
)
|
||||
|
||||
# Initialize generator
|
||||
generator = BlogOutlineGenerator(config)
|
||||
|
||||
# Store the generated outline in session state
|
||||
if 'outline' not in st.session_state:
|
||||
st.session_state.outline = None
|
||||
if 'section_contents' not in st.session_state:
|
||||
st.session_state.section_contents = {}
|
||||
|
||||
# Generate outline button with full width
|
||||
st.markdown('<div class="generate-outline-button">', unsafe_allow_html=True)
|
||||
if not topic:
|
||||
st.warning("Please enter a blog topic to generate the outline.")
|
||||
if st.button("Generate Outline", type="primary", use_container_width=True, disabled=not topic):
|
||||
with st.spinner("Generating outline and content..."):
|
||||
try:
|
||||
# Add progress bar
|
||||
progress_bar = st.progress(0)
|
||||
for i in range(100):
|
||||
time.sleep(0.01)
|
||||
progress_bar.progress(i + 1)
|
||||
|
||||
outline = generator.generate_outline(topic)
|
||||
st.session_state.outline = outline
|
||||
st.session_state.section_contents = generator.section_contents
|
||||
|
||||
# Display results
|
||||
st.success("Outline generated successfully!")
|
||||
|
||||
# Add copy button and display outline in full width
|
||||
st.markdown('<div class="outline-content">', unsafe_allow_html=True)
|
||||
outline_text = json.dumps(outline, indent=2)
|
||||
st.code(outline_text, language="json")
|
||||
st.button("Copy Outline", key="copy_outline",
|
||||
help="Copy the outline to clipboard",
|
||||
on_click=lambda: st.write(f'<script>navigator.clipboard.writeText(`{outline_text}`)</script>',
|
||||
unsafe_allow_html=True))
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display statistics
|
||||
display_stats(generator, outline)
|
||||
|
||||
# Output format selection
|
||||
output_format = st.radio(
|
||||
"Output Format",
|
||||
["Preview", "Markdown", "JSON", "HTML"]
|
||||
)
|
||||
|
||||
if output_format == "Preview":
|
||||
# Display outline with content and images
|
||||
st.markdown('<div class="preview-section">', unsafe_allow_html=True)
|
||||
for section, subsections in outline.items():
|
||||
content = generator.section_contents.get(section)
|
||||
display_section(section, subsections, content, generator)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
elif output_format == "Markdown":
|
||||
markdown_output = generator.to_markdown()
|
||||
st.markdown('<div class="outline-content">', unsafe_allow_html=True)
|
||||
st.code(markdown_output, language="markdown")
|
||||
st.download_button(
|
||||
"Download Markdown",
|
||||
markdown_output,
|
||||
file_name="blog_outline.md",
|
||||
mime="text/markdown"
|
||||
)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
elif output_format == "JSON":
|
||||
json_output = json.dumps({
|
||||
"outline": outline,
|
||||
"contents": {
|
||||
section: {
|
||||
"title": content.title,
|
||||
"content": content.content,
|
||||
"image_prompt": content.image_prompt,
|
||||
"image_path": content.image_path
|
||||
}
|
||||
for section, content in generator.section_contents.items()
|
||||
}
|
||||
}, indent=2)
|
||||
st.markdown('<div class="outline-content">', unsafe_allow_html=True)
|
||||
st.code(json_output, language="json")
|
||||
st.download_button(
|
||||
"Download JSON",
|
||||
json_output,
|
||||
file_name="blog_outline.json",
|
||||
mime="application/json"
|
||||
)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
elif output_format == "HTML":
|
||||
html_output = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{topic} - Blog Outline</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; max-width: 100%; margin: 0 auto; padding: 20px; }}
|
||||
.section {{ margin-bottom: 30px; }}
|
||||
.content {{ background: #f8f9fa; padding: 15px; border-radius: 4px; }}
|
||||
img {{ max-width: 100%; height: auto; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{topic}</h1>
|
||||
{generator.to_markdown().replace('#', '##')}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
st.markdown('<div class="outline-content">', unsafe_allow_html=True)
|
||||
st.code(html_output, language="html")
|
||||
st.download_button(
|
||||
"Download HTML",
|
||||
html_output,
|
||||
file_name="blog_outline.html",
|
||||
mime="text/html"
|
||||
)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error generating outline: {str(e)}")
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display the outline if it exists in session state
|
||||
if st.session_state.outline:
|
||||
st.markdown('<div class="preview-section">', unsafe_allow_html=True)
|
||||
for section, subsections in st.session_state.outline.items():
|
||||
content = st.session_state.section_contents.get(section)
|
||||
display_section(section, subsections, content, generator)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True) # Close the outline container
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,163 +0,0 @@
|
||||
# AI Blog Rewriter & Updater
|
||||
|
||||
A powerful AI-powered tool for rewriting and updating existing blog content with improved quality, factual accuracy, and SEO optimization.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Content Import
|
||||
- **URL Import**: Automatically extract content from any blog URL
|
||||
- **Manual Input**: Paste content directly with title, meta description, and author information
|
||||
- **Smart Content Extraction**: Preserves structure, headings, images, and metadata
|
||||
|
||||
### 2. Content Analysis
|
||||
- **Metrics Analysis**:
|
||||
- Word count
|
||||
- Sentence count
|
||||
- Paragraph count
|
||||
- Average words per sentence
|
||||
- Average sentences per paragraph
|
||||
- **Structure Analysis**:
|
||||
- Heading hierarchy
|
||||
- Content organization
|
||||
- Image analysis
|
||||
- **Age Analysis**:
|
||||
- Content age calculation
|
||||
- Publication date detection
|
||||
|
||||
### 3. Web Research
|
||||
- **Topic Extraction**: Automatically identifies key topics for fact-checking
|
||||
- **Multi-Source Research**: Gathers information from various sources
|
||||
- **Research Depth Control**: Choose between low, medium, and high research depth
|
||||
- **Source Organization**: Categorizes research by topic with source details
|
||||
|
||||
### 4. Rewriting Modes
|
||||
- **Standard Rewrite**: Improve clarity and flow while maintaining core message
|
||||
- **SEO Optimization**: Enhance content for search engines with targeted keywords
|
||||
- **Simplification**: Make complex content more accessible
|
||||
- **Expansion**: Add more details and examples
|
||||
- **Fact Check**: Update outdated information
|
||||
- **Tone Shift**: Change writing style while preserving content
|
||||
- **Modernization**: Update with current information and trends
|
||||
|
||||
### 5. Customization Options
|
||||
- **Tone Selection**:
|
||||
- Professional
|
||||
- Conversational
|
||||
- Academic
|
||||
- Enthusiastic
|
||||
- Authoritative
|
||||
- Friendly
|
||||
- Technical
|
||||
- Inspirational
|
||||
- **Length Control**:
|
||||
- Maintain original length
|
||||
- Create shorter version
|
||||
- Create longer version
|
||||
- Custom word count
|
||||
- **SEO Features**:
|
||||
- Focus keyword optimization
|
||||
- Meta description generation
|
||||
- Title optimization
|
||||
- **Special Instructions**: Add custom requirements for the rewrite
|
||||
|
||||
### 6. Image Generation
|
||||
- **AI Image Suggestions**: Get recommendations for relevant images
|
||||
- **Custom Image Generation**: Create images based on content
|
||||
- **Style Options**:
|
||||
- Realistic
|
||||
- Artistic
|
||||
- Cartoon
|
||||
- 3D Render
|
||||
- **Image Placement**: Suggested optimal placement within content
|
||||
|
||||
### 7. Export Options
|
||||
- **Preview Mode**: View formatted content
|
||||
- **Markdown Export**: Get clean markdown version
|
||||
- **Image Integration**: Include generated images with captions
|
||||
- **Meta Information**: Export with optimized title and meta description
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Import Content**
|
||||
- Choose between URL import or manual content entry
|
||||
- Provide necessary metadata (title, author, etc.)
|
||||
|
||||
2. **Analysis & Research**
|
||||
- Review content analysis metrics
|
||||
- Examine research findings
|
||||
- Identify areas for improvement
|
||||
|
||||
3. **Configure Rewrite Settings**
|
||||
- Select rewrite mode
|
||||
- Choose target tone
|
||||
- Set content length
|
||||
- Add focus keywords
|
||||
- Provide special instructions
|
||||
|
||||
4. **Review & Export**
|
||||
- Preview rewritten content
|
||||
- Generate suggested images
|
||||
- Export in desired format
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Dependencies
|
||||
- Streamlit for UI
|
||||
- BeautifulSoup for content extraction
|
||||
- GPT providers for text generation
|
||||
- Image generation capabilities
|
||||
- Web research APIs (Exa, Tavily)
|
||||
|
||||
### Key Components
|
||||
- `BlogRewriter` class: Core functionality
|
||||
- Content extraction and analysis
|
||||
- Research integration
|
||||
- AI-powered rewriting
|
||||
- Image generation
|
||||
- Export capabilities
|
||||
|
||||
### Error Handling
|
||||
- Robust error handling for URL extraction
|
||||
- Fallback mechanisms for content parsing
|
||||
- Graceful degradation for API failures
|
||||
- User-friendly error messages
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Content Import**
|
||||
- Use clean, well-structured URLs
|
||||
- Provide complete metadata for manual entry
|
||||
- Ensure content is properly formatted
|
||||
|
||||
2. **Research Settings**
|
||||
- Choose appropriate research depth
|
||||
- Review research findings carefully
|
||||
- Verify source credibility
|
||||
|
||||
3. **Rewrite Configuration**
|
||||
- Select appropriate tone for audience
|
||||
- Use relevant focus keywords
|
||||
- Provide clear special instructions
|
||||
|
||||
4. **Image Generation**
|
||||
- Use descriptive prompts
|
||||
- Choose appropriate style
|
||||
- Consider image placement
|
||||
|
||||
## Limitations
|
||||
|
||||
- Maximum content length for processing
|
||||
- API rate limits for research
|
||||
- Image generation constraints
|
||||
- Language support limitations
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Multi-language support
|
||||
- Advanced SEO analysis
|
||||
- Content structure templates
|
||||
- Collaborative editing
|
||||
- Integration with CMS platforms
|
||||
- Custom AI model selection
|
||||
- Advanced image editing
|
||||
- Content versioning
|
||||
@@ -1,11 +0,0 @@
|
||||
"""
|
||||
AI Blog Rewriter Module
|
||||
|
||||
This module provides the main entry point for the blog rewriter functionality,
|
||||
importing and using the utility and UI modules.
|
||||
"""
|
||||
|
||||
from .blog_rewriter_ui import write_blog_rewriter
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_blog_rewriter()
|
||||
@@ -1,624 +0,0 @@
|
||||
"""
|
||||
Blog Rewriter UI Module
|
||||
|
||||
This module contains the Streamlit interface for the blog rewriter,
|
||||
providing a user-friendly way to interact with the rewriting functionality.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import json
|
||||
from datetime import datetime
|
||||
from .blog_rewriter_utils import BlogRewriter, REWRITE_MODES, TONE_OPTIONS, MAX_META_DESCRIPTION_LENGTH
|
||||
|
||||
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("""
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 10px; margin-bottom: 20px;">
|
||||
<h3 style="margin-top: 0;">Revitalize Your Content</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
""", 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
|
||||
)
|
||||
|
||||
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
|
||||
st.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
|
||||
st.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.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.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.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.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"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{st.session_state.rewritten_content['title']}</title>
|
||||
<meta name="description" content="{st.session_state.rewritten_content.get('meta_description', '')}">
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }}
|
||||
h1, h2, h3, h4, h5, h6 {{ color: #333; }}
|
||||
img {{ max-width: 100%; height: auto; }}
|
||||
pre {{ background-color: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; }}
|
||||
blockquote {{ border-left: 5px solid #eee; padding-left: 15px; margin-left: 0; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{st.session_state.rewritten_content['title']}</h1>
|
||||
{st.session_state.rewritten_content['content']}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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.rerun()
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_blog_rewriter()
|
||||
@@ -1,595 +0,0 @@
|
||||
"""
|
||||
Blog Rewriter Utilities Module
|
||||
|
||||
This module contains the core functionality for rewriting and updating blog content,
|
||||
including content extraction, analysis, research, and rewriting capabilities.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# 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 ...ai_web_researcher.metaphor_basic_neural_web_search import metaphor_search_articles
|
||||
from ...ai_web_researcher.tavily_ai_search import do_tavily_ai_search
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 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"
|
||||
}
|
||||
|
||||
# Define tone options
|
||||
TONE_OPTIONS = [
|
||||
"Professional", "Conversational", "Academic", "Enthusiastic",
|
||||
"Authoritative", "Friendly", "Technical", "Inspirational"
|
||||
]
|
||||
|
||||
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',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
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 - try multiple strategies
|
||||
content = ""
|
||||
|
||||
# Strategy 1: Look for article tag
|
||||
article_tag = soup.find("article")
|
||||
if article_tag:
|
||||
content = article_tag.get_text(separator="\n\n")
|
||||
|
||||
# Strategy 2: Look for main content areas
|
||||
if not content:
|
||||
main_content = soup.find(["main", "div", "section"], class_=re.compile(r"content|article|post|entry|main|body"))
|
||||
if main_content:
|
||||
for elem in main_content.find_all(["nav", "aside", "footer", "comments", "script", "style", "header"]):
|
||||
elem.decompose()
|
||||
content = main_content.get_text(separator="\n\n")
|
||||
|
||||
# Strategy 3: Look for specific content classes
|
||||
if not content:
|
||||
content_classes = ["post-content", "entry-content", "article-content", "blog-content", "content-area"]
|
||||
for class_name in content_classes:
|
||||
content_div = soup.find("div", class_=class_name)
|
||||
if content_div:
|
||||
for elem in content_div.find_all(["nav", "aside", "footer", "comments", "script", "style", "header"]):
|
||||
elem.decompose()
|
||||
content = content_div.get_text(separator="\n\n")
|
||||
break
|
||||
|
||||
# Strategy 4: Look for content within body
|
||||
if not content:
|
||||
body = soup.find("body")
|
||||
if body:
|
||||
# Remove unwanted elements
|
||||
for elem in body.find_all(["nav", "aside", "footer", "comments", "script", "style", "header"]):
|
||||
elem.decompose()
|
||||
content = body.get_text(separator="\n\n")
|
||||
|
||||
# Clean up the content
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
content = re.sub(r'\s{2,}', ' ', content)
|
||||
content = content.strip()
|
||||
|
||||
# Extract headings with their hierarchy
|
||||
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 with more metadata
|
||||
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://")):
|
||||
base_url = "/".join(url.split("/")[:3])
|
||||
image_url = f"{base_url}/{image_url.lstrip('/')}"
|
||||
|
||||
images.append({
|
||||
"url": image_url,
|
||||
"alt_text": img.get("alt", ""),
|
||||
"title": img.get("title", ""),
|
||||
"class": img.get("class", []),
|
||||
"width": img.get("width"),
|
||||
"height": img.get("height")
|
||||
})
|
||||
|
||||
# Extract publish date with multiple strategies
|
||||
publish_date = None
|
||||
# Try meta tags first
|
||||
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 other meta tags
|
||||
for prop in ["datePublished", "dateCreated", "dateModified"]:
|
||||
date_meta = soup.find("meta", attrs={"property": prop})
|
||||
if date_meta and "content" in date_meta.attrs:
|
||||
publish_date = date_meta["content"]
|
||||
break
|
||||
|
||||
# Try HTML elements if meta tags failed
|
||||
if not publish_date:
|
||||
date_elem = soup.find(["time", "span", "div"], class_=re.compile(r"date|time|publish|posted|created"))
|
||||
if date_elem and date_elem.get_text():
|
||||
publish_date = date_elem.get_text().strip()
|
||||
|
||||
# Extract author with multiple strategies
|
||||
author = None
|
||||
# Try meta tags first
|
||||
author_meta = soup.find("meta", attrs={"name": "author"})
|
||||
if author_meta and "content" in author_meta.attrs:
|
||||
author = author_meta["content"]
|
||||
else:
|
||||
# Try other meta tags
|
||||
for prop in ["article:author", "author"]:
|
||||
author_meta = soup.find("meta", attrs={"property": prop})
|
||||
if author_meta and "content" in author_meta.attrs:
|
||||
author = author_meta["content"]
|
||||
break
|
||||
|
||||
# Try HTML elements if meta tags failed
|
||||
if not author:
|
||||
author_elem = soup.find(["a", "span", "div"], class_=re.compile(r"author|byline|writer|posted-by"))
|
||||
if author_elem and author_elem.get_text():
|
||||
author = author_elem.get_text().strip()
|
||||
|
||||
# Log content extraction results
|
||||
logger.info(f"Extracted content length: {len(content)} characters")
|
||||
logger.info(f"Found {len(headings)} headings")
|
||||
logger.info(f"Found {len(images)} images")
|
||||
logger.info(f"Publish date: {publish_date}")
|
||||
logger.info(f"Author: {author}")
|
||||
|
||||
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:
|
||||
if "T" in publish_date:
|
||||
pub_date = datetime.fromisoformat(publish_date.replace("Z", "+00:00"))
|
||||
else:
|
||||
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
|
||||
|
||||
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)
|
||||
topics_json = re.search(r'\[.*\]', topics_json, re.DOTALL)
|
||||
if topics_json:
|
||||
topics = json.loads(topics_json.group(0))
|
||||
else:
|
||||
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
|
||||
topic_results = {"topic": topic["topic"], "sources": []}
|
||||
|
||||
# Try Exa search first
|
||||
try:
|
||||
exa_results = metaphor_search_articles(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 = do_tavily_ai_search(topic["query"], num_results=num_results)
|
||||
if tavily_results:
|
||||
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
|
||||
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]):
|
||||
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)
|
||||
|
||||
# Clean the response of any invalid control characters
|
||||
response = ''.join(char for char in response if ord(char) >= 32 or char in '\n\r\t')
|
||||
|
||||
# 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:
|
||||
# If no JSON block found, try to find JSON-like content
|
||||
json_match = re.search(r'\{.*\}', response, re.DOTALL)
|
||||
if json_match:
|
||||
json_str = json_match.group(0)
|
||||
else:
|
||||
json_str = response
|
||||
|
||||
# Clean up the JSON string
|
||||
json_str = re.sub(r'```(json)?', '', json_str).strip()
|
||||
|
||||
# Remove any remaining invalid control characters
|
||||
json_str = ''.join(char for char in json_str if ord(char) >= 32 or char in '\n\r\t')
|
||||
|
||||
# Parse the JSON with error handling
|
||||
try:
|
||||
rewritten_content = json.loads(json_str)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON parsing error: {e}")
|
||||
# Try to fix common JSON issues
|
||||
json_str = json_str.replace('\\n', '\\\\n') # Fix escaped newlines
|
||||
json_str = json_str.replace('\\"', '"') # Fix escaped quotes
|
||||
json_str = json_str.replace('\\t', '\\\\t') # Fix escaped tabs
|
||||
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, "")
|
||||
logger.warning(f"Missing required field '{field}' in rewritten content")
|
||||
|
||||
# Ensure suggested_images exists
|
||||
if "suggested_images" not in rewritten_content:
|
||||
rewritten_content["suggested_images"] = []
|
||||
|
||||
# Clean up the content field
|
||||
if "content" in rewritten_content:
|
||||
# Remove any remaining invalid control characters
|
||||
rewritten_content["content"] = ''.join(
|
||||
char for char in rewritten_content["content"]
|
||||
if ord(char) >= 32 or char in '\n\r\t'
|
||||
)
|
||||
# Normalize whitespace
|
||||
rewritten_content["content"] = re.sub(r'\s+', ' ', rewritten_content["content"])
|
||||
rewritten_content["content"] = re.sub(r'\n{3,}', '\n\n', rewritten_content["content"])
|
||||
|
||||
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 ""
|
||||
Reference in New Issue
Block a user