diff --git a/web/src/App.tsx b/web/src/App.tsx index 8a7176d6..5f103498 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,30 +1,582 @@ import { useState } from 'react'; +import { AppShell } from './components/layout/AppShell'; +import { MessageStream } from './components/chat/MessageStream'; +import { InputArea } from './components/chat/InputArea'; +import { SlimTabBar } from './components/doc/SlimTabBar'; +import { DeckDrawer } from './components/doc/DeckDrawer'; +import { BlockRenderer } from './components/doc/BlockRenderer'; +import { BlockEditorModal } from './components/doc/BlockEditorModal'; +import { CommentSidePanel } from './components/doc/CommentSidePanel'; +import { DocGovActionBar } from './components/doc/DocGovActionBar'; +import { ProposalConfirmCard } from './components/doc/ProposalConfirmCard'; +import { markdownToBlockAST } from './engine/markdownToBlock'; +import { blockASTToMarkdown } from './engine/blockToMarkdown'; +import { recomputeNumbering } from './engine/autoNumber'; +import { DocumentAST, DocumentBlock, BlockComment } from './types/block'; +import { ProjectSession, ChatMessage, DeliverableItem } from './types/workbench'; +import { UserRole } from './types/docgov'; + +const INITIAL_MD = `# 4K超高清荧光内窥镜系统立项与可行性报告 + +本产品主要用于微创腹腔手术中的组织成像与荧光导航,通过将近红外荧光技术与 4K 超高清摄像系统结合,实时显影胆道与血管解剖结构。 + +> **[合规依据]** 本项目需严格遵守 GB 9706.1-2020 医用电气安全通用要求与 YY 0569-2011 临床医用内窥镜专用要求。 + +## 关键技术指标与法规符合性 + +| 参数项 | 指标要求 | 检验依据 | 合规判定 | +| 光学中心分辨率 | 4K (3840x2160) @ 60fps | YY 0068.1-2008 | 符合 | +| 荧光激发波长 | 785nm / 805nm 近红外双波长 | 行业标准预研 | 待检 | +| 电气绝缘阻抗 | ≥ 100 MΩ (强化绝缘) | GB 9706.1-2020 | 符合 | + +### 电气安全与电磁兼容风险评价 + +根据 IEC 60601-1-2 标准,系统需在高频电刀同时工作时保持抗扰度,抗静电放电需达到接触 8kV / 空气 15kV。 + +\`\`\`mermaid +graph TD + A[4K荧光内窥镜头端] --> B[双CMOS光学分光模组] + B --> C[FPGA实时图像融合算法] + C --> D[4K医用级超低延迟监视器] +\`\`\` + +$$ +SNR_{fluorescence} = \\frac{S_{emission}}{\\sqrt{\\sigma_{ambient}^2 + \\sigma_{dark}^2}} +$$ + +--- +`; export default function App() { - const [theme, setTheme] = useState<'dark' | 'light'>('dark'); + // 会话与用户状态 + const [sessions, setSessions] = useState([ + { + sessionId: 'sess-001', + projectId: 1, + title: '4K超高清荧光内窥镜系统', + deviceType: '硬管荧光腹腔内窥镜', + stage: 'V0.5', + stageLabel: 'V0.5 方案成型与法规对齐', + messageCount: 5, + updatedAt: '14:20', + }, + { + sessionId: 'sess-002', + projectId: 2, + title: '便携式掌上无线超声诊断仪', + deviceType: '便携式超声探头', + stage: 'V0.1', + stageLabel: 'V0.1 需求摸底与产品定义', + messageCount: 3, + updatedAt: '昨天', + }, + ]); - const toggleTheme = () => { - const next = theme === 'dark' ? 'light' : 'dark'; - setTheme(next); - document.documentElement.setAttribute('data-theme', next); + const [activeSessionId, setActiveSessionId] = useState('sess-001'); + const [currentRole, setCurrentRole] = useState('author'); + const [currentUserName, setCurrentUserName] = useState('张工'); + + // 交付物状态大盘 + const [deliverables, setDeliverables] = useState([ + { + docId: 'doc-001', + taskKey: 'feasibility_reg', + title: '注册路径可行性评估报告', + stage: 'V0.5', + isFixedWorkflow: true, + lifecycleState: 'drafting', + isClaimed: true, + author: '张工', + reviewer: '李主管', + approver: '王总监', + updatedAt: '2026-09-08 14:15', + summary: '针对4K荧光腹腔镜分类界定、临床评价路径与法规标准准入全景评估。', + }, + { + docId: 'doc-002', + taskKey: 'research', + title: '微创腹腔镜全球竞品深度调研', + stage: 'V0.5', + isFixedWorkflow: true, + lifecycleState: 'reviewing', + isClaimed: true, + author: '张工', + reviewer: '李主管', + approver: '王总监', + updatedAt: '2026-09-08 11:30', + summary: '针对史赛克1688、卡尔史托斯Rubina荧光内窥镜性能全方位对比分析。', + }, + { + docId: 'doc-003', + taskKey: 'prelim_design', + title: '光学系统初步设计规格书', + stage: 'V0.5', + isFixedWorkflow: true, + lifecycleState: 'approved', + isClaimed: true, + author: '张工', + reviewer: '李主管', + approver: '王总监', + updatedAt: '2026-09-07 17:00', + summary: '4K荧光双路分光棱镜光路设计与近红外消色差物镜参数设计。', + }, + ]); + + const [activeDocId, setActiveDocId] = useState('doc-001'); + const [deckDrawerOpen, setDeckDrawerOpen] = useState(false); + + // 文档 AST + const [docAST, setDocAST] = useState(() => + markdownToBlockAST(INITIAL_MD, { + docId: 'doc-001', + title: '4K超高清荧光内窥镜系统立项与可行性报告', + taskKey: 'feasibility_reg', + version: 'V1.0', + lifecycleState: 'drafting', + isClaimed: true, + author: '张工', + reviewer: '李主管', + approver: '王总监', + }) + ); + + // 模态弹窗与侧栏 + const [editingBlock, setEditingBlock] = useState(null); + const [commentingBlock, setCommentingBlock] = useState(null); + const [pendingProposal, setPendingProposal] = useState<{ + changes: string; + impact: string; + risk: string; + patchBlockId?: string; + } | null>(null); + + // 消息流与思考脉冲 + const [isThinking, setIsThinking] = useState(false); + const [messages, setMessages] = useState([ + { + id: 'm-0', + sessionId: 'sess-001', + role: 'system', + content: '水木东方协同参谋小队已就位,当前项目进入【V0.5 方案成型与法规对齐】阶段。', + timestamp: '14:00', + }, + { + id: 'm-1', + sessionId: 'sess-001', + role: 'assistant', + authorName: '水木东方协同参谋', + intentTag: '🎯 推进立项阶段', + content: + '张工您好!参谋小队已依据法规知识库完成《4K超高清荧光内窥镜系统立项与可行性报告》初稿编制。右侧已就绪块状结构化视图,您可以直接审阅或下达修改指令。', + timestamp: '14:02', + phaseEvents: [ + { step: '1', icon: '👥', label: '组装小队', status: 'completed' }, + { step: '2', icon: '📚', label: '检索法规', status: 'completed' }, + { step: '3', icon: '⚙️', label: '执行编制', status: 'completed' }, + { step: '4', icon: '🛡️', label: '门禁把关', status: 'completed' }, + ], + recommendedCases: [ + { + docId: 'SMDF-MD-2025-09', + taskKey: 'feasibility_reg', + artifactTitle: '4K硬管内窥镜摄像系统注册申报经验', + sectionTitle: '电气安全与高频电刀抗扰度评价', + similarityScore: 0.89, + keyRegulations: ['GB 9706.1-2020', 'IEC 60601-1-2'], + summarySnippet: + '在型式检验中,腹腔内窥镜主机需特别注意电刀高频耦合对荧光信噪比的干扰,建议在前端光电接收处增加硬件陷波滤波。', + }, + ], + }, + ]); + + // 发送消息与对话驱动双轨逻辑 + const handleSendMessage = (text: string, attachments?: { name: string; size: string; type: string }[]) => { + const userMsg: ChatMessage = { + id: 'msg-' + Date.now(), + sessionId: activeSessionId, + role: 'user', + authorName: currentUserName, + content: text, + attachments, + timestamp: new Date().toTimeString().substring(0, 5), + }; + + setMessages((prev) => [...prev, userMsg]); + setIsThinking(true); + + // 智能体推演回复逻辑 + setTimeout(() => { + setIsThinking(false); + + // 对话驱动 1:审核人对话提出批注 + if (currentRole === 'reviewer' && (text.includes('批注') || text.includes('意见') || text.includes('补充'))) { + const targetBlock = docAST.rootBlock.children?.[3] || docAST.rootBlock.children?.[1]; + if (targetBlock) { + const newComment: BlockComment = { + id: 'cmt-' + Date.now(), + authorName: currentUserName, + authorRole: 'reviewer', + createdAt: new Date().toTimeString().substring(0, 5), + type: 'issue', + content: text, + resolved: false, + }; + targetBlock.comments = [...(targetBlock.comments || []), newComment]; + setDocAST({ ...docAST }); + } + + setMessages((prev) => [ + ...prev, + { + id: 'reply-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + intentTag: '💡 评审批注挂载', + content: `李主管,已为您将审核意见记录并挂载在右侧受控文档【${targetBlock?.numbering || ''} ${targetBlock?.content.substring(0, 16)}...】章节旁,编制人随时可见。`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + return; + } + + // 对话驱动 2:审核人对话批准推进 + if (currentRole === 'reviewer' && (text.includes('通过') || text.includes('同意'))) { + handlePassReview(); + setMessages((prev) => [ + ...prev, + { + id: 'reply-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + intentTag: '🛡️ 审核通过流转', + content: '李主管,已为您执行【审核通过】指令,文档已锁定并正式流转至王总监(批准人)进行终审。', + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + return; + } + + // 对话驱动 3:编制人认领保护拦截 (Change Proposal) + if (text.includes('修改') || text.includes('更新') || text.includes('改写') || text.includes('补充')) { + if (docAST.isClaimed) { + setPendingProposal({ + changes: `根据您的指令【${text}】,拟将第二章关键技术指标补充 4K 荧光抗扰度测试参数,并在表格中增加检验依据条款。`, + impact: '将直接影响后续型检大纲覆盖率,增加 IEC 60601-1-2 第 8 节要求。', + risk: '低风险合规强化项,无需重跑临床验证。', + patchBlockId: docAST.rootBlock.children?.[4]?.id, + }); + + setMessages((prev) => [ + ...prev, + { + id: 'reply-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + intentTag: '🛡️ 认领变更拦截', + content: + '检测到本文档已被张工认领受控。依据认领保护机制,已为您生成《变更提案》。请在右侧下方或对话卡片中确认是否采纳本次变更。', + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + return; + } + } + + // 常规参谋解答 + setMessages((prev) => [ + ...prev, + { + id: 'reply-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + intentTag: '💡 参谋解答', + content: `收到您的指令:“${text}”。参谋小队已依据医疗器械法规标准进行综合排查,相关指标已实时对齐。您可随时在右侧文档栏双击块进行微调。`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + }, 1200); + }; + + // 采纳变更提案 + const handleAcceptProposal = () => { + if (!pendingProposal) return; + const children = docAST.rootBlock.children || []; + const tableBlock = children.find((b) => b.type === 'table'); + if (tableBlock && tableBlock.properties?.tableData) { + tableBlock.properties.tableData.push(['电磁兼容抗扰度', '80MHz~6GHz 3V/m 强化测试', 'IEC 60601-1-2', '符合']); + } + recomputeNumbering(docAST.rootBlock); + setDocAST({ ...docAST }); + setPendingProposal(null); + + setMessages((prev) => [ + ...prev, + { + id: 'prop-acc-' + Date.now(), + sessionId: activeSessionId, + role: 'system', + content: '✅ 编制负责人已确认并采纳智能体变更提案,右侧受控文档已实时更新完成。', + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + }; + + // 块操作:保存编辑 + const handleSaveBlockEdit = (updated: DocumentBlock) => { + const children = docAST.rootBlock.children || []; + const idx = children.findIndex((b) => b.id === updated.id); + if (idx !== -1) { + children[idx] = updated; + recomputeNumbering(docAST.rootBlock); + setDocAST({ ...docAST }); + } + setEditingBlock(null); + }; + + // 块操作:增、删、移 + const handleInsertBlock = (index: number, position: 'above' | 'below') => { + const children = docAST.rootBlock.children || []; + const newBlock: DocumentBlock = { + id: 'blk_' + Math.random().toString(36).substring(2, 9), + type: 'paragraph', + content: '新插入的受控文档段落文本,双击可就地编辑...', + }; + const targetIdx = position === 'above' ? index : index + 1; + children.splice(targetIdx, 0, newBlock); + recomputeNumbering(docAST.rootBlock); + setDocAST({ ...docAST }); + }; + + const handleMoveBlock = (index: number, direction: 'up' | 'down') => { + const children = docAST.rootBlock.children || []; + const targetIdx = direction === 'up' ? index - 1 : index + 1; + if (targetIdx >= 0 && targetIdx < children.length) { + const temp = children[index]; + children[index] = children[targetIdx]; + children[targetIdx] = temp; + recomputeNumbering(docAST.rootBlock); + setDocAST({ ...docAST }); + } + }; + + const handleDeleteBlock = (index: number) => { + const children = docAST.rootBlock.children || []; + children.splice(index, 1); + recomputeNumbering(docAST.rootBlock); + setDocAST({ ...docAST }); + }; + + // 批注操作 + const handleAddComment = (comment: Omit) => { + if (!commentingBlock) return; + const newCmt: BlockComment = { + ...comment, + id: 'cmt_' + Math.random().toString(36).substring(2, 8), + createdAt: new Date().toTimeString().substring(0, 5), + }; + commentingBlock.comments = [...(commentingBlock.comments || []), newCmt]; + setDocAST({ ...docAST }); + }; + + // 法定生命周期流转 + const handleClaim = () => { + docAST.isClaimed = true; + setDocAST({ ...docAST }); + setDeliverables((prev) => + prev.map((d) => (d.docId === activeDocId ? { ...d, isClaimed: true } : d)) + ); + }; + + const handleSubmitReview = () => { + docAST.lifecycleState = 'reviewing'; + setDocAST({ ...docAST }); + setDeliverables((prev) => + prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'reviewing' } : d)) + ); + }; + + const handlePassReview = () => { + docAST.lifecycleState = 'approving'; + setDocAST({ ...docAST }); + setDeliverables((prev) => + prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'approving' } : d)) + ); + }; + + const handleRejectReview = (reason: string) => { + docAST.lifecycleState = 'drafting'; + setDocAST({ ...docAST }); + setDeliverables((prev) => + prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'drafting' } : d)) + ); + + setMessages((prev) => [ + ...prev, + { + id: 'rej-' + Date.now(), + sessionId: activeSessionId, + role: 'system', + content: `⚠️ 李主管(审核负责人)驳回了《${docAST.title}》,已退回编制阶段。驳回原因:${reason}`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + }; + + const handleApprove = () => { + docAST.lifecycleState = 'approved'; + setDocAST({ ...docAST }); + setDeliverables((prev) => + prev.map((d) => (d.docId === activeDocId ? { ...d, lifecycleState: 'approved' } : d)) + ); + + setMessages((prev) => [ + ...prev, + { + id: 'appr-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + authorName: '水木东方协同参谋', + intentTag: '🏆 终审批准生效', + content: `🎉 祝贺!王总监(批准人)已终审批准《${docAST.title}》!系统已自动对该受控交付物进行微粒度章节切片与向量化,成功注入全局标杆案例经验库!`, + timestamp: new Date().toTimeString().substring(0, 5), + recommendedCases: [ + { + docId: docAST.docId, + taskKey: docAST.taskKey, + artifactTitle: docAST.title, + sectionTitle: '终审已生效标杆案例', + similarityScore: 1.0, + keyRegulations: ['GB 9706.1-2020', 'YY 0569-2011', 'IEC 60601-1-2'], + summarySnippet: '本交付物已通过编制、审核、批准法定三级签署闭环,作为往期标杆经验向全平台开放自推荐。', + }, + ], + }, + ]); + }; + + // 导出 DOCX / Markdown + const handleExportDocx = () => { + const md = blockASTToMarkdown(docAST); + const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${docAST.title}_${docAST.version}_受控交付物.md`; + a.click(); + URL.revokeObjectURL(url); }; return ( -
-

水木东方 · 医疗器械现代 AI 协同工作台

-

当前主题: {theme}

- -
+ { + const newSess: ProjectSession = { + sessionId: 'sess-' + Date.now(), + projectId: Date.now(), + title: '新建医疗器械研发项目', + deviceType: '通用医疗器械', + stage: 'V0.1', + stageLabel: 'V0.1 需求摸底', + messageCount: 0, + updatedAt: '刚刚', + }; + setSessions([newSess, ...sessions]); + setActiveSessionId(newSess.sessionId); + }} + currentRole={currentRole} + currentUserName={currentUserName} + onChangeRole={(r, name) => { + setCurrentRole(r); + setCurrentUserName(name); + }} + chatContent={ +
+ + {pendingProposal && ( +
+ setPendingProposal(null)} + /> +
+ )} + +
+ } + docContent={ +
+ {/* 36px 极简 Tab 条 */} + setDeckDrawerOpen(!deckDrawerOpen)} + onExportDocx={handleExportDocx} + /> + + {/* 全景大盘抽屉 */} + setDeckDrawerOpen(false)} + deliverables={deliverables} + activeDocId={activeDocId} + onSelectDoc={setActiveDocId} + /> + + {/* 块状文档渲染器 */} + + + {/* 底部法定流转动作条 */} + + + {/* 块就地编辑弹窗 */} + {editingBlock && ( + setEditingBlock(null)} + /> + )} + + {/* 审核批注侧栏 */} + {commentingBlock && ( + setCommentingBlock(null)} + /> + )} +
+ } + /> ); } diff --git a/web/src/components/doc/BlockEditorModal.tsx b/web/src/components/doc/BlockEditorModal.tsx new file mode 100644 index 00000000..cdf047c0 --- /dev/null +++ b/web/src/components/doc/BlockEditorModal.tsx @@ -0,0 +1,277 @@ +import React, { useState } from 'react'; +import { DocumentBlock } from '../../types/block'; +import { X, Check, Table, Heading, AlignLeft, ShieldAlert } from 'lucide-react'; + +interface BlockEditorModalProps { + block: DocumentBlock; + onSave: (updated: DocumentBlock) => void; + onClose: () => void; +} + +export const BlockEditorModal: React.FC = ({ block, onSave, onClose }) => { + const [content, setContent] = useState(block.content || ''); + const [level, setLevel] = useState(block.level || 1); + const [tableData, setTableData] = useState( + block.properties?.tableData ? JSON.parse(JSON.stringify(block.properties.tableData)) : [] + ); + + const handleSave = () => { + const updated: DocumentBlock = { + ...block, + content, + level: block.type === 'heading' ? level : block.level, + properties: { + ...block.properties, + tableData: block.type === 'table' ? tableData : block.properties?.tableData, + }, + metadata: { + ...block.metadata, + lastModifiedBy: '张工 (编制负责人)', + lastModifiedAt: new Date().toISOString().replace('T', ' ').substring(0, 16), + }, + }; + onSave(updated); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + handleSave(); + } + }; + + const updateTableCell = (r: number, c: number, val: string) => { + const next = [...tableData]; + next[r][c] = val; + setTableData(next); + }; + + const addTableRow = () => { + if (tableData.length === 0) return; + const colCount = tableData[0].length; + setTableData([...tableData, Array.from({ length: colCount }, () => '新数据')]); + }; + + return ( +
+
+ {/* 弹窗顶栏 */} +
+
+ {block.type === 'heading' && } + {block.type === 'table' && } + {block.type === 'callout' && } + {block.type === 'paragraph' && } + + 编辑文档块 ({block.type.toUpperCase()}) + + + + + + + {/* 弹窗主体表单 */} +
+ {block.type === 'heading' && ( +
+ +
+ {[1, 2, 3, 4, 5, 6].map((lvl) => ( + + ))} +
+
+ )} + + {block.type === 'table' ? ( +
+
+ + 就地表格矩阵编辑 (三线表规范): + + +
+ +
+
+ + {tableData.map((row, r) => ( + + {row.map((cell, c) => ( + + ))} + + ))} + +
+ updateTableCell(r, c, e.target.value)} + style={{ + width: '100%', + padding: '4px 6px', + border: '1px solid transparent', + background: r === 0 ? 'var(--bg-subtle)' : 'transparent', + color: 'var(--text-strong)', + fontWeight: r === 0 ? 600 : 400, + }} + /> +
+
+
+ ) : ( +
+ +