"""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
. Loads mermaid and watches the DOM, so any
# diagram Gradio swaps in after an analysis is rendered automatically.
MERMAID_HEAD = """
"""
def _e(text: str) -> str:
return html.escape(text or "")
def header_html() -> str:
return (
'")
def sources_html() -> str:
chips = "".join(f'{_e(s)}' for s in config.SOURCE_HINTS)
return (
''
'
'
"Where did your code come from?
"
f'
{chips}
'
'
Upload a .zip of your project (or one file). '
"Don’t paste passwords or API keys — we’ll flag and hide them "
"if you do.
"
"
")
def banner(text: str, kind: str = "info") -> str:
return f'{text}
' if text else ""
def story_html(s: ProjectStory) -> str:
steps = "".join(
f'{_e(sec.heading)}
'
f'
{_e(sec.body)}
'
for sec in s.steps)
return (
''
f"
{_e(s.title)}
"
f'
{_e(s.overview)}
'
f"{steps}"
"
")
def plain_panel_html(s: ProjectStory) -> str:
return (
''
'
Plain English
'
f'
{_e(s.plain_overview or s.overview)}
'
"
")
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''
f'
{emoji}
'
'
'
f'
{_e(f.path)}'
f'
{_e(label)}'
f'
{_e(f.safety_reason)}
'
"
")
legend = ('🟢 safe to change · 🟠 change carefully · '
"🔴 don’t touch unless you know why
")
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 = ' (check this)' if d.risky else ""
rows.append(
f'📦
'
f'
{_e(d.name)}{flag}'
f'
{_e(d.plain)}
')
return "".join(rows)