Resolved merge conflicts in LinkedIn README and Twitter dashboard
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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']
|
||||
@@ -339,4 +339,4 @@ def smart_tweet_generator():
|
||||
st.code(tweet_texts)
|
||||
|
||||
if __name__ == "__main__":
|
||||
smart_tweet_generator()
|
||||
smart_tweet_generator()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user