File size: 2,972 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 | """Turn the project graph into a Mermaid diagram — built from real edges.
The architecture map is **not** drawn by the model. It's generated directly from
`ProjectModel` (the resolved import edges, grouped by plain-English role), so it
is always faithful to the actual code. We render it client-side with mermaid.js
inside a Gradio HTML component.
"""
from __future__ import annotations
import html as _html
import posixpath
import config
from schema import ProjectModel
# Order roles top-to-bottom so the map reads like a journey: front door at top.
_ROLE_ORDER = ("entry", "frontend", "backend", "data", "config", "util", "test", "other")
def _node_id(idx: int) -> str:
return f"n{idx}"
def build_mermaid(model: ProjectModel) -> str:
"""Return Mermaid `flowchart` source for the project."""
if not model.files:
return "flowchart TD\n empty[No code files found]"
ids = {f.path: _node_id(i) for i, f in enumerate(model.files)}
lines: list[str] = ["flowchart TD"]
# Group nodes into subgraphs by role.
by_role: dict[str, list] = {}
for f in model.files:
by_role.setdefault(f.role, []).append(f)
for role in _ROLE_ORDER:
group = by_role.get(role)
if not group:
continue
label = config.ROLE_LABEL.get(role, role)
lines.append(f' subgraph {role}["{label}"]')
for f in group:
base = posixpath.basename(f.path)
lines.append(f' {ids[f.path]}["{_esc(base)}"]')
lines.append(" end")
# Edges: A --> B means "A uses B".
for f in model.files:
for dep in f.depends_on:
if dep in ids:
lines.append(f" {ids[f.path]} --> {ids[dep]}")
# Colour each node by role (safe-to-edit is its own panel; here we show roles).
for role in _ROLE_ORDER:
if role in by_role:
color = config.ROLE_COLOR.get(role, "#9aa0a6")
lines.append(f" classDef {role} fill:{color},stroke:#1d2430,color:#fff,rx:8,ry:8;")
for f in model.files:
lines.append(f" class {ids[f.path]} {f.role};")
return "\n".join(lines)
def _esc(text: str) -> str:
# Mermaid node labels: keep it simple, strip quotes/brackets that break parsing.
return text.replace('"', "").replace("[", "(").replace("]", ")")
# Loaded once in the page <head> (see ui.theme.MERMAID_HEAD). A MutationObserver
# renders any `.mermaid` block Gradio swaps in after an update — the reliable way
# to get diagrams to (re)draw on dynamic content in a Gradio Space.
def render_html(model: ProjectModel) -> str:
"""The mermaid block for a gr.HTML component (loader lives in the page head)."""
safe = _html.escape(build_mermaid(model))
# `key` forces a fresh, unprocessed node each render so the observer redraws it.
import time
key = int(time.time() * 1000)
return (f'<div class="sc-diagram">'
f'<pre class="mermaid" data-key="{key}">{safe}</pre></div>')
|