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:
+517
-101
@@ -17,11 +17,304 @@ from SPARC import config
|
|||||||
|
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
page_title="SPARC Dashboard",
|
page_title="SPARC Dashboard",
|
||||||
page_icon="📊",
|
page_icon="⚡",
|
||||||
layout="wide",
|
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
|
@st.cache_resource
|
||||||
def get_analyzer():
|
def get_analyzer():
|
||||||
@@ -43,37 +336,44 @@ def get_db_client():
|
|||||||
|
|
||||||
|
|
||||||
def render_header():
|
def render_header():
|
||||||
"""Render the dashboard header."""
|
"""Render the modern dashboard header."""
|
||||||
st.title("SPARC Dashboard")
|
st.markdown("""
|
||||||
st.markdown("**Semiconductor Patent & Analytics Report Core**")
|
<div class="nav-container">
|
||||||
st.markdown("---")
|
<div class="nav-brand">
|
||||||
|
<h1>⚡ SPARC</h1>
|
||||||
|
<span>Semiconductor Patent Analytics</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
|
||||||
def render_sidebar():
|
def render_navigation():
|
||||||
"""Render the sidebar with navigation and controls."""
|
"""Render horizontal tab navigation at the top."""
|
||||||
st.sidebar.title("Navigation")
|
tabs = st.tabs(["🔍 Company Analysis", "📦 Batch Analysis", "📊 Analytics", "ℹ️ About"])
|
||||||
page = st.sidebar.radio(
|
return tabs
|
||||||
"Select Page",
|
|
||||||
["Company Analysis", "Batch Analysis", "Analytics", "About"],
|
|
||||||
)
|
|
||||||
return page
|
|
||||||
|
|
||||||
|
|
||||||
def render_company_analysis():
|
def render_company_analysis():
|
||||||
"""Render single company analysis page."""
|
"""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:
|
# Search card
|
||||||
company_name = st.text_input(
|
with st.container():
|
||||||
"Company Name",
|
col1, col2 = st.columns([3, 1])
|
||||||
placeholder="e.g., nvidia, intel, amd",
|
|
||||||
help="Enter the company name to analyze their patent portfolio",
|
|
||||||
)
|
|
||||||
|
|
||||||
with col2:
|
with col1:
|
||||||
analyze_btn = st.button("Analyze", type="primary", use_container_width=True)
|
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:
|
if analyze_btn and company_name:
|
||||||
with st.spinner(f"Analyzing {company_name}..."):
|
with st.spinner(f"Analyzing {company_name}..."):
|
||||||
@@ -81,45 +381,57 @@ def render_company_analysis():
|
|||||||
result = analyzer._analyze_company_safe(company_name)
|
result = analyzer._analyze_company_safe(company_name)
|
||||||
|
|
||||||
if result.success:
|
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)
|
col1, col2, col3 = st.columns(3)
|
||||||
with col1:
|
with col1:
|
||||||
st.metric("Patents Analyzed", result.patent_count)
|
st.metric("Patents Found", result.patent_count)
|
||||||
with col2:
|
with col2:
|
||||||
st.metric("Status", "Success")
|
st.metric("Analysis Status", "Complete")
|
||||||
with col3:
|
with col3:
|
||||||
st.metric("Timestamp", result.timestamp.strftime("%H:%M:%S"))
|
st.metric("Timestamp", result.timestamp.strftime("%H:%M:%S"))
|
||||||
|
|
||||||
# Analysis content
|
st.markdown("")
|
||||||
st.subheader("AI Analysis")
|
|
||||||
st.markdown(result.analysis)
|
# 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:
|
else:
|
||||||
st.error(f"Analysis failed: {result.error}")
|
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():
|
def render_batch_analysis():
|
||||||
"""Render batch analysis page."""
|
"""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(
|
st.markdown("")
|
||||||
"Analyze multiple companies simultaneously. Enter company names separated by commas or newlines."
|
|
||||||
)
|
|
||||||
|
|
||||||
companies_input = st.text_area(
|
# Input section
|
||||||
"Company Names",
|
col1, col2 = st.columns([2, 1])
|
||||||
placeholder="nvidia\namd\nintel\nqualcomm",
|
|
||||||
height=150,
|
|
||||||
)
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
with col1:
|
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:
|
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(
|
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:
|
if analyze_btn and companies_input:
|
||||||
@@ -134,7 +446,7 @@ def render_batch_analysis():
|
|||||||
st.warning("Please enter at least one company name")
|
st.warning("Please enter at least one company name")
|
||||||
return
|
return
|
||||||
|
|
||||||
st.info(f"Starting analysis of {len(companies)} companies...")
|
st.info(f"🔄 Starting analysis of {len(companies)} companies...")
|
||||||
|
|
||||||
# Progress tracking
|
# Progress tracking
|
||||||
progress_bar = st.progress(0)
|
progress_bar = st.progress(0)
|
||||||
@@ -154,10 +466,12 @@ def render_batch_analysis():
|
|||||||
)
|
)
|
||||||
|
|
||||||
progress_bar.progress(1.0)
|
progress_bar.progress(1.0)
|
||||||
status_text.text("Analysis complete!")
|
status_text.text("✓ Analysis complete!")
|
||||||
|
|
||||||
|
st.markdown("")
|
||||||
|
|
||||||
# Summary metrics
|
# 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)
|
col1, col2, col3, col4 = st.columns(4)
|
||||||
with col1:
|
with col1:
|
||||||
st.metric("Total Companies", result.total_companies)
|
st.metric("Total Companies", result.total_companies)
|
||||||
@@ -178,7 +492,7 @@ def render_batch_analysis():
|
|||||||
df = pd.DataFrame(
|
df = pd.DataFrame(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"Company": r.company_name,
|
"Company": r.company_name.upper(),
|
||||||
"Patents": r.patent_count,
|
"Patents": r.patent_count,
|
||||||
"Status": "Success" if r.success else "Failed",
|
"Status": "Success" if r.success else "Failed",
|
||||||
}
|
}
|
||||||
@@ -191,16 +505,34 @@ def render_batch_analysis():
|
|||||||
x="Company",
|
x="Company",
|
||||||
y="Patents",
|
y="Patents",
|
||||||
color="Status",
|
color="Status",
|
||||||
color_discrete_map={"Success": "#28a745", "Failed": "#dc3545"},
|
color_discrete_map={"Success": "#10b981", "Failed": "#ef4444"},
|
||||||
title="Patents per Company",
|
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.plotly_chart(fig, use_container_width=True)
|
||||||
|
|
||||||
|
st.markdown("")
|
||||||
|
|
||||||
# Individual results
|
# Individual results
|
||||||
st.subheader("Individual Results")
|
st.markdown('<p class="section-header">Detailed Results</p>', unsafe_allow_html=True)
|
||||||
for r in result.results:
|
for r in result.results:
|
||||||
|
status_icon = "✓" if r.success else "✗"
|
||||||
|
status_class = "status-success" if r.success else "status-error"
|
||||||
with st.expander(
|
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:
|
if r.success:
|
||||||
st.markdown(r.analysis)
|
st.markdown(r.analysis)
|
||||||
@@ -210,21 +542,28 @@ def render_batch_analysis():
|
|||||||
|
|
||||||
def render_analytics():
|
def render_analytics():
|
||||||
"""Render analytics page with database insights."""
|
"""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()
|
db_client = get_db_client()
|
||||||
|
|
||||||
if not db_client:
|
if not db_client:
|
||||||
st.warning(
|
st.markdown("")
|
||||||
"Database mode is not enabled. Set USE_DATABASE=true in your .env file to enable analytics."
|
st.markdown("""
|
||||||
)
|
<div class="info-box">
|
||||||
st.info(
|
<strong>⚠️ Database Not Connected</strong><br>
|
||||||
"Analytics features require storing analysis results in PostgreSQL for historical tracking."
|
<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
|
return
|
||||||
|
|
||||||
|
st.markdown("")
|
||||||
|
|
||||||
# Time range selector
|
# 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:
|
try:
|
||||||
analytics = db_client.get_analytics(days=days)
|
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!")
|
st.info("No analytics data available yet. Run some analyses first!")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
st.markdown("")
|
||||||
|
|
||||||
# Summary metrics
|
# Summary metrics
|
||||||
st.subheader("Summary")
|
|
||||||
col1, col2, col3 = st.columns(3)
|
col1, col2, col3 = st.columns(3)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
@@ -249,6 +589,8 @@ def render_analytics():
|
|||||||
types = len(analytics.get("by_type", {}))
|
types = len(analytics.get("by_type", {}))
|
||||||
st.metric("Analysis Types", types)
|
st.metric("Analysis Types", types)
|
||||||
|
|
||||||
|
st.markdown("")
|
||||||
|
|
||||||
# Charts
|
# Charts
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
@@ -256,10 +598,17 @@ def render_analytics():
|
|||||||
by_company = analytics.get("by_company", {})
|
by_company = analytics.get("by_company", {})
|
||||||
if by_company:
|
if by_company:
|
||||||
df = pd.DataFrame(
|
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(
|
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)
|
st.plotly_chart(fig, use_container_width=True)
|
||||||
|
|
||||||
@@ -269,19 +618,29 @@ def render_analytics():
|
|||||||
df = pd.DataFrame(
|
df = pd.DataFrame(
|
||||||
[{"Type": k, "Count": v} for k, v in by_type.items()]
|
[{"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.plotly_chart(fig, use_container_width=True)
|
||||||
|
|
||||||
|
st.markdown("")
|
||||||
|
|
||||||
# Recent messages
|
# 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)
|
messages = db_client.get_messages(limit=10)
|
||||||
|
|
||||||
if messages:
|
if messages:
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
with st.expander(
|
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"):
|
if msg.get("response"):
|
||||||
st.markdown(msg["response"][:500] + "...")
|
st.markdown(msg["response"][:500] + "...")
|
||||||
|
|
||||||
@@ -291,70 +650,127 @@ def render_analytics():
|
|||||||
|
|
||||||
def render_about():
|
def render_about():
|
||||||
"""Render about page."""
|
"""Render about page."""
|
||||||
st.header("About SPARC")
|
st.markdown('<p class="section-header">About SPARC</p>', unsafe_allow_html=True)
|
||||||
|
|
||||||
st.markdown(
|
col1, col2 = st.columns([2, 1])
|
||||||
"""
|
|
||||||
**SPARC** (Semiconductor Patent & Analytics Report Core) is a patent analysis
|
|
||||||
system that estimates company performance by analyzing their patent portfolios
|
|
||||||
using LLM-powered insights.
|
|
||||||
|
|
||||||
### 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
|
st.markdown("")
|
||||||
- **Intelligent Parsing**: Extracts key sections from patent PDFs
|
st.markdown("**Key Features**")
|
||||||
- **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
|
|
||||||
|
|
||||||
### 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
|
for icon, title, desc in features:
|
||||||
- **AI**: Claude 3.5 Sonnet via OpenRouter
|
st.markdown(f"""
|
||||||
- **Database**: PostgreSQL
|
<div class="feature-item">
|
||||||
- **Dashboard**: Streamlit, Plotly
|
<span class="feature-icon">{icon}</span>
|
||||||
- **Patent Data**: SerpAPI Google Patents
|
<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`
|
st.markdown("")
|
||||||
- Health Check: `http://localhost:8000/health`
|
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
|
# 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:
|
with col1:
|
||||||
db_client = get_db_client()
|
db_client = get_db_client()
|
||||||
if 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:
|
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:
|
with col2:
|
||||||
analyzer = get_analyzer()
|
analyzer = get_analyzer()
|
||||||
if 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:
|
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():
|
def main():
|
||||||
"""Main dashboard entry point."""
|
"""Main dashboard entry point."""
|
||||||
render_header()
|
render_header()
|
||||||
page = render_sidebar()
|
tabs = render_navigation()
|
||||||
|
|
||||||
if page == "Company Analysis":
|
with tabs[0]:
|
||||||
render_company_analysis()
|
render_company_analysis()
|
||||||
elif page == "Batch Analysis":
|
with tabs[1]:
|
||||||
render_batch_analysis()
|
render_batch_analysis()
|
||||||
elif page == "Analytics":
|
with tabs[2]:
|
||||||
render_analytics()
|
render_analytics()
|
||||||
elif page == "About":
|
with tabs[3]:
|
||||||
render_about()
|
render_about()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user