| """Gradio theme + HTML builders shared by app.py. |
| |
| All the rich panels (the story, the safe-to-edit list, banners) are rendered as |
| HTML so we control the look precisely — the custom design is a hackathon badge |
| and an explicit requirement, not default Gradio. |
| """ |
| from __future__ import annotations |
|
|
| import html |
| from pathlib import Path |
|
|
| import gradio as gr |
|
|
| import config |
| from schema import ProjectModel, ProjectStory |
|
|
| THEME = gr.themes.Soft( |
| primary_hue=gr.themes.colors.indigo, |
| neutral_hue=gr.themes.colors.stone, |
| font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], |
| ).set( |
| body_background_fill="#f7f4ef", |
| block_background_fill="#ffffff", |
| block_border_width="1px", |
| block_radius="18px", |
| ) |
|
|
|
|
| def css() -> str: |
| return (Path(__file__).parent / "styles.css").read_text() |
|
|
|
|
| |
| |
| MERMAID_HEAD = """ |
| <script type="module"> |
| import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs'; |
| mermaid.initialize({ startOnLoad: false, theme: 'neutral', |
| flowchart: { curve: 'basis', useMaxWidth: true } }); |
| async function draw() { |
| const blocks = document.querySelectorAll('.mermaid:not([data-processed])'); |
| if (blocks.length) { try { await mermaid.run({ nodes: blocks }); } catch (e) {} } |
| } |
| const obs = new MutationObserver(() => draw()); |
| obs.observe(document.body, { childList: true, subtree: true }); |
| window.addEventListener('load', draw); |
| </script> |
| """ |
|
|
|
|
| def _e(text: str) -> str: |
| return html.escape(text or "") |
|
|
|
|
| def header_html() -> str: |
| return ( |
| '<div id="sc-header">' |
| '<span class="mark">Story<span class="accent">Code</span></span>' |
| '<span class="tag">You built it. Now understand it — as a story.</span>' |
| "</div>") |
|
|
|
|
| def sources_html() -> str: |
| chips = "".join(f'<span class="sc-chip">{_e(s)}</span>' for s in config.SOURCE_HINTS) |
| return ( |
| '<div class="sc-card">' |
| '<div style="font-weight:600;color:#1d2430;margin-bottom:8px;">' |
| "Where did your code come from?</div>" |
| f'<div class="sc-sources">{chips}</div>' |
| '<div class="sc-privacy">Upload a <strong>.zip</strong> of your project (or one file). ' |
| "Don’t paste passwords or API keys — <strong>we’ll flag and hide them</strong> " |
| "if you do.</div>" |
| "</div>") |
|
|
|
|
| def banner(text: str, kind: str = "info") -> str: |
| return f'<div class="sc-banner {kind}">{text}</div>' if text else "" |
|
|
|
|
| def story_html(s: ProjectStory) -> str: |
| steps = "".join( |
| f'<div class="sc-step"><div class="h">{_e(sec.heading)}</div>' |
| f'<div class="b">{_e(sec.body)}</div></div>' |
| for sec in s.steps) |
| return ( |
| '<div class="sc-story sc-card">' |
| f"<h2>{_e(s.title)}</h2>" |
| f'<div class="overview">{_e(s.overview)}</div>' |
| f"{steps}" |
| "</div>") |
|
|
|
|
| def plain_panel_html(s: ProjectStory) -> str: |
| return ( |
| '<div class="sc-plain">' |
| '<div class="label">Plain English</div>' |
| f'<div class="body">{_e(s.plain_overview or s.overview)}</div>' |
| "</div>") |
|
|
|
|
| def safe_to_edit_html(model: ProjectModel) -> str: |
| |
| order = {config.DANGER: 0, config.CAREFUL: 1, config.SAFE: 2} |
| files = sorted(model.files, key=lambda f: (order.get(f.safety, 1), -f.fan_in, f.path)) |
| rows = [] |
| for f in files: |
| emoji = config.SAFETY_EMOJI.get(f.safety, "🟠") |
| label = config.SAFETY_LABEL.get(f.safety, "") |
| rows.append( |
| f'<div class="sc-file {f.safety}">' |
| f'<div class="dot">{emoji}</div>' |
| '<div class="body">' |
| f'<span class="name">{_e(f.path)}</span>' |
| f'<span class="verdict">{_e(label)}</span>' |
| f'<div class="reason">{_e(f.safety_reason)}</div>' |
| "</div></div>") |
| legend = ('<div class="sc-legend">🟢 safe to change · 🟠 change carefully · ' |
| "🔴 don’t touch unless you know why</div>") |
| return legend + "".join(rows) |
|
|
|
|
| def deps_html(model: ProjectModel) -> str: |
| if not model.deps: |
| return banner("No dependency files (requirements.txt / package.json) found.", "info") |
| rows = [] |
| for d in model.deps: |
| flag = ' <span style="color:#c2503f;font-size:12px;">(check this)</span>' if d.risky else "" |
| rows.append( |
| f'<div class="sc-file"><div class="dot">📦</div><div class="body">' |
| f'<span class="name">{_e(d.name)}</span>{flag}' |
| f'<div class="reason">{_e(d.plain)}</div></div></div>') |
| return "".join(rows) |
|
|