Resolved merge conflicts in LinkedIn README and Twitter dashboard

This commit is contained in:
ajaysi
2025-04-16 18:32:43 +05:30
14 changed files with 790 additions and 1089 deletions

View File

@@ -333,7 +333,7 @@ To use the LinkedIn AI Writer:
## Requirements
- Python 3.11
- Python 3.8+
- Streamlit
- OpenAI API key (for GPT models)
- Optional: LinkedIn API credentials (for future integration)

View File

@@ -29,10 +29,24 @@ from .modules.article_generator.linkedin_article_generator import linkedin_artic
from .modules.carousel_generator.linkedin_carousel_generator import linkedin_carousel_generator_ui
from .modules.video_script_generator.linkedin_video_script_generator import linkedin_video_script_generator_ui
from .modules.comment_response_generator.linkedin_comment_response_generator_ui import linkedin_comment_response_generator_ui
from .modules.profile_optimizer.linkedin_profile_optimizer_ui import linkedin_profile_optimizer_ui
from .modules.poll_generator import linkedin_poll_generator_ui
from .modules.company_page_generator import linkedin_company_page_generator_ui
# Import image generation
from ...gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
# Create a wrapper for the async profile optimizer UI
def linkedin_profile_optimizer_ui_wrapper():
"""Wrapper function to call the async LinkedIn Profile Optimizer UI."""
import asyncio
asyncio.run(linkedin_profile_optimizer_ui())
# Create a wrapper for the async company page generator UI
def linkedin_company_page_generator_ui_wrapper():
"""Wrapper function to call the async LinkedIn Company Page Generator UI."""
import asyncio
asyncio.run(linkedin_company_page_generator_ui())
def linkedin_main_menu():
"""Main function for the LinkedIn AI Writer."""
@@ -161,8 +175,8 @@ def linkedin_main_menu():
"description": "Enhance LinkedIn profiles to improve visibility and professional appeal.",
"color": "#0A66C2",
"category": "Profile & Personal Branding",
"function": None,
"status": "coming_soon",
"function": linkedin_profile_optimizer_ui_wrapper,
"status": "active",
"features": [
"Headline optimization",
"About section generation",
@@ -182,8 +196,8 @@ def linkedin_main_menu():
"description": "Create engaging polls that drive interaction and gather insights.",
"color": "#0A66C2",
"category": "Profile & Personal Branding",
"function": None,
"status": "coming_soon",
"function": linkedin_poll_generator_ui,
"status": "active",
"features": [
"Question formulation optimization",
"Option generation based on topic",
@@ -205,8 +219,8 @@ def linkedin_main_menu():
"description": "Create content for company pages that builds brand awareness and engagement.",
"color": "#0A66C2",
"category": "Business & Marketing",
"function": None,
"status": "coming_soon",
"function": linkedin_company_page_generator_ui_wrapper,
"status": "active",
"features": [
"Company culture post generation",
"Product/service announcement templates",
@@ -516,7 +530,7 @@ def linkedin_main_menu():
""", unsafe_allow_html=True)
# Add a button to access the tool
if st.button(f"Use {tool['name']}", key=f"btn_{tool['name']}"):
if st.button(f"Use {tool['name']}", key=f"btn_{tool['category']}_{tool['name']}"):
# Store the selected tool in session state
st.session_state.selected_tool = tool
st.rerun()

View File

@@ -31,31 +31,32 @@ async def linkedin_profile_optimizer_ui():
st.info("Upload your profile information for a comprehensive analysis")
# Profile Data Input
with st.expander("Enter Profile Information", expanded=True):
profile_data = {
"headline": st.text_input("Current Headline"),
"about": st.text_area("About Section"),
"industry": st.text_input("Industry"),
"current_role": st.text_input("Current Role"),
"experience": [],
"skills": st.text_area("Current Skills (one per line)").split("\n"),
"education": st.text_area("Education (one per line)").split("\n")
}
# Experience Input
st.subheader("Work Experience")
num_experiences = st.number_input("Number of experiences to add", min_value=0, max_value=10, value=1)
for i in range(num_experiences):
with st.expander(f"Experience {i+1}"):
exp = {
"role": st.text_input(f"Role {i+1}"),
"company": st.text_input(f"Company {i+1}"),
"description": st.text_area(f"Description {i+1}")
}
profile_data["experience"].append(exp)
st.subheader("Enter Profile Information")
profile_data = {
"headline": st.text_input("Current Headline", key="profile_headline"),
"about": st.text_area("About Section", key="profile_about"),
"industry": st.text_input("Industry", key="profile_industry"),
"current_role": st.text_input("Current Role", key="profile_role"),
"experience": [],
"skills": st.text_area("Current Skills (one per line)", key="profile_skills").split("\n"),
"education": st.text_area("Education (one per line)", key="profile_education").split("\n")
}
if st.button("Analyze Profile"):
# Experience Input
st.subheader("Work Experience")
num_experiences = st.number_input("Number of experiences to add", min_value=0, max_value=10, value=1, key="profile_num_exp")
for i in range(num_experiences):
st.markdown(f"**Experience {i+1}**")
exp = {
"role": st.text_input(f"Role {i+1}", key=f"profile_role_{i}"),
"company": st.text_input(f"Company {i+1}", key=f"profile_company_{i}"),
"description": st.text_area(f"Description {i+1}", key=f"profile_desc_{i}")
}
profile_data["experience"].append(exp)
st.divider()
if st.button("Analyze Profile", key="profile_analyze_btn"):
with st.spinner("Analyzing your profile..."):
analysis = await optimizer.analyze_profile_strength(profile_data)
@@ -83,11 +84,11 @@ async def linkedin_profile_optimizer_ui():
st.header("Headline Optimizer")
st.info("Optimize your headline for better visibility and impact")
current_headline = st.text_input("Current Headline")
industry = st.text_input("Industry")
role = st.text_input("Current/Target Role")
current_headline = st.text_input("Current Headline", key="headline_current")
industry = st.text_input("Industry", key="headline_industry")
role = st.text_input("Current/Target Role", key="headline_role")
if st.button("Optimize Headline"):
if st.button("Optimize Headline", key="headline_optimize_btn"):
with st.spinner("Generating optimized headline..."):
headline_optimization = await optimizer.optimize_headline(
current_headline,
@@ -110,11 +111,11 @@ async def linkedin_profile_optimizer_ui():
st.header("About Section Generator")
st.info("Create an engaging and professional About section")
current_about = st.text_area("Current About Section")
achievements = st.text_area("Key Achievements (one per line)").split("\n")
target_audience = st.text_input("Target Audience")
current_about = st.text_area("Current About Section", key="about_current")
achievements = st.text_area("Key Achievements (one per line)", key="about_achievements").split("\n")
target_audience = st.text_input("Target Audience", key="about_audience")
if st.button("Generate About Section"):
if st.button("Generate About Section", key="about_generate_btn"):
with st.spinner("Generating optimized About section..."):
about_optimization = await optimizer.generate_about_section(
current_about,
@@ -128,8 +129,9 @@ async def linkedin_profile_optimizer_ui():
st.subheader("Section Structure")
for section, explanation in about_optimization['structure_explanation'].items():
with st.expander(section):
st.write(explanation)
st.markdown(f"**{section}**")
st.write(explanation)
st.divider()
st.subheader("Impact Factors")
for factor in about_optimization['impact_factors']:
@@ -141,44 +143,47 @@ async def linkedin_profile_optimizer_ui():
st.info("Enhance your work experience descriptions for maximum impact")
experiences = []
num_exp = st.number_input("Number of experiences to enhance", min_value=1, max_value=10, value=1)
num_exp = st.number_input("Number of experiences to enhance", min_value=1, max_value=10, value=1, key="exp_num")
for i in range(num_exp):
with st.expander(f"Experience {i+1}"):
exp = {
"role": st.text_input(f"Role {i+1}"),
"company": st.text_input(f"Company {i+1}"),
"description": st.text_area(f"Current Description {i+1}")
}
experiences.append(exp)
st.markdown(f"**Experience {i+1}**")
exp = {
"role": st.text_input(f"Role {i+1}", key=f"exp_role_{i}"),
"company": st.text_input(f"Company {i+1}", key=f"exp_company_{i}"),
"description": st.text_area(f"Current Description {i+1}", key=f"exp_desc_{i}")
}
experiences.append(exp)
st.divider()
if st.button("Enhance Experiences"):
if st.button("Enhance Experiences", key="exp_enhance_btn"):
with st.spinner("Enhancing experience descriptions..."):
enhanced_experiences = await optimizer.enhance_experience_descriptions(experiences)
for i, exp in enumerate(enhanced_experiences):
with st.expander(f"Enhanced Experience {i+1}"):
st.subheader(f"{exp['role']} at {exp['company']}")
st.markdown(exp['enhanced_description'])
st.subheader("Key Achievements")
for achievement in exp['achievements']:
st.success(achievement)
st.subheader("Keywords Used")
for keyword in exp['keywords']:
st.info(keyword)
st.markdown(f"**Enhanced Experience {i+1}**")
st.subheader(f"{exp['role']} at {exp['company']}")
st.markdown(exp['enhanced_description'])
st.subheader("Key Achievements")
for achievement in exp['achievements']:
st.success(achievement)
st.subheader("Keywords Used")
for keyword in exp['keywords']:
st.info(keyword)
st.divider()
# Skills Recommender Tab
with tabs[4]:
st.header("Skills Recommender")
st.info("Get personalized skill recommendations for your profile")
current_skills = st.text_area("Current Skills (one per line)").split("\n")
industry = st.text_input("Industry (for skills)")
role = st.text_input("Role (for skills)")
current_skills = st.text_area("Current Skills (one per line)", key="skills_current").split("\n")
industry = st.text_input("Industry (for skills)", key="skills_industry")
role = st.text_input("Role (for skills)", key="skills_role")
if st.button("Get Skill Recommendations"):
if st.button("Get Skill Recommendations", key="skills_recommend_btn"):
with st.spinner("Analyzing and recommending skills..."):
skill_recommendations = await optimizer.recommend_skills(
current_skills,
@@ -204,6 +209,7 @@ async def linkedin_profile_optimizer_ui():
st.subheader("Skill Categories")
for category, skills in skill_recommendations['skill_categories'].items():
with st.expander(category):
for skill in skills:
st.write(f"- {skill}")
st.markdown(f"**{category}**")
for skill in skills:
st.write(f"- {skill}")
st.divider()

View File

@@ -1,100 +0,0 @@
import time #Iwish
import os
import json
import requests
import streamlit as st
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
def tweet_writer():
""" AI Tweet Generator """
with st.expander("**PRO-TIP** - Read the instructions below.", expanded=True):
col1, col2 = st.columns([5, 5])
with col1:
hook = st.text_input(
label="**What's the tweet about? (Hook)**",
placeholder="e.g., Discover the future of tech today!",
help="Provide a compelling opening statement or question to grab attention."
)
with col2:
target_audience = st.text_input(
label="**Target Audience**",
placeholder="e.g., technology enthusiasts, travel lovers",
help="Describe the audience you want to target with this tweet."
)
col3, col4 = st.columns([5, 5])
with col3:
tweet_tone = st.selectbox(
label="**Tweet Tone**",
options=["Humorous", "Informative", "Inspirational", "Serious", "Casual"],
help="Choose the tone you'd like the tweet to have."
)
with col4:
cta = st.text_input(
label="**Call to Action (Optional)**",
placeholder="e.g., Retweet this if you agree! (Leave blank if not applicable)",
help="Provide a call to action if you'd like to include one."
)
col5, col6 = st.columns([5, 5])
with col5:
keywords_hashtags = st.text_input(
label="**Keywords/Hashtags**",
placeholder="e.g., #AI #Innovation",
help="Provide 2-3 relevant keywords or hashtags."
)
with col6:
tweet_length = st.selectbox(
"Tweet Length (Optional)",
options=["Short (under 100 characters)", "Medium (100-200 characters)", "Long (200+ characters)"],
help="Choose the desired tweet length.",
)
if st.button('**Write Tweets**'):
if not target_audience or not hook:
st.error("🚫 Please provide all required inputs.")
else:
with st.status("Assigning AI professional to write your tweets...", expanded=True) as status:
response = tweet_generator(target_audience, hook, tweet_tone, cta, keywords_hashtags, tweet_length)
if response:
st.subheader(f'**🧕👩: Your Tweets!**')
st.markdown(response)
else:
st.error("💥**Failed to generate tweets. Please try again!**")
def tweet_generator(target_audience, hook, tone_style, cta, keywords_hashtags, tweet_length):
""" Tweet Generator """
prompt = f"""
You are a social media expert creating tweets for an audience interested in {target_audience}.
Write 5 engaging, concise, and visually appealing tweets that each:
1. Start with a compelling hook based on the following input: "{hook}"
2. Include the following call to action: "{cta}"
3. Use 2-3 relevant keywords/hashtags, including: "{keywords_hashtags}"
4. Adopt the following tone/style: "{tone_style}"
5. Adhere to the following length requirement: {tweet_length}
Make sure to keep the tone consistent with the selected style and platform context.
Here are some examples of call-to-actions to include (if no specific CTA was provided):
- Retweet this if you agree!
- Share your thoughts in the comments!
- Learn more at [link]
- Follow for more {target_audience} content.
Output each tweet separated by a newline.
"""
try:
response = llm_text_gen(prompt)
return response
except Exception as err:
st.error(f"Exit: Failed to get response from LLM: {err}")
exit(1)

View File

@@ -0,0 +1,9 @@
"""
Twitter Tweet Generator Module
A comprehensive suite of tools for generating and optimizing tweets.
"""
from .smart_tweet_generator import smart_tweet_generator
__all__ = ['smart_tweet_generator']

View File

@@ -339,4 +339,4 @@ def smart_tweet_generator():
st.code(tweet_texts)
if __name__ == "__main__":
smart_tweet_generator()
smart_tweet_generator()

View File

@@ -13,7 +13,7 @@ logger.add(sys.stdout,
)
from .openai_text_gen import openai_chatgpt
from .gemini_pro_text import gemini_text_response
from .gemini_pro_text import gemini_text_response, gemini_structured_json_response
from .anthropic_text_gen import anthropic_text_response
from .deepseek_text_gen import deepseek_text_response
from ...utils.read_main_config_params import read_return_config_section

View File

@@ -16,7 +16,7 @@ from lib.ai_writers.ai_news_article_writer import ai_news_generation
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.linkedin_writer.linkedin_ai_writer import linkedin_main_menu
from lib.ai_writers.twitter_ai_writer import tweet_writer
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.youtube_writers.youtube_ai_writer import youtube_main_menu
from lib.ai_writers.web_url_ai_writer import blog_from_url
@@ -468,9 +468,9 @@ def ai_social_writer():
if "facebook" in selected_platform:
facebook_main_menu()
elif "linkedin" in selected_platform:
linked_post_writer()
linkedin_main_menu()
elif "twitter" in selected_platform:
tweet_writer()
run_dashboard()
elif "instagram" in selected_platform:
insta_writer()
elif "youtube" in selected_platform:

View File

@@ -1,245 +1,32 @@
"""AI providers setup component."""
"""AI providers setup component - Wrapper for the actual setup UI."""
import streamlit as st
from loguru import logger
from typing import Dict, Any
from ..manager import APIKeyManager
from .base import render_navigation_buttons, render_step_indicator, render_tab_style
from ..wizard_state import next_step, update_progress
from datetime import datetime
def validate_api_key(key: str) -> bool:
"""Validate if an API key is properly formatted."""
if not key:
return False
# Basic validation - check if key is not empty and has minimum length
return len(key.strip()) > 0
from .ai_providers_setup import render_ai_providers_setup # Import the refactored setup UI
def render_ai_providers(api_key_manager: APIKeyManager) -> Dict[str, Any]:
"""Render the AI providers setup step."""
logger.info("[render_ai_providers] Starting AI providers setup")
"""Renders the AI providers setup step by calling the dedicated setup function."""
logger.debug("[render_ai_providers] Calling render_ai_providers_setup")
try:
# Initialize wizard state if not already initialized
if 'wizard_state' not in st.session_state:
st.session_state.wizard_state = {
'current_step': 1,
'total_steps': 6,
'progress': 0,
'completed_steps': set(),
'last_updated': datetime.now()
}
logger.info("[render_ai_providers] Initialized wizard state")
# The actual UI, saving, validation, and feedback are now handled within render_ai_providers_setup
# This function acts primarily as a placeholder in the step sequence if needed,
# or can be bypassed entirely if the main wizard calls render_ai_providers_setup directly.
# Store API key manager in session state for update_progress
st.session_state['api_key_manager'] = api_key_manager
# Main content
st.markdown("""
<div class='setup-header'>
<h2>🤖 AI Providers Setup</h2>
<p>Configure your AI service providers for content generation</p>
</div>
""", unsafe_allow_html=True)
# Create tabs for different AI providers
tabs = st.tabs(["Primary Providers", "Additional Providers"])
# Track if any changes were made
changes_made = False
has_valid_key = False
validation_message = ""
with tabs[0]:
st.markdown("### Primary AI Providers")
st.markdown("Configure the main AI providers for content creation")
# Create a grid layout for AI provider cards
col1, col2 = st.columns(2)
with col1:
# OpenAI Card
with st.container():
st.markdown("""
<div class="ai-provider-card">
<div class="ai-provider-header">
<div class="ai-provider-icon">🤖</div>
<div class="ai-provider-title">OpenAI</div>
</div>
<div class="ai-provider-content">
<p>Power your content with GPT-4 and GPT-3.5 models</p>
<div class="ai-provider-input">
""", unsafe_allow_html=True)
openai_key = st.text_input(
"OpenAI API Key",
type="password",
key="openai_key",
help="Enter your OpenAI API key"
)
if openai_key:
if validate_api_key(openai_key):
st.markdown("""
<div class="ai-provider-status status-valid">
✓ API key configured
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div class="ai-provider-status status-invalid">
⚠️ Invalid API key format
</div>
""", unsafe_allow_html=True)
with st.expander("📋 How to get your OpenAI API key", expanded=False):
st.markdown("""
**Step-by-step guide:**
1. Go to [OpenAI's website](https://platform.openai.com)
2. Sign up or log in to your account
3. Navigate to the API section
4. Click "Create new secret key"
5. Copy the generated key and paste it here
**Note:** Keep your API key secure and never share it publicly.
""")
st.markdown("</div></div></div>", unsafe_allow_html=True)
with col2:
# Google Card
with st.container():
st.markdown("""
<div class="ai-provider-card">
<div class="ai-provider-header">
<div class="ai-provider-icon">🔍</div>
<div class="ai-provider-title">Google Gemini</div>
</div>
<div class="ai-provider-content">
<p>Leverage Google's powerful Gemini models</p>
<div class="ai-provider-input">
""", unsafe_allow_html=True)
google_key = st.text_input(
"Google API Key",
type="password",
key="google_key",
help="Enter your Google API key"
)
if google_key:
if validate_api_key(google_key):
st.markdown("""
<div class="ai-provider-status status-valid">
✓ API key configured
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div class="ai-provider-status status-invalid">
⚠️ Invalid API key format
</div>
""", unsafe_allow_html=True)
with st.expander("📋 How to get your Google API key", expanded=False):
st.markdown("""
**Step-by-step guide:**
1. Visit [Google AI Studio](https://makersuite.google.com/app/apikey)
2. Sign in with your Google account
3. Click "Create API key"
4. Copy the generated key and paste it here
**Note:** Make sure to enable the Gemini API in your Google Cloud Console.
""")
st.markdown("</div></div></div>", unsafe_allow_html=True)
with tabs[1]:
st.markdown("### Additional AI Providers")
st.markdown("Configure additional AI providers for enhanced capabilities")
# Create a grid layout for additional provider cards
col1, col2 = st.columns(2)
with col1:
# Anthropic Card (Coming Soon)
with st.container():
st.markdown("""
<div class="ai-provider-card disabled">
<div class="ai-provider-header">
<div class="ai-provider-icon">🧠</div>
<div class="ai-provider-title">Anthropic <span class="coming-soon-badge">Coming Soon</span></div>
</div>
<div class="ai-provider-content">
<p>Access Claude for advanced content generation</p>
</div>
</div>
""", unsafe_allow_html=True)
st.info("Anthropic integration will be available in the next update")
with col2:
# Mistral Card (Coming Soon)
with st.container():
st.markdown("""
<div class="ai-provider-card disabled">
<div class="ai-provider-header">
<div class="ai-provider-icon">⚡</div>
<div class="ai-provider-title">Mistral <span class="coming-soon-badge">Coming Soon</span></div>
</div>
<div class="ai-provider-content">
<p>Use Mistral's efficient language models</p>
</div>
</div>
""", unsafe_allow_html=True)
st.info("Mistral integration will be available in the next update")
# Track changes and validate keys
if any([openai_key, google_key]):
changes_made = True
# Check if at least one valid API key is provided
if validate_api_key(openai_key) or validate_api_key(google_key):
has_valid_key = True
validation_message = "✅ At least one AI provider configured successfully"
else:
validation_message = "⚠️ Please provide at least one valid API key"
else:
validation_message = "⚠️ Please configure at least one AI provider to continue"
# Display validation message
if validation_message:
if "" in validation_message:
st.success(validation_message)
else:
st.warning(validation_message)
# Navigation buttons
if render_navigation_buttons(1, 6, changes_made):
if has_valid_key:
# Store the API keys in a separate session state key
st.session_state['api_keys'] = {
'openai': openai_key if validate_api_key(openai_key) else None,
'google': google_key if validate_api_key(google_key) else None
}
# Save API keys to .env file
if validate_api_key(openai_key):
api_key_manager.save_api_key("openai", openai_key)
logger.info("[render_ai_providers] OpenAI API key saved to .env file")
if validate_api_key(google_key):
api_key_manager.save_api_key("gemini", google_key)
logger.info("[render_ai_providers] Google Gemini API key saved to .env file")
# Update progress and move to next step
st.session_state['current_step'] = 2 # Set the next step explicitly
update_progress()
st.rerun() # Rerun to apply the changes
else:
st.error("Please configure at least one valid AI provider to continue")
return {"current_step": 1, "changes_made": changes_made}
# Store the manager instance if needed by other potential logic (unlikely now)
if 'api_key_manager' not in st.session_state:
st.session_state['api_key_manager'] = api_key_manager
# Call the function that now contains all the rendering and logic for this step
component_state = render_ai_providers_setup(api_key_manager)
# Return the state from the setup function, although it might not be used directly
return component_state
except Exception as e:
error_msg = f"Error in AI providers setup: {str(e)}"
logger.error(f"[render_ai_providers] {error_msg}")
st.error(error_msg)
return {"current_step": 1, "error": error_msg}
error_msg = f"Error calling AI providers setup: {str(e)}"
logger.error(f"[render_ai_providers] {error_msg}", exc_info=True)
st.error("An error occurred while setting up AI providers.")
# Ensure consistency in error return format if expected by the caller
return {"error": error_msg}

View File

@@ -5,110 +5,214 @@ from loguru import logger
import streamlit as st
import os
import sys
# Corrected import: Assuming validation functions are in validation.py in the parent directory
from ..validation import (
test_openai_api_key,
test_gemini_api_key,
# test_anthropic_api_key, # Keep commented if not used or add if needed
# test_deepseek_api_key, # Keep commented if not used or add if needed
test_mistral_api_key
)
# Helper function to validate a specific provider's key
def _validate_provider_key(provider_name: str, key_value: str) -> bool:
"""Validate the API key for a given provider."""
if not key_value:
logger.debug(f"Validation: Key for {provider_name} is empty.")
return False
try:
logger.debug(f"Validating key for {provider_name}...")
if provider_name == "openai":
# Ensure the function exists in validation.py
if callable(getattr(sys.modules[__name__], 'test_openai_api_key', None)):
is_valid = test_openai_api_key(key_value)
else:
logger.error("test_openai_api_key not found in validation module")
is_valid = False # Assume invalid if test func missing
elif provider_name == "gemini":
if callable(getattr(sys.modules[__name__], 'test_gemini_api_key', None)):
is_valid = test_gemini_api_key(key_value)
else:
logger.error("test_gemini_api_key not found in validation module")
is_valid = False
elif provider_name == "mistral":
if callable(getattr(sys.modules[__name__], 'test_mistral_api_key', None)):
is_valid = test_mistral_api_key(key_value)
else:
logger.error("test_mistral_api_key not found in validation module")
is_valid = False
else:
logger.warning(f"Validation not implemented for provider: {provider_name}")
return False # Or True if unknown providers are allowed without validation
logger.info(f"Validation result for {provider_name}: {'Valid' if is_valid else 'Invalid'}")
return is_valid
except Exception as e:
logger.error(f"Error validating key for {provider_name}: {e}", exc_info=True)
return False
# Callback function for handling API key input changes
def _handle_api_key_change(provider_name: str, api_key_manager):
"""Save and validate API key when input changes."""
key_input_widget_key = f"{provider_name}_key_input"
status_widget_key = f"{provider_name}_status"
# Check if the input widget key exists in session state
if key_input_widget_key not in st.session_state:
logger.warning(f"Input widget key '{key_input_widget_key}' not found in session state.")
return
key_value = st.session_state[key_input_widget_key]
current_status = st.session_state.get(status_widget_key)
logger.debug(f"Handling change for {provider_name}. Key: {'***' if key_value else 'Empty'}. Current status: {current_status}")
# If key is empty, reset status
if not key_value:
api_key_manager.save_api_key(provider_name, "") # Ensure empty key is saved
st.session_state[status_widget_key] = "unsaved"
logger.info(f"Cleared API key for {provider_name}.")
return
# Set status to saving/validating
st.session_state[status_widget_key] = "saving"
st.rerun() # Rerun to show the spinner immediately
try:
# Save the key using the manager
logger.debug(f"Saving key for {provider_name}...")
api_key_manager.save_api_key(provider_name, key_value)
logger.info(f"Saved API key for {provider_name}.")
# Validate the key
is_valid = _validate_provider_key(provider_name, key_value)
# Update status based on validation result
if is_valid:
st.session_state[status_widget_key] = "valid"
else:
st.session_state[status_widget_key] = "invalid"
except Exception as e:
logger.error(f"Error during saving/validation for {provider_name}: {e}", exc_info=True)
st.session_state[status_widget_key] = "error"
def render_ai_providers_setup(api_key_manager) -> Dict[str, Any]:
"""
Render the AI providers setup component.
Render the AI providers setup component with immediate feedback.
Args:
api_key_manager: API key manager instance
Returns:
Dict[str, Any]: Component state
Dict[str, Any]: Component state (not directly used here, handled by state manager)
"""
try:
logger.info("[render_ai_providers_setup] Rendering AI providers setup")
# Initialize status in session state if not present
for provider in ["openai", "gemini", "mistral"]:
status_key = f"{provider}_status"
if status_key not in st.session_state:
# Check if a key exists and try to validate it on first load
existing_key = api_key_manager.get_api_key(provider)
if existing_key:
if _validate_provider_key(provider, existing_key):
st.session_state[status_key] = "valid"
else:
# Keep it unsaved/invalid on load if pre-existing key is bad
# Or maybe set to invalid? Let's choose unsaved for now.
st.session_state[status_key] = "invalid"
else:
st.session_state[status_key] = "unsaved"
# Display section header
st.header("Step 1: Select AI Providers")
st.header("Step 1: Configure AI Providers")
st.markdown("""
Configure your AI providers to enable advanced content generation capabilities.
Choose and set up the AI services you want to use.
Enter your API keys below. They will be validated automatically.
""")
# Create columns for different providers
col1, col2 = st.columns(2)
# --- OpenAI ---
st.subheader("OpenAI (Required)")
st.markdown("Get your API key from: [OpenAI Dashboard](https://platform.openai.com/account/api-keys)")
openai_key = api_key_manager.get_api_key("openai")
st.text_input(
"OpenAI API Key",
value=openai_key if openai_key else "",
type="password",
key="openai_key_input",
on_change=_handle_api_key_change,
args=("openai", api_key_manager)
)
# Feedback Area for OpenAI
openai_status = st.session_state.get("openai_status", "unsaved")
feedback_placeholder_openai = st.empty()
if openai_status == "saving":
feedback_placeholder_openai.info("Validating OpenAI key...", icon="")
elif openai_status == "valid":
feedback_placeholder_openai.success("OpenAI key saved and valid!", icon="")
elif openai_status == "invalid":
feedback_placeholder_openai.error("Invalid OpenAI key. Please check and try again.", icon="")
elif openai_status == "error":
feedback_placeholder_openai.error("Error saving/validating OpenAI key.", icon="⚠️")
with col1:
st.subheader("OpenAI")
st.markdown("""
OpenAI's GPT models provide powerful natural language processing capabilities.
Get your API key from: [OpenAI Dashboard](https://platform.openai.com/account/api-keys)
""")
openai_key = api_key_manager.get_api_key("openai")
openai_input = st.text_input(
"OpenAI API Key",
value=openai_key if openai_key else "",
type="password",
key="openai_key_input"
)
with col2:
st.subheader("Google Gemini")
st.markdown("""
Google's Gemini models offer advanced AI capabilities.
Get your API key from: [Google AI Studio](https://makersuite.google.com/app/apikey)
""")
gemini_key = api_key_manager.get_api_key("gemini")
gemini_input = st.text_input(
"Gemini API Key",
value=gemini_key if gemini_key else "",
type="password",
key="gemini_key_input"
)
# Optional AI Provider
st.subheader("Additional AI Provider (Optional)")
col1, col2 = st.columns(2)
with col1:
st.markdown("""
Mistral AI provides an alternative model for content generation.
Get your API key from: [Mistral Platform](https://console.mistral.ai/api-keys/)
""")
mistral_key = api_key_manager.get_api_key("mistral")
mistral_input = st.text_input(
"Mistral API Key (Optional)",
value=mistral_key if mistral_key else "",
type="password",
key="mistral_key_input"
)
# Add a note about saving
st.info("""
Note: At least one AI provider (OpenAI or Google Gemini) is required.
Click Continue to save your keys and proceed.
""")
# Save keys if they've changed when proceeding to next step
if st.session_state.get('wizard_current_step', 1) > 1:
if openai_input != openai_key:
api_key_manager.save_api_key("openai", openai_input)
logger.info("[render_ai_providers_setup] OpenAI API key saved")
if gemini_input != gemini_key:
api_key_manager.save_api_key("gemini", gemini_input)
logger.info("[render_ai_providers_setup] Gemini API key saved")
if mistral_input != mistral_key:
api_key_manager.save_api_key("mistral", mistral_input)
logger.info("[render_ai_providers_setup] Mistral API key saved")
# Validate that at least one required provider is configured
if not (openai_input or gemini_input):
st.error("Please configure at least one AI provider (OpenAI or Google Gemini) to proceed.")
return {"current_step": 1, "can_proceed": False}
return {"current_step": 1, "can_proceed": bool(openai_input or gemini_input)}
# --- Google Gemini ---
st.subheader("Google Gemini (Required)")
st.markdown("Get your API key from: [Google AI Studio](https://makersuite.google.com/app/apikey)")
gemini_key = api_key_manager.get_api_key("gemini")
st.text_input(
"Gemini API Key",
value=gemini_key if gemini_key else "",
type="password",
key="gemini_key_input",
on_change=_handle_api_key_change,
args=("gemini", api_key_manager)
)
# Feedback Area for Gemini
gemini_status = st.session_state.get("gemini_status", "unsaved")
feedback_placeholder_gemini = st.empty()
if gemini_status == "saving":
feedback_placeholder_gemini.info("Validating Gemini key...", icon="")
elif gemini_status == "valid":
feedback_placeholder_gemini.success("Gemini key saved and valid!", icon="")
elif gemini_status == "invalid":
feedback_placeholder_gemini.error("Invalid Gemini key. Please check and try again.", icon="")
elif gemini_status == "error":
feedback_placeholder_gemini.error("Error saving/validating Gemini key.", icon="⚠️")
# --- Mistral AI (Optional) ---
st.subheader("Mistral AI (Optional)")
st.markdown("Get your API key from: [Mistral Platform](https://console.mistral.ai/api-keys/)")
mistral_key = api_key_manager.get_api_key("mistral")
st.text_input(
"Mistral API Key",
value=mistral_key if mistral_key else "",
type="password",
key="mistral_key_input",
on_change=_handle_api_key_change,
args=("mistral", api_key_manager)
)
# Feedback Area for Mistral
mistral_status = st.session_state.get("mistral_status", "unsaved")
feedback_placeholder_mistral = st.empty()
if mistral_status == "saving":
feedback_placeholder_mistral.info("Validating Mistral key...", icon="")
elif mistral_status == "valid":
feedback_placeholder_mistral.success("Mistral key saved and valid!", icon="")
elif mistral_status == "invalid":
feedback_placeholder_mistral.error("Invalid Mistral key. Please check and try again.", icon="")
elif mistral_status == "error":
feedback_placeholder_mistral.error("Error saving/validating Mistral key.", icon="⚠️")
# --- Final Notes ---
st.info("Note: At least one AI provider (OpenAI or Google Gemini) must have a valid API key to proceed.")
# Return value is not strictly needed if navigation relies on session state status
return {}
except Exception as e:
error_msg = f"Error in AI providers setup: {str(e)}"
logger.error(f"[render_ai_providers_setup] {error_msg}")
error_msg = f"Error rendering AI providers setup: {str(e)}"
logger.error(f"[render_ai_providers_setup] {error_msg}", exc_info=True)
st.error(error_msg)
return {"current_step": 1, "error": error_msg}
return {"error": error_msg}

View File

@@ -4,346 +4,236 @@ import streamlit as st
from loguru import logger
from typing import Dict, Any
from ..manager import APIKeyManager
from .base import render_navigation_buttons
import os
from dotenv import load_dotenv
from dotenv import load_dotenv # Keep if api_key_manager uses it
import sys
# Corrected import: Assuming validation functions are in validation.py in the parent directory
from ..validation import (
test_serpapi_key,
test_tavily_key,
test_metaphor_key,
test_firecrawl_key
# Add others if needed later, e.g., test_bing_key, test_google_search_key
)
# Configure logger
logger.remove() # Remove default handler
logger.add(
"logs/ai_research_setup.log",
rotation="500 MB",
retention="10 days",
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
)
logger.add(
sys.stdout,
level="INFO",
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
)
# Configure logger (assuming configured elsewhere or keep minimal here)
logger.add(sys.stderr, level="INFO") # Keep simple example if needed
# Helper function to validate a specific research provider's key
def _validate_research_key(provider_name: str, key_value: str) -> bool:
"""Validate the API key for a given research provider."""
if not key_value:
logger.debug(f"Validation: Key for {provider_name} is empty.")
return False
try:
logger.debug(f"Validating key for {provider_name}...")
# Ensure the function exists in validation.py before calling
if provider_name == "serpapi":
if callable(getattr(sys.modules[__name__], 'test_serpapi_key', None)):
is_valid = test_serpapi_key(key_value)
else:
logger.error("test_serpapi_key not found in validation module")
is_valid = False
elif provider_name == "tavily":
if callable(getattr(sys.modules[__name__], 'test_tavily_key', None)):
is_valid = test_tavily_key(key_value)
else:
logger.error("test_tavily_key not found in validation module")
is_valid = False
elif provider_name == "metaphor":
if callable(getattr(sys.modules[__name__], 'test_metaphor_key', None)):
is_valid = test_metaphor_key(key_value)
else:
logger.error("test_metaphor_key not found in validation module")
is_valid = False
elif provider_name == "firecrawl":
if callable(getattr(sys.modules[__name__], 'test_firecrawl_key', None)):
is_valid = test_firecrawl_key(key_value)
else:
logger.error("test_firecrawl_key not found in validation module")
is_valid = False
else:
logger.warning(f"Validation not implemented for research provider: {provider_name}")
return False # Default to False for unknown providers
logger.info(f"Validation result for {provider_name}: {'Valid' if is_valid else 'Invalid'}")
return is_valid
except Exception as e:
logger.error(f"Error validating key for {provider_name}: {e}", exc_info=True)
return False
# Callback function for handling API key input changes
def _handle_research_key_change(provider_name: str, api_key_manager):
"""Save and validate research API key when input changes."""
key_input_widget_key = f"{provider_name}_key_input"
status_widget_key = f"{provider_name}_status"
if key_input_widget_key not in st.session_state:
logger.warning(f"Input widget key '{key_input_widget_key}' not found in session state.")
return
key_value = st.session_state[key_input_widget_key]
current_status = st.session_state.get(status_widget_key)
logger.debug(f"Handling research key change for {provider_name}. Key: {'***' if key_value else 'Empty'}. Current status: {current_status}")
if not key_value:
api_key_manager.save_api_key(provider_name, "")
st.session_state[status_widget_key] = "unsaved"
logger.info(f"Cleared API key for {provider_name}.")
return
st.session_state[status_widget_key] = "saving"
st.rerun()
try:
logger.debug(f"Saving key for {provider_name}...")
api_key_manager.save_api_key(provider_name, key_value)
logger.info(f"Saved API key for {provider_name}.")
is_valid = _validate_research_key(provider_name, key_value)
if is_valid:
st.session_state[status_widget_key] = "valid"
else:
st.session_state[status_widget_key] = "invalid"
except Exception as e:
logger.error(f"Error during saving/validation for {provider_name}: {e}", exc_info=True)
st.session_state[status_widget_key] = "error"
def render_ai_research_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]:
"""Render the AI research setup step."""
"""Render the AI research setup step with immediate feedback."""
logger.info("[render_ai_research_setup] Rendering AI research setup component")
research_providers = ["serpapi", "tavily", "metaphor", "firecrawl"]
# Initialize statuses
for provider in research_providers:
status_key = f"{provider}_status"
if status_key not in st.session_state:
existing_key = api_key_manager.get_api_key(provider)
if existing_key:
if _validate_research_key(provider, existing_key):
st.session_state[status_key] = "valid"
else:
st.session_state[status_key] = "invalid"
else:
st.session_state[status_key] = "unsaved"
st.markdown("""
<div class='setup-header'>
<h2>🔍 AI Research Setup</h2>
<p>Configure your AI research providers for content analysis and research</p>
<h2>Step 3: Configure AI Research Tools (Optional)</h2>
<p>Set up API keys for enhanced web research, crawling, and analysis. These are optional but recommended.</p>
</div>
""", unsafe_allow_html=True)
# Create two columns for different search types
col1, col2 = st.columns(2)
# --- SerpAPI ---
with col1:
st.markdown("### The Usual")
# SerpAPI Card
st.markdown("""
<div class="ai-provider-card">
<div class="ai-provider-header">
<div class="ai-provider-icon">🔎</div>
<div class="ai-provider-title">SerpAPI</div>
</div>
<div class="ai-provider-description">
Access search engine results for research
</div>
<div class="ai-provider-input">
""", unsafe_allow_html=True)
serpapi_key = st.text_input(
st.subheader("SerpAPI")
st.markdown("Access real-time search engine results. Get key: [SerpAPI](https://serpapi.com)")
serpapi_key_val = api_key_manager.get_api_key("serpapi")
st.text_input(
"SerpAPI Key",
value=serpapi_key_val if serpapi_key_val else "",
type="password",
key="serpapi_key",
help="Enter your SerpAPI key"
key="serpapi_key_input",
on_change=_handle_research_key_change,
args=("serpapi", api_key_manager)
)
if serpapi_key:
st.markdown("""
<div class="ai-provider-status status-valid">
✓ API key configured
</div>
""", unsafe_allow_html=True)
st.markdown("""
<div class="api-info-section">
<details>
<summary>📋 How to get your SerpAPI key</summary>
<div class="api-info-content">
<p><strong>Step-by-step guide:</strong></p>
<ol>
<li>Visit <a href="https://serpapi.com" target="_blank">SerpAPI</a></li>
<li>Create an account</li>
<li>Go to your dashboard</li>
<li>Copy your API key</li>
<li>Paste it here</li>
</ol>
<p><strong>Note:</strong> SerpAPI provides real-time search results from multiple engines.</p>
</div>
</details>
</div>
""", unsafe_allow_html=True)
st.markdown("</div></div>", unsafe_allow_html=True)
# Feedback Area
serpapi_status = st.session_state.get("serpapi_status", "unsaved")
feedback_placeholder_serpapi = st.empty()
if serpapi_status == "saving":
feedback_placeholder_serpapi.info("Validating SerpAPI key...", icon="")
elif serpapi_status == "valid":
feedback_placeholder_serpapi.success("SerpAPI key saved and valid!", icon="")
elif serpapi_status == "invalid":
feedback_placeholder_serpapi.error("Invalid SerpAPI key.", icon="")
elif serpapi_status == "error":
feedback_placeholder_serpapi.error("Error saving/validating SerpAPI key.", icon="⚠️")
# Firecrawl Card
st.markdown("""
<div class="ai-provider-card">
<div class="ai-provider-header">
<div class="ai-provider-icon">🕷️</div>
<div class="ai-provider-title">Firecrawl</div>
</div>
<div class="ai-provider-description">
Web content extraction and analysis
</div>
<div class="ai-provider-input">
""", unsafe_allow_html=True)
firecrawl_key = st.text_input(
# --- Firecrawl ---
with col1:
st.subheader("Firecrawl")
st.markdown("Web content extraction and crawling. Get key: [Firecrawl](https://www.firecrawl.dev/account)")
firecrawl_key_val = api_key_manager.get_api_key("firecrawl")
st.text_input(
"Firecrawl API Key",
value=firecrawl_key_val if firecrawl_key_val else "",
type="password",
key="firecrawl_key",
help="Enter your Firecrawl API key"
key="firecrawl_key_input",
on_change=_handle_research_key_change,
args=("firecrawl", api_key_manager)
)
if firecrawl_key:
st.markdown("""
<div class="ai-provider-status status-valid">
✓ API key configured
</div>
""", unsafe_allow_html=True)
st.markdown("""
<div class="api-info-section">
<details>
<summary>📋 How to get your Firecrawl API key</summary>
<div class="api-info-content">
<p><strong>Step-by-step guide:</strong></p>
<ol>
<li>Visit <a href="https://www.firecrawl.dev/account" target="_blank">Firecrawl</a></li>
<li>Create an account</li>
<li>Go to your dashboard</li>
<li>Generate your API key</li>
<li>Copy and paste it here</li>
</ol>
<p><strong>Note:</strong> Firecrawl provides powerful web content extraction and analysis capabilities.</p>
</div>
</details>
</div>
""", unsafe_allow_html=True)
st.markdown("</div></div>", unsafe_allow_html=True)
# Feedback Area
firecrawl_status = st.session_state.get("firecrawl_status", "unsaved")
feedback_placeholder_firecrawl = st.empty()
if firecrawl_status == "saving":
feedback_placeholder_firecrawl.info("Validating Firecrawl key...", icon="")
elif firecrawl_status == "valid":
feedback_placeholder_firecrawl.success("Firecrawl key saved and valid!", icon="")
elif firecrawl_status == "invalid":
feedback_placeholder_firecrawl.error("Invalid Firecrawl key.", icon="")
elif firecrawl_status == "error":
feedback_placeholder_firecrawl.error("Error saving/validating Firecrawl key.", icon="⚠️")
# --- Tavily ---
with col2:
st.markdown("### AI Deep Research")
# Tavily Card
st.markdown("""
<div class="ai-provider-card">
<div class="ai-provider-header">
<div class="ai-provider-icon">🤖</div>
<div class="ai-provider-title">Tavily AI</div>
</div>
<div class="ai-provider-description">
AI-powered search with semantic understanding
</div>
<div class="ai-provider-input">
""", unsafe_allow_html=True)
tavily_key = st.text_input(
st.subheader("Tavily AI")
st.markdown("AI-powered search & summarization. Get key: [Tavily](https://tavily.com)")
tavily_key_val = api_key_manager.get_api_key("tavily")
st.text_input(
"Tavily API Key",
value=tavily_key_val if tavily_key_val else "",
type="password",
key="tavily_key",
help="Enter your Tavily API key"
key="tavily_key_input",
on_change=_handle_research_key_change,
args=("tavily", api_key_manager)
)
if tavily_key:
st.markdown("""
<div class="ai-provider-status status-valid">
✓ API key configured
</div>
""", unsafe_allow_html=True)
st.markdown("""
<div class="api-info-section">
<details>
<summary>📋 How to get your Tavily API key</summary>
<div class="api-info-content">
<p><strong>Step-by-step guide:</strong></p>
<ol>
<li>Visit <a href="https://tavily.com" target="_blank">Tavily</a></li>
<li>Create an account</li>
<li>Go to API settings</li>
<li>Generate a new API key</li>
<li>Copy and paste it here</li>
</ol>
<p><strong>Note:</strong> Tavily provides AI-powered semantic search capabilities.</p>
</div>
</details>
</div>
""", unsafe_allow_html=True)
st.markdown("</div></div>", unsafe_allow_html=True)
# Metaphor/Exa Card
st.markdown("""
<div class="ai-provider-card">
<div class="ai-provider-header">
<div class="ai-provider-icon">🧠</div>
<div class="ai-provider-title">Metaphor/Exa</div>
</div>
<div class="ai-provider-description">
Neural search engine for deep research
</div>
<div class="ai-provider-input">
""", unsafe_allow_html=True)
metaphor_key = st.text_input(
# Feedback Area
tavily_status = st.session_state.get("tavily_status", "unsaved")
feedback_placeholder_tavily = st.empty()
if tavily_status == "saving":
feedback_placeholder_tavily.info("Validating Tavily key...", icon="")
elif tavily_status == "valid":
feedback_placeholder_tavily.success("Tavily key saved and valid!", icon="")
elif tavily_status == "invalid":
feedback_placeholder_tavily.error("Invalid Tavily key.", icon="")
elif tavily_status == "error":
feedback_placeholder_tavily.error("Error saving/validating Tavily key.", icon="⚠️")
# --- Metaphor/Exa ---
with col2:
st.subheader("Metaphor/Exa")
st.markdown("Neural search for deep research. Get key: [Metaphor/Exa](https://metaphor.systems)")
metaphor_key_val = api_key_manager.get_api_key("metaphor")
st.text_input(
"Metaphor/Exa API Key",
value=metaphor_key_val if metaphor_key_val else "",
type="password",
key="metaphor_key",
help="Enter your Metaphor/Exa API key"
key="metaphor_key_input",
on_change=_handle_research_key_change,
args=("metaphor", api_key_manager)
)
if metaphor_key:
st.markdown("""
<div class="ai-provider-status status-valid">
✓ API key configured
</div>
""", unsafe_allow_html=True)
st.markdown("""
<div class="api-info-section">
<details>
<summary>📋 How to get your Metaphor/Exa API key</summary>
<div class="api-info-content">
<p><strong>Step-by-step guide:</strong></p>
<ol>
<li>Visit <a href="https://metaphor.systems" target="_blank">Metaphor/Exa</a></li>
<li>Create an account</li>
<li>Navigate to API settings</li>
<li>Generate your API key</li>
<li>Copy and paste it here</li>
</ol>
<p><strong>Note:</strong> Metaphor/Exa provides neural search capabilities for deep research.</p>
</div>
</details>
</div>
""", unsafe_allow_html=True)
st.markdown("</div></div>", unsafe_allow_html=True)
# Disabled Options Expander
# Feedback Area
metaphor_status = st.session_state.get("metaphor_status", "unsaved")
feedback_placeholder_metaphor = st.empty()
if metaphor_status == "saving":
feedback_placeholder_metaphor.info("Validating Metaphor/Exa key...", icon="")
elif metaphor_status == "valid":
feedback_placeholder_metaphor.success("Metaphor/Exa key saved and valid!", icon="")
elif metaphor_status == "invalid":
feedback_placeholder_metaphor.error("Invalid Metaphor/Exa key.", icon="")
elif metaphor_status == "error":
feedback_placeholder_metaphor.error("Error saving/validating Metaphor/Exa key.", icon="⚠️")
# --- Coming Soon ---
with st.expander("🔜 Coming Soon - More Search Options", expanded=False):
st.markdown("""
<div style='opacity: 0.7;'>
<h4>Bing Search API</h4>
<p>Microsoft's powerful search API with web, news, and image search capabilities.</p>
<h4>Google Search API</h4>
<p>Google's programmable search engine with customizable search parameters.</p>
<p><em>These integrations are under development and will be available soon!</em></p>
</div>
""", unsafe_allow_html=True)
# Track changes
changes_made = bool(serpapi_key or tavily_key or metaphor_key or firecrawl_key)
# Navigation buttons with correct arguments
if render_navigation_buttons(3, 5, changes_made):
if changes_made:
try:
# Load existing .env file if it exists
load_dotenv()
# Create or update .env file with new API keys
with open('.env', 'a') as f:
if serpapi_key:
f.write(f"\nSERPAPI_KEY={serpapi_key}")
logger.info("[render_ai_research_setup] Saved SerpAPI key")
if tavily_key:
f.write(f"\nTAVILY_API_KEY={tavily_key}")
logger.info("[render_ai_research_setup] Saved Tavily API key")
if metaphor_key:
f.write(f"\nMETAPHOR_API_KEY={metaphor_key}")
logger.info("[render_ai_research_setup] Saved Metaphor API key")
if firecrawl_key:
f.write(f"\nFIRECRAWL_API_KEY={firecrawl_key}")
logger.info("[render_ai_research_setup] Saved Firecrawl API key")
# Store the API keys in session state
st.session_state['api_keys'] = {
'serpapi': serpapi_key,
'tavily': tavily_key,
'metaphor': metaphor_key,
'firecrawl': firecrawl_key
}
# Update progress and move to next step
st.session_state['current_step'] = 4
st.rerun()
except Exception as e:
error_msg = f"Error saving API keys: {str(e)}"
logger.error(f"[render_ai_research_setup] {error_msg}")
st.error(error_msg)
else:
st.error("Please configure at least one research provider to continue")
# Detailed Information Section
st.markdown("""
---
### Understanding Your Research Options
#### The Usual: Traditional Search
**SerpAPI**
- Real-time search results from multiple search engines
- Access to structured data from search results
- Great for gathering general information and market research
- Includes features like:
- Web search results
- News articles
- Knowledge graphs
- Related questions
#### AI Deep Research: Advanced Search Capabilities
**Tavily AI**
- AI-powered search with semantic understanding
- Automatically summarizes and analyzes search results
- Perfect for:
- Deep research tasks
- Academic research
- Fact-checking
- Real-time information gathering
**Metaphor/Exa**
- Neural search engine that understands context and meaning
- Specialized in finding highly relevant content
- Ideal for:
- Technical research
- Finding similar content
- Discovering patterns in research
- Understanding topic landscapes
#### Choosing the Right Tool
1. **For General Research:**
- Start with SerpAPI for broad coverage and structured data
2. **For Deep Analysis:**
- Use Tavily AI when you need AI-powered insights
- Choose Metaphor/Exa for neural search and pattern discovery
3. **For Comprehensive Research:**
- Combine multiple tools to get the most complete picture
- Use SerpAPI for initial research
- Follow up with AI tools for deeper insights
> **Pro Tip:** Configure multiple providers to ensure you have backup options and can cross-reference results for better accuracy.
""")
return {"current_step": 3, "changes_made": changes_made}
st.info("Integrations for Bing Search and Google Search APIs are planned.")
st.info("You can skip this step if you don't need these research tools. Click Continue to proceed.")
return {}

View File

@@ -3,16 +3,17 @@
import streamlit as st
from typing import Dict, Any
from loguru import logger
from ..styles import API_KEY_MANAGER_STYLES
from ..styles import API_KEY_MANAGER_STYLES # Assuming styles are correctly imported
from ..wizard_state import (
get_current_step,
next_step,
previous_step,
can_proceed_to_next_step
get_current_step, # Keep if used elsewhere
next_step, # Keep if used elsewhere
previous_step, # Keep if used elsewhere
can_proceed_to_next_step # Keep if used elsewhere
)
def render_step_indicator(current_step: int, total_steps: int) -> None:
"""Render the step indicator."""
# Existing step indicator code... (Keep as is)
try:
st.markdown("""
<style>
@@ -67,11 +68,11 @@ def render_step_indicator(current_step: int, total_steps: int) -> None:
""", unsafe_allow_html=True)
steps = [
("🔑", "AI LLM", 1),
("🤖", "Website Setup", 2),
("👤", "AI Research", 3),
("🔑", "AI Providers", 1),
("🌐", "Website Setup", 2),
("🔍", "AI Research", 3),
("🎨", "Personalization", 4),
("🔄", "Integrations", 5),
("🔗", "Integrations", 5),
("", "Complete", 6)
]
@@ -97,19 +98,11 @@ def render_step_indicator(current_step: int, total_steps: int) -> None:
logger.error(f"Error rendering step indicator: {str(e)}")
st.error("Error displaying step indicator")
def render_navigation_buttons(current_step: int, total_steps: int, changes_made: bool = False) -> bool:
"""Render the navigation buttons with modern glassmorphic styling.
Args:
current_step (int): Current step number
total_steps (int): Total number of steps
changes_made (bool): Whether changes were made in the current step
Returns:
bool: True if next/complete button was clicked, False otherwise
"""
def render_navigation_buttons(current_step: int, total_steps: int) -> None:
"""Render the navigation buttons with validation logic for steps 1 and 3."""
col1, col2, col3 = st.columns([1, 2, 1])
proceed_error_placeholder = col2.empty() # Placeholder for error message
with col1:
if current_step > 1:
if st.button("**← Back**", use_container_width=True, key="back_button"):
@@ -119,18 +112,72 @@ def render_navigation_buttons(current_step: int, total_steps: int, changes_made:
with col3:
if current_step < total_steps:
next_text = "**Continue →**"
if st.button(next_text, use_container_width=True, disabled=not changes_made, key="next_button"):
return True
else:
if st.button("**Complete Setup ✓**", use_container_width=True, type="primary", key="complete_button"):
# Save the configuration
st.success("✅ Setup completed successfully!")
return True
return False
button_disabled = False
error_message = ""
if current_step == 1:
# --- Step 1 Specific Validation ---
openai_valid = st.session_state.get("openai_status") == "valid"
gemini_valid = st.session_state.get("gemini_status") == "valid"
if not (openai_valid or gemini_valid):
button_disabled = True
error_message = "Please ensure at least one required AI provider (OpenAI or Gemini) has a valid API key to continue."
logger.debug(f"Step 1 validation: OpenAI Valid={openai_valid}, Gemini Valid={gemini_valid}, Proceed={not button_disabled}")
elif current_step == 3:
# --- Step 3 Specific Validation ---
research_providers = ["serpapi", "tavily", "metaphor", "firecrawl"]
invalid_key_found = False
for provider in research_providers:
status = st.session_state.get(f"{provider}_status")
# Disable if any *entered* key is invalid or in error state
if status in ["invalid", "error"]:
invalid_key_found = True
break
if invalid_key_found:
button_disabled = True
error_message = f"Please ensure any entered research API keys are valid before continuing. Check {provider.capitalize()} key."
logger.debug(f"Step 3 validation: Invalid Key Found={invalid_key_found}, Proceed={not button_disabled}")
# --- Default Logic for Other Steps ---
# else: # No specific validation for other steps currently
# button_disabled = False
# --- Render Button ---
if st.button(next_text, use_container_width=True, disabled=button_disabled, key="next_button"):
if button_disabled:
# Should not happen if disabled, but safeguard
proceed_error_placeholder.error(error_message if error_message else "Cannot proceed.", icon="⚠️")
logger.warning(f"Continue button clicked on Step {current_step} while disabled.")
else:
# Proceed to next step
logger.info(f"Proceeding from step {current_step} to {current_step + 1}")
st.session_state['current_step'] = current_step + 1
st.rerun()
# Show error persistently if button is disabled
elif button_disabled:
proceed_error_placeholder.error(error_message, icon="⚠️")
elif current_step == total_steps:
# --- Final Step Logic ---
final_step_can_complete = True # Replace with actual final validation logic
if st.button("**Complete Setup ✓**", use_container_width=True, type="primary", disabled=not final_step_can_complete, key="complete_button"):
if final_step_can_complete:
logger.info("Setup completed successfully!")
st.session_state['setup_complete'] = True
st.success("✅ Setup completed successfully!")
st.balloons()
st.rerun()
else:
proceed_error_placeholder.error("Please complete all required steps before finishing.", icon="⚠️")
logger.warning("Complete Setup clicked but final validation failed.")
elif not final_step_can_complete:
proceed_error_placeholder.error("Please complete all required steps before finishing.", icon="⚠️")
def render_tab_style() -> None:
"""Render enhanced tab styling."""
# Existing tab style code... (Keep as is)
st.markdown("""
<style>
.stTabs [data-baseweb="tab-list"] {
@@ -175,6 +222,7 @@ def render_tab_style() -> None:
def render_success_message():
"""Render the success message with glassmorphic design."""
# Existing success message code... (Keep as is)
st.markdown("""
<div class="success-message">
<h3 style='color: white; margin-bottom: 12px; font-size: 1.4em;'>✅ API keys saved successfully!</h3>

View File

@@ -2,265 +2,208 @@
import streamlit as st
from loguru import logger
from ...website_analyzer import analyze_website
from ...website_analyzer.seo_analyzer import analyze_seo
# Removed website_analyzer imports as analysis is separate now
# from ...website_analyzer import analyze_website
# from ...website_analyzer.seo_analyzer import analyze_seo
import asyncio
import sys
from typing import Dict, Any
from ..manager import APIKeyManager
from .base import render_navigation_buttons
import requests
import ssl
import socket
from urllib.parse import urlparse
from ..manager import APIKeyManager
# Navigation is handled in base.py now
# from .base import render_navigation_buttons
# Configure logger (minimal example)
logger.add(sys.stderr, level="INFO")
# --- Validation Helpers ---
def _is_valid_url_format(url: str) -> bool:
"""Checks if the URL has a valid basic format (scheme and netloc)."""
try:
result = urlparse(url)
return all([result.scheme in ['http', 'https'], result.netloc])
except ValueError:
return False
def _check_url_reachability(url: str) -> tuple[bool, str]:
"""Checks if the URL is reachable and returns status code or error."""
try:
response = requests.head(url, allow_redirects=True, timeout=5) # HEAD request is faster
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
logger.info(f"URL {url} reachable, status code: {response.status_code}")
return True, f"Reachable (Status: {response.status_code})"
except requests.exceptions.Timeout:
logger.warning(f"URL {url} timed out.")
return False, "Timeout: Server did not respond in time."
except requests.exceptions.RequestException as e:
logger.warning(f"URL {url} not reachable: {e}")
# Provide a more user-friendly message for common errors
if isinstance(e, requests.exceptions.ConnectionError):
return False, "Connection Error: Could not connect to the server."
elif isinstance(e, requests.exceptions.HTTPError):
return False, f"HTTP Error: {e.response.status_code}"
return False, f"Error: {type(e).__name__}"
def _check_ssl_certificate(url: str) -> tuple[bool, str]:
"""Checks if the URL has a valid SSL certificate (for https)."""
parsed_url = urlparse(url)
if parsed_url.scheme != 'https':
return True, "(HTTP URL)" # Not applicable for http
hostname = parsed_url.netloc
port = 443
context = ssl.create_default_context()
try:
with socket.create_connection((hostname, port), timeout=3) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
# Basic check: does it exist? More thorough checks (expiry, chain) are possible
if cert:
logger.info(f"SSL certificate found for {hostname}")
return True, "Valid SSL Certificate"
else:
logger.warning(f"No SSL certificate found for {hostname}")
return False, "No SSL Certificate found"
except ssl.SSLCertVerificationError as e:
logger.warning(f"SSL Verification Error for {hostname}: {e}")
return False, f"SSL Verification Error: {e.verify_message}"
except socket.timeout:
logger.warning(f"SSL check timed out for {hostname}")
return False, "SSL Check Timeout"
except Exception as e:
logger.error(f"Error checking SSL for {hostname}: {e}", exc_info=True)
return False, f"SSL Check Error: {type(e).__name__}"
# --- Main Component Logic ---
def _validate_website_url(url: str) -> tuple[str, str]:
"""Performs quick validation (format, reachability, basic SSL)."""
if not url:
return "unsaved", ""
# 1. Format Check
if not _is_valid_url_format(url):
logger.warning(f"Invalid URL format: {url}")
return "invalid_format", "Invalid URL format. Please include http:// or https://"
# 2. Reachability Check
reachable, reach_status = _check_url_reachability(url)
if not reachable:
logger.warning(f"URL not reachable: {url} ({reach_status})")
return "unreachable", reach_status # Return specific error message
# 3. Basic SSL Check (only if reachable and HTTPS)
if urlparse(url).scheme == 'https':
ssl_valid, ssl_status = _check_ssl_certificate(url)
if not ssl_valid:
logger.warning(f"SSL check failed for {url} ({ssl_status})")
return "ssl_error", ssl_status # Return specific error message
logger.info(f"URL validation successful for: {url}")
return "valid", "URL is valid and reachable."
def _handle_website_url_change(api_key_manager: APIKeyManager):
"""Save and validate website URL when input changes."""
url_input_widget_key = "website_url_input"
status_widget_key = "website_url_status"
if url_input_widget_key not in st.session_state:
logger.warning(f"Input widget key '{url_input_widget_key}' not found.")
return
url_value = st.session_state[url_input_widget_key]
logger.debug(f"Handling website URL change. URL: {url_value}")
# Save the URL regardless of validity for now (maybe refine later)
# api_key_manager might not be the right place, consider storing directly in session state
# or a dedicated config manager if this isn't an API key.
# Let's store in session_state for now.
st.session_state['configured_website_url'] = url_value
logger.info(f"Saved website URL to session state: {url_value}")
if not url_value:
st.session_state[status_widget_key] = ("unsaved", "")
logger.info("Cleared website URL.")
return
st.session_state[status_widget_key] = ("saving", "") # Indicate validation is running
st.rerun()
try:
validation_status, message = _validate_website_url(url_value)
st.session_state[status_widget_key] = (validation_status, message)
logger.info(f"Website URL validation complete. Status: {validation_status}, Msg: {message}")
except Exception as e:
logger.error(f"Error during website URL validation: {e}", exc_info=True)
st.session_state[status_widget_key] = ("error", "An unexpected error occurred during validation.")
# Configure logger to output to both file and stdout
logger.remove() # Remove default handler
logger.add(
"logs/website_setup.log",
rotation="500 MB",
retention="10 days",
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
)
logger.add(
sys.stdout,
level="INFO",
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
)
def render_website_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]:
"""Render the website setup step.
Args:
api_key_manager (APIKeyManager): The API key manager instance
Returns:
Dict[str, Any]: Current state
"""
"""Render the website setup step with immediate feedback."""
logger.info("[render_website_setup] Rendering website setup component")
st.markdown("### Step 2: Website Setup")
status_key = "website_url_status"
# Initialize status
if status_key not in st.session_state:
st.session_state[status_key] = ("unsaved", "")
# Optionally pre-validate if a URL exists from previous session/config
# pre_existing_url = api_key_manager.get_config("website_url") # Example
# if pre_existing_url:
# st.session_state[status_key] = _validate_website_url(pre_existing_url)
st.markdown("""
<div class='setup-header'>
<h2>Step 2: Website Setup (Optional)</h2>
<p>Enter your primary website URL. This helps Alwrity personalize suggestions and analyze your content.</p>
</div>
""", unsafe_allow_html=True)
# Create two columns for input and results
col1, col2 = st.columns([1, 1])
# Get current value from session state if available, otherwise empty
current_url = st.session_state.get('configured_website_url', "")
with col1:
st.markdown("#### Enter Website URL")
url = st.text_input("Website URL", placeholder="https://example.com")
logger.debug(f"[render_website_setup] URL input value: {url}")
analyze_type = st.radio(
"Analysis Type",
["Basic Analysis", "Full Analysis with SEO"],
help="Choose between basic website analysis or comprehensive SEO analysis"
)
if st.button("Analyze Website"):
if url:
with st.spinner("Analyzing website..."):
try:
logger.info(f"[render_website_setup] Starting website analysis for URL: {url}")
# Call the analyze_website function
results = analyze_website(url)
# If full analysis is selected, add SEO analysis
if analyze_type == "Full Analysis with SEO":
seo_results = analyze_seo(url)
if seo_results.success:
results['data']['seo_analysis'] = {
'overall_score': seo_results.overall_score,
'meta_tags': {
'title': seo_results.meta_tags.title,
'description': seo_results.meta_tags.description,
'keywords': seo_results.meta_tags.keywords,
'has_robots': seo_results.meta_tags.has_robots,
'has_sitemap': seo_results.meta_tags.has_sitemap
},
'content': {
'word_count': seo_results.content.word_count,
'readability_score': seo_results.content.readability_score,
'content_quality_score': seo_results.content.content_quality_score,
'headings_structure': seo_results.content.headings_structure,
'keyword_density': seo_results.content.keyword_density
},
'recommendations': [
{
'priority': rec.priority,
'category': rec.category,
'issue': rec.issue,
'recommendation': rec.recommendation,
'impact': rec.impact
}
for rec in seo_results.recommendations
]
}
logger.debug(f"[render_website_setup] Analysis results received: {results.get('success', False)}")
# Store results in session state
st.session_state.website_analysis = results
logger.info("[render_website_setup] Results stored in session state")
if not results.get('success', False):
error_msg = results.get('error', 'Analysis failed')
logger.error(f"[render_website_setup] Analysis failed: {error_msg}")
st.error(error_msg)
else:
logger.info("[render_website_setup] Analysis completed successfully")
st.success("✅ Website analysis completed successfully!")
except Exception as e:
error_msg = f"Analysis failed: {str(e)}"
logger.error(f"[render_website_setup] {error_msg}")
st.error(error_msg)
else:
logger.warning("[render_website_setup] No URL provided")
st.warning("Please enter a valid URL")
st.text_input(
"Website URL",
value=current_url,
placeholder="https://example.com",
key="website_url_input",
on_change=_handle_website_url_change,
args=(api_key_manager,) # Pass manager if needed by save logic
)
with col2:
st.markdown("#### Analysis Results")
# Check if we have analysis results
if 'website_analysis' in st.session_state:
results = st.session_state.website_analysis
if results.get('success', False):
data = results.get('data', {})
analysis = data.get('analysis', {})
# Create tabs for different sections
if analyze_type == "Full Analysis with SEO":
tab1, tab2, tab3, tab4, tab5 = st.tabs([
"Basic Metrics",
"Content Analysis",
"SEO Analysis",
"Technical SEO",
"Strategy"
])
else:
tab1, tab2, tab3, tab4 = st.tabs([
"Basic Metrics",
"Content Analysis",
"Technical Info",
"Strategy"
])
with tab1:
st.markdown("##### Basic Metrics")
basic_info = analysis.get('basic_info', {})
st.write(f"Status Code: {basic_info.get('status_code')}")
st.write(f"Content Type: {basic_info.get('content_type')}")
st.write(f"Title: {basic_info.get('title')}")
st.write(f"Meta Description: {basic_info.get('meta_description')}")
# SSL Info
ssl_info = analysis.get('ssl_info', {})
if ssl_info.get('has_ssl'):
st.success("SSL Certificate is valid")
st.write(f"Expiry: {ssl_info.get('expiry')}")
else:
st.error("No valid SSL certificate found")
with tab2:
st.markdown("##### Content Analysis")
content_info = analysis.get('content_info', {})
# Content Overview
st.markdown("###### 📊 Content Overview")
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Word Count", content_info.get('word_count', 0))
with col2:
st.metric("Headings", content_info.get('heading_count', 0))
with col3:
st.metric("Images", content_info.get('image_count', 0))
with col4:
st.metric("Links", content_info.get('link_count', 0))
if analyze_type == "Full Analysis with SEO":
with tab3:
st.markdown("##### SEO Analysis")
seo_data = data.get('seo_analysis', {})
# Display SEO Score
seo_score = seo_data.get('overall_score', 0)
st.markdown(f"### SEO Score: {seo_score}/100")
st.progress(seo_score / 100)
# Meta Tags Analysis
st.markdown("#### Meta Tags Analysis")
meta_analysis = seo_data.get('meta_tags', {})
for key, value in meta_analysis.items():
if isinstance(value, bool):
st.write(f"{'' if value else ''} {key.replace('_', ' ').title()}")
elif isinstance(value, dict):
st.write(f"**{key.replace('_', ' ').title()}:**")
st.write(f"Status: {value.get('status', 'N/A')}")
st.write(f"Value: {value.get('value', 'N/A')}")
if value.get('recommendation'):
st.write(f"Recommendation: {value['recommendation']}")
else:
st.write(f"**{key.replace('_', ' ').title()}:** {value}")
# Content Analysis
st.markdown("#### AI Content Analysis")
content_analysis = seo_data.get('content', {})
st.write(f"**Word Count:** {content_analysis.get('word_count', 0)}")
st.write(f"**Readability Score:** {content_analysis.get('readability_score', 0)}/100")
st.write(f"**Content Quality Score:** {content_analysis.get('content_quality_score', 0)}/100")
# Recommendations
st.markdown("#### SEO Recommendations")
recommendations = seo_data.get('recommendations', [])
for rec in recommendations:
st.write(f"**{rec.get('priority', '').upper()} Priority - {rec.get('category', '')}**")
st.write(f"Issue: {rec.get('issue', '')}")
st.write(f"Recommendation: {rec.get('recommendation', '')}")
st.write(f"Impact: {rec.get('impact', '')}")
st.write("---")
with tab4:
st.markdown("##### Technical SEO")
technical_seo = seo_data.get('technical_analysis', {})
# Mobile Friendliness
st.markdown("#### Mobile Friendliness")
mobile_friendly = technical_seo.get('mobile_friendly', False)
st.write(f"{'' if mobile_friendly else ''} Mobile Friendly")
# Page Speed
st.markdown("#### Page Speed")
speed_metrics = technical_seo.get('speed_metrics', {})
for metric, value in speed_metrics.items():
st.write(f"**{metric.replace('_', ' ').title()}:** {value}")
# Technical Issues
st.markdown("#### Technical Issues")
issues = technical_seo.get('issues', [])
for issue in issues:
st.write(f"{issue}")
with tab4 if analyze_type == "Basic Analysis" else tab5:
st.markdown("##### Strategy Recommendations")
strategy_info = analysis.get('strategy', {})
if strategy_info:
for category, recommendations in strategy_info.items():
st.markdown(f"###### {category.replace('_', ' ').title()}")
for rec in recommendations:
st.write(f"{rec}")
else:
st.info("No strategy recommendations available")
else:
error_msg = results.get('error', 'Analysis failed')
logger.error(f"[render_website_setup] Displaying error: {error_msg}")
st.error(error_msg)
else:
logger.debug("[render_website_setup] No analysis results in session state")
st.info("Enter a URL and click 'Analyze Website' to see results")
# --- Feedback Area ---
status, message = st.session_state.get(status_key, ("unsaved", ""))
feedback_placeholder = st.empty()
# Navigation buttons
if render_navigation_buttons(2, 5, True):
# Move to next step (AI Research Setup)
st.session_state.current_step = 3
st.session_state.next_step = "ai_research_setup"
st.rerun()
return {"current_step": 2, "changes_made": True}
if status == "saving":
feedback_placeholder.info("Validating URL...", icon="")
elif status == "valid":
feedback_placeholder.success(message, icon="")
elif status == "invalid_format":
feedback_placeholder.error(f"Format Error: {message}", icon="")
elif status == "unreachable":
feedback_placeholder.error(f"Reachability Error: {message}", icon="")
elif status == "ssl_error":
feedback_placeholder.warning(f"SSL Warning: {message}", icon="⚠️") # Warning for SSL
elif status == "error":
feedback_placeholder.error(f"Validation Error: {message}", icon="⚠️")
elif status == "unsaved" and current_url: # Show warning if field has text but isn't validated yet
feedback_placeholder.warning("URL not yet validated.", icon="⚠️")
# --- Removed Analysis Section ---
# The detailed website analysis should be a separate feature, not part of the initial setup validation.
st.markdown("--- ---“)
st.markdown(" *The detailed Website Analyzer tool is available separately in the main application.*")
st.info("Entering your website URL is optional. Click Continue to proceed.")
# Return value is not strictly needed if navigation relies on session state status
return {}
# Removed old analysis logic and button handling as it's handled in base.py

View File

@@ -9,7 +9,7 @@ from lib.utils.settings_page import render_settings_page
# Import social media writer functions
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.twitter_ai_writer import tweet_writer
from lib.ai_writers.twitter_writers import run_dashboard
from lib.ai_writers.insta_ai_writer import insta_writer
from lib.ai_writers.youtube_writers.youtube_ai_writer import youtube_main_menu
@@ -322,7 +322,7 @@ def setup_alwrity_ui():
social_tools_submenu = {
"Facebook": ("📘", lambda: facebook_main_menu()),
"LinkedIn": ("💼", lambda: linkedin_main_menu()),
"Twitter": ("🐦", lambda: tweet_writer()),
"Twitter": ("🐦", lambda: run_dashboard()),
"Instagram": ("📸", lambda: insta_writer()),
"YouTube": ("🎥", lambda: youtube_main_menu())
}