MindMap / src /streamlit_app.py
daemon03's picture
inital commit
240e5bc
"""
Technical Mindmap Generator - Main Streamlit Application
Interactive web app for generating radial mindmaps from technical keywords
Features:
- Modern UI inspired by GPT, Claude, and Perplexity
- Sequential API calls for optimal data quality
- Interactive radial mindmaps with PyVis
- Query history and session management
- Real-time data from Tavily, Knowledge Graph, and Gemini APIs
"""
import streamlit as st
import sys
from pathlib import Path
import time
# Add project root to path
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
# Import project modules
try:
from config.settings import settings, display_settings_info
from utils.api_handler import fetch_mindmap_data
from utils.mindmap_generator import MindmapGenerator
except ImportError as e:
st.error(f"Import Error: {e}")
st.info("Make sure all project files are in the correct directories")
st.stop()
# ============================================================================
# PAGE CONFIGURATION
# ============================================================================
st.set_page_config(
page_title="Technical Mindmap Generator 🧠",
page_icon="🧠",
layout="wide",
initial_sidebar_state="collapsed",
menu_items={
'Get Help': None,
'Report a bug': None,
'About': "# Technical Mindmap Generator\nPowered by Gemini, Tavily, and Knowledge Graph APIs"
}
)
# ============================================================================
# CUSTOM CSS STYLING
# ============================================================================
st.markdown("""
<style>
/* Import Google Fonts - Claude uses serif fonts */
@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700&family=Inter:wght@400;500;600;700&display=swap');
/* ============================================================
MAIN CONTAINER - Claude's Warm Aesthetic
============================================================ */
.main {
background: #faf9f5;
padding: 2rem;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Remove default padding */
.block-container {
padding-top: 2rem;
padding-bottom: 3rem;
max-width: 1000px;
}
/* ============================================================
HEADER SECTION
============================================================ */
h1 {
color: #3d3929 !important;
text-align: center !important;
font-size: 3.2rem !important;
font-weight: 700 !important;
margin-bottom: 0.5rem !important;
letter-spacing: -0.5px !important;
font-family: 'Merriweather', serif !important;
}
/* Subtitle */
.subtitle {
color: #6b6656 !important;
text-align: center !important;
font-size: 1.2rem !important;
margin-bottom: 3rem !important;
font-weight: 400 !important;
font-family: 'Inter', sans-serif !important;
line-height: 1.6 !important;
}
/* ============================================================
INPUT CONTAINER - Claude's Clean Design
============================================================ */
.stTextInput > div > div > input {
background: #ffffff !important;
color: #3d3929 !important;
border-radius: 12px !important;
padding: 18px 24px !important;
font-size: 16px !important;
font-weight: 400 !important;
border: 1.5px solid #ddd9cc !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06) !important;
transition: all 0.2s ease !important;
font-family: 'Inter', sans-serif !important;
}
.stTextInput > div > div > input:focus {
border-color: #c15f3c !important;
box-shadow: 0 0 0 3px rgba(193, 95, 60, 0.1),
0 1px 3px rgba(0, 0, 0, 0.06) !important;
outline: none !important;
}
.stTextInput > div > div > input::placeholder {
color: #9b9688;
font-weight: 400;
}
/* ============================================================
BUTTON STYLING - Claude's Orange
============================================================ */
.stButton > button {
background: #c15f3c !important;
color: #ffffff !important;
border-radius: 10px !important;
padding: 14px 32px !important;
font-size: 15px !important;
font-weight: 600 !important;
border: none !important;
box-shadow: 0 2px 4px rgba(193, 95, 60, 0.2) !important;
transition: all 0.2s ease !important;
font-family: 'Inter', sans-serif !important;
letter-spacing: 0.3px !important;
}
.stButton > button:hover {
background: #a14a2f !important;
box-shadow: 0 4px 8px rgba(193, 95, 60, 0.25) !important;
transform: translateY(-1px) !important;
}
.stButton > button:active {
transform: translateY(0) !important;
box-shadow: 0 1px 2px rgba(193, 95, 60, 0.2) !important;
}
/* ============================================================
MINDMAP CARD - Clean White Container
============================================================ */
.mindmap-card {
background: #ffffff;
border-radius: 16px;
padding: 2.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08),
0 1px 2px rgba(0, 0, 0, 0.04);
margin: 2rem 0;
border: 1px solid #ebe9e0;
}
.mindmap-card h3 {
color: #3d3929 !important;
font-weight: 700 !important;
margin-bottom: 1rem !important;
font-size: 1.6rem !important;
font-family: 'Merriweather', serif !important;
}
.mindmap-card p {
color: #6b6656;
font-size: 1rem;
margin-bottom: 1.5rem;
line-height: 1.6;
}
/* ============================================================
SUCCESS/INFO/ERROR MESSAGES - Claude Style
============================================================ */
.stSuccess {
background: #f0f8f5 !important;
border-radius: 12px !important;
padding: 1.2rem !important;
border-left: 4px solid #4a9d7f !important;
color: #3d3929 !important;
font-family: 'Inter', sans-serif !important;
}
.stInfo {
background: #f5f3ed !important;
border-radius: 12px !important;
padding: 1.2rem !important;
border-left: 4px solid #c15f3c !important;
color: #3d3929 !important;
font-family: 'Inter', sans-serif !important;
}
.stError {
background: #fef2f2 !important;
border-radius: 12px !important;
padding: 1.2rem !important;
border-left: 4px solid #dc2626 !important;
color: #3d3929 !important;
font-family: 'Inter', sans-serif !important;
}
.stWarning {
background: #fef9f3 !important;
border-radius: 12px !important;
padding: 1.2rem !important;
border-left: 4px solid #ea9d3e !important;
color: #3d3929 !important;
font-family: 'Inter', sans-serif !important;
}
/* ============================================================
SPINNER STYLING
============================================================ */
.stSpinner > div {
border-color: #c15f3c transparent #c15f3c transparent !important;
}
/* ============================================================
INFO BOX - Claude's Warm Design
============================================================ */
.info-box {
background: #ffffff;
border-left: 4px solid #c15f3c;
border-radius: 12px;
padding: 2rem;
color: #3d3929;
margin: 2.5rem 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #ebe9e0;
border-left: 4px solid #c15f3c;
}
.info-box h3 {
color: #c15f3c !important;
margin-bottom: 1.5rem !important;
font-weight: 700 !important;
font-family: 'Merriweather', serif !important;
}
.info-box ol {
margin-left: 1.5rem;
line-height: 1.8;
}
.info-box li {
margin: 1rem 0;
font-size: 1rem;
color: #3d3929;
}
.info-box p {
line-height: 1.7;
font-size: 1rem;
color: #3d3929;
}
.info-box strong {
color: #3d3929;
font-weight: 600;
}
.info-box em {
color: #6b6656;
font-style: italic;
}
/* ============================================================
EXPANDER STYLING
============================================================ */
.streamlit-expanderHeader {
background: #f9f8f4 !important;
border-radius: 10px !important;
font-weight: 600 !important;
padding: 1rem 1.5rem !important;
border: 1px solid #ebe9e0 !important;
color: #3d3929 !important;
font-family: 'Inter', sans-serif !important;
}
.streamlit-expanderHeader:hover {
background: #f5f3ed !important;
border-color: #ddd9cc !important;
}
/* ============================================================
METRICS - Claude Style
============================================================ */
[data-testid="stMetricValue"] {
font-size: 2rem !important;
color: #c15f3c !important;
font-weight: 700 !important;
font-family: 'Inter', sans-serif !important;
}
[data-testid="stMetricLabel"] {
font-size: 0.9rem !important;
font-weight: 600 !important;
color: #6b6656 !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
}
/* ============================================================
SIDEBAR - Warm Neutral
============================================================ */
[data-testid="stSidebar"] {
background: #eeece2;
border-right: 1px solid #ddd9cc;
}
[data-testid="stSidebar"] .stButton > button {
background: #ffffff !important;
border: 1.5px solid #ddd9cc !important;
color: #3d3929 !important;
border-radius: 10px !important;
padding: 10px 16px !important;
font-size: 0.9rem !important;
font-weight: 600 !important;
transition: all 0.2s ease !important;
font-family: 'Inter', sans-serif !important;
}
[data-testid="stSidebar"] .stButton > button:hover {
background: #f5f3ed !important;
border-color: #c15f3c !important;
color: #c15f3c !important;
}
[data-testid="stSidebar"] h1 {
font-size: 1.3rem !important;
color: #3d3929 !important;
margin-bottom: 1rem !important;
font-family: 'Merriweather', serif !important;
}
[data-testid="stSidebar"] .stMarkdown {
color: #3d3929;
font-family: 'Inter', sans-serif;
}
/* ============================================================
HORIZONTAL RULE
============================================================ */
hr {
border: none;
height: 1px;
background: #ebe9e0;
margin: 2rem 0;
}
/* ============================================================
LINKS - Claude Orange
============================================================ */
a {
color: #c15f3c !important;
text-decoration: none !important;
font-weight: 500 !important;
transition: color 0.2s ease !important;
}
a:hover {
color: #a14a2f !important;
text-decoration: underline !important;
}
/* ============================================================
HIDE STREAMLIT BRANDING
============================================================ */
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
header {visibility: hidden;}
/* ============================================================
CUSTOM SCROLLBAR - Claude Style
============================================================ */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f5f3ed;
}
::-webkit-scrollbar-thumb {
background: #c15f3c;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a14a2f;
}
/* ============================================================
TYPOGRAPHY IMPROVEMENTS
============================================================ */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: #3d3929;
line-height: 1.6;
}
h2, h3, h4, h5, h6 {
font-family: 'Merriweather', serif;
color: #3d3929;
}
/* ============================================================
RESPONSIVE DESIGN
============================================================ */
@media (max-width: 768px) {
h1 {
font-size: 2.2rem !important;
}
.subtitle {
font-size: 1rem !important;
}
.mindmap-card {
padding: 1.5rem;
}
.stTextInput > div > div > input {
font-size: 15px !important;
padding: 14px 18px !important;
}
.stButton > button {
font-size: 14px !important;
padding: 12px 24px !important;
}
}
/* ============================================================
SMOOTH INTERACTIONS
============================================================ */
* {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
html {
scroll-behavior: smooth;
}
/* ============================================================
CLEAN, MINIMAL AESTHETIC
============================================================ */
.stApp {
background: #faf9f5;
}
/* Remove unnecessary decorations */
.stDeployButton {
display: none;
}
</style>
""", unsafe_allow_html=True)
# ============================================================================
# SESSION STATE MANAGEMENT
# ============================================================================
def initialize_session_state():
"""Initialize all session state variables"""
if 'mindmap_history' not in st.session_state:
st.session_state.mindmap_history = []
if 'current_mindmap' not in st.session_state:
st.session_state.current_mindmap = None
if 'query_count' not in st.session_state:
st.session_state.query_count = 0
if 'last_keyword' not in st.session_state:
st.session_state.last_keyword = ""
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
def validate_api_keys() -> tuple[bool, list[str]]:
"""
Validate that all required API keys are configured
Returns:
Tuple of (is_valid, missing_keys_list)
"""
return settings.validate_api_keys()
def generate_mindmap(keyword: str) -> dict:
"""
Generate mindmap for given keyword using API handler
Args:
keyword: Technical keyword to analyze
Returns:
Dictionary with mindmap data and metadata
"""
try:
result = fetch_mindmap_data(
keyword=keyword,
gemini_key=settings.gemini_api_key,
tavily_key=settings.tavily_api_key,
kg_api_key=settings.google_cloud_api_key
)
return result
except Exception as e:
st.error(f"Error generating mindmap: {str(e)}")
return None
# ============================================================================
# MAIN APPLICATION
# ============================================================================
def main():
"""Main application logic"""
# Initialize session state
initialize_session_state()
# ========================================================================
# HEADER SECTION
# ========================================================================
st.markdown("<h1>🧠 Technical Mindmap Generator</h1>", unsafe_allow_html=True)
st.markdown(
"<p class='subtitle'>Transform technical keywords into interactive visual mindmaps powered by AI</p>",
unsafe_allow_html=True
)
# ========================================================================
# API KEY VALIDATION
# ========================================================================
is_valid, missing_keys = validate_api_keys()
if not is_valid:
st.error(f"⚠️ Missing API Keys: {', '.join(missing_keys)}")
st.info("Please configure your API keys in the `.env` file before using the application.")
with st.expander("πŸ“ How to set up API keys"):
st.markdown("""
1. Create a `.env` file in the project root
2. Add your API keys:
```
GEMINI_API_KEY=your_gemini_key
TAVILY_API_KEY=your_tavily_key
```
3. Restart the application
**Get API Keys:**
- Gemini: https://ai.google.dev/
- Tavily: https://tavily.com/
- Google Cloud: https://console.cloud.google.com/
""")
st.stop()
# ========================================================================
# INPUT SECTION
# ========================================================================
# Center the input field
col1, col2, col3 = st.columns([1, 3, 1])
with col2:
keyword = st.text_input(
"",
placeholder="e.g., Machine Learning, Blockchain, Kubernetes, Neural Networks...",
key=f"keyword_input_{st.session_state.query_count}",
label_visibility="collapsed",
help="Enter any technical keyword or concept"
)
# Center the button
btn_col1, btn_col2, btn_col3 = st.columns([1, 1, 1])
with btn_col2:
generate_button = st.button(
"πŸš€ Generate Mindmap",
use_container_width=True,
type="primary"
)
# ========================================================================
# MINDMAP GENERATION
# ========================================================================
if generate_button and keyword:
# Validate keyword
if len(keyword.strip()) < 2:
st.warning("⚠️ Please enter a longer keyword (at least 2 characters)")
else:
# Show loading animation
with st.spinner(f"πŸ” Analyzing '{keyword}'...\n\n⚑ This may take 10-15 seconds..."):
# Add progress messages
progress_placeholder = st.empty()
progress_placeholder.info("πŸ“‘ Step 1/3: Fetching web data from Tavily...")
time.sleep(1)
progress_placeholder.info("πŸ”— Step 2/3: Querying Knowledge Graph...")
time.sleep(1)
progress_placeholder.info("πŸ€– Step 3/3: Synthesizing with Gemini AI...")
# Generate mindmap
result = generate_mindmap(keyword)
# Clear progress messages
progress_placeholder.empty()
if result:
# Store in session state
st.session_state.current_mindmap = result
st.session_state.mindmap_history.append({
'keyword': keyword,
'data': result,
'timestamp': time.time()
})
st.session_state.query_count += 1
st.session_state.last_keyword = keyword
st.success(f"βœ… Mindmap generated successfully for '{keyword}'!")
else:
st.error("❌ Failed to generate mindmap. Please try again.")
# ========================================================================
# DISPLAY MINDMAP
# ========================================================================
if st.session_state.current_mindmap:
st.markdown("<div class='mindmap-card'>", unsafe_allow_html=True)
# Display mindmap
mindmap_data = st.session_state.current_mindmap['mindmap']
st.markdown(f"### πŸ—ΊοΈ Mindmap: {st.session_state.last_keyword}")
st.markdown("*Zoom, pan, and hover over nodes for details*")
try:
generator = MindmapGenerator(height="700px", width="100%")
generator.render_in_streamlit(mindmap_data)
except Exception as e:
st.error(f"Error rendering mindmap: {e}")
st.markdown("</div>", unsafe_allow_html=True)
# ====================================================================
# METADATA SECTION
# ====================================================================
with st.expander("πŸ“Š View Generation Details", expanded=False):
metadata = st.session_state.current_mindmap['metadata']
# Metrics in columns
metric_col1, metric_col2, metric_col3, metric_col4 = st.columns(4)
with metric_col1:
st.metric("🎯 Keyword", metadata['keyword'])
with metric_col2:
st.metric("πŸ”— Total Nodes", metadata.get('total_nodes', 0))
with metric_col3:
st.metric("πŸ“š Sources", len(metadata.get('tavily_sources', [])))
with metric_col4:
st.metric("πŸ” KG Entities", metadata.get('kg_entities_count', 0))
st.markdown("---")
# Display sources
if metadata.get('tavily_sources'):
st.subheader("πŸ“š Information Sources")
for i, source in enumerate(metadata['tavily_sources'][:8], 1):
st.markdown(f"{i}. [{source}]({source})")
else:
# ====================================================================
# WELCOME / INFO SECTION
# ====================================================================
st.markdown("""
<div class='info-box'>
<h3>πŸ’‘ How it works:</h3>
<ol>
<li><strong>Tavily API</strong>: Gathers real-time web context and discovers related terms</li>
<li><strong>Knowledge Graph API</strong>: Provides structured entity relationships and descriptions</li>
<li><strong>Gemini AI</strong>: Synthesizes all data into a comprehensive mindmap structure</li>
</ol>
<p style='margin-top:1.5rem; font-size:1.1rem;'>
<strong>Simply enter a technical keyword above and click "Generate Mindmap" to begin!</strong>
</p>
<p style='margin-top:1rem; color:#95e1d3;'>
πŸ’‘ <em>Tip: Try keywords like "Kubernetes", "Machine Learning", "Quantum Computing", or "Blockchain"</em>
</p>
</div>
""", unsafe_allow_html=True)
# ========================================================================
# SIDEBAR - HISTORY & SETTINGS
# ========================================================================
with st.sidebar:
st.title("πŸ“œ Query History")
if st.session_state.mindmap_history:
st.markdown(f"**Total Queries:** {len(st.session_state.mindmap_history)}")
st.markdown("---")
# Display history in reverse order (most recent first)
for i, item in enumerate(reversed(st.session_state.mindmap_history)):
keyword_display = item['keyword']
# Create button with emoji
if st.button(
f"πŸ”„ {keyword_display}",
key=f"history_{i}",
help=f"Load mindmap for '{keyword_display}'"
):
st.session_state.current_mindmap = item['data']
st.session_state.last_keyword = item['keyword']
st.rerun()
else:
st.info("No queries yet.\nGenerate your first mindmap!")
st.markdown("---")
# Settings section
st.markdown("### βš™οΈ Settings")
st.markdown(f"**Max Nodes:** {settings.max_nodes}")
st.markdown(f"**Max Depth:** {settings.max_depth}")
st.markdown(f"**Caching:** {'Enabled' if settings.cache_enabled else 'Disabled'}")
st.markdown("---")
# About section
with st.expander("ℹ️ About"):
st.markdown("""
**Technical Mindmap Generator**
Version 1.0.0
A proof-of-concept application that creates interactive visual mindmaps for technical concepts using AI-powered APIs.
**Technologies:**
- Streamlit
- PyVis
- Gemini AI
- Tavily Search
- Knowledge Graph
**Created by:** Your Name
""")
# ============================================================================
# APPLICATION ENTRY POINT
# ============================================================================
if __name__ == "__main__":
main()