| """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 |
|
|
| |
| _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"] |
|
|
| |
| 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") |
|
|
| |
| for f in model.files: |
| for dep in f.depends_on: |
| if dep in ids: |
| lines.append(f" {ids[f.path]} --> {ids[dep]}") |
|
|
| |
| 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: |
| |
| return text.replace('"', "").replace("[", "(").replace("]", ")") |
|
|
|
|
| |
| |
| |
| 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)) |
| |
| import time |
| key = int(time.time() * 1000) |
| return (f'<div class="sc-diagram">' |
| f'<pre class="mermaid" data-key="{key}">{safe}</pre></div>') |
|
|