#!/usr/bin/env python3 """Serve the SupplyAgent business-design visualizations locally.""" from __future__ import annotations import argparse import html import mimetypes import threading import webbrowser from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import unquote, urlparse VISUALIZATION_DIR = Path(__file__).resolve().parents[1] PAGES_DIR = VISUALIZATION_DIR / "pages" PAGES: tuple[tuple[str, str, str], ...] = ( ( "全局分类树-需求热度与关系分析.html", "00 · 平台全局需求地图", "用全量冰柱热力图呈现冷热分布,并从上游数据直接形成数百条平台需求和可追溯 Reason。", ), ( "graph-structure-options.html", "01 · 图结构方案比较", "比较单一大树、自由图和树骨架关系图。", ), ( "graph-structure-left-to-right.html", "02 · 从左到右的树图结构", "展示主题、分类树、具体节点、平台需求和证据的阅读方向。", ), ( "business-graph-model.html", "03 · 业务图节点与边", "展示分类节点、需求元素、平台需求、关系和证据。", ), ( "demand-feedback-loop.html", "04 · 需求—内容—表现闭环", "展示真实线上表现如何反向更新需求强度。", ), ( "final-output-structure.html", "05 · 最终输出结构", "展示主题方向、平台需求和证据卡的组织方式。", ), ( "node-drilldown-evidence-chain.html", "06 · 节点下钻证据链", "展示分类节点如何下挂元素、视频、短观点、长讨论并提取平台需求。", ), ( "waiting-business-scope.html", "过程页 A · 业务范围确认", "对话过程中的可视化等待页。", ), ( "waiting-demand-pipeline.html", "过程页 B · 需求流水线确认", "对话过程中的可视化等待页。", ), ( "waiting-strength-lifecycle.html", "过程页 C · 强度生命周期确认", "对话过程中的可视化等待页。", ), ) BASE_CSS = """ * { box-sizing: border-box; } html, body { min-height: 100%; } body { margin: 0; color: #0f172a; background: #f5f7fb; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.5; } a { color: inherit; } .topbar { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 24px; color: #e2e8f0; background: rgba(15, 23, 42, .96); border-bottom: 1px solid #334155; } .topbar a { text-decoration: none; } .topbar .brand { font-weight: 700; color: #fff; } .topbar .meta { color: #94a3b8; font-size: 13px; } .content { width: min(1480px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 48px; } h1, h2, h3 { line-height: 1.25; } h2 { margin: 0 0 8px; font-size: 26px; } .subtitle { color: #64748b; margin: 0 0 20px; } .options { display: flex; flex-direction: column; gap: 12px; } .option, .card { cursor: pointer; } .option.selected, .card.selected { border-color: #2563eb !important; box-shadow: 0 0 0 3px rgba(37, 99, 235, .14) !important; } .gallery-title { margin: 4px 0 8px; font-size: 30px; } .gallery-intro { max-width: 880px; color: #64748b; margin: 0 0 24px; } .gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); gap: 16px; } .gallery-card { display: block; padding: 18px; text-decoration: none; background: #fff; border: 1px solid #dbe3ec; border-radius: 15px; transition: .18s ease; } .gallery-card:hover { transform: translateY(-2px); border-color: #60a5fa; box-shadow: 0 12px 28px rgba(15, 23, 42, .09); } .gallery-card h3 { margin: 0 0 7px; font-size: 17px; } .gallery-card p { margin: 0; color: #64748b; font-size: 13px; } .reference { margin-top: 24px; padding: 18px; border: 1px solid #dbe3ec; border-radius: 15px; background: #fff; } .reference img { display: block; width: min(100%, 650px); height: auto; margin-top: 12px; border: 1px solid #e2e8f0; } """ def _page_metadata(filename: str) -> tuple[str, str] | None: for item_filename, title, description in PAGES: if filename == item_filename: return title, description return None def _document(title: str, body: str, *, subtitle: str = "") -> bytes: subtitle_html = ( f'
{html.escape(subtitle)}
' if subtitle else "" ) document = f""" {html.escape(title)} · SupplyAgent
SupplyAgent 业务可视化 {subtitle_html}
{body}
""" return document.encode("utf-8") def _gallery() -> bytes: cards = [] for filename, title, description in PAGES: cards.append( f"""

{html.escape(title)}

{html.escape(description)}

""" ) body = f"""

SupplyAgent 业务框架可视化

用户提供的参考图

用于理解全局分类树、分类节点下挂元素,以及元素如何扩展成明确语言和长段讨论。

全局分类树局部参考图 分类节点下挂元素参考图 元素扩展为语言和讨论参考图 全局需求地图留白调整参考图
""" return _document("可视化目录", body, subtitle="本地设计资料") class VisualizationHandler(SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=str(VISUALIZATION_DIR), **kwargs) def do_GET(self) -> None: # noqa: N802 path = unquote(urlparse(self.path).path) if path == "/": self._send_html(_gallery()) return if path.startswith("/view/"): filename = Path(path.removeprefix("/view/")).name metadata = _page_metadata(filename) page_path = PAGES_DIR / filename if metadata is None or not page_path.is_file(): self.send_error(404, "Visualization not found") return title, description = metadata fragment = page_path.read_text(encoding="utf-8") self._send_html(_document(title, fragment, subtitle=description)) return super().do_GET() def _send_html(self, payload: bytes) -> None: self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) def log_message(self, format: str, *args: object) -> None: print(f"[visualization] {self.address_string()} - {format % args}") def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=8765) parser.add_argument("--open", action="store_true", dest="open_browser") args = parser.parse_args() mimetypes.add_type("image/jpeg", ".jpeg") server = ThreadingHTTPServer((args.host, args.port), VisualizationHandler) url_host = "localhost" if args.host in {"127.0.0.1", "0.0.0.0"} else args.host url = f"http://{url_host}:{server.server_port}/" print(f"SupplyAgent visualization server: {url}") if args.open_browser: threading.Timer(0.3, webbrowser.open, args=(url,)).start() try: server.serve_forever() except KeyboardInterrupt: pass finally: server.server_close() if __name__ == "__main__": main()