Files
maestro/design/ui_kits/console/index.html
T
wangjia af690701bf feat(web): Markdown 渲染器 + 拆解轨迹 + 需你处理区 + 复审报告折叠卡片
- RichDoc(marked+DOMPurify+highlight.js CDN)统一渲染 gate spec/复审报告/任务树 SPEC;- Transcript 组件:拆解评审「拆解轨迹」懒加载 planner run transcript;- 「审批闸」改名「需你处理」并并入 needs_attention(requeue/接管/取消 + 完整复审报告,按代码/安全分段折叠卡片、标裁决徽章);- 子任务显 scopeFiles 文件范围 + frozen「待拆解确认」标。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 23:32:30 +08:00

191 lines
11 KiB
HTML

<!-- @dsCard group="Console" viewport="1440x900" name="Web 调度台" subtitle="三栏看板整屏复刻:项目 · 审批闸+任务树 · 事件流(可点击)" -->
<!-- @startingPoint section="Screens" subtitle="Maestro Web 调度台整屏" viewport="1440x900" -->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MAESTRO · 任务调度台</title>
<link rel="stylesheet" href="../../styles.css">
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<!-- Markdown 渲染(window.MaestroRichDoc 依赖)-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11.9.0/styles/github-dark.min.css" integrity="sha384-wH75j6z1lH97ZOpMOInqhgKzFkAInZPPSPlZpYKYTOqsaizPvhQZmAtLcPKXpLyH" crossorigin="anonymous" />
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js" integrity="sha384-/TQbtLCAerC3jgaim+N78RZSDYV7ryeoBCVqTuzRrFec2akfBkHS7ACQ3PQhvMVi" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js" integrity="sha384-+VfUPEb0PdtChMwmBcBmykRMDd+v6D/oFmB3rZM/puCMDYcIvF968OimRh4KQY9a" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11.9.0/highlight.min.js" integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" crossorigin="anonymous"></script>
<script src="../../_ds_bundle.js"></script>
<script src="data.js"></script>
<script src="i18n.js"></script>
<style>
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { background: var(--bg); color: var(--ink); font-family: var(--mono); font-size: 13px; line-height: 1.55; overflow: hidden; }
body::before {
content: ''; position: fixed; inset: 0; z-index: 999; pointer-events: none;
background:
repeating-linear-gradient(0deg, rgba(0,0,0,.16) 0 1px, transparent 1px 3px),
radial-gradient(ellipse at 50% 40%, transparent 55%, rgba(0,0,0,.5));
opacity: .5;
}
[data-theme="light"] body::before { opacity: .12; }
@keyframes maestro-locate {
0%, 35% { background: var(--amber-dim); box-shadow: inset 2px 0 0 var(--amber); }
100% { background: transparent; box-shadow: none; }
}
::selection { background: var(--green-dim); color: #fff; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--green-dim); }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="RichDoc.jsx"></script>
<script type="text/babel" src="Transcript.jsx"></script>
<script type="text/babel" src="Sidebar.jsx"></script>
<script type="text/babel" src="EventPanel.jsx"></script>
<script type="text/babel" src="Topbar.jsx"></script>
<script type="text/babel" src="GateSection.jsx"></script>
<script type="text/babel" src="TaskTree.jsx"></script>
<script type="text/babel" src="ArchiveSection.jsx"></script>
<script type="text/babel">
const { Toast } = window.MaestroDesignSystem_a6a290;
const M = window.MAESTRO_MOCK;
const I18N = window.MAESTRO_I18N;
function App() {
const [currentId, setCurrentId] = React.useState('p1');
const [approvals, setApprovals] = React.useState(M.approvals);
const [events, setEvents] = React.useState(M.events);
const [showConfig, setShowConfig] = React.useState(false);
const [archiveItem, setArchiveItem] = React.useState(null);
const [toasts, setToasts] = React.useState([]);
const [lang, setLang] = React.useState(() => {
const saved = localStorage.getItem('maestro-kit-lang');
return I18N[saved] ? saved : 'zh';
});
const [theme, setTheme] = React.useState(() => localStorage.getItem('maestro-kit-theme') || 'dark');
const t = I18N[lang];
React.useEffect(() => {
document.documentElement.dataset.theme = theme;
localStorage.setItem('maestro-kit-theme', theme);
}, [theme]);
React.useEffect(() => { localStorage.setItem('maestro-kit-lang', lang); }, [lang]);
const [collapsed, setCollapsed] = React.useState(() => localStorage.getItem('maestro-kit-sidebar') === 'collapsed');
const [evCollapsed, setEvCollapsed] = React.useState(() => localStorage.getItem('maestro-kit-events') === 'collapsed');
const toggleEvents = () => setEvCollapsed((c) => {
localStorage.setItem('maestro-kit-events', c ? 'open' : 'collapsed');
return !c;
});
const toggleCollapse = () => setCollapsed((c) => {
localStorage.setItem('maestro-kit-sidebar', c ? 'open' : 'collapsed');
return !c;
});
// 栏宽可拖拽(带最小/最大约束,持久化);中栏保证 ≥ CENTER_MIN,避免被两侧挤压
const SIDE_MIN = 200, SIDE_MAX = 420, EV_MIN = 240, EV_MAX = 520, CENTER_MIN = 480;
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const sideMax = () => Math.min(SIDE_MAX, window.innerWidth - (evCollapsed ? 52 : evW) - CENTER_MIN);
const evMax = () => Math.min(EV_MAX, window.innerWidth - (collapsed ? 52 : sideW) - CENTER_MIN);
const [sideW, setSideW] = React.useState(() => clamp(Number(localStorage.getItem('maestro-kit-sidew')) || 232, SIDE_MIN, SIDE_MAX));
const [evW, setEvW] = React.useState(() => clamp(Number(localStorage.getItem('maestro-kit-evw')) || 320, EV_MIN, EV_MAX));
const [dragging, setDragging] = React.useState(null);
React.useEffect(() => { localStorage.setItem('maestro-kit-sidew', sideW); }, [sideW]);
React.useEffect(() => { localStorage.setItem('maestro-kit-evw', evW); }, [evW]);
const startDrag = (which) => (e) => {
e.preventDefault();
setDragging(which);
const startX = e.clientX;
const startW = which === 'side' ? sideW : evW;
const onMove = (ev) => {
if (which === 'side') setSideW(clamp(startW + (ev.clientX - startX), SIDE_MIN, Math.max(SIDE_MIN, sideMax())));
else setEvW(clamp(startW - (ev.clientX - startX), EV_MIN, Math.max(EV_MIN, evMax())));
};
const onUp = () => {
setDragging(null);
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
document.body.style.userSelect = '';
};
document.body.style.userSelect = 'none';
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp);
};
const Resizer = ({ which }) => (
<div onPointerDown={startDrag(which)} title="拖拽调整宽度"
style={{
position: 'absolute', top: 0, bottom: 0, width: 9, cursor: 'col-resize', zIndex: 40,
...(which === 'side' ? { left: sideW - 4 } : { right: evW - 4 }),
display: 'flex', justifyContent: 'center',
}}>
<span style={{ width: 1, background: dragging === which ? 'var(--green)' : 'var(--line-soft)', boxShadow: dragging === which ? '0 0 8px var(--green)' : 'none', transition: 'background .12s' }}></span>
</div>
);
const project = M.projects.find((p) => p.id === currentId);
const isMain = currentId === 'p1';
const tasks = isMain ? M.tasks : [];
const agents = isMain ? M.agents : [];
const gates = isMain ? approvals : [];
const toast = (kind, text) => {
const id = Date.now();
setToasts((t) => [...t, { id, kind, text }]);
setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 2600);
};
const now = () => new Date().toTimeString().slice(0, 8);
const decide = (a, action, reason) => {
setApprovals((list) => list.filter((x) => x.id !== a.id));
if (action === 'accept') {
setEvents((ev) => [{ type: 'approval.granted', time: now(), detail: a.title + ' · ' + a.gate + '_review' + t.gatePassed }, ...ev]);
toast('ok', t.toastAccepted + a.title);
} else {
setEvents((ev) => [{ type: 'approval.rejected', time: now(), detail: a.title + ' ↳ ' + reason }, ...ev]);
toast('warn', t.toastRejected);
}
};
return (
<div style={{ position: 'relative', display: 'grid', gridTemplateColumns: (collapsed ? '52px' : sideW + 'px') + ' minmax(0,1fr) ' + (evCollapsed ? '52px' : evW + 'px'), height: '100vh' }}>
{!collapsed ? <Resizer which="side" /> : null}
{!evCollapsed ? <Resizer which="ev" /> : null}
<window.MaestroKitSidebar projects={M.projects} currentId={currentId} t={t}
global={M.global} user={M.user} summary={M.agentSummary} onGlobalConfig={() => toast('warn', t.toastModal)}
collapsed={collapsed} onToggleCollapse={toggleCollapse}
onSelect={setCurrentId} onNewProject={() => toast('warn', t.toastModal)} />
<main style={{ overflowY: 'auto', padding: '0 22px 60px' }}>
<window.MaestroKitTopbar project={project} t={t} theme={theme}
onToggleTheme={() => setTheme(theme === 'light' ? 'dark' : 'light')}
lang={lang} onSelectLang={setLang}
counts={{ gate: gates.length, ready: isMain ? 1 : 0, run: agents.length, blocked: isMain ? 1 : 0, total: isMain ? 5 : 0 }}
onSync={() => toast('ok', t.toastSynced)}
onToggleConfig={() => setShowConfig(!showConfig)} />
{showConfig ? <window.MaestroKitConfigPanel project={project} t={t}
onSave={() => { setShowConfig(false); toast('ok', t.toastSaved); }}
onSync={() => toast('ok', t.toastSynced)}
onClose={() => setShowConfig(false)} /> : null}
<window.MaestroKitAgentSection agents={agents} quota={isMain ? M.quota : null} t={t} />
<window.MaestroKitGateSection approvals={gates} onDecide={decide} t={t} />
{tasks.length ? (
<window.MaestroKitTaskSection tasks={tasks} onToast={toast} t={t} />
) : (
<div style={{ padding: '46px 0', textAlign: 'center', color: 'var(--faint)', border: '1px dashed var(--line)', borderRadius: 6, marginTop: 18 }}>
<b style={{ display: 'block', fontSize: 15, color: 'var(--muted)', marginBottom: 6, letterSpacing: '.2em' }}>{t.empty}</b>
{t.emptyTasks}
</div>
)}
{isMain ? <window.MaestroKitArchiveSection items={M.archived} t={t} onOpen={setArchiveItem} /> : null}
</main>
<window.MaestroKitEventPanel events={events} collapsed={evCollapsed} onToggleCollapse={toggleEvents} t={t} />
{archiveItem ? <window.MaestroKitArchiveModal item={archiveItem} t={t} onClose={() => setArchiveItem(null)} /> : null}
<div style={{ position: 'fixed', bottom: 18, left: '50%', transform: 'translateX(-50%)', zIndex: 1200, display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center' }}>
{toasts.map((t) => <Toast key={t.id} kind={t.kind}>{t.text}</Toast>)}
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>