| """Streamlit UI for the GitHub Repository Analyzer.""" |
|
|
| import json |
| import os |
| import sys |
| from pathlib import Path |
|
|
| |
| APP_ROOT = Path(__file__).resolve().parent |
| PROJECT_ROOT = APP_ROOT.parent |
| if str(PROJECT_ROOT) not in sys.path: |
| sys.path.insert(0, str(PROJECT_ROOT)) |
|
|
| _env = PROJECT_ROOT / ".env" |
| if _env.exists(): |
| for _line in _env.read_text(encoding="utf-8").splitlines(): |
| _line = _line.strip() |
| if _line and not _line.startswith("#") and "=" in _line: |
| _k, _, _v = _line.partition("=") |
| os.environ.setdefault(_k.strip(), _v.strip()) |
|
|
| import streamlit as st |
|
|
| from src.repo_analyzer import Config, analyze_full |
| from src.repo_analyzer.claims import claims_for_reporting |
| from src.repo_analyzer.pdf_parser import parse_pdf_to_sections |
| from src.repo_analyzer.pdf_parser_marker import ( |
| is_marker_available, |
| parse_pdf_to_sections_marker, |
| ) |
| from src.repo_analyzer.pdf_report import build_pdf_report |
| from src.repo_analyzer.report import build_text_report |
|
|
| LEADERBOARD_ROOT = PROJECT_ROOT / "leaderboard" |
| LEADERBOARD_MANIFEST = LEADERBOARD_ROOT / "manifest.json" |
|
|
| CHECK_LABELS = { |
| "readme": "README", |
| "license": "License", |
| "commits": "Commits (>5)", |
| "execution_files": "Entry-point files", |
| "requirements": "Requirements file", |
| "tests": "Tests", |
| "data_files": "Data files", |
| "experiment_scripts": "Experiment scripts", |
| } |
|
|
|
|
| def load_leaderboard_manifest() -> list[dict]: |
| """Return the lightweight saved-report index.""" |
| if not LEADERBOARD_MANIFEST.exists(): |
| return [] |
|
|
| data = json.loads(LEADERBOARD_MANIFEST.read_text(encoding="utf-8")) |
| if not isinstance(data, list): |
| raise ValueError("leaderboard/manifest.json must contain a JSON array") |
|
|
| return sorted(data, key=lambda entry: (entry.get("language", ""), entry.get("rank", 0))) |
|
|
|
|
| def filter_leaderboard_manifest(manifest: list[dict], language: str) -> list[dict]: |
| """Return saved reports for one language sorted by rank.""" |
| entries = [entry for entry in manifest if entry.get("language") == language] |
| return sorted(entries, key=lambda entry: entry.get("rank", 0)) |
|
|
|
|
| def resolve_leaderboard_path(path_text: str) -> Path: |
| """Resolve a manifest path and keep it inside the leaderboard folder.""" |
| path = Path(path_text) |
| candidate = path if path.is_absolute() else PROJECT_ROOT / path |
| candidate = candidate.resolve() |
| leaderboard_root = LEADERBOARD_ROOT.resolve() |
|
|
| if not candidate.is_relative_to(leaderboard_root): |
| raise ValueError(f"Refusing to load path outside leaderboard: {path_text}") |
| return candidate |
|
|
|
|
| def load_saved_report(entry: dict) -> dict: |
| """Load a saved report JSON from the manifest entry selected by the user.""" |
| report_path = resolve_leaderboard_path(entry["json_path"]) |
| return json.loads(report_path.read_text(encoding="utf-8")) |
|
|
|
|
| def clear_pdf_cache(prefix: str) -> None: |
| for suffix in ("pdf_en", "pdf_ru"): |
| st.session_state.pop(f"{prefix}_{suffix}", None) |
|
|
|
|
| def render_report( |
| report: dict, |
| *, |
| paper_name: str | None = None, |
| repo_url: str | None = None, |
| pdf_cache_prefix: str, |
| openrouter_key: str | None = None, |
| model: str | None = None, |
| file_stem: str = "report", |
| show_back_button: bool = False, |
| back_button_key: str = "saved_report_back", |
| back_state_prefix: str = "leaderboard", |
| ) -> None: |
| """Render the report UI used by both fresh and saved analyses.""" |
| summary = report["summary"] |
| score = summary["score"] |
| breakdown = summary["score_breakdown"] |
| repo_type = summary["repo_type"] |
| display_repo_url = repo_url or report.get("repo_url", "") |
|
|
| |
|
|
| col_dl0, col_dl1, col_dl2, col_dl3, col_dl4 = st.columns([1, 0.5, 0.5, 1, 2]) |
| with col_dl0: |
| if show_back_button: |
| if st.button(":material/arrow_back: Back to saved reports", key=back_button_key, type="primary"): |
| st.session_state.pop(f"{back_state_prefix}_selected", None) |
| st.session_state.pop(f"{back_state_prefix}_report", None) |
| st.session_state.pop(f"{back_state_prefix}_report_path", None) |
| st.session_state.pop(f"{back_state_prefix}_loading", None) |
| clear_pdf_cache(pdf_cache_prefix) |
| st.rerun() |
| with col_dl1: |
| st.download_button( |
| ":material/download: Download JSON report", |
| data=json.dumps(report, ensure_ascii=False, indent=2), |
| file_name=f"{file_stem}.json", |
| mime="application/json", |
| key=f"{pdf_cache_prefix}_download_json", |
| ) |
| with col_dl2: |
| pdf_en_key = f"{pdf_cache_prefix}_pdf_en" |
| if pdf_en_key not in st.session_state: |
| st.session_state[pdf_en_key] = build_pdf_report(report, lang="en", paper_name=paper_name) |
| st.download_button( |
| ":material/file_save: Download PDF (EN)", |
| data=st.session_state[pdf_en_key], |
| file_name=f"{file_stem}_en.pdf", |
| mime="application/pdf", |
| key=f"{pdf_cache_prefix}_download_pdf_en", |
| ) |
| with col_dl3: |
| pdf_ru_key = f"{pdf_cache_prefix}_pdf_ru" |
| if pdf_ru_key not in st.session_state: |
| st.session_state[pdf_ru_key] = build_pdf_report( |
| report, |
| lang="ru", |
| openrouter_key=openrouter_key, |
| model=model, |
| paper_name=paper_name, |
| ) |
| st.download_button( |
| ":material/file_save: Download PDF (RU)", |
| data=st.session_state[pdf_ru_key], |
| file_name=f"{file_stem}_ru.pdf", |
| mime="application/pdf", |
| key=f"{pdf_cache_prefix}_download_pdf_ru", |
| ) |
| with col_dl4: |
| with st.expander("Text report"): |
| st.code(build_text_report(report), language=None) |
|
|
| st.divider() |
|
|
| |
|
|
| col_score, col_meta = st.columns([1, 3]) |
| with col_score: |
| color = "var(--app-success)" if score >= 70 else "var(--app-warning)" if score >= 40 else "var(--app-danger)" |
| st.markdown( |
| f"<div style='text-align:center;padding:20px'>" |
| f"<div style='font-size:24px;color:var(--app-text-muted)'>Sanity Score:</div>" |
| f"<span style='font-size:72px;font-weight:bold;color:{color}'>{score}</span>" |
| f"<span style='font-size:24px;color:var(--app-text-muted)'>/100</span>" |
| f"</div>", |
| unsafe_allow_html=True, |
| ) |
| with col_meta: |
| if paper_name: |
| st.markdown(f"**Student report:** {paper_name}") |
| if display_repo_url: |
| st.markdown(f"**Repository:** {display_repo_url}") |
| st.markdown(f"**Type:** {repo_type.replace('_', ' ').title()}") |
| st.markdown(f"**Analyzed:** {report.get('analyzed_at', 'unknown')}") |
|
|
| st.divider() |
|
|
| |
|
|
| st.subheader("Repository Checks") |
|
|
| applicable_checks = [ |
| (key, label) |
| for key, label in CHECK_LABELS.items() |
| if key in breakdown and breakdown[key].get("applicable") is not False |
| ] |
|
|
| if applicable_checks: |
| cols = st.columns(min(len(applicable_checks), 4)) |
| for i, (key, label) in enumerate(applicable_checks): |
| bd = breakdown.get(key, {}) |
| passed = bd.get("passed", False) |
| weight = bd.get("weight", 0) |
| earned = bd.get("earned", 0) |
| icon = "✅" if passed else "❌" |
| with cols[i % len(cols)]: |
| st.metric(label=f"{icon} {label}", value=f"{earned}/{weight} pts") |
| else: |
| st.info("No repository checks are available for this report.") |
|
|
| st.divider() |
|
|
| |
|
|
| st.subheader("Code Quality (informational)") |
| col1, col2 = st.columns(2) |
| with col1: |
| syntax = summary["syntax"] |
| ok = syntax.get("ok") |
| if ok is True: |
| st.success(f"Syntax: {syntax.get('summary', 'ok')}") |
| elif ok is False: |
| st.error(f"Syntax errors: {syntax.get('summary', '')}") |
| for err in syntax.get("errors", [])[:5]: |
| st.code(err, language=None) |
| else: |
| st.warning(f"Syntax: {syntax.get('summary', 'unavailable')}") |
| with col2: |
| docs = summary["docstrings"] |
| pct = docs.get("coverage_pct") |
| if pct is not None: |
| st.metric("Docstring coverage", f"{pct}%", help=docs.get("summary", "")) |
| else: |
| st.warning(f"Docstrings: {docs.get('summary', 'unavailable')}") |
|
|
| |
|
|
| if "claims_analysis" in report: |
| st.divider() |
| ca = report["claims_analysis"] |
| stats = ca["stats"] |
| rate_pct = stats["implementation_rate_pct"] |
| source_total = stats.get("source_total", stats["total"]) |
| scored_total = stats.get("scored_total", stats["total"]) |
| excluded_low = stats.get("excluded_low_verifiability", 0) |
| excluded_invalid = stats.get("excluded_invalid_verifiability", 0) |
| hidden_low_confidence = stats.get("hidden_low_confidence", 0) |
| selection = ca.get("selection", {}) |
|
|
| st.subheader("Code-Claims Analysis") |
| caption_parts = [] |
| if selection.get("only_high_medium_verifiability"): |
| caption_parts.append( |
| f"Verified {scored_total} of {source_total} extracted claims; " |
| f"excluded {excluded_low} low-verifiability and {excluded_invalid} missing/invalid claims." |
| ) |
| if "hide_low_confidence" in selection: |
| caption_parts.append( |
| f"Showing {stats['total']} claims; hidden {hidden_low_confidence} low-confidence claims. " |
| "Implementation rate is calculated from shown claims." |
| ) |
| if caption_parts: |
| st.caption(" ".join(caption_parts)) |
|
|
| c1, c2, c3 = st.columns(3) |
| c1.metric("Claims shown", f"{stats['total']} / {scored_total}") |
| c2.metric("Implemented", f"{stats['implemented']} / {stats['total']}") |
| rate_color = ( |
| "var(--app-success)" if rate_pct >= 70 else "var(--app-warning)" if rate_pct >= 40 else "var(--app-danger)" |
| ) |
| c3.markdown( |
| f"<div style='text-align:center'>" |
| f"<div style='font-size:13px;color:var(--app-text-muted);margin-bottom:4px'>Implementation rate</div>" |
| f"<span style='font-size:40px;font-weight:bold;color:{rate_color}'>{rate_pct}%</span>" |
| f"</div>", |
| unsafe_allow_html=True, |
| ) |
|
|
| st.markdown("#### Claims") |
| claims_to_render = claims_for_reporting(ca) |
| for claim in claims_to_render: |
| impl = claim.get("implementation", {}) |
| done = impl.get("implemented", False) |
| conf = impl.get("confidence", "") |
| evidence = impl.get("evidence_file") or "" |
| explanation = impl.get("explanation", "") |
| icon = "✅" if done else "❌" |
| cat = claim.get("category", "") |
| val = claim.get("value") |
|
|
| with st.expander(f"{icon} {claim.get('claim', '')}"): |
| cols2 = st.columns([2, 2]) |
| with cols2[0]: |
| st.markdown(f"**Category:** `{cat}`") |
| if val: |
| st.markdown(f"**Value:** `{val}`") |
| st.markdown(f"**Verifiability:** {claim.get('verifiability', '')}") |
| if claim.get("section_name"): |
| st.markdown(f"**Section:** {claim['section_name']}") |
| with cols2[1]: |
| st.markdown(f"**Confidence:** {conf}") |
| if evidence: |
| st.markdown(f"**File:** `{evidence}`") |
| if claim.get("original_text"): |
| st.markdown(f"*\"{claim['original_text']}\"*") |
| if explanation: |
| st.info(explanation) |
| if claim.get("contradiction"): |
| st.warning("⚠️ Contradicts another claim in the paper") |
|
|
|
|
| def run_analysis( |
| *, |
| repo_url: str, |
| paper_file, |
| ignore_low_verifiability: bool, |
| hide_low_confidence: bool, |
| openrouter_key: str, |
| github_token: str | None, |
| model: str, |
| ) -> dict | None: |
| """Parse optional paper input, run repository analysis, and return a report.""" |
| paper_sections = None |
| pre_extracted_claims = None |
|
|
| if paper_file is not None: |
| if paper_file.name.endswith(".pdf"): |
| with st.spinner("Parsing PDF…"): |
| try: |
| pdf_bytes = paper_file.read() |
| if is_marker_available(): |
| try: |
| paper_sections = parse_pdf_to_sections_marker( |
| pdf_bytes, |
| openrouter_key=openrouter_key or None, |
| model=model or None, |
| ) |
| parser_used = "marker" |
| except Exception as marker_err: |
| st.warning(f"marker failed ({marker_err}), falling back to pymupdf.") |
| paper_sections = parse_pdf_to_sections(pdf_bytes) |
| parser_used = "pymupdf" |
| else: |
| paper_sections = parse_pdf_to_sections(pdf_bytes) |
| parser_used = "pymupdf" |
| st.success(f"Parsed {len(paper_sections)} sections from PDF (via {parser_used}).") |
| except Exception as e: |
| st.error(f"PDF parsing failed: {e}") |
| return None |
| else: |
| try: |
| data = json.loads(paper_file.read().decode("utf-8")) |
| except Exception as e: |
| st.error(f"Could not parse JSON: {e}") |
| return None |
|
|
| if isinstance(data, dict) and "result" in data and isinstance(data["result"], list): |
| pre_extracted_claims = data["result"] |
| st.success(f"Loaded {len(pre_extracted_claims)} pre-extracted claims — skipping PDF parsing.") |
| elif isinstance(data, list) and data and "claim" in data[0]: |
| pre_extracted_claims = data |
| st.success(f"Loaded {len(pre_extracted_claims)} pre-extracted claims — skipping PDF parsing.") |
| else: |
| paper_sections = data |
| st.success("Loaded sections JSON.") |
|
|
| config = Config( |
| repo_url=repo_url, |
| openrouter_key=openrouter_key, |
| github_token=github_token or None, |
| model=model, |
| ) |
|
|
| progress_bar = st.progress(0.0) |
| status = st.empty() |
|
|
| def on_progress(msg: str, pct: float) -> None: |
| progress_bar.progress(min(pct, 1.0)) |
| status.text(msg) |
|
|
| try: |
| report = analyze_full( |
| config, |
| paper_sections=paper_sections, |
| pre_extracted_claims=pre_extracted_claims, |
| ignore_low_verifiability=ignore_low_verifiability, |
| hide_low_confidence=hide_low_confidence, |
| on_progress=on_progress, |
| ) |
| except Exception as e: |
| progress_bar.empty() |
| status.empty() |
| st.error(f"Analysis failed: {e}") |
| return None |
|
|
| progress_bar.progress(1.0) |
| status.empty() |
| progress_bar.empty() |
| return report |
|
|
|
|
| def render_new_analysis_tab( |
| *, |
| analyze_btn: bool, |
| repo_url: str, |
| paper_file, |
| ignore_low_verifiability: bool, |
| hide_low_confidence: bool, |
| openrouter_key: str, |
| github_token: str | None, |
| model: str, |
| ) -> None: |
|
|
| if analyze_btn: |
| report = run_analysis( |
| repo_url=repo_url, |
| paper_file=paper_file, |
| ignore_low_verifiability=ignore_low_verifiability, |
| hide_low_confidence=hide_low_confidence, |
| openrouter_key=openrouter_key, |
| github_token=github_token, |
| model=model, |
| ) |
| if report is not None: |
| st.session_state["report"] = report |
| st.session_state["paper_name"] = paper_file.name if paper_file is not None else None |
| clear_pdf_cache("live_report") |
|
|
| if "report" not in st.session_state: |
| st.info( |
| "Fill in the repository URL and OpenRouter API key, then click **Analyze**.\n\n" |
| "Optionally upload a PDF article to verify which claims from the paper are implemented in the code." |
| ) |
| return |
|
|
| render_report( |
| st.session_state["report"], |
| paper_name=st.session_state.get("paper_name"), |
| repo_url=repo_url, |
| pdf_cache_prefix="live_report", |
| openrouter_key=openrouter_key, |
| model=model, |
| file_stem="report", |
| show_back_button=False, |
| ) |
|
|
|
|
| def select_saved_report(entry: dict, *, language: str) -> None: |
| state_prefix = f"leaderboard_{language}" |
| st.session_state[f"{state_prefix}_selected"] = entry |
| st.session_state.pop(f"{state_prefix}_report", None) |
| st.session_state.pop(f"{state_prefix}_report_path", None) |
| st.session_state[f"{state_prefix}_loading"] = True |
| clear_pdf_cache(f"saved_report_{language}") |
| st.rerun() |
|
|
|
|
| def render_saved_report_list(manifest: list[dict], *, language: str) -> None: |
| st.subheader("List of Saved Reports") |
|
|
| if not manifest: |
| st.info(f"No saved {language.upper()} reports found in leaderboard/manifest.json.") |
| return |
|
|
| headers = st.columns([0.1, 3, 0.5, 0.5], vertical_alignment="center") |
| headers[0].markdown("**#**") |
| headers[1].markdown("**Student Last Name**") |
| headers[2].markdown("**Sanity Score**") |
| headers[3].markdown("**Claim-Code Score**") |
|
|
| for entry in manifest: |
| rank = entry.get("rank", "") |
| name = entry.get("name") or entry.get("last_name") or entry.get("label", "") |
| sanity_score = entry.get("sanity_score", entry.get("score", "?")) |
| claim_code_score = entry.get("claim_code_score", "?") |
|
|
| cols = st.columns([0.1, 3, 0.5, 0.5], vertical_alignment="center") |
| with cols[0]: |
| st.markdown(f"**{rank}**") |
| with cols[1]: |
| if st.button(name, key=f"saved_report_{language}_open_{rank}", use_container_width=True): |
| select_saved_report(entry, language=language) |
| with cols[2]: |
| st.markdown(f"**{sanity_score}/100**") |
| with cols[3]: |
| st.markdown(f"**{claim_code_score}%**") |
|
|
|
|
| def render_saved_reports_tab(*, language: str, openrouter_key: str | None, model: str | None) -> None: |
| try: |
| manifest = filter_leaderboard_manifest(load_leaderboard_manifest(), language) |
| except Exception as e: |
| st.error(f"Could not load leaderboard manifest: {e}") |
| return |
|
|
| state_prefix = f"leaderboard_{language}" |
| cache_prefix = f"saved_report_{language}" |
|
|
| selected = st.session_state.get(f"{state_prefix}_selected") |
| if not selected: |
| render_saved_report_list(manifest, language=language) |
| return |
|
|
| selected_path = selected.get("json_path") |
| try: |
| if ( |
| st.session_state.get(f"{state_prefix}_report_path") != selected_path |
| or f"{state_prefix}_report" not in st.session_state |
| ): |
| loading_key = f"{state_prefix}_loading" |
| if st.session_state.pop(loading_key, False): |
| with st.status("Loading saved report...", expanded=False) as status: |
| st.session_state[f"{state_prefix}_report"] = load_saved_report(selected) |
| st.session_state[f"{state_prefix}_report_path"] = selected_path |
| status.update(label="Saved report loaded.", state="complete", expanded=False) |
| else: |
| with st.spinner("Loading saved report..."): |
| st.session_state[f"{state_prefix}_report"] = load_saved_report(selected) |
| st.session_state[f"{state_prefix}_report_path"] = selected_path |
| except Exception as e: |
| st.error(f"Could not load saved report: {e}") |
| return |
|
|
| label = selected.get("label", "saved_report") |
| render_report( |
| st.session_state[f"{state_prefix}_report"], |
| paper_name=label, |
| pdf_cache_prefix=cache_prefix, |
| openrouter_key=openrouter_key, |
| model=model, |
| file_stem=label, |
| show_back_button=True, |
| back_button_key=f"{cache_prefix}_back", |
| back_state_prefix=state_prefix, |
| ) |
|
|
|
|
| st.set_page_config( |
| page_title="🐝 OSA.Edu", |
| page_icon="🔍", |
| layout="wide", |
| ) |
|
|
| st.markdown( |
| """ |
| <div class="app-header"> |
| <h1 class="app-title">🔍🐝 OSA.Edu</h1> |
| <p class="app-caption">Evaluates thesis project repositories on a 0-100 scale and verifies paper claims.</p> |
| </div> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
| |
|
|
| with st.sidebar: |
| st.header("Repository") |
| repo_url = st.text_input( |
| "GitHub URL", |
| placeholder="https://github.com/owner/repo", |
| ) |
|
|
| st.header("Paper") |
| st.caption("Upload a PDF, pre-parsed sections JSON, or pre-extracted claims JSON to enable claim verification.") |
| paper_file = st.file_uploader("Article", type=["pdf", "json"]) |
| ignore_low_verifiability = st.checkbox( |
| "Verify and show only high/medium-verifiability claims", |
| value=True, |
| help=( |
| "Low, missing, and invalid verifiability values are excluded before code verification " |
| "and do not affect the implementation rate." |
| ), |
| ) |
| hide_low_confidence = st.checkbox( |
| "Hide low-confidence verification results", |
| value=True, |
| help="Hidden low-confidence results are excluded from reports and the implementation rate.", |
| ) |
|
|
| st.header("API Keys") |
| openrouter_key = st.text_input( |
| "OpenRouter API Key", |
| type="password", |
| value=os.environ.get("OPENROUTER_KEY", ""), |
| ) |
| github_token = st.text_input( |
| "GitHub Token (optional)", |
| type="password", |
| value=os.environ.get("GITHUB_TOKEN", ""), |
| help="Needed for private repos or to avoid rate limits.", |
| ) |
|
|
| st.header("Model") |
| model = st.selectbox( |
| "Model", |
| options=[ |
| "openai/gpt-4o", |
| "openai/gpt-4o-mini", |
| "deepseek/deepseek-v3", |
| "anthropic/claude-3.5-haiku", |
| "anthropic/claude-3.5-sonnet", |
| ], |
| label_visibility="collapsed", |
| accept_new_options=True, |
| ) |
|
|
| analyze_btn = st.button( |
| "Analyze", |
| type="primary", |
| disabled=not (repo_url and openrouter_key), |
| use_container_width=True, |
| ) |
|
|
| |
|
|
| app_css = """ |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@600;700&display=swap'); |
| |
| :root { |
| --app-font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; |
| --app-title-font-family: "Space Grotesk", var(--app-font-family); |
| --app-bg: #F6F7F9; |
| --app-surface: #FFFFFF; |
| --app-surface-muted: #EEF2F5; |
| --app-text: #172033; |
| --app-text-muted: #667085; |
| --app-border: #D7DEE8; |
| --app-primary: #0F766E; |
| --app-primary-hover: #115E59; |
| --app-accent: #F59E0B; |
| --app-success: #16A34A; |
| --app-warning: #D97706; |
| --app-danger: #DC2626; |
| } |
| |
| html, |
| body, |
| .stApp, |
| [data-testid="stAppViewContainer"], |
| [data-testid="stSidebar"], |
| [data-testid="stMarkdownContainer"], |
| label, |
| input, |
| textarea, |
| select, |
| .stButton button, |
| .stDownloadButton button { |
| font-family: var(--app-font-family); |
| } |
| |
| .stApp, |
| [data-testid="stAppViewContainer"] { |
| background: var(--app-bg); |
| color: var(--app-text); |
| } |
| |
| [data-testid="stHeader"] { |
| background: rgba(246, 247, 249, 0.92); |
| } |
| |
| [data-testid="stSidebar"] > div:first-child { |
| background: var(--app-surface-muted); |
| border-right: 1px solid var(--app-border); |
| } |
| |
| .app-header { |
| margin: 0.25rem auto 1.4rem; |
| text-align: center; |
| } |
| |
| .app-title { |
| color: var(--app-text); |
| font-family: var(--app-title-font-family); |
| font-size: 56px; |
| font-weight: 700; |
| letter-spacing: 0; |
| line-height: 1.05; |
| margin: 0; |
| } |
| |
| .app-caption { |
| color: var(--app-text-muted); |
| font-size: 16px; |
| line-height: 1.45; |
| margin: 0.45rem 0 0; |
| } |
| |
| h1, |
| h2, |
| h3, |
| h4, |
| h5, |
| h6, |
| p, |
| li, |
| label, |
| [data-testid="stMarkdownContainer"] { |
| color: var(--app-text); |
| } |
| |
| a { |
| color: var(--app-primary); |
| } |
| |
| a:hover { |
| color: var(--app-primary-hover); |
| } |
| |
| hr { |
| border-color: var(--app-border); |
| } |
| |
| .stButton > button, |
| .stDownloadButton > button { |
| background: var(--app-surface); |
| border: 1px solid var(--app-border); |
| border-radius: 8px; |
| color: var(--app-text); |
| } |
| |
| .stButton > button:hover, |
| .stDownloadButton > button:hover { |
| border-color: var(--app-primary); |
| color: var(--app-primary); |
| } |
| |
| .stButton > button[kind="primary"] { |
| background: var(--app-primary); |
| border-color: var(--app-primary); |
| color: #FFFFFF; |
| } |
| |
| .stButton > button[kind="primary"] *, |
| .stButton > button[kind="primary"] [data-testid="stMarkdownContainer"], |
| .stButton > button[kind="primary"] [data-testid="stMarkdownContainer"] p { |
| color: #FFFFFF !important; |
| } |
| |
| .stButton > button[kind="primary"]:hover { |
| background: var(--app-primary-hover); |
| border-color: var(--app-primary-hover); |
| color: #FFFFFF; |
| } |
| |
| [data-testid="stMetric"] { |
| background: var(--app-surface); |
| border: 1px solid var(--app-border); |
| border-radius: 8px; |
| padding: 0.75rem 0.9rem; |
| } |
| |
| [data-testid="stAlert"] { |
| border-radius: 8px; |
| } |
| |
| div[data-testid="stTabs"] [data-baseweb="tab-list"], |
| div[data-testid="stTabs"] div[role="tablist"], |
| div[data-testid="stTabs"] > div:first-child > div:first-child { |
| justify-content: center; |
| border-bottom: 1px solid var(--app-border); |
| } |
| |
| div[data-testid="stTabs"] [data-testid="stTab"] { |
| flex: 0 0 auto; |
| height: 3rem !important; |
| min-height: 3rem !important; |
| padding: 0 2.25rem !important; |
| color: var(--app-text-muted) !important; |
| } |
| |
| div[data-testid="stTabs"] [data-testid="stTab"], |
| div[data-testid="stTabs"] [data-testid="stTab"] *, |
| div[data-testid="stTabs"] [data-testid="stTab"] [data-testid="stMarkdownContainer"], |
| div[data-testid="stTabs"] [data-testid="stTab"] [data-testid="stMarkdownContainer"] *, |
| div[data-testid="stTabs"] [data-testid="stTab"] .material-symbols-rounded { |
| font-size: 24px !important; |
| line-height: 1.15 !important; |
| } |
| |
| div[data-testid="stTabs"] [data-testid="stTab"][data-selected], |
| div[data-testid="stTabs"] [data-testid="stTab"][data-selected] * { |
| color: var(--app-primary) !important; |
| font-weight: 700; |
| } |
| |
| div[data-testid="stTabs"] .react-aria-SelectionIndicator, |
| div[data-testid="stTabs"] [data-baseweb="tab-highlight"] { |
| background-color: var(--app-primary); |
| } |
| </style> |
| """ |
|
|
| st.markdown(app_css, unsafe_allow_html=True) |
|
|
| tab_saved_reports_en, tab_saved_reports_ru, tab_new_analysis = st.tabs( |
| [ |
| ":material/list: Saved Reports EN", |
| ":material/list: Saved Reports RU", |
| ":material/add: New Analysis", |
| ] |
| ) |
|
|
| with tab_saved_reports_en: |
| render_saved_reports_tab(language="en", openrouter_key=openrouter_key or None, model=model or None) |
|
|
| with tab_saved_reports_ru: |
| render_saved_reports_tab(language="ru", openrouter_key=openrouter_key or None, model=model or None) |
|
|
| with tab_new_analysis: |
| render_new_analysis_tab( |
| analyze_btn=analyze_btn, |
| repo_url=repo_url, |
| paper_file=paper_file, |
| ignore_low_verifiability=ignore_low_verifiability, |
| hide_low_confidence=hide_low_confidence, |
| openrouter_key=openrouter_key, |
| github_token=github_token, |
| model=model, |
| ) |
|
|