diff --git a/lib/ai_writers/linkedin_writer/README.md b/lib/ai_writers/linkedin_writer/README.md
index 7ce65837..191c8247 100644
--- a/lib/ai_writers/linkedin_writer/README.md
+++ b/lib/ai_writers/linkedin_writer/README.md
@@ -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)
diff --git a/lib/ai_writers/linkedin_writer/linkedin_ai_writer.py b/lib/ai_writers/linkedin_writer/linkedin_ai_writer.py
index 2749de5b..f48be704 100644
--- a/lib/ai_writers/linkedin_writer/linkedin_ai_writer.py
+++ b/lib/ai_writers/linkedin_writer/linkedin_ai_writer.py
@@ -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()
diff --git a/lib/ai_writers/linkedin_writer/modules/profile_optimizer/linkedin_profile_optimizer_ui.py b/lib/ai_writers/linkedin_writer/modules/profile_optimizer/linkedin_profile_optimizer_ui.py
index 551ae076..439c5d86 100644
--- a/lib/ai_writers/linkedin_writer/modules/profile_optimizer/linkedin_profile_optimizer_ui.py
+++ b/lib/ai_writers/linkedin_writer/modules/profile_optimizer/linkedin_profile_optimizer_ui.py
@@ -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}")
\ No newline at end of file
+ st.markdown(f"**{category}**")
+ for skill in skills:
+ st.write(f"- {skill}")
+ st.divider()
\ No newline at end of file
diff --git a/lib/ai_writers/twitter_ai_writer.py b/lib/ai_writers/twitter_ai_writer.py
deleted file mode 100644
index 719fd0e6..00000000
--- a/lib/ai_writers/twitter_ai_writer.py
+++ /dev/null
@@ -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)
diff --git a/lib/ai_writers/twitter_writers/tweet_generator/__init__.py b/lib/ai_writers/twitter_writers/tweet_generator/__init__.py
new file mode 100644
index 00000000..5fa6c3b7
--- /dev/null
+++ b/lib/ai_writers/twitter_writers/tweet_generator/__init__.py
@@ -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']
\ No newline at end of file
diff --git a/lib/ai_writers/twitter_writers/twitter_dashboard.py b/lib/ai_writers/twitter_writers/twitter_dashboard.py
index 5627b349..d69032ee 100644
--- a/lib/ai_writers/twitter_writers/twitter_dashboard.py
+++ b/lib/ai_writers/twitter_writers/twitter_dashboard.py
@@ -339,4 +339,4 @@ def smart_tweet_generator():
st.code(tweet_texts)
if __name__ == "__main__":
- smart_tweet_generator()
\ No newline at end of file
+ smart_tweet_generator()
diff --git a/lib/gpt_providers/text_generation/main_text_generation.py b/lib/gpt_providers/text_generation/main_text_generation.py
index d3cd3e1b..68642f77 100644
--- a/lib/gpt_providers/text_generation/main_text_generation.py
+++ b/lib/gpt_providers/text_generation/main_text_generation.py
@@ -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
diff --git a/lib/utils/alwrity_utils.py b/lib/utils/alwrity_utils.py
index 51cc726c..b4fad895 100644
--- a/lib/utils/alwrity_utils.py
+++ b/lib/utils/alwrity_utils.py
@@ -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:
diff --git a/lib/utils/api_key_manager/components/ai_providers.py b/lib/utils/api_key_manager/components/ai_providers.py
index 441ef2f5..1bc3d92e 100644
--- a/lib/utils/api_key_manager/components/ai_providers.py
+++ b/lib/utils/api_key_manager/components/ai_providers.py
@@ -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("""
-
- """, 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("""
-
-
-
-
Power your content with GPT-4 and GPT-3.5 models
-
", unsafe_allow_html=True)
-
- with col2:
- # Google Card
- with st.container():
- st.markdown("""
-
-
-
-
Leverage Google's powerful Gemini models
-
", 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("""
-
-
-
-
Access Claude for advanced content generation
-
-
- """, 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("""
-
-
-
-
Use Mistral's efficient language models
-
-
- """, 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}
\ No newline at end of file
+ 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}
diff --git a/lib/utils/api_key_manager/components/ai_providers_setup.py b/lib/utils/api_key_manager/components/ai_providers_setup.py
index 1ab045f6..198d7011 100644
--- a/lib/utils/api_key_manager/components/ai_providers_setup.py
+++ b/lib/utils/api_key_manager/components/ai_providers_setup.py
@@ -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}
diff --git a/lib/utils/api_key_manager/components/ai_research_setup.py b/lib/utils/api_key_manager/components/ai_research_setup.py
index 6eeaf853..3df488b7 100644
--- a/lib/utils/api_key_manager/components/ai_research_setup.py
+++ b/lib/utils/api_key_manager/components/ai_research_setup.py
@@ -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="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}"
-)
+# 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("""
""", 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("""
-
-
-
- Access search engine results for research
-
-
", 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("""
-
-
-
- Web content extraction and analysis
-
-
", 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("""
-
-
-
- AI-powered search with semantic understanding
-
-
", unsafe_allow_html=True)
-
- # Metaphor/Exa Card
- st.markdown("""
-
-
-
- Neural search engine for deep research
-
-
", 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("""
-
-
Bing Search API
-
Microsoft's powerful search API with web, news, and image search capabilities.
-
-
Google Search API
-
Google's programmable search engine with customizable search parameters.
-
-
These integrations are under development and will be available soon!
-
- """, 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 {}
diff --git a/lib/utils/api_key_manager/components/base.py b/lib/utils/api_key_manager/components/base.py
index 953df736..66ed8c41 100644
--- a/lib/utils/api_key_manager/components/base.py
+++ b/lib/utils/api_key_manager/components/base.py
@@ -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("""