Revert ai_research_setup.py and base.py to the state before commit 036fde9.

This commit is contained in:
ajaysi
2025-04-16 19:36:15 +05:30
parent 3a871d4de0
commit 5da0562aa5
2 changed files with 357 additions and 287 deletions

View File

@@ -4,228 +4,346 @@ import streamlit as st
from loguru import logger from loguru import logger
from typing import Dict, Any from typing import Dict, Any
from ..manager import APIKeyManager from ..manager import APIKeyManager
from .base import render_navigation_buttons
import os import os
from dotenv import load_dotenv # Keep if api_key_manager uses it from dotenv import load_dotenv
import sys import sys
# Configure logger (assuming configured elsewhere or keep minimal here) # Configure logger
logger.add(sys.stderr, level="INFO") # Keep simple example if needed logger.remove() # Remove default handler
logger.add(
# Helper function to validate a specific research provider's key "logs/ai_research_setup.log",
def _validate_research_key(provider_name: str, key_value: str) -> bool: rotation="500 MB",
"""Validate the API key for a given research provider.""" retention="10 days",
if not key_value: level="DEBUG",
logger.debug(f"Validation: Key for {provider_name} is empty.") format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
return False )
try: logger.add(
logger.debug(f"Validating key for {provider_name}...") sys.stdout,
# Ensure the function exists in validation.py before calling level="INFO",
if provider_name == "serpapi": format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
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]: def render_ai_research_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]:
"""Render the AI research setup step with immediate feedback.""" """Render the AI research setup step."""
logger.info("[render_ai_research_setup] Rendering AI research setup component") 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(""" st.markdown("""
<div class='setup-header'> <div class='setup-header'>
<h2>Step 3: Configure AI Research Tools (Optional)</h2> <h2>🔍 AI Research Setup</h2>
<p>Set up API keys for enhanced web research, crawling, and analysis. These are optional but recommended.</p> <p>Configure your AI research providers for content analysis and research</p>
</div> </div>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
# Create two columns for different search types
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
# --- SerpAPI ---
with col1: with col1:
st.subheader("SerpAPI") st.markdown("### The Usual")
st.markdown("Access real-time search engine results. Get key: [SerpAPI](https://serpapi.com)")
serpapi_key_val = api_key_manager.get_api_key("serpapi") # SerpAPI Card
st.text_input( 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(
"SerpAPI Key", "SerpAPI Key",
value=serpapi_key_val if serpapi_key_val else "",
type="password", type="password",
key="serpapi_key_input", key="serpapi_key",
on_change=_handle_research_key_change, help="Enter your SerpAPI key"
args=("serpapi", api_key_manager)
) )
# 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 --- if serpapi_key:
with col1: st.markdown("""
st.subheader("Firecrawl") <div class="ai-provider-status status-valid">
st.markdown("Web content extraction and crawling. Get key: [Firecrawl](https://www.firecrawl.dev/account)") ✓ API key configured
firecrawl_key_val = api_key_manager.get_api_key("firecrawl") </div>
st.text_input( """, 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)
# 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 API Key", "Firecrawl API Key",
value=firecrawl_key_val if firecrawl_key_val else "",
type="password", type="password",
key="firecrawl_key_input", key="firecrawl_key",
on_change=_handle_research_key_change, help="Enter your Firecrawl API key"
args=("firecrawl", api_key_manager)
) )
# 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 --- 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)
with col2: with col2:
st.subheader("Tavily AI") st.markdown("### AI Deep Research")
st.markdown("AI-powered search & summarization. Get key: [Tavily](https://tavily.com)")
tavily_key_val = api_key_manager.get_api_key("tavily") # Tavily Card
st.text_input( 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(
"Tavily API Key", "Tavily API Key",
value=tavily_key_val if tavily_key_val else "",
type="password", type="password",
key="tavily_key_input", key="tavily_key",
on_change=_handle_research_key_change, help="Enter your Tavily API key"
args=("tavily", api_key_manager)
) )
# 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 --- if tavily_key:
with col2: st.markdown("""
st.subheader("Metaphor/Exa") <div class="ai-provider-status status-valid">
st.markdown("Neural search for deep research. Get key: [Metaphor/Exa](https://metaphor.systems)") ✓ API key configured
metaphor_key_val = api_key_manager.get_api_key("metaphor") </div>
st.text_input( """, 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(
"Metaphor/Exa API Key", "Metaphor/Exa API Key",
value=metaphor_key_val if metaphor_key_val else "",
type="password", type="password",
key="metaphor_key_input", key="metaphor_key",
on_change=_handle_research_key_change, help="Enter your Metaphor/Exa API key"
args=("metaphor", api_key_manager)
) )
# 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 --- 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
with st.expander("🔜 Coming Soon - More Search Options", expanded=False): with st.expander("🔜 Coming Soon - More Search Options", expanded=False):
st.info("Integrations for Bing Search and Google Search APIs are planned.") 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>
st.info("You can skip this step if you don't need these research tools. Click Continue to proceed.") <h4>Google Search API</h4>
<p>Google's programmable search engine with customizable search parameters.</p>
return {} <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}

