File size: 4,758 Bytes
71d239c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
"""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)