Files
maestro/design/ui_kits/console/Transcript.jsx
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

49 lines
2.9 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 历史 transcript 回放:拉某任务指定 kind 的 run 的转录(/api/tasks/:id/runs → /api/runs/:id/transcript),
// 渲染成结构化行(text / tool_use / tool_result / result)。挂 window.MaestroTranscript,供拆解评审「拆解轨迹」等处复用。
// 转录已是脱敏 + 截断的扁平 schema{type, text?, name?, input?, isError?}(见 src/executor/transcript.ts)。
function TcLine({ m }) {
if (m.type === 'tool_use') {
return <div style={{ padding: '3px 0' }}><span style={{ color: 'var(--cyan)', fontWeight: 600 }}> {m.name}</span>{m.input ? <span style={{ color: 'var(--muted)' }}> {m.input}</span> : null}</div>;
}
if (m.type === 'tool_result') {
return <div style={{ padding: '1px 0 5px 16px', color: m.isError ? 'var(--red)' : 'var(--faint)', fontSize: 11.5, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}> {m.text || '(空)'}</div>;
}
if (m.type === 'result') return <div style={{ padding: '3px 0', color: 'var(--green)' }}> {m.text || 'done'}</div>;
return <div style={{ padding: '3px 0', color: 'var(--ink)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{m.text}</div>;
}
function MaestroTranscript({ taskId, kind }) {
const [state, setState] = React.useState({ loading: true, msgs: null, err: false });
React.useEffect(() => {
let alive = true;
setState({ loading: true, msgs: null, err: false });
(async () => {
try {
const runs = await fetch('/api/tasks/' + taskId + '/runs').then((r) => r.ok ? r.json() : Promise.reject(new Error('http')));
const list = Array.isArray(runs) ? runs : [];
const run = (kind ? list.filter((r) => r.kind === kind) : list)
.slice().sort((a, b) => String(b.startedAt || '').localeCompare(String(a.startedAt || '')))[0] || list[0];
if (!run) { if (alive) setState({ loading: false, msgs: [], err: false }); return; }
const d = await fetch('/api/runs/' + run.id + '/transcript').then((r) => r.ok ? r.json() : Promise.reject(new Error('http')));
const msgs = Array.isArray(d) ? d : (d.messages || []);
if (alive) setState({ loading: false, msgs, err: false });
} catch (e) {
if (alive) setState({ loading: false, msgs: null, err: true });
}
})();
return () => { alive = false; };
}, [taskId, kind]);
const hint = (txt) => <div style={{ fontSize: 11.5, color: 'var(--faint)', padding: '8px 2px' }}>{txt}</div>;
if (state.loading) return hint('… 加载轨迹');
if (state.err) return hint('轨迹取不到');
if (!state.msgs || !state.msgs.length) return hint('无轨迹记录');
return (
<div style={{ maxHeight: 440, overflowY: 'auto', fontFamily: 'var(--mono)', fontSize: 12.5, lineHeight: 1.6, padding: '4px 2px' }}>
{state.msgs.map((m, i) => <TcLine key={m.seq != null ? m.seq : i} m={m} />)}
</div>
);
}
window.MaestroTranscript = MaestroTranscript;