v_graph_visualizer.html 9.41 KB
<!DOCTYPE html>
<!--
  v_graph_visualizer — 静态原型(mock 数据,无构建步骤)

  用途:给 Claude Code 在实现 PH3 U-026 时用作视觉+结构参照,不是最终代码,
  但 DOM 结构 / class 命名 / 数据字段名刻意和真实实现保持一致,可以直接在这个
  基础上接入真实 c_ws_subscribe 推送数据,而不必重新设计布局。

  对应 SDD 条目:
  - 02-V-界面与交互.md §2.7(v_graph_visualizer)+ §2.7.1(v_log_viewer,本原型未展开,
    真实页面里通过 click_view_raw_log 从 Work Node 卡片展开)
  - 03-C-控制与接口.md 3.6 节:work_node_execution.status 六态着色语义
    done=绿 / running=蓝 / reflecting|retrying=黄 / waiting_for_quota=浅蓝+时钟图标(信息态,非告警)
    / blocked|failed=红 / pending=灰
  - sdd-mvcp-contract.json view_contract.v_graph_visualizer:
    fields=[{name:"run_id", visible_when:"task_has_active_run"}]
    events=[{event:"hover_node", target_controller_id:"c_work_node_get", payload:{node_exec_id}}]

  DOM 约定(供 CC 直接复用):
  - 每个 Work Node 卡片:class="work-node status-<work_node_execution.status 枚举值>"
  - data-node-exec-id 属性对应真实的 node_exec_id,真实实现里 hover 事件从这里读取
  - waiting_for_quota 状态额外带 data-quota-limit-type / data-quota-wait-since 两个属性,
    对应 03-C 3.6 节退避重试语义里要展示的 quota_limit_type + 已等待时长
