"""
ResearchPilot AI โ Single-Page UI
All sections on one scrollable page with sticky nav.
Fixes: anchor links, SVG via components.html, PDF crash, renamed labels.
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import json
import streamlit as st
import streamlit.components.v1 as components
from graph.graph_builder import graph
from tools.pdf_generator import generate_pdf
st.set_page_config(
page_title="ResearchPilot AI",
page_icon="๐งญ",
layout="wide",
initial_sidebar_state="expanded",
)
# โโ CSS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown("""""", unsafe_allow_html=True)
# โโ STICKY NAV โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Uses JS to inject the nav above Streamlit's own header
st.markdown("""
""", unsafe_allow_html=True)
# โโ HEADER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown("""
๐งญ ResearchPilot AI
Autonomous Multi-Agent Research ยท Router ยท Statistics ยท Domain Expert ยท Fact Checker ยท Writer
""", unsafe_allow_html=True)
# โโ SIDEBAR โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
with st.sidebar:
st.markdown("### โ๏ธ Options")
generate_thumbnail = st.toggle("Generate cover card", value=True)
st.markdown("### ๐ก Try a Topic")
st.markdown(
""
"Curated picks that showcase rich charts, expert analysis & fact-checking. "
"Click any card to load it instantly.
",
unsafe_allow_html=True,
)
# Curated examples โ each one is chosen to trigger multiple agents and
# produce strong chart-worthy statistics, so a first-time user sees the
# system at its best.
EXAMPLES = [
{"emoji": "๐", "label": "AI in Healthcare", "query": "Impact of AI on healthcare market growth: adoption rates, investment trends and future outlook"},
{"emoji": "๐", "label": "Renewable Energy", "query": "Global renewable energy adoption trends: solar, wind and investment growth by region"},
{"emoji": "๐ค", "label": "Multi-Agent AI", "query": "Future of multi-agent AI systems: architecture, real-world adoption and key players"},
{"emoji": "๐ฐ", "label": "AI in Banking", "query": "AI in banking and financial services: fraud detection, market size and adoption statistics"},
{"emoji": "โก", "label": "Electric Vehicles", "query": "Future of electric vehicles worldwide: sales growth, battery costs and market share by region"},
{"emoji": "๐ก๏ธ", "label": "Cybersecurity 2026", "query": "Cybersecurity threat landscape 2026: emerging attack trends, breach costs and industry response"},
{"emoji": "๐", "label": "AI in Education", "query": "AI transformation in education: adoption rates, learning outcomes and classroom case studies"},
{"emoji": "๐", "label": "Climate Change", "query": "Climate change: data, risks and emerging solutions across global regions"},
]
# 2-column grid of clickable example cards
cols = st.columns(2)
for idx, ex in enumerate(EXAMPLES):
with cols[idx % 2]:
if st.button(
f"{ex['emoji']} {ex['label']}",
use_container_width=True,
key=f"ex_{idx}",
help=ex["query"],
):
st.session_state["query_input"] = ex["query"]
st.rerun()
st.markdown(
"", unsafe_allow_html=True
)
if st.button("๐ฒ Surprise me", use_container_width=True, key="ex_random"):
import random
st.session_state["query_input"] = random.choice(EXAMPLES)["query"]
st.rerun()
st.markdown("---")
st.markdown("### ๐ค Pipeline")
for icon, name, desc in [
("๐ง ","Planner", "Query โ tasks + domain"),
("๐","Router", "Decides active agents"),
("๐","Research", "Deep web intelligence"),
("๐","Statistics", "Mine quantitative data"),
("๐","Expert", "Domain expert analysis"),
("โ
","FactCheck", "Verify key claims"),
("๐","References", "APA source formatting"),
("๐","Viz", "6 interactive charts"),
("๐ผ๏ธ","Cover", "SVG cover card"),
("โ๏ธ","Writer", "Structured report"),
("๐","QualityGate","Score + retry"),
]:
st.markdown(
f""
f"{icon} {name}
"
f"{desc}
",
unsafe_allow_html=True)
st.markdown("---")
if st.session_state.get("rf_has_run"):
if st.button("๐ New Research", use_container_width=True):
for k in list(st.session_state.keys()):
if k.startswith("rf_") or k.startswith("pdf_cache_"):
del st.session_state[k]
st.rerun()
st.markdown("### ๐ Links")
st.markdown(
"[๐ GitHub Repo](https://github.com/vishal815) ยท "
"[๐ค LinkedIn](https://www.linkedin.com/in/vishal-lazrus/) ยท "
"[๐ Portfolio](https://vishal-lazrus-portfolio.vercel.app/)"
)
# โโ QUERY INPUT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
col_q, col_btn = st.columns([5, 1])
with col_q:
query = st.text_input(
"๐ Research Topic",
value=st.session_state.get("query_input",""),
placeholder="e.g. How is generative AI transforming software development in 2025?",
key="query_input",
label_visibility="collapsed",
)
with col_btn:
run_btn = st.button("๐ Research", type="primary", use_container_width=True)
st.markdown("
", unsafe_allow_html=True)
NODE_ORDER = ["planner","router","workers","visualization","cover","writer","quality_gate"]
def node_pills(completed, current=None):
labels = {
"planner":"๐ง Planner","router":"๐ Router","workers":"โ๏ธ Workers",
"visualization":"๐ Viz","cover":"๐ผ๏ธ Cover",
"writer":"โ๏ธ Writer","quality_gate":"๐ Gate",
"synthesizer":"โ๏ธ Writer","thumbnail":"๐ผ๏ธ Cover",
}
parts = []
for n in NODE_ORDER:
lbl = labels.get(n, n)
if n in completed: parts.append(f"โ
{lbl}")
elif n == current: parts.append(f"โก {lbl}")
else: parts.append(f"โ {lbl}")
return "" + " ".join(parts) + "
"
def log_class(l):
return "log-ok" if "Done" in l or "โ
" in l else (
"log-run" if any(x in l for x in ["Starting","Searching","Mining","Building","Applying","Evaluating","Merging","Generating","Verifying","Formatting"]) else
"log-info")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# PIPELINE RUN โ cached in session_state so download clicks
# (which trigger a Streamlit rerun) don't re-run the agents.
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if run_btn and query.strip():
st.session_state["rf_query"] = query
st.session_state["rf_has_run"] = True
st.session_state.pop("rf_final", None) # force fresh run
should_run_pipeline = (
st.session_state.get("rf_has_run", False)
and "rf_final" not in st.session_state
)
if should_run_pipeline:
initial = {
"query":query,"todos":[],"research_plan":"",
"active_agents":[],"topic_domain":"general",
"virtual_files":{},"structured_data":{},"citations":[],
"chart_json":[],"thumbnail_svg":None,
"final_report":"","quality_score":0.0,
"retry_count":0,"agent_logs":[],"agent_timings":{},
"generate_thumbnail":generate_thumbnail,
}
completed, final = [], dict(initial)
# โโ SECTION 1: RESEARCH (live) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.markdown('
๐ฌ Research โ Live Pipeline
', unsafe_allow_html=True)
status_box = st.empty()
graph_box = st.empty()
agent_box = st.empty()
with st.spinner("ResearchPilot AI is working..."):
for update in graph.stream(initial):
node_name = list(update.keys())[0]
node_data = update[node_name]
completed.append(node_name)
for k, v in node_data.items():
if v is not None:
final[k] = v
logs = final.get("agent_logs", [])
active = final.get("active_agents", [])
recent = logs[-6:]
log_html = "".join(
f"
โ {l}
" for l in recent
)
status_box.markdown(
f"
"
f"
โก Running: {node_name}"
f"
{log_html}
",
unsafe_allow_html=True,
)
graph_box.markdown(node_pills(completed, node_name), unsafe_allow_html=True)
if active:
chips = "".join(f"
๐ค {a}" for a in active)
agent_box.markdown(
f"
"
f"Active agents: {chips}
",
unsafe_allow_html=True,
)
# โโ Save to session_state so download-button reruns reuse it โโ
st.session_state["rf_final"] = final
st.session_state["rf_completed"] = completed
# Metrics row
score = final.get("quality_score", 0)
active = final.get("active_agents", [])
domain = final.get("topic_domain", "general")
vfs = final.get("virtual_files", {})
sc_cls = "badge-hi" if score>=7 else ("badge-md" if score>=4 else "badge-lo")
st.markdown(
f"
"
f"
"
f"
"
f"
QUALITY SCORE
{score:.1f}/10
"
f"
",
unsafe_allow_html=True,
)
# VFS files
if vfs:
st.markdown("**๐ Agent Outputs**")
for fname, content in vfs.items():
icon = "๐" if "csv" in fname else ("๐" if "research" in fname else ("๐" if "ref" in fname or "cite" in fname else "๐"))
with st.expander(f"{icon} {fname}"):
if fname.endswith(".csv"):
try:
import pandas as pd, io
df = pd.read_csv(io.StringIO(content))
st.dataframe(df, use_container_width=True)
except Exception:
st.text_area("", content, height=150, disabled=True, key=f"vfs_{fname}")
else:
st.text_area("", content, height=160, disabled=True, key=f"vfs_{fname}")
with st.expander("๐ Full Agent Log"):
for l in final.get("agent_logs", []):
st.markdown(f"
โ {l}
", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True) # close sec-wrap
# โโ SECTION 2: DATA & CHARTS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.markdown('
๐ Data & Charts
', unsafe_allow_html=True)
chart_list = final.get("chart_json", [])
structured = final.get("structured_data", {})
if chart_list:
try:
import plotly.graph_objects as go
# Show charts in 2-column grid where possible
c_pairs = [chart_list[i:i+2] for i in range(0, len(chart_list), 2)]
for pair in c_pairs:
if len(pair) == 2:
ca, cb = st.columns(2)
with ca:
st.plotly_chart(go.Figure(json.loads(pair[0])),
use_container_width=True, key=f"c{chart_list.index(pair[0])}")
with cb:
st.plotly_chart(go.Figure(json.loads(pair[1])),
use_container_width=True, key=f"c{chart_list.index(pair[1])}")
else:
st.plotly_chart(go.Figure(json.loads(pair[0])),
use_container_width=True, key=f"c{chart_list.index(pair[0])}")
except ImportError:
st.warning("Install plotly: `pip install plotly`")
else:
st.info("No charts โ Statistics agent was not activated for this topic.")
# CSV download
csv_data = vfs.get("statistics_data.csv","")
if csv_data:
col_csv1, col_csv2 = st.columns([1,4])
with col_csv1:
st.download_button("โฌ๏ธ Download CSV", data=csv_data,
file_name="researchpilot_data.csv", mime="text/csv",
use_container_width=True, key="csv_dl_first")
with col_csv2:
if structured.get("summary"):
st.markdown(f"> ๐ {structured['summary']}")
if structured.get("metrics"):
st.markdown("**๐ข Data Table**")
import pandas as pd
df = pd.DataFrame(structured["metrics"])
st.dataframe(df, use_container_width=True, hide_index=True)
# Cover thumbnail
thumb = final.get("thumbnail_svg")
if thumb:
st.markdown("**๐ผ๏ธ Cover Card**")
components.html(
f'
{thumb}
',
height=460, scrolling=False,
)
st.markdown("
", unsafe_allow_html=True)
# โโ SECTION 3: REFERENCES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.markdown('
๐ References
', unsafe_allow_html=True)
citations = final.get("citations", [])
if citations:
st.markdown(f"
{len(citations)} sources found
",
unsafe_allow_html=True)
for c in citations:
url = c.get("url","")
link = f'
{url[:70]}โฆ' if url else ""
st.markdown(
f"
"
f"[{c.get('number','')}] "
f"{c.get('title','')}
"
f"{c.get('apa','')}
{link}"
f"
",
unsafe_allow_html=True,
)
else:
ref_text = vfs.get("references.txt","")
if ref_text and ref_text != "No references extracted.":
st.markdown(ref_text)
else:
st.info("No references extracted for this topic.")
st.markdown("
", unsafe_allow_html=True)
# โโ SECTION 4: REPORT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.markdown('
๐ Report
', unsafe_allow_html=True)
report = final.get("final_report","")
if report:
st.markdown(f'
{report}
', unsafe_allow_html=True)
st.markdown("---")
dl1, dl2 = st.columns(2)
with dl1:
st.download_button(
"โฌ๏ธ Download Markdown", data=report,
file_name=f"researchpilot_{query[:28].replace(' ','_')}.md",
mime="text/markdown", use_container_width=True, key="md_dl_first",
)
with dl2:
pdf_key = f"pdf_cache_{hash(report) % 100000}"
if pdf_key not in st.session_state:
with st.spinner("Building PDF..."):
st.session_state[pdf_key] = generate_pdf(
report=report, query=query,
quality_score=final.get("quality_score",0),
citations=final.get("citations",[]),
chart_json_list=final.get("chart_json",[]),
thumbnail_svg=None,
active_agents=final.get("active_agents",[]),
)
pdf_bytes = st.session_state[pdf_key]
if pdf_bytes:
st.download_button(
"โฌ๏ธ Download PDF", data=pdf_bytes,
file_name=f"researchpilot_{query[:28].replace(' ','_')}.pdf",
mime="application/pdf", use_container_width=True, key="pdf_dl_first",
)
else:
st.caption("PDF needs fpdf2: `pip install fpdf2`")
st.markdown("
", unsafe_allow_html=True)
# โโ SECTION 5: TRACE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.markdown('
โฑ๏ธ Trace
', unsafe_allow_html=True)
timings = final.get("agent_timings",{})
if timings:
try:
import plotly.graph_objects as go
fig_t = go.Figure(go.Bar(
x=list(timings.keys()),
y=list(timings.values()),
marker=dict(color=["#6366f1","#8b5cf6","#34d399","#f59e0b",
"#06b6d4","#ef4444","#10b981"][:len(timings)],
line=dict(color="#0f172a",width=1)),
text=[f"{v:.1f}s" for v in timings.values()],
textposition="outside", textfont=dict(color="#e2e8f0"),
hovertemplate="%{x}: %{y:.2f}s
",
))
fig_t.update_layout(
title=dict(text="Node Execution Time (seconds)",font=dict(color="#67e8f9",size=15)),
plot_bgcolor="#0d1a1a", paper_bgcolor="#0d1a1a",
font=dict(color="#e2e8f0"),
xaxis=dict(gridcolor="#1e293b"),
yaxis=dict(gridcolor="#1e293b", title="Seconds"),
margin=dict(l=20,r=20,t=50,b=20), height=300,
)
st.plotly_chart(fig_t, use_container_width=True, key="timing_chart")
except ImportError:
for k,v in timings.items():
st.write(f"`{k}`: {v:.2f}s")
total = sum(timings.values()) if timings else 0
c1,c2,c3,c4 = st.columns(4)
c1.metric("โฑ Total Time", f"{total:.1f}s")
c2.metric("๐ Domain", domain.upper())
c3.metric("๐ค Agents", len(active))
c4.metric("๐ Retries", final.get("retry_count",0))
langsmith = os.getenv("LANGCHAIN_API_KEY","")
if langsmith:
st.markdown("**[๐ View LangSmith Trace โ](https://smith.langchain.com)**")
else:
st.info("Add `LANGCHAIN_API_KEY` to .env to enable LangSmith traces at smith.langchain.com")
st.markdown("
", unsafe_allow_html=True)
st.divider()
st.markdown("### ๐ Links")
st.markdown(
"[๐ GitHub Repo](https://github.com/vishal815) ยท "
"[๐ค LinkedIn](https://www.linkedin.com/in/vishal-lazrus/) ยท "
"[๐ Portfolio](https://vishal-lazrus-portfolio.vercel.app/)"
)
elif run_btn and not query.strip():
st.warning("Please enter a research topic.")
elif st.session_state.get("rf_has_run") and "rf_final" in st.session_state:
# โโ Rerun triggered by a download button โ reuse cached result, skip re-running agents โโ
final = st.session_state["rf_final"]
completed = st.session_state.get("rf_completed", [])
query = st.session_state.get("rf_query", query)
score = final.get("quality_score", 0)
active = final.get("active_agents", [])
domain = final.get("topic_domain", "general")
vfs = final.get("virtual_files", {})
sc_cls = "badge-hi" if score>=7 else ("badge-md" if score>=4 else "badge-lo")
# โโ SECTION 1: RESEARCH (cached summary, no re-streaming) โ
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.markdown('
๐ฌ Research โ Pipeline Summary
', unsafe_allow_html=True)
st.markdown(node_pills(completed, None), unsafe_allow_html=True)
if active:
chips = "".join(f"
๐ค {a}" for a in active)
st.markdown(f"
Active agents: {chips}
", unsafe_allow_html=True)
st.markdown(
f"
"
f"
"
f"
"
f"
QUALITY SCORE
{score:.1f}/10
"
f"
",
unsafe_allow_html=True,
)
if vfs:
st.markdown("**๐ Agent Outputs**")
for fname, content in vfs.items():
icon = "๐" if "csv" in fname else ("๐" if "research" in fname else ("๐" if "ref" in fname or "cite" in fname else "๐"))
with st.expander(f"{icon} {fname}"):
if fname.endswith(".csv"):
try:
import pandas as pd, io
df = pd.read_csv(io.StringIO(content))
st.dataframe(df, use_container_width=True)
except Exception:
st.text_area("", content, height=150, disabled=True, key=f"vfs_cached_{fname}")
else:
st.text_area("", content, height=160, disabled=True, key=f"vfs_cached_{fname}")
with st.expander("๐ Full Agent Log"):
for l in final.get("agent_logs", []):
st.markdown(f"
โ {l}
", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True)
# โโ SECTION 2: DATA & CHARTS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.markdown('
๐ Data & Charts
', unsafe_allow_html=True)
chart_list = final.get("chart_json", [])
structured = final.get("structured_data", {})
if chart_list:
try:
import plotly.graph_objects as go
c_pairs = [chart_list[i:i+2] for i in range(0, len(chart_list), 2)]
for pair in c_pairs:
if len(pair) == 2:
ca, cb = st.columns(2)
with ca:
st.plotly_chart(go.Figure(json.loads(pair[0])), use_container_width=True, key=f"cc{chart_list.index(pair[0])}")
with cb:
st.plotly_chart(go.Figure(json.loads(pair[1])), use_container_width=True, key=f"cc{chart_list.index(pair[1])}")
else:
st.plotly_chart(go.Figure(json.loads(pair[0])), use_container_width=True, key=f"cc{chart_list.index(pair[0])}")
except ImportError:
st.warning("Install plotly: `pip install plotly`")
else:
st.info("No charts โ Statistics agent was not activated for this topic.")
csv_data = vfs.get("statistics_data.csv","")
if csv_data:
col_csv1, col_csv2 = st.columns([1,4])
with col_csv1:
st.download_button("โฌ๏ธ Download CSV", data=csv_data,
file_name="researchpilot_data.csv", mime="text/csv",
use_container_width=True, key="csv_dl_cached")
with col_csv2:
if structured.get("summary"):
st.markdown(f"> ๐ {structured['summary']}")
if structured.get("metrics"):
st.markdown("**๐ข Data Table**")
import pandas as pd
df = pd.DataFrame(structured["metrics"])
st.dataframe(df, use_container_width=True, hide_index=True)
thumb = final.get("thumbnail_svg")
if thumb:
st.markdown("**๐ผ๏ธ Cover Card**")
components.html(
f'
{thumb}
',
height=460, scrolling=False,
)
st.markdown("
", unsafe_allow_html=True)
# โโ SECTION 3: REFERENCES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.markdown('
๐ References
', unsafe_allow_html=True)
citations = final.get("citations", [])
if citations:
st.markdown(f"
{len(citations)} sources found
", unsafe_allow_html=True)
for c in citations:
url = c.get("url","")
link = f'
{url[:70]}โฆ' if url else ""
st.markdown(
f"
"
f"[{c.get('number','')}] "
f"{c.get('title','')}
"
f"{c.get('apa','')}
{link}"
f"
",
unsafe_allow_html=True,
)
else:
ref_text = vfs.get("references.txt","")
if ref_text and ref_text != "No references extracted.":
st.markdown(ref_text)
else:
st.info("No references extracted for this topic.")
st.markdown("
", unsafe_allow_html=True)
# โโ SECTION 4: REPORT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.markdown('
๐ Report
', unsafe_allow_html=True)
report = final.get("final_report","")
if report:
st.markdown(f'
{report}
', unsafe_allow_html=True)
st.markdown("---")
dl1, dl2 = st.columns(2)
with dl1:
st.download_button(
"โฌ๏ธ Download Markdown", data=report,
file_name=f"researchpilot_{query[:28].replace(' ','_')}.md",
mime="text/markdown", use_container_width=True, key="md_dl_cached",
)
with dl2:
pdf_key = f"pdf_cache_{hash(report) % 100000}"
if pdf_key not in st.session_state:
with st.spinner("Building PDF..."):
st.session_state[pdf_key] = generate_pdf(
report=report, query=query,
quality_score=final.get("quality_score",0),
citations=final.get("citations",[]),
chart_json_list=final.get("chart_json",[]),
thumbnail_svg=None,
active_agents=final.get("active_agents",[]),
)
pdf_bytes = st.session_state[pdf_key]
if pdf_bytes:
st.download_button(
"โฌ๏ธ Download PDF", data=pdf_bytes,
file_name=f"researchpilot_{query[:28].replace(' ','_')}.pdf",
mime="application/pdf", use_container_width=True, key="pdf_dl_cached",
)
else:
st.caption("PDF needs fpdf2: `pip install fpdf2`")
st.markdown("
", unsafe_allow_html=True)
# โโ SECTION 5: TRACE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
st.markdown('
โฑ๏ธ Trace
', unsafe_allow_html=True)
timings = final.get("agent_timings",{})
if timings:
try:
import plotly.graph_objects as go
fig_t = go.Figure(go.Bar(
x=list(timings.keys()), y=list(timings.values()),
marker=dict(color=["#6366f1","#8b5cf6","#34d399","#f59e0b","#06b6d4","#ef4444","#10b981"][:len(timings)],
line=dict(color="#0f172a",width=1)),
text=[f"{v:.1f}s" for v in timings.values()],
textposition="outside", textfont=dict(color="#e2e8f0"),
hovertemplate="%{x}: %{y:.2f}s
",
))
fig_t.update_layout(
title=dict(text="Node Execution Time (seconds)",font=dict(color="#67e8f9",size=15)),
plot_bgcolor="#0d1a1a", paper_bgcolor="#0d1a1a",
font=dict(color="#e2e8f0"),
xaxis=dict(gridcolor="#1e293b"), yaxis=dict(gridcolor="#1e293b", title="Seconds"),
margin=dict(l=20,r=20,t=50,b=20), height=300,
)
st.plotly_chart(fig_t, use_container_width=True, key="timing_chart_cached")
except ImportError:
for k,v in timings.items():
st.write(f"`{k}`: {v:.2f}s")
total = sum(timings.values()) if timings else 0
c1,c2,c3,c4 = st.columns(4)
c1.metric("โฑ Total Time", f"{total:.1f}s")
c2.metric("๐ Domain", domain.upper())
c3.metric("๐ค Agents", len(active))
c4.metric("๐ Retries", final.get("retry_count",0))
langsmith = os.getenv("LANGCHAIN_API_KEY","")
if langsmith:
st.markdown("**[๐ View LangSmith Trace โ](https://smith.langchain.com)**")
else:
st.info("Add `LANGCHAIN_API_KEY` to .env to enable LangSmith traces at smith.langchain.com")
st.markdown("
", unsafe_allow_html=True)
st.divider()
st.markdown("### ๐ Links")
st.markdown(
"[๐ GitHub Repo](https://github.com/vishal815) ยท "
"[๐ค LinkedIn](https://www.linkedin.com/in/vishal-lazrus/) ยท "
"[๐ Portfolio](https://vishal-lazrus-portfolio.vercel.app/)"
)
else:
# Landing placeholders โ one per section, each styled distinctly
for anchor, sec_cls, title_cls, icon, title, desc in [
("sec-research", "sec-research", "t-indigo", "๐ฌ", "Research", "Live agent pipeline, domain detection, VFS file browser"),
("sec-charts", "sec-charts", "t-green", "๐", "Data & Charts", "6 interactive Plotly charts + CSV export + cover card"),
("sec-references","sec-references", "t-purple", "๐", "References", "APA-formatted sources extracted from the web"),
("sec-report", "sec-report", "t-amber", "๐", "Report", "Full structured report โ Markdown + PDF download"),
("sec-trace", "sec-trace", "t-cyan", "โฑ๏ธ", "Trace", "Per-node timing chart + LangSmith integration"),
]:
st.markdown(f'', unsafe_allow_html=True)
st.markdown(
f''
f'
{icon} {title}
'
f'
{desc}
'
f'
',
unsafe_allow_html=True,
)
st.markdown("---")
st.markdown("### ๐ Links")
st.markdown(
"[๐ GitHub Repo](https://github.com/vishal815) ยท "
"[๐ค LinkedIn](https://www.linkedin.com/in/vishal-lazrus/) ยท "
"[๐ Portfolio](https://vishal-lazrus-portfolio.vercel.app/)"
)