feat: redesign dashboard with modern UI

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>
This commit is contained in:
2026-03-13 15:36:56 -04:00
parent d371ceeec8
commit 7eb72ab549
+517 -101
View File
@@ -17,11 +17,304 @@ from SPARC import config
st.set_page_config(
page_title="SPARC Dashboard",
page_icon="📊",
page_icon="",
layout="wide",
initial_sidebar_state="expanded",
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():
@@ -43,37 +336,44 @@ def get_db_client():
def render_header():
"""Render the dashboard header."""
st.title("SPARC Dashboard")
st.markdown("**Semiconductor Patent & Analytics Report Core**")
st.markdown("---")
"""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_sidebar():
"""Render the sidebar with navigation and controls."""
st.sidebar.title("Navigation")
page = st.sidebar.radio(
"Select Page",
["Company Analysis", "Batch Analysis", "Analytics", "About"],
)
return page
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.header("Company Patent Analysis")
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.")
col1, col2 = st.columns([2, 1])
st.markdown("")
with col1:
company_name = st.text_input(
"Company Name",
placeholder="e.g., nvidia, intel, amd",
help="Enter the company name to analyze their patent portfolio",
)
# Search card
with st.container():
col1, col2 = st.columns([3, 1])
with col2:
analyze_btn = st.button("Analyze", type="primary", use_container_width=True)
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}..."):
@@ -81,45 +381,57 @@ def render_company_analysis():
result = analyzer._analyze_company_safe(company_name)
if result.success:
st.success(f"Analysis complete for {company_name}")
st.success(f"Analysis complete for {company_name.upper()}")
# Metrics row
st.markdown("")
# Metrics row with custom styling
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Patents Analyzed", result.patent_count)
st.metric("Patents Found", result.patent_count)
with col2:
st.metric("Status", "Success")
st.metric("Analysis Status", "Complete")
with col3:
st.metric("Timestamp", result.timestamp.strftime("%H:%M:%S"))
# Analysis content
st.subheader("AI Analysis")
st.markdown(result.analysis)
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.header("Batch Company Analysis")
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(
"Analyze multiple companies simultaneously. Enter company names separated by commas or newlines."
)
st.markdown("")
companies_input = st.text_area(
"Company Names",
placeholder="nvidia\namd\nintel\nqualcomm",
height=150,
)
# Input section
col1, col2 = st.columns([2, 1])
col1, col2 = st.columns(2)
with col1:
max_workers = st.slider("Concurrent Workers", 1, 5, 3)
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
"🚀 Run Batch Analysis", type="primary", use_container_width=True
)
if analyze_btn and companies_input:
@@ -134,7 +446,7 @@ def render_batch_analysis():
st.warning("Please enter at least one company name")
return
st.info(f"Starting analysis of {len(companies)} companies...")
st.info(f"🔄 Starting analysis of {len(companies)} companies...")
# Progress tracking
progress_bar = st.progress(0)
@@ -154,10 +466,12 @@ def render_batch_analysis():
)
progress_bar.progress(1.0)
status_text.text("Analysis complete!")
status_text.text("Analysis complete!")
st.markdown("")
# Summary metrics
st.subheader("Results Summary")
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)
@@ -178,7 +492,7 @@ def render_batch_analysis():
df = pd.DataFrame(
[
{
"Company": r.company_name,
"Company": r.company_name.upper(),
"Patents": r.patent_count,
"Status": "Success" if r.success else "Failed",
}
@@ -191,16 +505,34 @@ def render_batch_analysis():
x="Company",
y="Patents",
color="Status",
color_discrete_map={"Success": "#28a745", "Failed": "#dc3545"},
title="Patents per Company",
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.subheader("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"{'' if r.success else ''} {r.company_name} ({r.patent_count} patents)"
f"{status_icon} {r.company_name.upper()} {r.patent_count} patents"
):
if r.success:
st.markdown(r.analysis)
@@ -210,21 +542,28 @@ def render_batch_analysis():
def render_analytics():
"""Render analytics page with database insights."""
st.header("Analytics Dashboard")
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.warning(
"Database mode is not enabled. Set USE_DATABASE=true in your .env file to enable analytics."
)
st.info(
"Analytics features require storing analysis results in PostgreSQL for historical tracking."
)
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
days = st.selectbox("Time Range", [7, 14, 30, 90], index=0)
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)
@@ -233,8 +572,9 @@ def render_analytics():
st.info("No analytics data available yet. Run some analyses first!")
return
st.markdown("")
# Summary metrics
st.subheader("Summary")
col1, col2, col3 = st.columns(3)
with col1:
@@ -249,6 +589,8 @@ def render_analytics():
types = len(analytics.get("by_type", {}))
st.metric("Analysis Types", types)
st.markdown("")
# Charts
col1, col2 = st.columns(2)
@@ -256,10 +598,17 @@ def render_analytics():
by_company = analytics.get("by_company", {})
if by_company:
df = pd.DataFrame(
[{"Company": k, "Count": v} for k, v in by_company.items()]
[{"Company": k.upper(), "Count": v} for k, v in by_company.items()]
)
fig = px.pie(
df, values="Count", names="Company", title="Analyses by Company"
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)
@@ -269,19 +618,29 @@ def render_analytics():
df = pd.DataFrame(
[{"Type": k, "Count": v} for k, v in by_type.items()]
)
fig = px.bar(df, x="Type", y="Count", title="Analyses by Type")
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.subheader("Recent Analyses")
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')} - {msg.get('analysis_type', 'N/A')} ({msg.get('timestamp', 'N/A')})"
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')}")
st.markdown(f"**Model:** `{msg.get('model', 'N/A')}`")
if msg.get("response"):
st.markdown(msg["response"][:500] + "...")
@@ -291,70 +650,127 @@ def render_analytics():
def render_about():
"""Render about page."""
st.header("About SPARC")
st.markdown('<p class="section-header">About SPARC</p>', unsafe_allow_html=True)
st.markdown(
"""
**SPARC** (Semiconductor Patent & Analytics Report Core) is a patent analysis
system that estimates company performance by analyzing their patent portfolios
using LLM-powered insights.
col1, col2 = st.columns([2, 1])
### Features
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.
""")
- **Patent Retrieval**: Automated collection via SerpAPI's Google Patents engine
- **Intelligent Parsing**: Extracts key sections from patent PDFs
- **AI Analysis**: Uses Claude 3.5 Sonnet for deep analysis
- **Batch Processing**: Analyze multiple companies concurrently
- **REST API**: FastAPI web service for integration
- **Analytics**: Track and visualize analysis history
st.markdown("")
st.markdown("**Key Features**")
### Technology Stack
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"),
]
- **Backend**: Python, FastAPI
- **AI**: Claude 3.5 Sonnet via OpenRouter
- **Database**: PostgreSQL
- **Dashboard**: Streamlit, Plotly
- **Patent Data**: SerpAPI Google Patents
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)
### Links
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)
- API Docs: `http://localhost:8000/docs`
- Health Check: `http://localhost:8000/health`
"""
)
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.subheader("System Status")
st.markdown('<p class="section-header">System Status</p>', unsafe_allow_html=True)
col1, col2 = st.columns(2)
col1, col2, col3 = st.columns(3)
with col1:
db_client = get_db_client()
if db_client:
st.success("Database: Connected")
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.warning("Database: Not configured")
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.success("Analyzer: Ready")
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.error("Analyzer: Not initialized")
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()
page = render_sidebar()
tabs = render_navigation()
if page == "Company Analysis":
with tabs[0]:
render_company_analysis()
elif page == "Batch Analysis":
with tabs[1]:
render_batch_analysis()
elif page == "Analytics":
with tabs[2]:
render_analytics()
elif page == "About":
with tabs[3]:
render_about()