Blog writer enhancements & fixes
7
.gitignore
vendored
@@ -10,6 +10,13 @@
|
|||||||
pycache
|
pycache
|
||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
|
gemini-native-image*
|
||||||
|
*.mp4
|
||||||
|
*.mp3
|
||||||
|
*.wav
|
||||||
|
*.avi
|
||||||
|
*.mov
|
||||||
|
*.flv
|
||||||
*.pyo
|
*.pyo
|
||||||
*.pyd
|
*.pyd
|
||||||
*.pyw
|
*.pyw
|
||||||
|
|||||||
635
lib/ai_writers/ai_blog_writer/ai_blog_generator.py
Normal 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")
|
||||||
382
lib/ai_writers/ai_blog_writer/ai_blog_generator_utils.py
Normal 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
|
||||||
252
lib/ai_writers/ai_blog_writer/blog_writer_styles.py
Normal 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)
|
||||||
273
lib/ai_writers/ai_writer_dashboard.py
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
import streamlit as st
|
||||||
|
from lib.utils.alwrity_utils import (essay_writer, ai_news_writer, ai_finance_ta_writer)
|
||||||
|
|
||||||
|
from lib.ai_writers.ai_story_writer.story_writer import story_input_section
|
||||||
|
from lib.ai_writers.ai_product_description_writer import write_ai_prod_desc
|
||||||
|
from lib.ai_writers.ai_copywriter.copywriter_dashboard import copywriter_dashboard
|
||||||
|
from lib.ai_writers.linkedin_writer import LinkedInAIWriter
|
||||||
|
#from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner
|
||||||
|
from lib.ai_writers.ai_blog_writer.ai_blog_generator import ai_blog_writer_page
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
def list_ai_writers():
|
||||||
|
"""Return a list of available AI writers with their metadata (no UI rendering)."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "AI Blog Writer",
|
||||||
|
"icon": "📝",
|
||||||
|
"description": "Generate comprehensive blog posts from keywords, URLs, or uploaded content",
|
||||||
|
"category": "Content Creation",
|
||||||
|
"function": ai_blog_writer_page,
|
||||||
|
"path": "ai_blog_writer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Story Writer",
|
||||||
|
"icon": "📚",
|
||||||
|
"description": "Create engaging stories and narratives with AI assistance",
|
||||||
|
"category": "Creative Writing",
|
||||||
|
"function": story_input_section,
|
||||||
|
"path": "story_writer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Essay writer",
|
||||||
|
"icon": "✍️",
|
||||||
|
"description": "Generate well-structured essays on any topic",
|
||||||
|
"category": "Academic",
|
||||||
|
"function": essay_writer,
|
||||||
|
"path": "essay_writer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Write News reports",
|
||||||
|
"icon": "📰",
|
||||||
|
"description": "Create professional news articles and reports",
|
||||||
|
"category": "Journalism",
|
||||||
|
"function": ai_news_writer,
|
||||||
|
"path": "news_writer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Write Financial TA report",
|
||||||
|
"icon": "📊",
|
||||||
|
"description": "Generate technical analysis reports for financial markets",
|
||||||
|
"category": "Finance",
|
||||||
|
"function": ai_finance_ta_writer,
|
||||||
|
"path": "financial_writer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AI Product Description Writer",
|
||||||
|
"icon": "🛍️",
|
||||||
|
"description": "Create compelling product descriptions that drive sales",
|
||||||
|
"category": "E-commerce",
|
||||||
|
"function": write_ai_prod_desc,
|
||||||
|
"path": "product_writer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AI Copywriter",
|
||||||
|
"icon": "✒️",
|
||||||
|
"description": "Generate persuasive copy for marketing and advertising",
|
||||||
|
"category": "Marketing",
|
||||||
|
"function": copywriter_dashboard,
|
||||||
|
"path": "copywriter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LinkedIn AI Writer",
|
||||||
|
"icon": "💼",
|
||||||
|
"description": "Create professional LinkedIn content that engages your network",
|
||||||
|
"category": "Professional",
|
||||||
|
"function": lambda: LinkedInAIWriter().run(),
|
||||||
|
"path": "linkedin_writer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_ai_writers():
|
||||||
|
"""Render the AI Writers dashboard UI with a professional, clickable card layout."""
|
||||||
|
logger.info("Initializing AI Writers Dashboard")
|
||||||
|
writers = list_ai_writers()
|
||||||
|
logger.info(f"Found {len(writers)} AI writers")
|
||||||
|
|
||||||
|
# Add custom CSS for a professional dashboard with VIBRANT clickable cards
|
||||||
|
st.markdown("""
|
||||||
|
<style>
|
||||||
|
/* Base UI improvements */
|
||||||
|
body, .main .block-container {
|
||||||
|
background: linear-gradient(135deg, #f0f4f8 0%, #e6eef7 100%) !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-family: 'Helvetica Neue', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main layout improvements */
|
||||||
|
.main .block-container {
|
||||||
|
padding: 1.5rem 2rem 2rem 2rem !important;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard header */
|
||||||
|
.dashboard-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
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.07);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.dashboard-header h1 {
|
||||||
|
font-size: 2.6em;
|
||||||
|
font-family: 'Helvetica Neue', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1976d2;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.dashboard-header p {
|
||||||
|
font-size: 1.15em;
|
||||||
|
color: #546e7a;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling st.button to look like a clickable card - PREMIUM VIBRANT */
|
||||||
|
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button {
|
||||||
|
/* Vivid Gradient Background - More saturated blue-purple */
|
||||||
|
background: linear-gradient(135deg, #8a2be2 0%, #4169e1 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
padding: 1.8rem 1.5rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: 'Helvetica Neue', sans-serif;
|
||||||
|
transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1); /* Smoother, more premium transition */
|
||||||
|
box-shadow: 0 8px 20px rgba(77, 5, 232, 0.25), 0 2px 6px rgba(0, 0, 0, 0.15); /* Layered shadow for depth */
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 190px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translateY(0); /* Starting position for hover animation */
|
||||||
|
position: relative; /* For pseudo-element effects */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle shine effect on cards */
|
||||||
|
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -50%;
|
||||||
|
width: 150%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||||
|
transform: skewX(-20deg);
|
||||||
|
transition: 0.5s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dynamic hover effects with gradient shift */
|
||||||
|
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button:hover {
|
||||||
|
background: linear-gradient(135deg, #4169e1 0%, #8a2be2 100%); /* Reverse gradient on hover */
|
||||||
|
transform: translateY(-8px) scale(1.05); /* More dramatic lift */
|
||||||
|
box-shadow: 0 15px 30px rgba(77, 5, 232, 0.4), 0 5px 15px rgba(0, 0, 0, 0.2); /* Deeper shadow */
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animate shine on hover */
|
||||||
|
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button:hover::after {
|
||||||
|
left: 100%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button:hover > div > p strong {
|
||||||
|
color: #ffffff; /* Bright white on hover */
|
||||||
|
text-shadow: 0 0 15px rgba(255,255,255,0.5); /* Glow effect on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Target the paragraph generated by Streamlit inside the button */
|
||||||
|
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button > div > p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: rgba(255, 255, 255, 0.9); /* Slightly dimmed base text */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon (first line/element) - MORE PROMINENT */
|
||||||
|
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button > div > p::first-line {
|
||||||
|
font-size: 2.3em; /* Larger icon */
|
||||||
|
line-height: 1.2;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 0 0 10px rgba(255,255,255,0.4); /* Light glow effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title (strong tag from markdown) - ENHANCED CONTRAST */
|
||||||
|
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button > div > p strong {
|
||||||
|
font-size: 1.4em; /* Larger title */
|
||||||
|
font-weight: 700; /* Bolder */
|
||||||
|
color: #ffffff;
|
||||||
|
display: block;
|
||||||
|
margin: 0.7rem 0;
|
||||||
|
text-shadow: 1px 1px 4px rgba(0,0,0,0.3); /* Stronger shadow for better contrast */
|
||||||
|
letter-spacing: 0.5px; /* Slight letter spacing for premium feel */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Description - IMPROVED CONTRAST */
|
||||||
|
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button > div > p {
|
||||||
|
font-size: 1rem; /* Slightly larger for readability */
|
||||||
|
color: rgba(255, 255, 255, 0.95); /* Better contrast */
|
||||||
|
text-shadow: 0 1px 2px rgba(0,0,0,0.1); /* Subtle shadow for text */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Column adjustments for consistent card height */
|
||||||
|
[data-testid="column"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem; /* Consistent gap */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide Streamlit default title */
|
||||||
|
.stApp > header {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# Dashboard header
|
||||||
|
st.markdown("""
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<h1>🚀 AI Content Creation Suite</h1>
|
||||||
|
<p>Welcome! Select the perfect AI writer tool from the options below to start creating amazing content.</p>
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# Create columns for the grid layout
|
||||||
|
cols = st.columns(3)
|
||||||
|
|
||||||
|
# Render buttons styled as cards for each writer
|
||||||
|
for idx, writer in enumerate(writers):
|
||||||
|
with cols[idx % 3]:
|
||||||
|
# Prepare the button label using simple Markdown with newlines
|
||||||
|
button_label = f"{writer['icon']}\n**{writer['name']}**\n{writer['description']}"
|
||||||
|
|
||||||
|
if st.button(
|
||||||
|
button_label,
|
||||||
|
key=f"writer_{writer['path']}",
|
||||||
|
help=f"Click to use the {writer['name']}", # More specific help text
|
||||||
|
use_container_width=True,
|
||||||
|
):
|
||||||
|
logger.info(f"Selected writer: {writer['name']} with path: {writer['path']}")
|
||||||
|
st.session_state.selected_writer = writer
|
||||||
|
st.query_params["writer"] = writer['path']
|
||||||
|
logger.info(f"Updated query params with writer: {writer['path']}")
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
logger.info("Finished rendering AI Writers Dashboard")
|
||||||
|
# Return writers list, though it's not strictly needed if only rendering UI
|
||||||
|
return writers
|
||||||
|
|
||||||
|
# Remove the old ai_writers function since it's now integrated into get_ai_writers
|
||||||
@@ -13,26 +13,114 @@ logger.add(sys.stdout,
|
|||||||
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
|
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||||
|
|
||||||
|
|
||||||
def write_blog_google_serp(search_keyword, search_results):
|
def write_blog_google_serp(keywords, search_results, blog_params=None):
|
||||||
"""Combine the given online research and GPT blog content"""
|
"""
|
||||||
prompt = f"""
|
Write a blog post using search results from Google SERP.
|
||||||
As expert Creative Content writer,
|
|
||||||
I want you to write highly detailed blog post, that explores {search_keyword} and also include 5 FAQs.
|
|
||||||
|
|
||||||
I want the post to offer unique insights, relatable examples, and a fresh perspective on the topic.
|
|
||||||
Here are some Google search results to spark your creativity on {search_keyword}:
|
|
||||||
\n\n
|
|
||||||
\"\"\"{search_results}\"\"\"
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.info("Generating blog and FAQs from Google web search results.")
|
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:
|
try:
|
||||||
response = llm_text_gen(prompt)
|
# 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
|
return response
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"Exit: Failed to get response from LLM: {err}")
|
logger.error(f"Error generating blog from search results: {err}")
|
||||||
exit(1)
|
raise
|
||||||
|
|
||||||
|
|
||||||
def improve_blog_intro(blog_content, blog_intro):
|
def improve_blog_intro(blog_content, blog_intro):
|
||||||
|
|||||||
@@ -53,216 +53,177 @@ def generate_with_retry(prompt, system_prompt=None):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def long_form_generator(content_keywords):
|
def long_form_generator(keywords, search_params=None, blog_params=None):
|
||||||
"""
|
"""
|
||||||
Write long form content using prompt chaining and iterative generation.
|
Generate a long-form blog post based on the given keywords
|
||||||
|
|
||||||
Parameters:
|
Args:
|
||||||
content_keywords (str): The main keywords or topic for the long-form content.
|
keywords (str): Topic or keywords for the blog post
|
||||||
|
search_params (dict, optional): Search parameters for research
|
||||||
|
blog_params (dict, optional): Blog content characteristics
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Initialize default parameters if not provided
|
||||||
|
if blog_params is None:
|
||||||
|
blog_params = {
|
||||||
|
"blog_length": 3000, # Default longer for long-form content
|
||||||
|
"blog_tone": "Professional",
|
||||||
|
"blog_demographic": "Professional",
|
||||||
|
"blog_type": "Informational",
|
||||||
|
"blog_language": "English"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Ensure we have a higher word count for long-form content
|
||||||
|
if blog_params.get("blog_length", 0) < 2500:
|
||||||
|
blog_params["blog_length"] = max(3000, blog_params.get("blog_length", 0))
|
||||||
|
|
||||||
|
# Extract parameters with defaults
|
||||||
|
blog_length = blog_params.get("blog_length", 3000)
|
||||||
|
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")
|
||||||
|
|
||||||
|
st.subheader(f"Long-form {blog_type} Blog ({blog_length}+ words)")
|
||||||
|
|
||||||
|
with st.status("Generating comprehensive long-form content...", expanded=True) as status:
|
||||||
|
# Step 1: Generate outline
|
||||||
|
status.update(label="Creating detailed content outline...")
|
||||||
|
|
||||||
|
# Use a customized prompt based on the blog parameters
|
||||||
|
outline_prompt = f"""
|
||||||
|
As an expert content strategist writing in a {blog_tone} tone for {blog_demographic} audience,
|
||||||
|
create a detailed outline for a comprehensive {blog_type} blog post about "{keywords}"
|
||||||
|
that will be approximately {blog_length} words in {blog_language}.
|
||||||
|
|
||||||
|
The outline should include:
|
||||||
|
1. An engaging headline
|
||||||
|
2. 5-7 main sections with descriptive headings
|
||||||
|
3. 2-3 subsections under each main section
|
||||||
|
4. Key points to cover in each section
|
||||||
|
5. Ideas for relevant examples or case studies
|
||||||
|
6. Suggestions for data points or statistics to include
|
||||||
|
|
||||||
|
Format the outline in markdown with proper headings and bullet points.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
outline = llm_text_gen(outline_prompt)
|
||||||
|
st.markdown("### Content Outline")
|
||||||
|
st.markdown(outline)
|
||||||
|
status.update(label="Outline created successfully ✓")
|
||||||
|
|
||||||
|
# Step 2: Research the topic using the search parameters
|
||||||
|
status.update(label="Researching topic details...")
|
||||||
|
research_results = research_topic(keywords, search_params)
|
||||||
|
status.update(label="Research completed ✓")
|
||||||
|
|
||||||
|
# Step 3: Generate the full content
|
||||||
|
status.update(label=f"Writing {blog_length}+ word {blog_tone} {blog_type} content...")
|
||||||
|
|
||||||
|
full_content_prompt = f"""
|
||||||
|
You are a professional content writer who specializes in {blog_type} content with a {blog_tone} tone
|
||||||
|
for {blog_demographic} audiences. Write a comprehensive, in-depth blog post in {blog_language} about:
|
||||||
|
|
||||||
|
"{keywords}"
|
||||||
|
|
||||||
|
Use this outline as your structure:
|
||||||
|
{outline}
|
||||||
|
|
||||||
|
And incorporate these research findings where relevant:
|
||||||
|
{research_results}
|
||||||
|
|
||||||
|
The blog post should:
|
||||||
|
- Be approximately {blog_length} words
|
||||||
|
- Include an engaging introduction and strong conclusion
|
||||||
|
- Use appropriate subheadings for all sections in the outline
|
||||||
|
- Include examples, data points, and actionable insights
|
||||||
|
- Be formatted in markdown with proper headings, bullet points, and emphasis
|
||||||
|
- Maintain a {blog_tone} tone throughout
|
||||||
|
- Address the needs and interests of a {blog_demographic} audience
|
||||||
|
|
||||||
|
Do not include phrases like "according to research" or "based on the outline" in your content.
|
||||||
|
"""
|
||||||
|
|
||||||
|
full_content = llm_text_gen(full_content_prompt)
|
||||||
|
status.update(label="Long-form content generated successfully! ✓", state="complete")
|
||||||
|
|
||||||
|
# Display the full content
|
||||||
|
st.markdown("### Your Complete Long-form Blog Post")
|
||||||
|
st.markdown(full_content)
|
||||||
|
|
||||||
|
return full_content
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
status.update(label=f"Error generating long-form content: {str(e)}", state="error")
|
||||||
|
st.error(f"Failed to generate long-form content: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def research_topic(keywords, search_params=None):
|
||||||
|
"""
|
||||||
|
Research a topic using search parameters and return a summary
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keywords (str): Topic to research
|
||||||
|
search_params (dict, optional): Search parameters
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The generated long-form content.
|
str: Research summary
|
||||||
"""
|
"""
|
||||||
with st.status("Start Writing Long Form Article, Hold my Beer..", expanded=True) as status:
|
# Display a placeholder for research results
|
||||||
# Read the main_config to define tone, character, personality of the content to be generated.
|
placeholder = st.empty()
|
||||||
try:
|
placeholder.info("Researching topic... Please wait.")
|
||||||
status.update(label=f"Starting to write content on {content_keywords}.")
|
|
||||||
logger.info(f"Starting to write content on {content_keywords}.")
|
|
||||||
# Define persona and writing guidelines
|
|
||||||
content_tone, target_audience, content_type, content_language, output_format, content_length = read_return_config_section('blog_characteristics')
|
|
||||||
except Exception as err:
|
|
||||||
logger.error(f"Failed to Read config params from main_config: {err}")
|
|
||||||
st.error(f"Failed to Read config params from main_config: {err}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filepath = os.path.join(os.environ["PROMPTS_DIR"], "long_form_ai_writer.prompts")
|
from .keywords_to_blog_streamlit import do_tavily_ai_search
|
||||||
status.update(label=f"Reading Prompts from {filepath}.")
|
|
||||||
# Check if file exists
|
# Use provided search params or defaults
|
||||||
if not os.path.exists(filepath):
|
if search_params is None:
|
||||||
raise FileNotFoundError(f"File {filepath} does not exist")
|
search_params = {
|
||||||
with open(filepath, 'r') as file:
|
"max_results": 10,
|
||||||
prompts = yaml.safe_load(file)
|
"search_depth": "advanced",
|
||||||
except Exception as err:
|
"time_range": "year"
|
||||||
st.error(f"Exit: Failed to read prompts from {filepath}: {err}")
|
}
|
||||||
logger.error(f"Exit: Failed to read prompts from {filepath}: {err}")
|
|
||||||
exit(1)
|
# Conduct research using Tavily
|
||||||
|
tavily_results = do_tavily_ai_search(
|
||||||
writing_guidelines = prompts.get('writing_guidelines').format(
|
keywords,
|
||||||
content_language=content_language,
|
max_results=search_params.get("max_results", 10),
|
||||||
content_tone=content_tone,
|
search_depth=search_params.get("search_depth", "advanced"),
|
||||||
content_type=content_type,
|
include_domains=search_params.get("include_domains", []),
|
||||||
output_format=output_format,
|
time_range=search_params.get("time_range", "year")
|
||||||
content_keywords=content_keywords,
|
|
||||||
target_audience=target_audience
|
|
||||||
)
|
|
||||||
|
|
||||||
content_title = prompts.get('content_title').format(
|
|
||||||
content_language=content_language,
|
|
||||||
content_keywords=content_keywords,
|
|
||||||
target_audience=target_audience
|
|
||||||
)
|
)
|
||||||
|
|
||||||
content_outline = prompts.get('content_outline').format(
|
# Extract research data
|
||||||
content_language=content_language,
|
research_data = ""
|
||||||
content_title='{content_title}',
|
if tavily_results and len(tavily_results) == 3:
|
||||||
content_type=content_type,
|
results, titles, answer = tavily_results
|
||||||
target_audience=target_audience
|
|
||||||
)
|
if answer and len(answer) > 50:
|
||||||
|
research_data += f"Summary: {answer}\n\n"
|
||||||
|
|
||||||
|
if results and 'results' in results and len(results['results']) > 0:
|
||||||
|
research_data += "Key Sources:\n"
|
||||||
|
for i, result in enumerate(results['results'][:7], 1):
|
||||||
|
title = result.get('title', 'Untitled Source')
|
||||||
|
content_snippet = result.get('content', '')[:300] + "..."
|
||||||
|
research_data += f"{i}. {title}\n{content_snippet}\n\n"
|
||||||
|
|
||||||
starting_prompt = prompts.get('starting_prompt').format(
|
# If research data is empty or too short, provide a generic response
|
||||||
content_language=content_language,
|
if not research_data or len(research_data) < 100:
|
||||||
content_title='{content_title}',
|
research_data = f"No specific research data found for '{keywords}'. Please provide more specific information in your content."
|
||||||
content_outline='{content_outline}',
|
|
||||||
writing_guidelines=writing_guidelines
|
|
||||||
)
|
|
||||||
|
|
||||||
continuation_prompt = prompts.get('continuation_prompt').format(
|
placeholder.success("Research completed successfully!")
|
||||||
content_language=content_language,
|
return research_data
|
||||||
content_title='{content_title}',
|
|
||||||
content_outline='{content_outline}',
|
|
||||||
content_text='{content_text}',
|
|
||||||
web_research_result='{web_research_result}',
|
|
||||||
writing_guidelines=writing_guidelines
|
|
||||||
)
|
|
||||||
|
|
||||||
# Do SERP web research for given keywords to generate title and outline.
|
|
||||||
web_research_result, g_titles = do_google_serp_search(content_keywords)
|
|
||||||
|
|
||||||
# Generate prompts
|
|
||||||
try:
|
|
||||||
content_title = generate_with_retry(content_title.format(web_research_result=web_research_result))
|
|
||||||
logger.info(f"The title of the content is: {content_title}")
|
|
||||||
status.update(label=f"The title of the content is: {content_title}")
|
|
||||||
except Exception as err:
|
|
||||||
logger.error(f"Content title Generation Error: {err}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
except Exception as e:
|
||||||
content_outline = generate_with_retry(content_outline.format(
|
placeholder.error(f"Research failed: {str(e)}")
|
||||||
content_title=content_title,
|
return f"Unable to gather research for '{keywords}'. Please continue with the content based on your knowledge."
|
||||||
web_research_result=web_research_result))
|
finally:
|
||||||
logger.info(f"The content Outline is: {content_outline}\n\n")
|
# Remove the placeholder after a short delay
|
||||||
status.update(label=f"Completed with Content Outline.")
|
import time
|
||||||
except Exception as err:
|
time.sleep(1)
|
||||||
logger.error(f"Failed to generate content outline: {err}")
|
placeholder.empty()
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
status.update(label=f"Do web research with Tavily to provide context for content creation.")
|
|
||||||
logger.info("Do web research with Tavily to provide context for content creation.")
|
|
||||||
# Do Metaphor/Exa AI search.
|
|
||||||
table_data = []
|
|
||||||
web_research_result, m_titles, t_titles = do_tavily_ai_search(content_keywords, max_results=5)
|
|
||||||
for item in web_research_result.get("results"):
|
|
||||||
title = item.get("title", "")
|
|
||||||
snippet = item.get("content", "")
|
|
||||||
table_data.append([title, snippet])
|
|
||||||
web_research_result = table_data
|
|
||||||
except Exception as err:
|
|
||||||
logger.error(f"Failed to do Tavily AI search: {err}")
|
|
||||||
st.error(f"Failed to do Tavily AI search: {err}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
starting_draft = generate_with_retry(starting_prompt.format(
|
|
||||||
content_title=content_title,
|
|
||||||
content_outline=content_outline,
|
|
||||||
web_research_result=web_research_result,
|
|
||||||
writing_guidelines=writing_guidelines))
|
|
||||||
except Exception as err:
|
|
||||||
st.error(f"Failed to Generate Starting draft: {err}")
|
|
||||||
logger.error(f"Failed to Generate Starting draft: {err}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info(f"Starting to write on the outline introduction.")
|
|
||||||
draft = starting_draft
|
|
||||||
continuation = generate_with_retry(continuation_prompt.format(
|
|
||||||
content_title=content_title,
|
|
||||||
content_outline=content_outline,
|
|
||||||
content_text=draft,
|
|
||||||
web_research_result=web_research_result,
|
|
||||||
writing_guidelines=writing_guidelines))
|
|
||||||
except Exception as err:
|
|
||||||
logger.error(f"Failed to write the initial draft: {err}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Add the continuation to the initial draft, keep building the story until we see 'IAMDONE'
|
|
||||||
try:
|
|
||||||
draft += '\n\n' + continuation
|
|
||||||
except Exception as err:
|
|
||||||
logger.error(f"Failed as: {err} and {continuation}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.info(f"Writing in progress... Current draft length: {len(draft)} characters")
|
|
||||||
status.update(label=f"Writing in progress... Current draft length: {len(draft)} characters")
|
|
||||||
search_terms = f"""
|
|
||||||
I will provide you with content outline below, your task is to read the outline & return 8 google search keywords.
|
|
||||||
Your response will be used to do web research for writing on the given outline.
|
|
||||||
Do not explain your response, provide 8 google search sentences encompassing the given content outline.
|
|
||||||
Important: Provide the search term results as comma separated values.\n\n
|
|
||||||
Content Outline:\n
|
|
||||||
'{content_outline}'
|
|
||||||
"""
|
|
||||||
search_words = generate_with_retry(search_terms)
|
|
||||||
status.update(label=f"Search terms from written draft: {search_words}")
|
|
||||||
|
|
||||||
while 'IAMDONE' not in continuation:
|
|
||||||
#web_research_result, m_titles = do_metaphor_ai_research(content_keywords)
|
|
||||||
str_list = re.split(r',\s*', search_words)
|
|
||||||
# Strip quotes from each element
|
|
||||||
str_list = [s.strip('\'"') for s in str_list]
|
|
||||||
|
|
||||||
# for search_term in str_list:
|
|
||||||
# web_research_result, m_titles, t_titles = do_tavily_ai_search(search_term, max_results=5)
|
|
||||||
# status.update(label=f"Search terms from written draft: {search_term}")
|
|
||||||
# for item in web_research_result.get("results"):
|
|
||||||
# title = item.get("title", "")
|
|
||||||
# snippet = item.get("content", "")
|
|
||||||
# table_data.append([title, snippet])
|
|
||||||
# web_research_result = table_data
|
|
||||||
|
|
||||||
try:
|
|
||||||
continuation = generate_with_retry(continuation_prompt.format(
|
|
||||||
content_title=content_title,
|
|
||||||
content_outline=content_outline,
|
|
||||||
content_text=draft,
|
|
||||||
web_research_result=web_research_result,
|
|
||||||
writing_guidelines=writing_guidelines))
|
|
||||||
|
|
||||||
draft += '\n\n' + continuation
|
|
||||||
logger.info(f"Writing in progress... Current draft length: {len(draft)} characters")
|
|
||||||
status.update(label=f"Writing in progress... Current draft length: {len(draft)} characters")
|
|
||||||
# At this point, the context is little stale. We should more web research on
|
|
||||||
# related queries as per the content outline, to augment the LLM context.
|
|
||||||
except Exception as err:
|
|
||||||
st.error(f"Failed to continually write long-form content: {err}")
|
|
||||||
logger.error(f"Failed to continually write the Essay: {err}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Remove 'IAMDONE' and print the final story
|
|
||||||
final = draft.replace('IAMDONE', '').strip()
|
|
||||||
status.update(label="Success: Finished writing Long form content.")
|
|
||||||
|
|
||||||
# # In long content sending the whole content for each content metadata is expensive.
|
|
||||||
# # https://ai.google.dev/gemini-api/docs/caching?lang=python
|
|
||||||
# #blog_title, blog_meta_desc, blog_tags, blog_categories = get_blog_metadata_longform(final)
|
|
||||||
# blog_categories = get_blog_metadata_longform(final)
|
|
||||||
# print("\n\n-----{blog_categories}------\n\n")
|
|
||||||
#
|
|
||||||
# status.update(label="Success: Finished with Title, Meta Description, Tags, categories")
|
|
||||||
# generated_image_filepath = None
|
|
||||||
# # TBD: Save the blog content as a .md file. Markdown or HTML ?
|
|
||||||
# save_blog_to_file(final, blog_title, blog_meta_desc, blog_tags, blog_categories, generated_image_filepath)
|
|
||||||
|
|
||||||
logger.info(f"\n{final}\n\n")
|
|
||||||
|
|
||||||
logger.info(f"\n\n ################ Finished writing Blog for : {content_keywords} #################### \n")
|
|
||||||
with st.expander("**Click to View the final content draft:**"):
|
|
||||||
st.markdown(f"\n{final}\n\n")
|
|
||||||
|
|
||||||
return final
|
|
||||||
|
|
||||||
|
|
||||||
def generate_long_form_content(content_keywords):
|
def generate_long_form_content(content_keywords):
|
||||||
|
|||||||
@@ -2,9 +2,116 @@ import streamlit as st
|
|||||||
import streamlit.components.v1 as components
|
import streamlit.components.v1 as components
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
import json
|
import json
|
||||||
|
import base64
|
||||||
|
|
||||||
from .tweet_generator import smart_tweet_generator
|
from .tweet_generator import smart_tweet_generator
|
||||||
|
|
||||||
|
def add_bg_from_base64(base64_string):
|
||||||
|
"""Add background image using base64 string."""
|
||||||
|
return f'''
|
||||||
|
<style>
|
||||||
|
.stApp {{
|
||||||
|
background-image: url("data:image/png;base64,{base64_string}");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Enhanced styling for containers */
|
||||||
|
.element-container, .stMarkdown, .stButton {{
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 10px 0;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Typography enhancements */
|
||||||
|
h1, h2, h3 {{
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-family: 'Helvetica Neue', sans-serif;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
}}
|
||||||
|
|
||||||
|
p, li {{
|
||||||
|
color: #e0e0e0 !important;
|
||||||
|
font-family: 'Helvetica Neue', sans-serif;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Button styling */
|
||||||
|
.stButton > button {{
|
||||||
|
background: linear-gradient(45deg, #1DA1F2, #0C85D0);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.stButton > button:hover {{
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Tab styling */
|
||||||
|
.stTabs [data-baseweb="tab-list"] {{
|
||||||
|
gap: 8px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.stTabs [data-baseweb="tab"] {{
|
||||||
|
background-color: transparent;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 5px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.stTabs [data-baseweb="tab"]:hover {{
|
||||||
|
background-color: rgba(29, 161, 242, 0.2);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Feature card styling */
|
||||||
|
.feature-card {{
|
||||||
|
background: linear-gradient(135deg, rgba(29, 161, 242, 0.1), rgba(0, 0, 0, 0.3));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.feature-card:hover {{
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Status badge styling */
|
||||||
|
.status-badge {{
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.status-active {{
|
||||||
|
background: linear-gradient(45deg, #00C853, #69F0AE);
|
||||||
|
color: #000000;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.status-coming-soon {{
|
||||||
|
background: linear-gradient(45deg, #FFD700, #FFA000);
|
||||||
|
color: #000000;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
'''
|
||||||
|
|
||||||
def load_feature_data() -> Dict:
|
def load_feature_data() -> Dict:
|
||||||
"""Load feature data from a structured format."""
|
"""Load feature data from a structured format."""
|
||||||
return {
|
return {
|
||||||
@@ -127,13 +234,13 @@ def load_feature_data() -> Dict:
|
|||||||
|
|
||||||
def render_feature_card(feature: Dict) -> None:
|
def render_feature_card(feature: Dict) -> None:
|
||||||
"""Render a single feature card with its details."""
|
"""Render a single feature card with its details."""
|
||||||
|
status_class = "status-active" if feature['status'] == 'active' else "status-coming-soon"
|
||||||
with st.container():
|
with st.container():
|
||||||
st.markdown(f"""
|
st.markdown(f"""
|
||||||
<div style='padding: 20px; border-radius: 10px; background-color: #f0f2f6; margin-bottom: 20px;'>
|
<div class='feature-card'>
|
||||||
<h3 style='margin: 0;'>{feature['icon']} {feature['name']}</h3>
|
<h3 style='color: #ffffff; margin: 0;'>{feature['icon']} {feature['name']}</h3>
|
||||||
<p style='margin: 10px 0;'>{feature['description']}</p>
|
<p style='color: #e0e0e0; margin: 10px 0;'>{feature['description']}</p>
|
||||||
<span style='background-color: {'#4CAF50' if feature['status'] == 'active' else '#ffd700'};
|
<span class='status-badge {status_class}'>
|
||||||
padding: 5px 10px; border-radius: 15px; font-size: 0.8em;'>
|
|
||||||
{feature['status'].title()}
|
{feature['status'].title()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,23 +257,43 @@ def render_category_section(category: Dict) -> None:
|
|||||||
with col2:
|
with col2:
|
||||||
render_feature_card(category['features'][1])
|
render_feature_card(category['features'][1])
|
||||||
|
|
||||||
|
def get_space_background() -> str:
|
||||||
|
"""Return base64 encoded space-themed background."""
|
||||||
|
return """iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN8/+F9PQAJYgN4hWvGzQAAAABJRU5ErkJggg==""" # This is a placeholder. You'll need to replace with actual base64 image
|
||||||
|
|
||||||
def run_dashboard():
|
def run_dashboard():
|
||||||
"""Main function to run the Twitter dashboard."""
|
"""Main function to run the Twitter dashboard."""
|
||||||
# Header
|
# Add space-themed background
|
||||||
st.title("🐦 Twitter AI Writer Dashboard")
|
st.markdown(add_bg_from_base64(get_space_background()), unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# Enhanced Header with gradient text
|
||||||
st.markdown("""
|
st.markdown("""
|
||||||
Welcome to your all-in-one Twitter content creation and management platform.
|
<div style='text-align: center; padding: 40px 0;'>
|
||||||
Explore our AI-powered tools to enhance your Twitter marketing strategy.
|
<h1 style='
|
||||||
""")
|
font-size: 3em;
|
||||||
|
background: linear-gradient(45deg, #1DA1F2, #ffffff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
'>🐦 Twitter AI Writer</h1>
|
||||||
|
<p style='
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #e0e0e0;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
'>Your all-in-one Twitter content creation and management platform.
|
||||||
|
Harness the power of AI to enhance your Twitter marketing strategy.</p>
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
# Load feature data
|
# Load feature data
|
||||||
features = load_feature_data()
|
features = load_feature_data()
|
||||||
|
|
||||||
# Create tabs for different sections
|
# Create tabs with enhanced styling
|
||||||
tab1, tab2, tab3 = st.tabs(["🎯 Quick Actions", "📊 Analytics", "⚙️ Settings"])
|
tab1, tab2, tab3 = st.tabs(["🎯 Quick Actions", "📊 Analytics", "⚙️ Settings"])
|
||||||
|
|
||||||
with tab1:
|
with tab1:
|
||||||
st.markdown("### 🚀 Quick Actions")
|
st.markdown("<h2 style='color: #ffffff;'>🚀 Quick Actions</h2>", unsafe_allow_html=True)
|
||||||
col1, col2, col3 = st.columns(3)
|
col1, col2, col3 = st.columns(3)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
@@ -199,11 +326,29 @@ def run_dashboard():
|
|||||||
if st.button(f"🚀 Launch {category['features'][0]['name']}", use_container_width=True):
|
if st.button(f"🚀 Launch {category['features'][0]['name']}", use_container_width=True):
|
||||||
category["features"][0]["function"]()
|
category["features"][0]["function"]()
|
||||||
|
|
||||||
# Footer
|
# Enhanced Footer
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("""
|
st.markdown("""
|
||||||
<div style='text-align: center;'>
|
<div style='text-align: center; padding: 20px; background: rgba(0, 0, 0, 0.5); border-radius: 10px;'>
|
||||||
<p>Need help? Check out our <a href='#'>documentation</a> or <a href='#'>contact support</a></p>
|
<p style='color: #ffffff; margin-bottom: 10px;'>Need assistance? We're here to help!</p>
|
||||||
|
<div style='display: flex; justify-content: center; gap: 20px;'>
|
||||||
|
<a href='#' style='
|
||||||
|
text-decoration: none;
|
||||||
|
color: #1DA1F2;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
'>📚 Documentation</a>
|
||||||
|
<a href='#' style='
|
||||||
|
text-decoration: none;
|
||||||
|
color: #1DA1F2;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
'>💬 Contact Support</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
|||||||
@@ -34,16 +34,53 @@ def llm_text_gen(prompt, system_prompt=None, json_struct=None):
|
|||||||
logger.debug(f"[llm_text_gen] Prompt length: {len(prompt)} characters")
|
logger.debug(f"[llm_text_gen] Prompt length: {len(prompt)} characters")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Read the config param to create system instruction for the LLM.
|
# Set default values for LLM parameters
|
||||||
gpt_provider, model, temperature, max_tokens, top_p, n, fp = read_return_config_section('llm_config')
|
gpt_provider = "google"
|
||||||
blog_tone, blog_demographic, blog_type, blog_language, \
|
model = "gemini-1.5-flash-latest"
|
||||||
blog_output_format, blog_length = read_return_config_section('blog_characteristics')
|
temperature = 0.7
|
||||||
|
max_tokens = 4000
|
||||||
|
top_p = 0.9
|
||||||
|
n = 1
|
||||||
|
fp = 16
|
||||||
|
|
||||||
logger.debug(f"[llm_text_gen] Config loaded successfully - Provider: {gpt_provider}, Model: {model}")
|
# Default blog characteristics
|
||||||
|
blog_tone = "Professional"
|
||||||
|
blog_demographic = "Professional"
|
||||||
|
blog_type = "Informational"
|
||||||
|
blog_language = "English"
|
||||||
|
blog_output_format = "markdown"
|
||||||
|
blog_length = 2000
|
||||||
|
|
||||||
|
# Try to read values from config, but keep defaults if any key is missing
|
||||||
|
try:
|
||||||
|
# Read LLM config
|
||||||
|
llm_config = read_return_config_section('llm_config')
|
||||||
|
if llm_config and len(llm_config) >= 4:
|
||||||
|
gpt_provider = llm_config[0] if llm_config[0] else gpt_provider
|
||||||
|
model = llm_config[1] if llm_config[1] else model
|
||||||
|
temperature = llm_config[2] if llm_config[2] else temperature
|
||||||
|
max_tokens = llm_config[3] if llm_config[3] else max_tokens
|
||||||
|
# Use default values for top_p, n, fp if they're not in the config
|
||||||
|
logger.debug(f"[llm_text_gen] LLM Config loaded: Provider={gpt_provider}, Model={model}, Temp={temperature}")
|
||||||
|
except Exception as err:
|
||||||
|
logger.warning(f"[llm_text_gen] Couldn't load LLM config completely, using defaults where needed: {err}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read blog characteristics
|
||||||
|
blog_chars = read_return_config_section('blog_characteristics')
|
||||||
|
if blog_chars and len(blog_chars) >= 6:
|
||||||
|
blog_tone = blog_chars[0] if blog_chars[0] else blog_tone
|
||||||
|
blog_demographic = blog_chars[1] if blog_chars[1] else blog_demographic
|
||||||
|
blog_type = blog_chars[2] if blog_chars[2] else blog_type
|
||||||
|
blog_language = blog_chars[3] if blog_chars[3] else blog_language
|
||||||
|
blog_output_format = blog_chars[4] if blog_chars[4] else blog_output_format
|
||||||
|
blog_length = blog_chars[5] if blog_chars[5] else blog_length
|
||||||
|
logger.debug(f"[llm_text_gen] Blog characteristics loaded: Tone={blog_tone}, Type={blog_type}")
|
||||||
|
except Exception as err:
|
||||||
|
logger.warning(f"[llm_text_gen] Couldn't load blog characteristics completely, using defaults where needed: {err}")
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"[llm_text_gen] Error reading config params: {err}")
|
logger.warning(f"[llm_text_gen] Using default settings due to config read error: {err}")
|
||||||
raise err
|
|
||||||
|
|
||||||
# Construct the system prompt with the sidebar config params if no custom system_prompt is provided
|
# Construct the system prompt with the sidebar config params if no custom system_prompt is provided
|
||||||
if system_prompt is None:
|
if system_prompt is None:
|
||||||
@@ -110,9 +147,13 @@ def llm_text_gen(prompt, system_prompt=None, json_struct=None):
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"Failed to get response from DeepSeek: {err}")
|
logger.error(f"Failed to get response from DeepSeek: {err}")
|
||||||
raise err
|
raise err
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unknown provider '{gpt_provider}', falling back to Google Gemini")
|
||||||
|
response = gemini_text_response(prompt, temperature, top_p, n, max_tokens, system_instructions)
|
||||||
|
return response
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"Failed to read LLM parameters: {err}")
|
logger.error(f"Failed to generate text: {err}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,272 +1,25 @@
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import PyPDF2
|
import PyPDF2
|
||||||
import tiktoken
|
|
||||||
import openai
|
import openai
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import tempfile
|
import tempfile
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lib.ai_web_researcher.gpt_online_researcher import gpt_web_researcher
|
|
||||||
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.ai_news_article_writer import ai_news_generation
|
from lib.ai_writers.ai_news_article_writer import ai_news_generation
|
||||||
#from lib.ai_writers.ai_agents_crew_writer import ai_agents_writers
|
|
||||||
from lib.ai_writers.ai_financial_writer import write_basic_ta_report
|
from lib.ai_writers.ai_financial_writer import write_basic_ta_report
|
||||||
from lib.ai_writers.ai_facebook_writer.facebook_ai_writer import facebook_main_menu
|
from lib.ai_writers.ai_facebook_writer.facebook_ai_writer import facebook_main_menu
|
||||||
from lib.ai_writers.linkedin_writer.linkedin_ai_writer import linkedin_main_menu
|
from lib.ai_writers.linkedin_writer.linkedin_ai_writer import linkedin_main_menu
|
||||||
from lib.ai_writers.twitter_writers.twitter_dashboard import run_dashboard
|
from lib.ai_writers.twitter_writers.twitter_dashboard import run_dashboard
|
||||||
from lib.ai_writers.insta_ai_writer import insta_writer
|
from lib.ai_writers.insta_ai_writer import insta_writer
|
||||||
from lib.ai_writers.youtube_writers.youtube_ai_writer import youtube_main_menu
|
from lib.ai_writers.youtube_writers.youtube_ai_writer import youtube_main_menu
|
||||||
from lib.ai_writers.web_url_ai_writer import blog_from_url
|
|
||||||
from lib.ai_writers.image_ai_writer import blog_from_image
|
|
||||||
from lib.ai_writers.ai_essay_writer import ai_essay_generator
|
from lib.ai_writers.ai_essay_writer import ai_essay_generator
|
||||||
from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||||
from lib.utils.voice_processing import record_voice
|
|
||||||
#from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner
|
#from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner
|
||||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||||
|
|
||||||
|
|
||||||
def is_youtube_link(text):
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def is_web_link(text):
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def process_input(input_text, uploaded_file):
|
|
||||||
if input_text and 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 input_text and is_web_link(input_text):
|
|
||||||
return "web_url"
|
|
||||||
|
|
||||||
elif input_text:
|
|
||||||
return "keywords"
|
|
||||||
|
|
||||||
if uploaded_file is not None:
|
|
||||||
file_details = {"filename": uploaded_file.name, "filetype": uploaded_file.type}
|
|
||||||
st.write(file_details)
|
|
||||||
if uploaded_file.type.startswith("text/"):
|
|
||||||
content = uploaded_file.read().decode("utf-8")
|
|
||||||
st.text(content)
|
|
||||||
|
|
||||||
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.")
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def blog_from_keyword():
|
|
||||||
""" Input blog keywords, research and write a factual blog."""
|
|
||||||
st.header("Blog Content Writer")
|
|
||||||
col1, col2, col3 = st.columns([2, 1.5, 0.5])
|
|
||||||
with col1:
|
|
||||||
user_input = st.text_area('**👇Enter Keywords/Title/YouTube Link/Web URLs**',
|
|
||||||
help='Provide keywords, titles, YouTube links, or web URLs to generate content.',
|
|
||||||
placeholder="""Write Blog From:
|
|
||||||
- Keywords/Blog Title: Provide keywords to web research & write blog.
|
|
||||||
- Attach file: Attach Text, Audio, Video, Image file to blog on.
|
|
||||||
- YouTube Link: Provide a YouTube video link to convert into blog.
|
|
||||||
- Web URLs: Provide web URL to write similar blog on.
|
|
||||||
- Provide Local folder location with your documents to use for content creation.""")
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
uploaded_file = st.file_uploader("**👇Attach files (Audio, Video, Image, Document)**",
|
|
||||||
type=["txt", "pdf", "docx", "jpg", "jpeg", "png", "mp3", "wav", "mp4", "mkv", "avi"],
|
|
||||||
help='Attach files such as audio, video, images, or documents.')
|
|
||||||
with col3:
|
|
||||||
audio_input = record_voice()
|
|
||||||
if audio_input:
|
|
||||||
st.info(audio_input)
|
|
||||||
|
|
||||||
# Validate the provided folder path
|
|
||||||
#st.info("🚨 Currently supported file formats are: PDF, plain text, CSV, Excel, Markdown, PowerPoint, and Word documents.")
|
|
||||||
|
|
||||||
temp_file_path = None
|
|
||||||
if uploaded_file is not None:
|
|
||||||
# Save the uploaded file to a temporary file
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix=uploaded_file.name) as temp_file:
|
|
||||||
temp_file.write(uploaded_file.read())
|
|
||||||
temp_file_path = temp_file.name
|
|
||||||
|
|
||||||
content_type = st.radio("**👇Select content type:**", ["Normal-length content", "Long-form content", "Experimental - AI Agents team"])
|
|
||||||
|
|
||||||
# Add an expandable section for advanced writing options
|
|
||||||
with st.expander("Advanced Writing Options", expanded=False):
|
|
||||||
# Option 1: Select content type
|
|
||||||
content_type = st.radio("**👇 Select content type:**",
|
|
||||||
["Normal-length content", "Long-form content", "Experimental - AI Agents team"])
|
|
||||||
|
|
||||||
# Option 2: Checkbox for 'Create SEO tags' (Checked by default)
|
|
||||||
create_seo_tags = st.checkbox('Create SEO tags', value=True,
|
|
||||||
help='Generate json-ld schema, Twitter, and Facebook tags.')
|
|
||||||
|
|
||||||
# Option 3: Checkbox for 'Generate Social Media content' (Unchecked by default)
|
|
||||||
generate_social_media = st.checkbox('Generate Social Media content', value=False,
|
|
||||||
help="Write Facebook, Instagram posts & tweets for generated blog. Needed for marketing your blogs.")
|
|
||||||
|
|
||||||
# Option 4: Checkbox for 'Do Content Analysis & Critique' (Unchecked by default)
|
|
||||||
content_analysis = st.checkbox('Do Content Analysis & Critique', value=False,
|
|
||||||
help="Blog Proof reading, Critique generated blog. Provide actionable changes & Editing options.")
|
|
||||||
|
|
||||||
# Display a message at the bottom for user guidance
|
|
||||||
st.info("🚨 Make sure to personalize content from the sidebar. Important.")
|
|
||||||
|
|
||||||
if st.button("Write Blog"):
|
|
||||||
# Clear the previous results from the screen
|
|
||||||
st.empty()
|
|
||||||
if user_input == "":
|
|
||||||
user_input = None
|
|
||||||
if not uploaded_file and not user_input and not audio_input:
|
|
||||||
st.error("🤬🤬 Either Enter/Type/Attach, can't read your mind.(yet..)")
|
|
||||||
st.stop()
|
|
||||||
else:
|
|
||||||
input_type = process_input(user_input, uploaded_file)
|
|
||||||
|
|
||||||
if input_type == "keywords":
|
|
||||||
if user_input and len(user_input.split()) >= 2:
|
|
||||||
if content_type == "Normal-length content":
|
|
||||||
try:
|
|
||||||
short_blog = write_blog_from_keywords(user_input)
|
|
||||||
st.markdown(short_blog)
|
|
||||||
except Exception as err:
|
|
||||||
st.error(f"🚫 Failed to write blog on {user_input}, Error: {err}")
|
|
||||||
elif content_type == "Long-form content":
|
|
||||||
try:
|
|
||||||
long_form_generator(user_input)
|
|
||||||
st.success(f"Successfully wrote long-form blog on: {user_input}")
|
|
||||||
except Exception as err:
|
|
||||||
st.error(f"🚫 Failed to write blog on {user_input}, Error: {err}")
|
|
||||||
elif content_type == "Experimental - AI Agents team":
|
|
||||||
try:
|
|
||||||
ai_agents_writers(user_input)
|
|
||||||
st.success(f"Successfully wrote content with AI agents on: {user_input}")
|
|
||||||
except Exception as err:
|
|
||||||
st.error(f"🚫 Failed to Write content with AI agents: {err}")
|
|
||||||
else:
|
|
||||||
st.error('🚫 Blog keywords should be at least two words long. Please try again.')
|
|
||||||
|
|
||||||
elif input_type == "youtube_url" or input_type == "audio_file":
|
|
||||||
if not generate_audio_blog(user_input):
|
|
||||||
st.stop()
|
|
||||||
|
|
||||||
elif input_type == "web_url":
|
|
||||||
blog_from_url(user_input)
|
|
||||||
|
|
||||||
elif input_type == "image_file":
|
|
||||||
blog_from_image(user_input, temp_file_path)
|
|
||||||
|
|
||||||
elif input_type == "PDF_file":
|
|
||||||
pdf_reader = PyPDF2.PdfReader(uploaded_file)
|
|
||||||
text = ""
|
|
||||||
combined_result = ""
|
|
||||||
# Create a placeholder for the progress bar
|
|
||||||
progress_bar = st.progress(0)
|
|
||||||
|
|
||||||
# Loop through each page with a progress bar
|
|
||||||
for page_num, page in enumerate(pdf_reader.pages):
|
|
||||||
text += page.extract_text()
|
|
||||||
# Replace newlines with spaces
|
|
||||||
text = text.replace("\n", " ")
|
|
||||||
# Use regex to add a space between words that are combined
|
|
||||||
text = re.sub(r"(\w)([A-Z])", r"\1 \2", text)
|
|
||||||
|
|
||||||
results = blog_from_pdf(text)
|
|
||||||
# Update the progress bar
|
|
||||||
progress_bar.progress((page_num + 1) / len(pdf_reader.pages))
|
|
||||||
combined_result += str(results[-1])
|
|
||||||
|
|
||||||
# Clear progress bar at the end
|
|
||||||
progress_bar.empty()
|
|
||||||
|
|
||||||
st.markdown(combined_result)
|
|
||||||
|
|
||||||
|
|
||||||
def blog_from_pdf(pdf_text):
|
|
||||||
""" Load in a long PDF and pull the text out. Create a prompt to be used to extract key bits of information.
|
|
||||||
Chunk up our document and process each chunk to pull any answers out. Combine them at the end.
|
|
||||||
This simple approach will then be extended to three more difficult questions.
|
|
||||||
"""
|
|
||||||
# FixME:
|
|
||||||
document = '<document>'
|
|
||||||
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'''
|
|
||||||
|
|
||||||
# Initialise 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:
|
|
||||||
results.append(extract_chunk(chunk, template_prompt))
|
|
||||||
|
|
||||||
#zipped = list(zip(*groups))
|
|
||||||
#zipped = [x for y in zipped for x in y if "Not specified" not in x and "__" not in x]
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# 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"""
|
|
||||||
client = openai.OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
|
|
||||||
prompt = template_prompt.replace('<document>', document)
|
|
||||||
|
|
||||||
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 ai_agents_team():
|
def ai_agents_team():
|
||||||
# Define options for AI Content Teams
|
# Define options for AI Content Teams
|
||||||
st.title("🐲 Your AI Agents Teams")
|
st.title("🐲 Your AI Agents Teams")
|
||||||
@@ -316,9 +69,9 @@ def content_agents():
|
|||||||
if content_keywords and len(content_keywords.split()) >= 2:
|
if content_keywords and len(content_keywords.split()) >= 2:
|
||||||
with st.spinner("Generating Content..."):
|
with st.spinner("Generating Content..."):
|
||||||
try:
|
try:
|
||||||
calendar_content = ai_agents_writers(content_keywords)
|
#calendar_content = ai_agents_writers(content_keywords)
|
||||||
st.success(f"Successfully generated content for: {content_keywords}")
|
st.success(f"🚫 Not implemented yet: {content_keywords}")
|
||||||
st.markdown(calendar_content)
|
#st.markdown(calendar_content)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
st.error(f"🚫 Failed to generate content with AI Agents: {err}")
|
st.error(f"🚫 Failed to generate content with AI Agents: {err}")
|
||||||
else:
|
else:
|
||||||
@@ -474,4 +227,4 @@ def ai_social_writer():
|
|||||||
elif "instagram" in selected_platform:
|
elif "instagram" in selected_platform:
|
||||||
insta_writer()
|
insta_writer()
|
||||||
elif "youtube" in selected_platform:
|
elif "youtube" in selected_platform:
|
||||||
youtube_main_menu()
|
youtube_main_menu()
|
||||||
@@ -1,57 +1,11 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
from lib.utils.alwrity_utils import (
|
|
||||||
blog_from_keyword, ai_agents_team, essay_writer, ai_news_writer,
|
|
||||||
ai_finance_ta_writer
|
|
||||||
)
|
|
||||||
from lib.alwrity_ui.similar_analysis import competitor_analysis
|
from lib.alwrity_ui.similar_analysis import competitor_analysis
|
||||||
from lib.alwrity_ui.keyword_web_researcher import do_web_research
|
from lib.alwrity_ui.keyword_web_researcher import do_web_research
|
||||||
from lib.ai_writers.ai_story_writer.story_writer import story_input_section
|
|
||||||
from lib.ai_writers.ai_product_description_writer import write_ai_prod_desc
|
|
||||||
from lib.ai_writers.ai_copywriter.copywriter_dashboard import copywriter_dashboard
|
|
||||||
from lib.ai_writers.linkedin_writer import LinkedInAIWriter
|
|
||||||
#from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner
|
|
||||||
|
|
||||||
|
|
||||||
def ai_writers():
|
|
||||||
options = [
|
|
||||||
"AI Blog Writer",
|
|
||||||
"Story Writer",
|
|
||||||
"Essay writer",
|
|
||||||
"Write News reports",
|
|
||||||
"Write Financial TA report",
|
|
||||||
"AI Product Description Writer",
|
|
||||||
"AI Copywriter",
|
|
||||||
"LinkedIn AI Writer",
|
|
||||||
"Quit"
|
|
||||||
]
|
|
||||||
choice = st.selectbox("**👇Select a content creation type:**", options, index=0, format_func=lambda x: f"📝 {x}")
|
|
||||||
|
|
||||||
if choice == "AI Blog Writer":
|
|
||||||
blog_from_keyword()
|
|
||||||
elif choice == "Story Writer":
|
|
||||||
story_input_section()
|
|
||||||
elif choice == "Essay writer":
|
|
||||||
essay_writer()
|
|
||||||
elif choice == "Write News reports":
|
|
||||||
ai_news_writer()
|
|
||||||
elif choice == "Write Financial TA report":
|
|
||||||
ai_finance_ta_writer()
|
|
||||||
elif choice == "AI Product Description Writer":
|
|
||||||
write_ai_prod_desc()
|
|
||||||
elif choice == "AI Copywriter":
|
|
||||||
# Initialize the copywriter dashboard
|
|
||||||
copywriter_dashboard()
|
|
||||||
elif choice == "LinkedIn AI Writer":
|
|
||||||
# Initialize the LinkedIn AI Writer
|
|
||||||
linkedin_writer = LinkedInAIWriter()
|
|
||||||
linkedin_writer.run()
|
|
||||||
elif choice == "Quit":
|
|
||||||
st.info("Thank you for using Alwrity. Goodbye!")
|
|
||||||
st.stop()
|
|
||||||
|
|
||||||
|
|
||||||
def content_planning_tools():
|
def content_planning_tools():
|
||||||
# Add custom CSS for compact layout
|
# A custom CSS for compact layout
|
||||||
st.markdown("""
|
st.markdown("""
|
||||||
<style>
|
<style>
|
||||||
/* Reduce top padding of main container */
|
/* Reduce top padding of main container */
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
from lib.utils.file_processor import load_image
|
from lib.utils.file_processor import load_image
|
||||||
from lib.utils.content_generators import content_planning_tools, ai_writers
|
from lib.utils.content_generators import content_planning_tools
|
||||||
from lib.utils.alwrity_utils import ai_social_writer
|
from lib.utils.alwrity_utils import ai_social_writer
|
||||||
from lib.utils.seo_tools import ai_seo_tools
|
from lib.utils.seo_tools import ai_seo_tools
|
||||||
from lib.utils.settings_page import render_settings_page
|
from lib.utils.settings_page import render_settings_page
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
# Import social media writer functions
|
# Import social media writer functions
|
||||||
from lib.ai_writers.ai_facebook_writer.facebook_ai_writer import facebook_main_menu
|
from lib.ai_writers.ai_facebook_writer.facebook_ai_writer import facebook_main_menu
|
||||||
@@ -12,6 +13,7 @@ from lib.ai_writers.linkedin_writer.linkedin_ai_writer import linkedin_main_menu
|
|||||||
from lib.ai_writers.twitter_writers import run_dashboard
|
from lib.ai_writers.twitter_writers import run_dashboard
|
||||||
from lib.ai_writers.insta_ai_writer import insta_writer
|
from lib.ai_writers.insta_ai_writer import insta_writer
|
||||||
from lib.ai_writers.youtube_writers.youtube_ai_writer import youtube_main_menu
|
from lib.ai_writers.youtube_writers.youtube_ai_writer import youtube_main_menu
|
||||||
|
from lib.ai_writers.ai_writer_dashboard import get_ai_writers, list_ai_writers
|
||||||
|
|
||||||
|
|
||||||
def setup_ui():
|
def setup_ui():
|
||||||
@@ -295,22 +297,26 @@ def setup_ui():
|
|||||||
|
|
||||||
def setup_alwrity_ui():
|
def setup_alwrity_ui():
|
||||||
"""Sets up the main navigation in the sidebar."""
|
"""Sets up the main navigation in the sidebar."""
|
||||||
|
logger.info("Setting up ALwrity UI")
|
||||||
|
|
||||||
# Initialize session state for active tab if not exists
|
# Initialize session state for active tab if not exists
|
||||||
if 'active_tab' not in st.session_state:
|
if 'active_tab' not in st.session_state:
|
||||||
st.session_state.active_tab = "Content Planning"
|
st.session_state.active_tab = "Content Planning"
|
||||||
|
logger.info(f"Initialized active_tab to: {st.session_state.active_tab}")
|
||||||
|
|
||||||
# Initialize session state for active sub-tab if not exists
|
# Initialize session state for active sub-tab if not exists
|
||||||
if 'active_sub_tab' not in st.session_state:
|
if 'active_sub_tab' not in st.session_state:
|
||||||
st.session_state.active_sub_tab = None
|
st.session_state.active_sub_tab = None
|
||||||
|
logger.info("Initialized active_sub_tab to None")
|
||||||
|
|
||||||
# Define the navigation items with their icons and functions
|
# Define the navigation items with their icons and functions
|
||||||
nav_items = {
|
nav_items = {
|
||||||
|
"AI Writers": ("📝", get_ai_writers),
|
||||||
"Content Planning": ("📅", content_planning_tools),
|
"Content Planning": ("📅", content_planning_tools),
|
||||||
"AI Writers": ("📝", ai_writers),
|
|
||||||
"Agents Teams": ("🤝", lambda: st.subheader("Agents Teams - Coming Soon!")),
|
|
||||||
"AI SEO Tools": ("🔍", ai_seo_tools),
|
"AI SEO Tools": ("🔍", ai_seo_tools),
|
||||||
"AI Social Tools": ("📱", None), # Set to None as we'll handle this separately
|
"AI Social Tools": ("📱", None), # Set to None as we'll handle this separately
|
||||||
"Ask Alwrity": ("💬", lambda: (
|
"Agents Teams(TBD)": ("🤝", lambda: st.subheader("Agents Teams - Coming Soon!")),
|
||||||
|
"Ask Alwrity(TBD)": ("💬", lambda: (
|
||||||
st.subheader("Chat with your Data, Chat with any Data.. COMING SOON !"),
|
st.subheader("Chat with your Data, Chat with any Data.. COMING SOON !"),
|
||||||
st.markdown("Create a collection by uploading files (PDF, MD, CSV, etc), or crawl a data source (Websites, more sources coming soon."),
|
st.markdown("Create a collection by uploading files (PDF, MD, CSV, etc), or crawl a data source (Websites, more sources coming soon."),
|
||||||
st.markdown("One can ask/chat, summarize and do semantic search over the uploaded data")
|
st.markdown("One can ask/chat, summarize and do semantic search over the uploaded data")
|
||||||
@@ -318,6 +324,8 @@ def setup_alwrity_ui():
|
|||||||
"ALwrity Settings": ("⚙️", render_settings_page)
|
"ALwrity Settings": ("⚙️", render_settings_page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(f"Defined {len(nav_items)} navigation items")
|
||||||
|
|
||||||
# Define sub-menu items for AI Social Tools
|
# Define sub-menu items for AI Social Tools
|
||||||
social_tools_submenu = {
|
social_tools_submenu = {
|
||||||
"Facebook": ("📘", lambda: facebook_main_menu()),
|
"Facebook": ("📘", lambda: facebook_main_menu()),
|
||||||
@@ -326,6 +334,8 @@ def setup_alwrity_ui():
|
|||||||
"Instagram": ("📸", lambda: insta_writer()),
|
"Instagram": ("📸", lambda: insta_writer()),
|
||||||
"YouTube": ("🎥", lambda: youtube_main_menu())
|
"YouTube": ("🎥", lambda: youtube_main_menu())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(f"Defined {len(social_tools_submenu)} social tools submenu items")
|
||||||
|
|
||||||
# Create sidebar navigation
|
# Create sidebar navigation
|
||||||
st.sidebar.markdown("### ALwrity Options")
|
st.sidebar.markdown("### ALwrity Options")
|
||||||
@@ -342,6 +352,7 @@ def setup_alwrity_ui():
|
|||||||
st.session_state.active_tab = name
|
st.session_state.active_tab = name
|
||||||
# Reset sub-tab when main tab changes
|
# Reset sub-tab when main tab changes
|
||||||
st.session_state.active_sub_tab = None
|
st.session_state.active_sub_tab = None
|
||||||
|
logger.info(f"Selected main tab: {name}")
|
||||||
|
|
||||||
# If AI Social Tools is active, show the sub-menu
|
# If AI Social Tools is active, show the sub-menu
|
||||||
if st.session_state.active_tab == "AI Social Tools":
|
if st.session_state.active_tab == "AI Social Tools":
|
||||||
@@ -367,6 +378,7 @@ def setup_alwrity_ui():
|
|||||||
if st.sidebar.button(f"{sub_icon} {sub_name}", key=button_key,
|
if st.sidebar.button(f"{sub_icon} {sub_name}", key=button_key,
|
||||||
help=f"Navigate to {sub_name}", use_container_width=True):
|
help=f"Navigate to {sub_name}", use_container_width=True):
|
||||||
st.session_state.active_sub_tab = sub_name
|
st.session_state.active_sub_tab = sub_name
|
||||||
|
logger.info(f"Selected social tool: {sub_name}")
|
||||||
|
|
||||||
# Close the div
|
# Close the div
|
||||||
st.sidebar.markdown('</div>', unsafe_allow_html=True)
|
st.sidebar.markdown('</div>', unsafe_allow_html=True)
|
||||||
@@ -379,6 +391,7 @@ def setup_alwrity_ui():
|
|||||||
st.session_state.active_tab = name
|
st.session_state.active_tab = name
|
||||||
# Reset sub-tab when main tab changes
|
# Reset sub-tab when main tab changes
|
||||||
st.session_state.active_sub_tab = None
|
st.session_state.active_sub_tab = None
|
||||||
|
logger.info(f"Selected main tab: {name}")
|
||||||
|
|
||||||
st.sidebar.markdown('</div>', unsafe_allow_html=True)
|
st.sidebar.markdown('</div>', unsafe_allow_html=True)
|
||||||
|
|
||||||
@@ -427,13 +440,47 @@ def setup_alwrity_ui():
|
|||||||
# Call the function directly without any title
|
# Call the function directly without any title
|
||||||
social_tools_submenu[st.session_state.active_sub_tab][1]()
|
social_tools_submenu[st.session_state.active_sub_tab][1]()
|
||||||
else:
|
else:
|
||||||
st.markdown("""
|
# Check if we're in the AI Writers section and handle writer selection
|
||||||
<style>
|
if st.session_state.active_tab == "AI Writers":
|
||||||
.main .block-container {
|
# Get the writer parameter from the URL using st.query_params
|
||||||
padding-top: 0.25rem !important;
|
writer = st.query_params.get("writer")
|
||||||
padding-bottom: 0;
|
logger.info(f"Current writer from query params: {writer}")
|
||||||
}
|
|
||||||
</style>
|
if writer:
|
||||||
""", unsafe_allow_html=True)
|
# Get the list of writers without rendering the dashboard
|
||||||
st.title(f"{nav_items[st.session_state.active_tab][0]} {st.session_state.active_tab}")
|
writers = list_ai_writers()
|
||||||
nav_items[st.session_state.active_tab][1]()
|
logger.info(f"Found {len(writers)} writers")
|
||||||
|
|
||||||
|
writer_found = False
|
||||||
|
for w in writers:
|
||||||
|
logger.info(f"Checking writer: {w['name']} with path: {w['path']}")
|
||||||
|
if w["path"] == writer:
|
||||||
|
writer_found = True
|
||||||
|
logger.info(f"Found matching writer: {w['name']}, executing function")
|
||||||
|
# Clear any existing content
|
||||||
|
st.empty()
|
||||||
|
# Execute the writer function
|
||||||
|
w["function"]()
|
||||||
|
break
|
||||||
|
|
||||||
|
if not writer_found:
|
||||||
|
logger.error(f"No writer found with path: {writer}")
|
||||||
|
st.error(f"No writer found with path: {writer}")
|
||||||
|
else:
|
||||||
|
# If no writer selected, show the dashboard
|
||||||
|
logger.info("No writer selected, showing dashboard")
|
||||||
|
get_ai_writers()
|
||||||
|
else:
|
||||||
|
# For all other tabs, show the title
|
||||||
|
st.markdown("""
|
||||||
|
<style>
|
||||||
|
.main .block-container {
|
||||||
|
padding-top: 0.25rem !important;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
st.title(f"{nav_items[st.session_state.active_tab][0]} {st.session_state.active_tab}")
|
||||||
|
nav_items[st.session_state.active_tab][1]()
|
||||||
|
|
||||||
|
logger.info("Finished setting up ALwrity UI")
|
||||||
@@ -16,7 +16,10 @@
|
|||||||
"GPT Provider": "google",
|
"GPT Provider": "google",
|
||||||
"Model": "gemini-1.5-flash-latest",
|
"Model": "gemini-1.5-flash-latest",
|
||||||
"Temperature": 0.7,
|
"Temperature": 0.7,
|
||||||
"Max Tokens": 4000
|
"Max Tokens": 4000,
|
||||||
|
"Top-p": 0.9,
|
||||||
|
"n": 1,
|
||||||
|
"fp": 16
|
||||||
},
|
},
|
||||||
"Search Engine Parameters": {
|
"Search Engine Parameters": {
|
||||||
"Geographic Location": "us",
|
"Geographic Location": "us",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 946 KiB |
|
Before Width: | Height: | Size: 694 KiB |
|
Before Width: | Height: | Size: 879 KiB |
|
Before Width: | Height: | Size: 779 KiB |
|
Before Width: | Height: | Size: 673 KiB |
|
Before Width: | Height: | Size: 591 KiB |
|
Before Width: | Height: | Size: 742 KiB |
|
Before Width: | Height: | Size: 838 KiB |
|
Before Width: | Height: | Size: 771 KiB |
|
Before Width: | Height: | Size: 794 KiB |
|
Before Width: | Height: | Size: 662 KiB |
|
Before Width: | Height: | Size: 831 KiB |
|
Before Width: | Height: | Size: 668 KiB |
|
Before Width: | Height: | Size: 766 KiB |
|
Before Width: | Height: | Size: 880 KiB |
|
Before Width: | Height: | Size: 877 KiB |
|
Before Width: | Height: | Size: 789 KiB |
|
Before Width: | Height: | Size: 692 KiB |
|
Before Width: | Height: | Size: 856 KiB |
|
Before Width: | Height: | Size: 805 KiB |
|
Before Width: | Height: | Size: 824 KiB |
|
Before Width: | Height: | Size: 898 KiB |
|
Before Width: | Height: | Size: 698 KiB |
|
After Width: | Height: | Size: 996 KiB |
|
After Width: | Height: | Size: 893 KiB |
|
After Width: | Height: | Size: 664 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 801 KiB |
@@ -1,3 +0,0 @@
|
|||||||
Alwrity web research reports will be saved in this folder.
|
|
||||||
You can change this by modifying SEARCH_SAVE_FILE environment variable, after running alwrity.py from the command prompt.
|
|
||||||
Better to change in the alwrity.py file itself, line no: 308
|
|
||||||