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

66 lines
3.6 KiB
React

// 共享 Markdown 渲染:marked 解析 + DOMPurify 净化 + highlight.js 代码高亮。
// 挂 window.MaestroRichDoc,供 GateSection / TaskTree 等在 vite app 与 standalone demo 两环境共用。
// 依赖 window.marked / window.DOMPurify(必需,CDN 全局)+ window.hljs(可选,缺则代码块不着色)。
function ensureRichDocCss() {
if (document.getElementById('maestro-richdoc-css')) return;
const s = document.createElement('style');
s.id = 'maestro-richdoc-css';
s.textContent = `
.m-richdoc { font-size: 12.8px; line-height: 1.7; color: var(--ink); word-break: break-word; }
.m-richdoc > :first-child { margin-top: 0; }
.m-richdoc > :last-child { margin-bottom: 0; }
.m-richdoc h1, .m-richdoc h2, .m-richdoc h3, .m-richdoc h4 { font-weight: 700; line-height: 1.3; margin: 16px 0 8px; letter-spacing: .01em; }
.m-richdoc h1 { font-size: 1.32em; color: var(--ink); border-bottom: 1px solid var(--line-soft); padding-bottom: 5px; }
.m-richdoc h2 { font-size: 1.16em; color: var(--green); }
.m-richdoc h3 { font-size: 1.05em; color: var(--cyan); }
.m-richdoc h4 { font-size: 1em; color: var(--muted); }
.m-richdoc p { margin: 7px 0; }
.m-richdoc ul, .m-richdoc ol { margin: 7px 0; padding-left: 22px; }
.m-richdoc li { margin: 3px 0; }
.m-richdoc li::marker { color: var(--green); }
.m-richdoc a { color: var(--cyan); text-decoration: none; border-bottom: 1px solid var(--cyan-dim); }
.m-richdoc a:hover { border-bottom-color: var(--cyan); }
.m-richdoc strong { color: var(--ink); font-weight: 700; }
.m-richdoc em { color: var(--muted); }
.m-richdoc code { font-family: var(--mono); font-size: .9em; color: var(--cyan); background: var(--panel-2); border-radius: 3px; padding: 1.5px 5px; }
.m-richdoc pre { background: var(--bg-deep); border: 1px solid var(--line-soft); border-radius: 5px; padding: 11px 13px; overflow-x: auto; margin: 9px 0; }
.m-richdoc pre code { display: block; font-family: var(--mono); font-size: 11.8px; line-height: 1.55; color: var(--ink); background: none; border: 0; padding: 0; }
.m-richdoc blockquote { margin: 9px 0; padding: 2px 13px; border-left: 3px solid var(--line); color: var(--muted); }
.m-richdoc table { border-collapse: collapse; margin: 9px 0; font-size: .95em; display: block; overflow-x: auto; }
.m-richdoc th, .m-richdoc td { border: 1px solid var(--line); padding: 5px 10px; text-align: left; }
.m-richdoc th { background: var(--panel-2); color: var(--ink); font-weight: 700; }
.m-richdoc hr { border: 0; border-top: 1px solid var(--line-soft); margin: 14px 0; }
.m-richdoc img { max-width: 100%; }
`;
document.head.appendChild(s);
}
function MaestroRichDoc({ md }) {
const ref = React.useRef(null);
ensureRichDocCss();
const ready = typeof window !== 'undefined' && !!window.marked && !!window.DOMPurify;
const html = React.useMemo(() => {
if (!ready || !md) return null;
try {
const raw = window.marked.parse(String(md), { gfm: true, breaks: false });
return window.DOMPurify.sanitize(raw);
} catch (e) {
return null;
}
}, [md, ready]);
React.useEffect(() => {
if (html && ref.current && window.hljs) {
ref.current.querySelectorAll('pre code').forEach((el) => {
try { window.hljs.highlightElement(el); } catch (e) { /* noop */ }
});
}
}, [html]);
// 兜底:库未就绪或解析失败 → 原文 pre-wrap,永不崩
if (html == null) {
return <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontFamily: 'var(--mono)', fontSize: 12.5, lineHeight: 1.6, color: 'var(--ink)' }}>{md || ''}</pre>;
}
return <div ref={ref} className="m-richdoc" dangerouslySetInnerHTML={{ __html: html }} />;
}
window.MaestroRichDoc = MaestroRichDoc;