storycode / ui /theme.py
Claude
Scaffold StoryCode: grounded code-story explainer for vibe coders
71d239c
Raw
History Blame Contribute Delete
4.76 kB
"""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()
# Injected once into the page <head>. Loads mermaid and watches the DOM, so any
# diagram Gradio swaps in after an analysis is rendered automatically.
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:
# Most dangerous first — that's what a nervous editor needs to see.
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)