View File

@@ -3,17 +3,16 @@
import streamlit as st import streamlit as st
from typing import Dict, Any from typing import Dict, Any
from loguru import logger from loguru import logger
from ..styles import API_KEY_MANAGER_STYLES # Assuming styles are correctly imported from ..styles import API_KEY_MANAGER_STYLES
from ..wizard_state import ( from ..wizard_state import (
get_current_step, # Keep if used elsewhere get_current_step,
next_step, # Keep if used elsewhere next_step,
previous_step, # Keep if used elsewhere previous_step,
can_proceed_to_next_step # Keep if used elsewhere can_proceed_to_next_step
) )
def render_step_indicator(current_step: int, total_steps: int) -> None: def render_step_indicator(current_step: int, total_steps: int) -> None:
"""Render the step indicator.""" """Render the step indicator."""
# Existing step indicator code... (Keep as is)
try: try:
st.markdown(""" st.markdown("""
<style> <style>
@@ -68,11 +67,11 @@ def render_step_indicator(current_step: int, total_steps: int) -> None:
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
steps = [ steps = [
("🔑", "AI Providers", 1), ("🔑", "AI LLM", 1),
("🌐", "Website Setup", 2), ("🤖", "Website Setup", 2),
("🔍", "AI Research", 3), ("👤", "AI Research", 3),
("🎨", "Personalization", 4), ("🎨", "Personalization", 4),
("🔗", "Integrations", 5), ("🔄", "Integrations", 5),
("", "Complete", 6) ("", "Complete", 6)
] ]
@@ -98,10 +97,18 @@ def render_step_indicator(current_step: int, total_steps: int) -> None:
logger.error(f"Error rendering step indicator: {str(e)}") logger.error(f"Error rendering step indicator: {str(e)}")
st.error("Error displaying step indicator") st.error("Error displaying step indicator")
def render_navigation_buttons(current_step: int, total_steps: int) -> None: def render_navigation_buttons(current_step: int, total_steps: int, changes_made: bool = False) -> bool:
"""Render the navigation buttons with validation logic for steps 1 and 3.""" """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
"""
col1, col2, col3 = st.columns([1, 2, 1]) col1, col2, col3 = st.columns([1, 2, 1])
proceed_error_placeholder = col2.empty() # Placeholder for error message
with col1: with col1:
if current_step > 1: if current_step > 1:
@@ -112,72 +119,18 @@ def render_navigation_buttons(current_step: int, total_steps: int) -> None:
with col3: with col3:
if current_step < total_steps: if current_step < total_steps:
next_text = "**Continue →**" next_text = "**Continue →**"
button_disabled = False if st.button(next_text, use_container_width=True, disabled=not changes_made, key="next_button"):
error_message = "" 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
if current_step == 1: return False
# --- 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: def render_tab_style() -> None:
"""Render enhanced tab styling.""" """Render enhanced tab styling."""
# Existing tab style code... (Keep as is)
st.markdown(""" st.markdown("""
<style> <style>
.stTabs [data-baseweb="tab-list"] { .stTabs [data-baseweb="tab-list"] {
@@ -222,7 +175,6 @@ def render_tab_style() -> None:
def render_success_message(): def render_success_message():
"""Render the success message with glassmorphic design.""" """Render the success message with glassmorphic design."""
# Existing success message code... (Keep as is)
st.markdown(""" st.markdown("""
<div class="success-message"> <div class="success-message">
<h3 style='color: white; margin-bottom: 12px; font-size: 1.4em;'>✅ API keys saved successfully!</h3> <h3 style='color: white; margin-bottom: 12px; font-size: 1.4em;'>✅ API keys saved successfully!</h3>