rewrite/frontend #2
-778
@@ -1,778 +0,0 @@
|
|||||||
"""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()
|
|
||||||
Reference in New Issue
Block a user