-->
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>v_graph_visualizer 原型(mock 数据)</title>
<style>
  :root {
    --status-pending: #9ca3af;
    --status-running: #3b82f6;
    --status-reflecting: #eab308;
    --status-waiting-quota: #7dd3fc;
    --status-blocked: #ef4444;
    --status-done: #22c55e;
    --border: #d1d5db;
    --text: #111827;
    --text-muted: #6b7280;
    --bg-panel: #f9fafb;
  }
  body { font-family: -apple-system, sans-serif; margin: 0; padding: 24px; background: #fff; color: var(--text); }
  .proto-banner { background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; padding: 8px 12px; font-size: 13px; margin-bottom: 20px; }
  h2 { font-size: 15px; margin: 0 0 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; }
  section { margin-bottom: 32px; }

  /* ---- Org Graph:固定三节点 ---- */
  .org-graph { display: flex; align-items: center; gap: 0; }
  .org-node { border: 2px solid var(--border); border-radius: 10px; padding: 14px 24px; text-align: center; background: var(--bg-panel); min-width: 120px; }
  .org-node.active { border-color: var(--status-running); background: #eff6ff; box-shadow: 0 0 0 3px rgba(59,130,246,.15); }
  .org-node .role { font-weight: 600; font-size: 14px; }
  .org-node .hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
  .org-arrow { flex: 0 0 60px; text-align: center; color: var(--text-muted); font-size: 20px; }

  /* ---- Work Graph:动态节点/边 ---- */
  .work-graph { position: relative; border: 1px dashed var(--border); border-radius: 8px; padding: 32px; background: var(--bg-panel); }
  .work-row { display: flex; gap: 24px; margin-bottom: 40px; justify-content: center; }
  .work-node {
    border: 2px solid var(--border);
    border-radius: 8px;
    padding: 10px 14px;
    min-width: 140px;
    background: #fff;
    cursor: pointer;
    position: relative;
    font-size: 13px;
  }
  .work-node .node-id { font-weight: 600; }
  .work-node .node-skill { color: var(--text-muted); font-size: 11px; margin-top: 2px; }
  .work-node.status-pending { border-color: var(--status-pending); }
  .work-node.status-running { border-color: var(--status-running); background: #eff6ff; }
  .work-node.status-reflecting, .work-node.status-retrying { border-color: var(--status-reflecting); background: #fefce8; }
  .work-node.status-waiting_for_quota { border-color: var(--status-waiting-quota); background: #f0f9ff; }
  .work-node.status-waiting_for_quota::after { content: "🕐"; position: absolute; top: -8px; right: -8px; font-size: 14px; }
  .work-node.status-blocked, .work-node.status-failed { border-color: var(--status-blocked); background: #fef2f2; }
  .work-node.status-done { border-color: var(--status-done); background: #f0fdf4; }

  /* ---- 图例 ---- */
  .legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: var(--text-muted); }
  .legend-item { display: flex; align-items: center; gap: 6px; }
  .legend-swatch { width: 14px; height: 14px; border-radius: 4px; border: 2px solid; }

  /* ---- hover tooltip(示意,真实实现走 c_work_node_get) ---- */
  .tooltip {
    position: absolute; z-index: 10; background: #111827; color: #fff; font-size: 12px;
    padding: 8px 10px; border-radius: 6px; max-width: 260px; display: none; pointer-events: none;
    line-height: 1.5;
  }
  .tooltip.show { display: block; }
</style>
</head>
<body>

<div class="proto-banner">
  这是 v_graph_visualizer 的静态原型(mock 数据,不连真实后端)。用于 PH3 U-026 开发前的视觉核对——
  DOM 结构 / class 命名 / data-* 属性均按 02-V §2.7、03-C 3.6 节约定设计,实现时可以直接复用这套结构接入
  c_ws_subscribe 推送的真实数据,不需要重新排版。
</div>

<section>
  <h2>Org Graph(固定三节点,02-V §2.7)</h2>
  <div class="org-graph">
    <div class="org-node">
      <div class="role">Planner</div>
      <div class="hint">已完成</div>
    </div>
    <div class="org-arrow">→</div>
    <div class="org-node active">
      <div class="role">Coder</div>
      <div class="hint">当前节点(task.assigned_org_node)</div>
    </div>
    <div class="org-arrow">→</div>
    <div class="org-node">
      <div class="role">Validator</div>
      <div class="hint">未开始</div>
    </div>
  </div>
</section>

<section>
  <h2>Work Graph(当前 work_graph_run.graph_manifest 渲染,节点按 status 着色)</h2>

  <div class="work-graph">
    <div class="work-row">
      <div class="work-node status-done" data-node-exec-id="node-001"
           onmouseover="showTip(event, 'node-001', 'skill: code-task-development', 'output_schema_ref: code_diff.schema.json', 'retry_count: 0')"
           onmouseout="hideTip()">
        <div class="node-id">worker:module-a</div>
        <div class="node-skill">code-task-development</div>
      </div>
      <div class="work-node status-done" data-node-exec-id="node-002"
           onmouseover="showTip(event, 'node-002', 'skill: code-task-development', 'output_schema_ref: code_diff.schema.json', 'retry_count: 1')"
           onmouseout="hideTip()">
        <div class="node-id">worker:module-b</div>
        <div class="node-skill">code-task-development</div>
      </div>
    </div>

    <div class="work-row">
      <div class="work-node status-waiting_for_quota" data-node-exec-id="node-003"
           data-quota-limit-type="session" data-quota-wait-since="12 分钟前"
           onmouseover="showTip(event, 'node-003', 'skill: code-agent-engineering', 'quota_limit_type: session', '已等待 12 分钟,自愈型阻塞')"
           onmouseout="hideTip()">
        <div class="node-id">worker:module-c</div>
        <div class="node-skill">code-agent-engineering</div>
      </div>
      <div class="work-node status-reflecting" data-node-exec-id="node-004"
           onmouseover="showTip(event, 'node-004', 'skill: debugging-toolkit', 'retry_count: 2', 'reflection_notes: unit test 断言不通过')"
           onmouseout="hideTip()">
        <div class="node-id">worker:module-d</div>
        <div class="node-skill">debugging-toolkit</div>
      </div>
    </div>

    <div class="work-row">
      <div class="work-node status-pending" data-node-exec-id="node-005"
           onmouseover="showTip(event, 'node-005', 'skill: software-planning-execution', 'output_schema_ref: test_report.schema.json', 'retry_count: 0')"
           onmouseout="hideTip()">
        <div class="node-id">fan-in:merge</div>
        <div class="node-skill">等待上游节点全部完成</div>
      </div>
    </div>
  </div>

  <div class="tooltip" id="tooltip"></div>

  <div class="legend" style="margin-top:16px;">
    <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-pending)"></span>pending</div>
    <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-running)"></span>running</div>
    <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-reflecting)"></span>reflecting / retrying</div>
    <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-waiting-quota)"></span>waiting_for_quota(信息态,非告警,见 03-C 3.6 节)</div>
    <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-blocked)"></span>blocked / failed</div>
    <div class="legend-item"><span class="legend-swatch" style="border-color:var(--status-done)"></span>done</div>
  </div>
</section>

<script>
  // 真实实现里这段逻辑对应 hover_node 事件 -> c_work_node_get(node_exec_id)
  // 这里用 data-* 属性里预置的 mock 文案代替真实接口返回
  function showTip(evt, nodeId, ...lines) {
    var tip = document.getElementById('tooltip');
    tip.innerHTML = '<b>' + nodeId + '</b><br>' + lines.join('<br>');
    tip.style.left = (evt.pageX + 12) + 'px';
    tip.style.top = (evt.pageY + 12) + 'px';
    tip.classList.add('show');
  }
  function hideTip() {
    document.getElementById('tooltip').classList.remove('show');
  }
</script>

</body>
</html>