7eb72ab549
Replace sidebar navigation with horizontal tabs and add comprehensive CSS styling with dark theme, glassmorphism cards, gradient accents, and improved visual hierarchy. Updates all page components with consistent modern design language. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
779 lines
24 KiB
Python
779 lines
24 KiB
Python
"""SPARC Visualization Dashboard.
|
||
|
||
A Streamlit-based dashboard for visualizing patent analysis results.
|
||
Run with: streamlit run dashboard.py
|
||
"""
|
||
|
||
import streamlit as st
|
||
import plotly.express as px
|
||
import plotly.graph_objects as go
|
||
import pandas as pd
|
||
from datetime import datetime, timedelta
|
||
|
||
from SPARC.analyzer import CompanyAnalyzer
|
||
from SPARC.database import DatabaseClient
|
||
from SPARC import config
|
||
|
||
|
||
st.set_page_config(
|
||
page_title="SPARC Dashboard",
|
||
page_icon="⚡",
|
||
layout="wide",
|
||
initial_sidebar_state="collapsed",
|
||
)
|
||
|
||
# Modern CSS styling
|
||
st.markdown("""
|
||
<style>
|
||
/* Hide default Streamlit elements */
|
||
#MainMenu {visibility: hidden;}
|
||
footer {visibility: hidden;}
|
||
header {visibility: hidden;}
|
||
|
||
/* Root variables for theming */
|
||
:root {
|
||
--primary: #6366f1;
|
||
--primary-dark: #4f46e5;
|
||
--secondary: #0ea5e9;
|
||
--success: #10b981;
|
||
--warning: #f59e0b;
|
||
--error: #ef4444;
|
||
--bg-dark: #0f172a;
|
||
--bg-card: #1e293b;
|
||
--bg-card-hover: #334155;
|
||
--text-primary: #f8fafc;
|
||
--text-secondary: #94a3b8;
|
||
--border: #334155;
|
||
}
|
||
|
||
/* Main app background */
|
||
.stApp {
|
||
background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
|
||
}
|
||
|
||
/* Top navigation bar */
|
||
.nav-container {
|
||
background: rgba(30, 41, 59, 0.8);
|
||
backdrop-filter: blur(12px);
|
||
border-bottom: 1px solid rgba(99, 102, 241, 0.2);
|
||
padding: 1rem 2rem;
|
||
margin: -1rem -1rem 2rem -1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.nav-brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.nav-brand h1 {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
background: linear-gradient(135deg, #6366f1, #0ea5e9);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
margin: 0;
|
||
}
|
||
|
||
.nav-brand span {
|
||
font-size: 0.75rem;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.1em;
|
||
}
|
||
|
||
/* Card styling */
|
||
.modern-card {
|
||
background: rgba(30, 41, 59, 0.6);
|
||
backdrop-filter: blur(8px);
|
||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||
border-radius: 16px;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1rem;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.modern-card:hover {
|
||
border-color: rgba(99, 102, 241, 0.4);
|
||
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.15);
|
||
}
|
||
|
||
/* Metric cards */
|
||
.metric-card {
|
||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(14, 165, 233, 0.1));
|
||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||
border-radius: 12px;
|
||
padding: 1.25rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 2rem;
|
||
font-weight: 700;
|
||
background: linear-gradient(135deg, #6366f1, #0ea5e9);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
|
||
.metric-label {
|
||
font-size: 0.875rem;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
/* Section headers */
|
||
.section-header {
|
||
font-size: 1.25rem;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
margin-bottom: 1rem;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 2px solid rgba(99, 102, 241, 0.3);
|
||
}
|
||
|
||
/* Input fields */
|
||
.stTextInput > div > div > input,
|
||
.stTextArea > div > div > textarea {
|
||
background: rgba(30, 41, 59, 0.8) !important;
|
||
border: 1px solid rgba(99, 102, 241, 0.3) !important;
|
||
border-radius: 10px !important;
|
||
color: var(--text-primary) !important;
|
||
padding: 0.75rem 1rem !important;
|
||
}
|
||
|
||
.stTextInput > div > div > input:focus,
|
||
.stTextArea > div > div > textarea:focus {
|
||
border-color: var(--primary) !important;
|
||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2) !important;
|
||
}
|
||
|
||
/* Buttons */
|
||
.stButton > button {
|
||
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
|
||
color: white !important;
|
||
border: none !important;
|
||
border-radius: 10px !important;
|
||
padding: 0.75rem 1.5rem !important;
|
||
font-weight: 600 !important;
|
||
transition: all 0.3s ease !important;
|
||
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.3) !important;
|
||
}
|
||
|
||
.stButton > button:hover {
|
||
transform: translateY(-2px) !important;
|
||
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4) !important;
|
||
}
|
||
|
||
/* Tabs styling */
|
||
.stTabs [data-baseweb="tab-list"] {
|
||
background: rgba(30, 41, 59, 0.6);
|
||
border-radius: 12px;
|
||
padding: 0.5rem;
|
||
gap: 0.5rem;
|
||
border: 1px solid rgba(99, 102, 241, 0.15);
|
||
}
|
||
|
||
.stTabs [data-baseweb="tab"] {
|
||
background: transparent;
|
||
border-radius: 8px;
|
||
color: var(--text-secondary);
|
||
padding: 0.75rem 1.5rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.stTabs [aria-selected="true"] {
|
||
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
|
||
color: white !important;
|
||
}
|
||
|
||
.stTabs [data-baseweb="tab-border"] {
|
||
display: none;
|
||
}
|
||
|
||
.stTabs [data-baseweb="tab-highlight"] {
|
||
display: none;
|
||
}
|
||
|
||
/* Expander styling */
|
||
.streamlit-expanderHeader {
|
||
background: rgba(30, 41, 59, 0.6) !important;
|
||
border: 1px solid rgba(99, 102, 241, 0.15) !important;
|
||
border-radius: 10px !important;
|
||
color: var(--text-primary) !important;
|
||
}
|
||
|
||
.streamlit-expanderContent {
|
||
background: rgba(30, 41, 59, 0.4) !important;
|
||
border: 1px solid rgba(99, 102, 241, 0.1) !important;
|
||
border-top: none !important;
|
||
border-radius: 0 0 10px 10px !important;
|
||
}
|
||
|
||
/* Slider */
|
||
.stSlider > div > div > div {
|
||
background: var(--primary) !important;
|
||
}
|
||
|
||
/* Select box */
|
||
.stSelectbox > div > div {
|
||
background: rgba(30, 41, 59, 0.8) !important;
|
||
border: 1px solid rgba(99, 102, 241, 0.3) !important;
|
||
border-radius: 10px !important;
|
||
}
|
||
|
||
/* Progress bar */
|
||
.stProgress > div > div > div {
|
||
background: linear-gradient(90deg, #6366f1, #0ea5e9) !important;
|
||
}
|
||
|
||
/* Alerts */
|
||
.stAlert {
|
||
border-radius: 10px !important;
|
||
border: none !important;
|
||
}
|
||
|
||
/* Metrics */
|
||
[data-testid="stMetricValue"] {
|
||
background: linear-gradient(135deg, #6366f1, #0ea5e9);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
font-weight: 700;
|
||
}
|
||
|
||
[data-testid="stMetricLabel"] {
|
||
color: var(--text-secondary) !important;
|
||
}
|
||
|
||
/* Plotly charts */
|
||
.js-plotly-plot {
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Status badges */
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 9999px;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.status-success {
|
||
background: rgba(16, 185, 129, 0.2);
|
||
color: #10b981;
|
||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||
}
|
||
|
||
.status-warning {
|
||
background: rgba(245, 158, 11, 0.2);
|
||
color: #f59e0b;
|
||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||
}
|
||
|
||
.status-error {
|
||
background: rgba(239, 68, 68, 0.2);
|
||
color: #ef4444;
|
||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||
}
|
||
|
||
/* Dividers */
|
||
hr {
|
||
border: none;
|
||
border-top: 1px solid rgba(99, 102, 241, 0.2);
|
||
margin: 1.5rem 0;
|
||
}
|
||
|
||
/* Info boxes */
|
||
.info-box {
|
||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(14, 165, 233, 0.05));
|
||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||
border-radius: 12px;
|
||
padding: 1rem 1.25rem;
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
/* Feature list */
|
||
.feature-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.75rem;
|
||
padding: 0.75rem 0;
|
||
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
|
||
}
|
||
|
||
.feature-icon {
|
||
color: var(--primary);
|
||
font-size: 1.25rem;
|
||
}
|
||
</style>
|
||
""", unsafe_allow_html=True)
|
||
|
||
|
||
@st.cache_resource
|
||
def get_analyzer():
|
||
"""Get or create the CompanyAnalyzer instance."""
|
||
return CompanyAnalyzer()
|
||
|
||
|
||
@st.cache_resource
|
||
def get_db_client():
|
||
"""Get database client if available."""
|
||
if config.use_database:
|
||
try:
|
||
client = DatabaseClient()
|
||
client.connect()
|
||
return client
|
||
except Exception:
|
||
return None
|
||
return None
|
||
|
||
|
||
def render_header():
|
||
"""Render the modern dashboard header."""
|
||
st.markdown("""
|
||
<div class="nav-container">
|
||
<div class="nav-brand">
|
||
<h1>⚡ SPARC</h1>
|
||
<span>Semiconductor Patent Analytics</span>
|
||
</div>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
|
||
|
||
def render_navigation():
|
||
"""Render horizontal tab navigation at the top."""
|
||
tabs = st.tabs(["🔍 Company Analysis", "📦 Batch Analysis", "📊 Analytics", "ℹ️ About"])
|
||
return tabs
|
||
|
||
|
||
def render_company_analysis():
|
||
"""Render single company analysis page."""
|
||
st.markdown('<p class="section-header">Single Company Analysis</p>', unsafe_allow_html=True)
|
||
st.markdown("Analyze a company's patent portfolio using AI-powered insights.")
|
||
|
||
st.markdown("")
|
||
|
||
# Search card
|
||
with st.container():
|
||
col1, col2 = st.columns([3, 1])
|
||
|
||
with col1:
|
||
company_name = st.text_input(
|
||
"Company Name",
|
||
placeholder="Enter company name (e.g., nvidia, intel, amd)",
|
||
help="Enter the company name to analyze their patent portfolio",
|
||
label_visibility="collapsed",
|
||
)
|
||
|
||
with col2:
|
||
analyze_btn = st.button("🔍 Analyze", type="primary", use_container_width=True)
|
||
|
||
if analyze_btn and company_name:
|
||
with st.spinner(f"Analyzing {company_name}..."):
|
||
analyzer = get_analyzer()
|
||
result = analyzer._analyze_company_safe(company_name)
|
||
|
||
if result.success:
|
||
st.success(f"✓ Analysis complete for {company_name.upper()}")
|
||
|
||
st.markdown("")
|
||
|
||
# Metrics row with custom styling
|
||
col1, col2, col3 = st.columns(3)
|
||
with col1:
|
||
st.metric("Patents Found", result.patent_count)
|
||
with col2:
|
||
st.metric("Analysis Status", "Complete")
|
||
with col3:
|
||
st.metric("Timestamp", result.timestamp.strftime("%H:%M:%S"))
|
||
|
||
st.markdown("")
|
||
|
||
# Analysis content in a styled container
|
||
st.markdown('<p class="section-header">AI Analysis Results</p>', unsafe_allow_html=True)
|
||
with st.container():
|
||
st.markdown(result.analysis)
|
||
|
||
else:
|
||
st.error(f"Analysis failed: {result.error}")
|
||
|
||
elif not company_name and analyze_btn:
|
||
st.warning("Please enter a company name to analyze.")
|
||
|
||
|
||
def render_batch_analysis():
|
||
"""Render batch analysis page."""
|
||
st.markdown('<p class="section-header">Batch Company Analysis</p>', unsafe_allow_html=True)
|
||
st.markdown("Analyze multiple companies simultaneously for comparative insights.")
|
||
|
||
st.markdown("")
|
||
|
||
# Input section
|
||
col1, col2 = st.columns([2, 1])
|
||
|
||
with col1:
|
||
companies_input = st.text_area(
|
||
"Company Names",
|
||
placeholder="Enter company names (one per line or comma-separated):\nnvidia\namd\nintel\nqualcomm",
|
||
height=150,
|
||
label_visibility="collapsed",
|
||
)
|
||
|
||
with col2:
|
||
st.markdown("**Configuration**")
|
||
max_workers = st.slider("Concurrent Workers", 1, 5, 3, help="Number of parallel analysis threads")
|
||
st.markdown("")
|
||
analyze_btn = st.button(
|
||
"🚀 Run Batch Analysis", type="primary", use_container_width=True
|
||
)
|
||
|
||
if analyze_btn and companies_input:
|
||
# Parse company names
|
||
companies = [
|
||
c.strip()
|
||
for c in companies_input.replace(",", "\n").split("\n")
|
||
if c.strip()
|
||
]
|
||
|
||
if not companies:
|
||
st.warning("Please enter at least one company name")
|
||
return
|
||
|
||
st.info(f"🔄 Starting analysis of {len(companies)} companies...")
|
||
|
||
# Progress tracking
|
||
progress_bar = st.progress(0)
|
||
status_text = st.empty()
|
||
|
||
analyzer = get_analyzer()
|
||
|
||
def update_progress(company: str, completed: int, total: int):
|
||
progress = completed / total
|
||
progress_bar.progress(progress)
|
||
status_text.text(f"Analyzing {company}... ({completed}/{total})")
|
||
|
||
result = analyzer.analyze_companies(
|
||
companies=companies,
|
||
max_workers=max_workers,
|
||
progress_callback=update_progress,
|
||
)
|
||
|
||
progress_bar.progress(1.0)
|
||
status_text.text("✓ Analysis complete!")
|
||
|
||
st.markdown("")
|
||
|
||
# Summary metrics
|
||
st.markdown('<p class="section-header">Results Summary</p>', unsafe_allow_html=True)
|
||
col1, col2, col3, col4 = st.columns(4)
|
||
with col1:
|
||
st.metric("Total Companies", result.total_companies)
|
||
with col2:
|
||
st.metric("Successful", result.successful)
|
||
with col3:
|
||
st.metric("Failed", result.failed)
|
||
with col4:
|
||
success_rate = (
|
||
(result.successful / result.total_companies * 100)
|
||
if result.total_companies > 0
|
||
else 0
|
||
)
|
||
st.metric("Success Rate", f"{success_rate:.1f}%")
|
||
|
||
# Results chart
|
||
if result.results:
|
||
df = pd.DataFrame(
|
||
[
|
||
{
|
||
"Company": r.company_name.upper(),
|
||
"Patents": r.patent_count,
|
||
"Status": "Success" if r.success else "Failed",
|
||
}
|
||
for r in result.results
|
||
]
|
||
)
|
||
|
||
fig = px.bar(
|
||
df,
|
||
x="Company",
|
||
y="Patents",
|
||
color="Status",
|
||
color_discrete_map={"Success": "#10b981", "Failed": "#ef4444"},
|
||
title="",
|
||
)
|
||
fig.update_layout(
|
||
plot_bgcolor="rgba(0,0,0,0)",
|
||
paper_bgcolor="rgba(0,0,0,0)",
|
||
font_color="#94a3b8",
|
||
legend=dict(
|
||
orientation="h",
|
||
yanchor="bottom",
|
||
y=1.02,
|
||
xanchor="right",
|
||
x=1
|
||
),
|
||
xaxis=dict(showgrid=False),
|
||
yaxis=dict(showgrid=True, gridcolor="rgba(99, 102, 241, 0.1)"),
|
||
)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
|
||
st.markdown("")
|
||
|
||
# Individual results
|
||
st.markdown('<p class="section-header">Detailed Results</p>', unsafe_allow_html=True)
|
||
for r in result.results:
|
||
status_icon = "✓" if r.success else "✗"
|
||
status_class = "status-success" if r.success else "status-error"
|
||
with st.expander(
|
||
f"{status_icon} {r.company_name.upper()} — {r.patent_count} patents"
|
||
):
|
||
if r.success:
|
||
st.markdown(r.analysis)
|
||
else:
|
||
st.error(r.error)
|
||
|
||
|
||
def render_analytics():
|
||
"""Render analytics page with database insights."""
|
||
st.markdown('<p class="section-header">Analytics Dashboard</p>', unsafe_allow_html=True)
|
||
st.markdown("Track historical analysis data and view insights.")
|
||
|
||
db_client = get_db_client()
|
||
|
||
if not db_client:
|
||
st.markdown("")
|
||
st.markdown("""
|
||
<div class="info-box">
|
||
<strong>⚠️ Database Not Connected</strong><br>
|
||
<span style="color: #94a3b8;">Set <code>USE_DATABASE=true</code> in your .env file to enable analytics tracking.</span>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
st.info("Analytics features require storing analysis results in PostgreSQL for historical tracking.")
|
||
return
|
||
|
||
st.markdown("")
|
||
|
||
# Time range selector
|
||
col1, col2, col3 = st.columns([1, 2, 1])
|
||
with col1:
|
||
days = st.selectbox("Time Range", [7, 14, 30, 90], index=0, format_func=lambda x: f"Last {x} days")
|
||
|
||
try:
|
||
analytics = db_client.get_analytics(days=days)
|
||
|
||
if not analytics:
|
||
st.info("No analytics data available yet. Run some analyses first!")
|
||
return
|
||
|
||
st.markdown("")
|
||
|
||
# Summary metrics
|
||
col1, col2, col3 = st.columns(3)
|
||
|
||
with col1:
|
||
total = analytics.get("total_messages", 0)
|
||
st.metric("Total Analyses", total)
|
||
|
||
with col2:
|
||
companies = len(analytics.get("by_company", {}))
|
||
st.metric("Companies Analyzed", companies)
|
||
|
||
with col3:
|
||
types = len(analytics.get("by_type", {}))
|
||
st.metric("Analysis Types", types)
|
||
|
||
st.markdown("")
|
||
|
||
# Charts
|
||
col1, col2 = st.columns(2)
|
||
|
||
with col1:
|
||
by_company = analytics.get("by_company", {})
|
||
if by_company:
|
||
df = pd.DataFrame(
|
||
[{"Company": k.upper(), "Count": v} for k, v in by_company.items()]
|
||
)
|
||
fig = px.pie(
|
||
df, values="Count", names="Company", title="Distribution by Company",
|
||
hole=0.4,
|
||
color_discrete_sequence=px.colors.sequential.Purp_r,
|
||
)
|
||
fig.update_layout(
|
||
plot_bgcolor="rgba(0,0,0,0)",
|
||
paper_bgcolor="rgba(0,0,0,0)",
|
||
font_color="#94a3b8",
|
||
)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
|
||
with col2:
|
||
by_type = analytics.get("by_type", {})
|
||
if by_type:
|
||
df = pd.DataFrame(
|
||
[{"Type": k, "Count": v} for k, v in by_type.items()]
|
||
)
|
||
fig = px.bar(df, x="Type", y="Count", title="Analysis Types",
|
||
color_discrete_sequence=["#6366f1"])
|
||
fig.update_layout(
|
||
plot_bgcolor="rgba(0,0,0,0)",
|
||
paper_bgcolor="rgba(0,0,0,0)",
|
||
font_color="#94a3b8",
|
||
xaxis=dict(showgrid=False),
|
||
yaxis=dict(showgrid=True, gridcolor="rgba(99, 102, 241, 0.1)"),
|
||
)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
|
||
st.markdown("")
|
||
|
||
# Recent messages
|
||
st.markdown('<p class="section-header">Recent Analyses</p>', unsafe_allow_html=True)
|
||
messages = db_client.get_messages(limit=10)
|
||
|
||
if messages:
|
||
for msg in messages:
|
||
with st.expander(
|
||
f"📄 {msg.get('company_name', 'Unknown').upper()} — {msg.get('analysis_type', 'N/A')} ({msg.get('timestamp', 'N/A')})"
|
||
):
|
||
st.markdown(f"**Model:** `{msg.get('model', 'N/A')}`")
|
||
if msg.get("response"):
|
||
st.markdown(msg["response"][:500] + "...")
|
||
|
||
except Exception as e:
|
||
st.error(f"Error fetching analytics: {e}")
|
||
|
||
|
||
def render_about():
|
||
"""Render about page."""
|
||
st.markdown('<p class="section-header">About SPARC</p>', unsafe_allow_html=True)
|
||
|
||
col1, col2 = st.columns([2, 1])
|
||
|
||
with col1:
|
||
st.markdown("""
|
||
**SPARC** (Semiconductor Patent & Analytics Report Core) is an AI-powered patent analysis
|
||
platform that evaluates company performance by analyzing their patent portfolios
|
||
with cutting-edge language models.
|
||
""")
|
||
|
||
st.markdown("")
|
||
st.markdown("**Key Features**")
|
||
|
||
features = [
|
||
("🔍", "Patent Retrieval", "Automated collection via SerpAPI's Google Patents"),
|
||
("📄", "Intelligent Parsing", "Extracts key sections from patent documents"),
|
||
("🤖", "AI Analysis", "Deep analysis powered by Claude 3.5 Sonnet"),
|
||
("⚡", "Batch Processing", "Analyze multiple companies concurrently"),
|
||
("🌐", "REST API", "FastAPI web service for seamless integration"),
|
||
("📊", "Analytics", "Track and visualize historical analysis data"),
|
||
]
|
||
|
||
for icon, title, desc in features:
|
||
st.markdown(f"""
|
||
<div class="feature-item">
|
||
<span class="feature-icon">{icon}</span>
|
||
<div>
|
||
<strong>{title}</strong><br>
|
||
<span style="color: #94a3b8; font-size: 0.875rem;">{desc}</span>
|
||
</div>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
|
||
with col2:
|
||
st.markdown("**Technology Stack**")
|
||
st.markdown("""
|
||
<div class="info-box">
|
||
<div style="display: grid; gap: 0.5rem;">
|
||
<div><span style="color: #6366f1;">Backend</span><br><span style="color: #94a3b8;">Python, FastAPI</span></div>
|
||
<div><span style="color: #6366f1;">AI Model</span><br><span style="color: #94a3b8;">Claude 3.5 Sonnet</span></div>
|
||
<div><span style="color: #6366f1;">Database</span><br><span style="color: #94a3b8;">PostgreSQL</span></div>
|
||
<div><span style="color: #6366f1;">Dashboard</span><br><span style="color: #94a3b8;">Streamlit, Plotly</span></div>
|
||
<div><span style="color: #6366f1;">Data Source</span><br><span style="color: #94a3b8;">SerpAPI Patents</span></div>
|
||
</div>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
|
||
st.markdown("")
|
||
st.markdown("**API Endpoints**")
|
||
st.code("http://localhost:8000/docs", language=None)
|
||
st.code("http://localhost:8000/health", language=None)
|
||
|
||
st.markdown("")
|
||
st.markdown("")
|
||
|
||
# System status
|
||
st.markdown('<p class="section-header">System Status</p>', unsafe_allow_html=True)
|
||
|
||
col1, col2, col3 = st.columns(3)
|
||
|
||
with col1:
|
||
db_client = get_db_client()
|
||
if db_client:
|
||
st.markdown("""
|
||
<div class="metric-card">
|
||
<div style="color: #10b981; font-size: 1.5rem;">●</div>
|
||
<div class="metric-label">Database</div>
|
||
<div style="color: #10b981; font-weight: 600;">Connected</div>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
else:
|
||
st.markdown("""
|
||
<div class="metric-card">
|
||
<div style="color: #f59e0b; font-size: 1.5rem;">●</div>
|
||
<div class="metric-label">Database</div>
|
||
<div style="color: #f59e0b; font-weight: 600;">Not Configured</div>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
|
||
with col2:
|
||
analyzer = get_analyzer()
|
||
if analyzer:
|
||
st.markdown("""
|
||
<div class="metric-card">
|
||
<div style="color: #10b981; font-size: 1.5rem;">●</div>
|
||
<div class="metric-label">Analyzer</div>
|
||
<div style="color: #10b981; font-weight: 600;">Ready</div>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
else:
|
||
st.markdown("""
|
||
<div class="metric-card">
|
||
<div style="color: #ef4444; font-size: 1.5rem;">●</div>
|
||
<div class="metric-label">Analyzer</div>
|
||
<div style="color: #ef4444; font-weight: 600;">Not Initialized</div>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
|
||
with col3:
|
||
st.markdown("""
|
||
<div class="metric-card">
|
||
<div style="color: #10b981; font-size: 1.5rem;">●</div>
|
||
<div class="metric-label">Dashboard</div>
|
||
<div style="color: #10b981; font-weight: 600;">Online</div>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
|
||
|
||
def main():
|
||
"""Main dashboard entry point."""
|
||
render_header()
|
||
tabs = render_navigation()
|
||
|
||
with tabs[0]:
|
||
render_company_analysis()
|
||
with tabs[1]:
|
||
render_batch_analysis()
|
||
with tabs[2]:
|
||
render_analytics()
|
||
with tabs[3]:
|
||
render_about()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|