Blog writer enhancements & fixes

This commit is contained in:
ajaysi
2025-04-29 08:55:47 +05:30
parent ef462f05f2
commit 9db20db0d1
45 changed files with 3000 additions and 3290 deletions

View File

@@ -0,0 +1,635 @@
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")
st.markdown("#### Enter Keywords, Title or URL")
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")
st.markdown("#### Upload Reference Content")
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")
st.markdown("#### Record Ideas")
audio_input = record_voice()
if audio_input:
st.success("Voice recorded!")
return user_input, uploaded_file, audio_input
def display_content_type_selection():
"""Display the content type selection section and return the selected type."""
# Content options in a cleaner layout
st.markdown("### 🔧 Content Configuration")
# Content type selection with better UI
st.markdown("#### Select Content Type")
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", expanded=False):
tabs = st.tabs(["Content Characteristics", "Content & 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 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()
# Get content type selection
content_type, selected_content_type = display_content_type_selection()
# Display advanced options and get configurations
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 Professional 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")

View File

@@ -0,0 +1,382 @@
import re
import os
import json
from loguru import logger
import PyPDF2
import streamlit as st
import tiktoken
import openai
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
from lib.ai_writers.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
# 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
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}")
# Ensure all blog parameters are properly passed
# This is important as the UI may have settings that aren't in the default blog_params
short_blog = write_blog_from_keywords(
user_input,
search_params=search_params,
blog_params=blog_params
)
st.markdown(short_blog)
return True
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
)
st.success(f"Successfully generated long-form content for: {user_input}")
return True
else:
st.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."""
with st.expander("Processing PDF Document", expanded=True):
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
"""
with st.spinner("Crafting your blog content... Please wait."):
if input_type == "keywords":
return process_keywords_input(user_input, search_params, blog_params, selected_content_type)
elif input_type == "youtube_url" or input_type == "audio_file":
return process_youtube_or_audio(user_input)
elif input_type == "web_url":
return process_web_url(user_input)
elif input_type == "image_file":
return process_image_input(user_input, uploaded_file)
elif input_type == "PDF_file":
return process_pdf_input(uploaded_file)
else:
st.error(f"Unsupported input type: {input_type}")
return False

View File

@@ -0,0 +1,252 @@
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)