From 9a4d5ebcaead50d690f9dbbe7bdcfda81f674c1c Mon Sep 17 00:00:00 2001 From: BitFun Research Date: Sat, 18 Jul 2026 15:10:55 +0800 Subject: [PATCH] feat: add bee-colony DAG canvas MiniApp and floating monitor panel --- .../product-domains/src/miniapp/builtin.rs | 10 + .../builtin/assets/bee-colony-dag/index.html | 24 ++ .../builtin/assets/bee-colony-dag/meta.json | 35 ++ .../builtin/assets/bee-colony-dag/style.css | 163 +++++++++ .../builtin/assets/bee-colony-dag/ui.js | 328 ++++++++++++++++++ src/web-ui/src/app/layout/AppLayout.tsx | 6 + .../src/app/layout/BeeColonyMonitor.scss | 102 ++++++ .../src/app/layout/BeeColonyMonitor.tsx | 147 ++++++++ 8 files changed, 815 insertions(+) create mode 100644 src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/index.html create mode 100644 src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/meta.json create mode 100644 src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/style.css create mode 100644 src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/ui.js create mode 100644 src/web-ui/src/app/layout/BeeColonyMonitor.scss create mode 100644 src/web-ui/src/app/layout/BeeColonyMonitor.tsx diff --git a/src/crates/contracts/product-domains/src/miniapp/builtin.rs b/src/crates/contracts/product-domains/src/miniapp/builtin.rs index 156212f46..a65ecb54c 100644 --- a/src/crates/contracts/product-domains/src/miniapp/builtin.rs +++ b/src/crates/contracts/product-domains/src/miniapp/builtin.rs @@ -159,6 +159,16 @@ pub const BUILTIN_APPS: &[BuiltinMiniAppBundle] = &[ worker_js: include_str!("builtin/assets/ppt-live/worker.js"), esm_dependencies_json: include_str!("builtin/assets/ppt-live/esm_dependencies.json"), }, + BuiltinMiniAppBundle { + id: "builtin-bee-colony-dag", + version: 1, + meta_json: include_str!("builtin/assets/bee-colony-dag/meta.json"), + html: include_str!("builtin/assets/bee-colony-dag/index.html"), + css: include_str!("builtin/assets/bee-colony-dag/style.css"), + ui_js: include_str!("builtin/assets/bee-colony-dag/ui.js"), + worker_js: "", + esm_dependencies_json: "[]", + }, ]; pub fn builtin_content_hash(app: &BuiltinMiniAppBundle) -> String { diff --git a/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/index.html b/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/index.html new file mode 100644 index 000000000..ce4ec25ff --- /dev/null +++ b/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/index.html @@ -0,0 +1,24 @@ + + + + + + 蜂群架构监控 + + + +
+
+

蜂群架构监控

+ 待命 +
+
+ +
+
+ 轮询中... +
+
+ + + diff --git a/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/meta.json b/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/meta.json new file mode 100644 index 000000000..885f23d0c --- /dev/null +++ b/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/meta.json @@ -0,0 +1,35 @@ +{ + "id": "builtin-bee-colony-dag", + "name": "蜂群架构监控", + "description": "蜂群八角色 DAG 架构图,实时监控各节点执行状态。Agent 推送状态更新,节点颜色实时变化。", + "icon": "GitBranch", + "category": "tool", + "tags": ["蜂群", "DAG", "监控", "Agent"], + "version": 1, + "created_at": 0, + "updated_at": 0, + "permissions": { + "fs": { + "read": ["{appdata}"], + "write": ["{appdata}"] + }, + "shell": { "allow": [] }, + "net": { "allow": [] }, + "node": { "enabled": false } + }, + "ai_context": null, + "i18n": { + "locales": { + "zh-CN": { + "name": "蜂群架构监控", + "description": "蜂群八角色 DAG 架构图,实时监控各节点执行状态。", + "tags": ["蜂群", "DAG", "监控", "Agent"] + }, + "en-US": { + "name": "Bee Colony Monitor", + "description": "Bee Colony 8-role DAG architecture with real-time node status monitoring.", + "tags": ["bee-colony", "DAG", "monitor", "Agent"] + } + } + } +} diff --git a/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/style.css b/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/style.css new file mode 100644 index 000000000..1416becce --- /dev/null +++ b/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/style.css @@ -0,0 +1,163 @@ +*, *::before, *::after { box-sizing: border-box; } +body, html { margin: 0; padding: 0; height: 100%; overflow: hidden; } + +:root { + /* Surfaces */ + --dag-bg: var(--bitfun-bg, #121214); + --dag-surface: var(--bitfun-bg-secondary, #1c1c1f); + --dag-surface-2: var(--bitfun-element-bg, rgba(255,255,255,0.06)); + --dag-border: var(--bitfun-border-subtle, rgba(255,255,255,0.10)); + + /* Text */ + --dag-text: var(--bitfun-text, #e8e8e8); + --dag-text-soft: var(--bitfun-text-secondary, #b0b0b0); + --dag-text-mute: var(--bitfun-text-muted, #858585); + + /* Semantic */ + --dag-accent: var(--bitfun-accent, #60a5fa); + --dag-success: var(--bitfun-success, #34d399); + --dag-error: var(--bitfun-error, #ef4444); + + /* Node status */ + --dag-idle: #3f3f46; + --dag-running: #60a5fa; + --dag-done: #34d399; + --dag-failed: #ef4444; + + --dag-radius: var(--bitfun-radius, 8px); + --dag-font: var(--bitfun-font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif); +} + +/* ---- Layout ---- */ +.bee-dag { + display: flex; + flex-direction: column; + height: 100vh; + background: var(--dag-bg); + color: var(--dag-text); + font-family: var(--dag-font); + font-size: 13px; + user-select: none; +} + +/* ---- Top bar ---- */ +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: var(--dag-surface); + border-bottom: 1px solid var(--dag-border); + flex-shrink: 0; +} + +.topbar__title { + margin: 0; + font-size: 15px; + font-weight: 600; + color: var(--dag-text); + letter-spacing: 0.02em; +} + +.topbar__badge { + display: inline-flex; + align-items: center; + padding: 3px 10px; + border-radius: 10px; + font-size: 11px; + font-weight: 500; + background: var(--dag-surface-2); + color: var(--dag-text-soft); + transition: background 0.3s, color 0.3s; +} + +.topbar__badge--running { + background: rgba(96,165,250,0.16); + color: var(--dag-running); +} + +.topbar__badge--done { + background: rgba(52,211,153,0.14); + color: var(--dag-done); +} + +.topbar__badge--failed { + background: rgba(239,68,68,0.14); + color: var(--dag-error); +} + +/* ---- Canvas ---- */ +.canvas-wrap { + flex: 1; + overflow: hidden; + position: relative; +} + +.dag-svg { + width: 100%; + height: 100%; + display: block; +} + +/* ---- Bottom bar ---- */ +.bottombar { + display: flex; + align-items: center; + justify-content: center; + padding: 6px 16px; + background: var(--dag-surface); + border-top: 1px solid var(--dag-border); + flex-shrink: 0; +} + +.bottombar__hint { + font-size: 11px; + color: var(--dag-text-mute); + letter-spacing: 0.03em; +} + +/* ---- SVG node animations ---- */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.55; } +} + +.node--running .dag-node-rect { + animation: pulse 1.2s ease-in-out infinite; +} + +/* ---- SVG elements ---- */ +.dag-edge { + stroke: rgba(255,255,255,0.16); + stroke-width: 1.6; +} + +.dag-edge--gate { + stroke: rgba(239,68,68,0.40); + stroke-dasharray: 6 3; +} + +.dag-node-rect { + transition: fill 0.35s ease; +} + +.dag-node-text { + fill: var(--dag-text); + font-family: var(--dag-font); + font-size: 13px; + font-weight: 600; +} + +.dag-node-sub { + fill: var(--dag-text-mute); + font-family: var(--dag-font); + font-size: 10px; + font-weight: 400; +} + +.dag-gate-dash { + fill: none; + stroke: rgba(239,68,68,0.45); + stroke-width: 1.8; + stroke-dasharray: 5 3; +} diff --git a/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/ui.js b/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/ui.js new file mode 100644 index 000000000..3d7b0d4d6 --- /dev/null +++ b/src/crates/contracts/product-domains/src/miniapp/builtin/assets/bee-colony-dag/ui.js @@ -0,0 +1,328 @@ +// Bee Colony DAG Monitor — main logic +// Renders an 8-role vertical DAG with real-time status polling via app.storage. +// Vanilla JS, no frameworks. SVG rendered via createElementNS. + +const SVG_NS = "http://www.w3.org/2000/svg"; + +// ── Role definitions ────────────────────────────────────────── +const ROLE_DEFS = [ + { id: "cmd", role: "\u6307\u6325\u5b98", tree: "\u6811 1-3 P\u2192C\u2192D", gate: false }, + { id: "sec", role: "\u79d8\u4e66 B01", tree: "L3 \u5e93\u68c0\u7d22", gate: false }, + { id: "pm", role: "\u4ea7\u54c1\u7ecf\u7406", tree: "R-ID \u9700\u6c42\u5b9a\u4e49", gate: false }, + { id: "plan", role: "\u89c4\u5212\u5e08", tree: "\u6811 4-6 P\u2192D\u2192C", gate: false }, + { id: "exec", role: "\u6267\u884c\u8005", tree: "\u6811 7-10 O\u2192O\u2192D\u2192A", gate: false }, + { id: "review", role: "\u5ba1\u67e5\u8005", tree: "\u529f\u80fd\u00b7\u5b89\u5168\u00b7Debug", gate: true }, + { id: "accept", role: "\u9a8c\u6536\u8005", tree: "R-ID \u9010\u9879\u95ed\u5408", gate: true }, + { id: "opt", role: "\u4f18\u5316\u8005", tree: "\u590d\u76d8\u00b7\u5f52\u6863\u00b7\u77e5\u8bc6\u5e93", gate: false }, +]; + +// ── Layout constants ────────────────────────────────────────── +const NODE_W = 180; +const NODE_H = 52; +const NODE_RX = 8; +const V_GAP = 88; // center-to-center vertical spacing +const PAD_X = 60; +const PAD_TOP = 30; +const PAD_BOT = 30; + +// ── State ────────────────────────────────────────────────────── +const state = { + layout: null, // { viewW, viewH, positions: [{x,y}] } + nodes: {}, // { cmd: {status, detail}, ... } + lastHash: "", +}; + +// ── Simple hash ──────────────────────────────────────────────── +function simpleHash(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const c = str.charCodeAt(i); + hash = ((hash << 5) - hash) + c; + hash |= 0; + } + return String(hash); +} + +// ── Compute layout ───────────────────────────────────────────── +function computeLayout(totalW, totalH) { + const cx = totalW / 2; + const positions = ROLE_DEFS.map((_, i) => ({ + x: cx - NODE_W / 2, + y: PAD_TOP + i * V_GAP, + })); + + const viewH = PAD_TOP + ROLE_DEFS.length * V_GAP + PAD_BOT; + + return { + viewW: totalW, + viewH: viewH, + positions: positions, + }; +} + +// ── Status helpers ───────────────────────────────────────────── +function getStatus(nodeId) { + const n = state.nodes[nodeId]; + return n && n.status ? n.status : "idle"; +} + +function getFillColor(status) { + switch (status) { + case "running": return "var(--dag-running)"; + case "done": return "var(--dag-done)"; + case "failed": return "var(--dag-failed)"; + default: return "var(--dag-idle)"; + } +} + +// ── Render ───────────────────────────────────────────────────── +function render() { + const svg = document.getElementById("dag-svg"); + if (!svg) return; + + // Measure container + const wrap = svg.parentElement; + const w = wrap.clientWidth || 800; + const h = wrap.clientHeight || 600; + + const layout = computeLayout(w, h); + state.layout = layout; + + svg.setAttribute("viewBox", "0 0 " + layout.viewW + " " + layout.viewH); + svg.setAttribute("preserveAspectRatio", "xMidYMid meet"); + + // Clear + while (svg.firstChild) svg.removeChild(svg.firstChild); + + // Defs: arrow marker + const defs = createSvgEl("defs"); + const marker = createSvgEl("marker", { + id: "arrow", + viewBox: "0 0 10 10", + refX: "5", + refY: "10", + markerWidth: "6", + markerHeight: "6", + orient: "auto", + }); + const arrowPath = createSvgEl("path", { + d: "M 0 0 L 5 10 L 10 0", + fill: "rgba(255,255,255,0.16)", + }); + marker.appendChild(arrowPath); + defs.appendChild(marker); + + // Gate edge marker + const markerGate = createSvgEl("marker", { + id: "arrow-gate", + viewBox: "0 0 10 10", + refX: "5", + refY: "10", + markerWidth: "6", + markerHeight: "6", + orient: "auto", + }); + const arrowPathGate = createSvgEl("path", { + d: "M 0 0 L 5 10 L 10 0", + fill: "rgba(239,68,68,0.40)", + }); + markerGate.appendChild(arrowPathGate); + defs.appendChild(markerGate); + svg.appendChild(defs); + + // ── Edges (behind nodes) ── + for (let i = 0; i < ROLE_DEFS.length - 1; i++) { + const src = layout.positions[i]; + const dst = layout.positions[i + 1]; + const isGate = ROLE_DEFS[i + 1].gate || ROLE_DEFS[i].gate; + + const x1 = src.x + NODE_W / 2; + const y1 = src.y + NODE_H; + const x2 = dst.x + NODE_W / 2; + const y2 = dst.y; + + const line = createSvgEl("line", { + x1: x1, y1: y1, + x2: x2, y2: y2, + class: "dag-edge" + (isGate ? " dag-edge--gate" : ""), + "marker-end": isGate ? "url(#arrow-gate)" : "url(#arrow)", + }); + svg.appendChild(line); + } + + // ── Nodes ── + for (let i = 0; i < ROLE_DEFS.length; i++) { + const def = ROLE_DEFS[i]; + const pos = layout.positions[i]; + const status = getStatus(def.id); + + const g = createSvgEl("g", { + class: "dag-node" + (status === "running" ? " node--running" : ""), + }); + + // Node rect + const rect = createSvgEl("rect", { + x: pos.x, + y: pos.y, + width: NODE_W, + height: NODE_H, + rx: NODE_RX, + ry: NODE_RX, + class: "dag-node-rect", + fill: getFillColor(status), + stroke: "rgba(255,255,255,0.08)", + "stroke-width": "1", + }); + g.appendChild(rect); + + // Gate dashed overlay + if (def.gate) { + const dash = createSvgEl("rect", { + x: pos.x + 1.5, + y: pos.y + 1.5, + width: NODE_W - 3, + height: NODE_H - 3, + rx: NODE_RX - 1, + ry: NODE_RX - 1, + class: "dag-gate-dash", + }); + g.appendChild(dash); + } + + // Role name + const textRole = createSvgEl("text", { + x: pos.x + NODE_W / 2, + y: pos.y + 21, + "text-anchor": "middle", + class: "dag-node-text", + }); + textRole.textContent = def.role; + g.appendChild(textRole); + + // Tree number + const textTree = createSvgEl("text", { + x: pos.x + NODE_W / 2, + y: pos.y + 38, + "text-anchor": "middle", + class: "dag-node-sub", + }); + textTree.textContent = def.tree; + g.appendChild(textTree); + + svg.appendChild(g); + } +} + +// ── SVG element helper ───────────────────────────────────────── +function createSvgEl(tag, attrs) { + const el = document.createElementNS(SVG_NS, tag); + if (attrs) { + for (const [k, v] of Object.entries(attrs)) { + el.setAttribute(k, String(v)); + } + } + return el; +} + +// ── Badge update ─────────────────────────────────────────────── +function updateBadge(raw) { + const badge = document.getElementById("status-badge"); + if (!badge) return; + + const nodes = raw && raw.nodes ? raw.nodes : {}; + const vals = Object.values(nodes); + const statuses = vals.map(function (n) { return n.status || "idle"; }); + + let label = "\u5f85\u547d"; + let cls = ""; + + if (statuses.length > 0) { + const hasFailed = statuses.indexOf("failed") !== -1; + const hasRunning = statuses.indexOf("running") !== -1; + const allDone = statuses.every(function (s) { return s === "done"; }); + const allIdle = statuses.every(function (s) { return s === "idle"; }); + + if (hasFailed) { + label = "\u5f02\u5e38"; + cls = "topbar__badge--failed"; + } else if (hasRunning) { + label = "\u6267\u884c\u4e2d"; + cls = "topbar__badge--running"; + } else if (allDone) { + label = "\u5b8c\u6210"; + cls = "topbar__badge--done"; + } else if (allIdle) { + label = "\u5f85\u547d"; + cls = ""; + } + } + + badge.textContent = label; + badge.className = "topbar__badge " + cls; +} + +// ── Polling ──────────────────────────────────────────────────── +async function pollState() { + try { + const raw = await app.storage.get("bee-colony-state"); + if (!raw) return; + + const hash = simpleHash(JSON.stringify(raw)); + if (hash === state.lastHash) return; + state.lastHash = hash; + + // Update node statuses + if (raw.nodes) { + for (const [id, nodeState] of Object.entries(raw.nodes)) { + state.nodes[id] = nodeState; + } + } + + render(); + updateBadge(raw); + } catch (_e) { + // Ignore polling errors — the MiniApp may be running + // in a context where app.storage is not yet available. + } +} + +// ── Init ─────────────────────────────────────────────────────── +async function init() { + // Load any previously persisted state + try { + const saved = await app.storage.get("bee-colony-state"); + if (saved && saved.nodes) { + for (const [id, nodeState] of Object.entries(saved.nodes)) { + state.nodes[id] = nodeState; + } + } + updateBadge(saved); + } catch (_e) { + // No saved state yet + } + + // Initial render to show the DAG structure + render(); + + // Start polling + setInterval(pollState, 500); + pollState(); + + // Update poll hint + const hint = document.getElementById("poll-hint"); + if (hint) { + hint.textContent = "\u8f6e\u8be2\u4e2d (500ms)"; + } + + // Handle resize + window.addEventListener("resize", function () { + render(); + }); +} + +// ── Boot ─────────────────────────────────────────────────────── +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); +} else { + init(); +} diff --git a/src/web-ui/src/app/layout/AppLayout.tsx b/src/web-ui/src/app/layout/AppLayout.tsx index 81e509112..3c953f9ff 100644 --- a/src/web-ui/src/app/layout/AppLayout.tsx +++ b/src/web-ui/src/app/layout/AppLayout.tsx @@ -51,6 +51,9 @@ const ToolbarMode = lazy(() => const FloatingMiniChat = lazy(() => import('./FloatingMiniChat').then(module => ({ default: module.FloatingMiniChat })) ); +const BeeColonyMonitor = lazy(() => + import('./BeeColonyMonitor').then(module => ({ default: module.BeeColonyMonitor })) +); const AboutDialog = lazy(() => import('../components/AboutDialog').then(module => ({ default: module.AboutDialog })) ); @@ -748,6 +751,9 @@ const AppLayout: React.FC = ({ className = '' }) => { )} + + + {pendingAcpSessionClients.length > 0 && (
diff --git a/src/web-ui/src/app/layout/BeeColonyMonitor.scss b/src/web-ui/src/app/layout/BeeColonyMonitor.scss new file mode 100644 index 000000000..2b1122e94 --- /dev/null +++ b/src/web-ui/src/app/layout/BeeColonyMonitor.scss @@ -0,0 +1,102 @@ +// BeeColonyMonitor — floating panel for bee-colony-dag MiniApp +$panel-width: 380px; +$panel-height: 560px; +$trigger-offset-bottom: 80px; +$trigger-offset-right: 20px; + +.bee-monitor { + position: fixed; + z-index: 700; + pointer-events: none; + &--open { pointer-events: auto; } +} + +.bee-monitor__button { + position: fixed; + bottom: $trigger-offset-bottom; + right: $trigger-offset-right; + width: 44px; + height: 44px; + border-radius: 50%; + border: 1px solid var(--bitfun-border-subtle); + background: var(--bitfun-bg-secondary); + color: var(--bitfun-text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.30); + transition: background 0.2s, color 0.2s, transform 0.2s; + pointer-events: auto; + z-index: 701; + &:hover { + background: var(--bitfun-element-hover); + color: var(--bitfun-accent); + transform: scale(1.08); + } + .bee-monitor--open & { opacity: 0; pointer-events: none; transform: scale(0.8); } +} + +.bee-monitor__backdrop { + position: fixed; + inset: 0; + background: transparent; + z-index: 698; +} + +.bee-monitor__panel { + position: fixed; + bottom: 136px; + right: $trigger-offset-right; + width: $panel-width; + height: $panel-height; + background: var(--bitfun-bg); + border: 1px solid var(--bitfun-border-subtle); + border-radius: 10px; + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.45); + display: flex; + flex-direction: column; + z-index: 699; + overflow: hidden; + transform: translateY(16px) scale(0.96); + opacity: 0; + pointer-events: none; + transition: transform 0.22s cubic-bezier(0.22, 0.61, 0.36, 1), opacity 0.18s ease; + &--open { transform: translateY(0) scale(1); opacity: 1; pointer-events: auto; } + &--maximized { + top: 40px; left: 40px; right: 40px; bottom: 40px; + width: auto; height: auto; + } +} + +.bee-monitor__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + border-bottom: 1px solid var(--bitfun-border-subtle); + flex-shrink: 0; + background: var(--bitfun-bg-secondary); +} +.bee-monitor__title { + font-size: 13px; font-weight: 600; + color: var(--bitfun-text); +} +.bee-monitor__header-actions { display: flex; gap: 4px; } +.bee-monitor__header-btn { + width: 28px; height: 28px; border-radius: 6px; + border: none; background: transparent; + color: var(--bitfun-text-secondary); cursor: pointer; + display: flex; align-items: center; justify-content: center; + &:hover { background: var(--bitfun-element-hover); color: var(--bitfun-text); } + &--close:hover { background: rgba(239,68,68,0.15); color: var(--bitfun-error); } +} + +.bee-monitor__body { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; } +.bee-monitor__loading, .bee-monitor__error { + flex: 1; display: flex; flex-direction: column; + align-items: center; justify-content: center; gap: 8px; + padding: 24px; text-align: center; + color: var(--bitfun-text-muted); font-size: 13px; + small { font-size: 11px; opacity: 0.7; } +} \ No newline at end of file diff --git a/src/web-ui/src/app/layout/BeeColonyMonitor.tsx b/src/web-ui/src/app/layout/BeeColonyMonitor.tsx new file mode 100644 index 000000000..ff1f90ce6 --- /dev/null +++ b/src/web-ui/src/app/layout/BeeColonyMonitor.tsx @@ -0,0 +1,147 @@ +/** + * BeeColonyMonitor — fixed floating panel that renders the bee-colony-dag + * MiniApp DAG visualization. Always accessible via a nav button; stays + * visible alongside other content without taking a full scene tab. + * + * Pattern: FloatingMiniChat-style floating panel with MiniAppRunner inside. + */ +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import { GitBranch, X, Minimize2, Maximize2 } from 'lucide-react'; +import { miniAppAPI } from '@/infrastructure/api/service-api/MiniAppAPI'; +import type { MiniApp } from '@/infrastructure/api/service-api/MiniAppAPI'; +import { useTheme } from '@/infrastructure/theme/hooks/useTheme'; +import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; +import { createLogger } from '@/shared/utils/logger'; +import MiniAppRunner from '@/app/scenes/miniapps/components/MiniAppRunner'; +import { useSceneStore } from '@/app/stores/sceneStore'; +import './BeeColonyMonitor.scss'; + +const log = createLogger('BeeColonyMonitor'); + +const BEE_COLONY_APP_ID = 'bee-colony-dag'; + +export const BeeColonyMonitor: React.FC = () => { + const { themeType } = useTheme(); + const { workspacePath } = useCurrentWorkspace(); + const activeTabId = useSceneStore((s) => s.activeTabId); + + const [isOpen, setIsOpen] = useState(false); + const [app, setApp] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [maximized, setMaximized] = useState(false); + + // Only show in agent scene (where the DAG is relevant) + const isAgentScene = useMemo( + () => typeof activeTabId === 'string' && activeTabId.startsWith('agentic:'), + [activeTabId], + ); + + const loadApp = useCallback(async () => { + setLoading(true); + setError(null); + try { + const loaded = await miniAppAPI.getMiniApp( + BEE_COLONY_APP_ID, + themeType ?? 'dark', + workspacePath || undefined, + ); + if (!loaded?.compiled_html?.trim()) { + setError('MiniApp not compiled'); + setApp(null); + return; + } + setApp(loaded); + } catch (err) { + log.error('Failed to load bee colony MiniApp', err); + setError(String(err)); + setApp(null); + } finally { + setLoading(false); + } + }, [themeType, workspacePath]); + + // Load app when panel opens + useEffect(() => { + if (isOpen && !app && !loading) { + void loadApp(); + } + }, [isOpen, app, loading, loadApp]); + + const handleToggle = useCallback(() => { + setIsOpen((prev) => !prev); + }, []); + + const handleClose = useCallback(() => { + setIsOpen(false); + }, []); + + // Don't render in non-agent scenes + if (!isAgentScene) return null; + + return ( +
+ {/* Backdrop */} + {isOpen &&
} + + {/* Trigger button — always visible in agent scenes */} + + + {/* Floating panel */} +
+ {/* Header */} +
+ 蜂群架构监控 +
+ + +
+
+ + {/* Body */} +
+ {loading && ( +
加载中...
+ )} + {error && !app && ( +
+

蜂群 MiniApp 未就绪

+ {error}. 请确保已编译部署。 +
+ )} + {app && } +
+
+
+ ); +}; + +export default BeeColonyMonitor;