diff --git a/web/src/App.tsx b/web/src/App.tsx index da566526..ac511dbe 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -37,6 +37,7 @@ import { resolveCardInMessages, cancelCardInMessages, } from './engine/cardActionEngine'; +import { createProjectTemplateDeliverables } from './engine/templateFactory'; import { loadSessions, saveSessions, @@ -185,6 +186,20 @@ export const INITIAL_DELIVERABLES: DeliverableItem[] = [ }, ]; +// 为默认第二项目 (便携式超声探头,projectId: 2) 预置全套 7 份生命周期模板副本 +const P2_INITIAL = createProjectTemplateDeliverables({ + projectId: 2, + projectName: '便携式掌上无线超声诊断仪', + deviceType: '便携式超声探头', + creatorId: 1, + creatorName: '张工', +}); + +export const ALL_INITIAL_DELIVERABLES: DeliverableItem[] = [ + ...INITIAL_DELIVERABLES, + ...P2_INITIAL.deliverables, +]; + export const MOCK_USERS: UserProfile[] = [ { id: 1, @@ -251,7 +266,7 @@ export default function App() { // 全生命周期完整交付物大盘清单 (含工作流受控、项目协作共享、个人私密生成三大分类,支持持久化) const [deliverables, setDeliverables] = useState(() => - loadDeliverables(INITIAL_DELIVERABLES) + loadDeliverables(ALL_INITIAL_DELIVERABLES) ); // 自动持久化 deliverables @@ -267,15 +282,26 @@ export default function App() { const [rightsModalOpen, setRightsModalOpen] = useState(false); const [settingsModalOpen, setSettingsModalOpen] = useState(false); + // 获取当前激活会话的项目ID (默认为 1) + const currentProjectId = useMemo(() => { + const activeSess = sessions.find((s) => s.sessionId === activeSessionId); + return activeSess ? activeSess.projectId : 1; + }, [sessions, activeSessionId]); + + // 严格按 projectId 隔离当前项目的交付物 + const currentProjectDeliverables = useMemo(() => { + return deliverables.filter((doc) => doc.projectId === currentProjectId); + }, [deliverables, currentProjectId]); + // 基于当前登录用户过滤可见交付物 (私密文档严格按 creatorId 隔离,流程与共享文档全员可见) const visibleDeliverables = useMemo(() => { - return deliverables.filter((doc) => { + return currentProjectDeliverables.filter((doc) => { if (doc.category === 'workflow' || doc.category === 'shared') { return true; } return doc.creatorId === currentUser.id; }); - }, [deliverables, currentUser.id]); + }, [currentProjectDeliverables, currentUser.id]); // 当用户切换或可见列表变动时,自动校准激活文档 useEffect(() => { @@ -293,7 +319,7 @@ export default function App() { // 文档缓存与当前 AST const [docCache, setDocCache] = useState>(() => { - const initialCache: Record = {}; + const initialCache: Record = { ...P2_INITIAL.docASTMap }; Object.entries(MOCK_DOCUMENTS).forEach(([id, meta]) => { initialCache[id] = markdownToBlockAST(meta.markdown, { docId: meta.docId, @@ -314,7 +340,10 @@ export default function App() { return initialCache; }); - const docAST = docCache[activeDocId] || docCache['doc-001']; + const docAST = + docCache[activeDocId] || + (visibleDeliverables[0] && docCache[visibleDeliverables[0].docId]) || + docCache['doc-001']; // 根据当前激活文档与用户权限矩阵,计算当前生效角色 const currentDocRole: UserRole = (() => { @@ -841,6 +870,77 @@ export default function App() { } } + // 3. 检查当前项目中是否存在待填报的模板副本 + const matchedTemplate = currentProjectDeliverables.find((d) => { + if (!d.isTemplateOnly) return false; + if (text.includes('需求') || text.includes('摸底') || text.includes('用户')) { + return d.taskKey === 'intake' || d.taskKey === 'user_requirements'; + } + if (text.includes('调研') || text.includes('竞品')) { + return d.taskKey === 'research'; + } + if (text.includes('可行性') || text.includes('注册') || text.includes('报告')) { + return d.taskKey === 'feasibility_reg' || d.taskKey === 'feasibility_dev'; + } + if (text.includes('设计') || text.includes('规格') || text.includes('架构')) { + return d.taskKey === 'prelim_design'; + } + if (text.includes('报价') || text.includes('工时') || text.includes('预算')) { + return d.taskKey === 'cdo_quote'; + } + return false; + }); + + if (matchedTemplate) { + const existingAst = docCache[matchedTemplate.docId]; + const updatedAst: DocumentAST = { + ...existingAst, + lifecycleState: 'drafting', + isClaimed: true, + summary: `智能体参谋小队已依据您的指令“${text.slice(0, 40)}...”完成深度填报与法规对齐。`, + updatedAt: new Date().toISOString().replace('T', ' ').substring(0, 16), + }; + + if (updatedAst.rootBlock && updatedAst.rootBlock.children) { + const firstP = updatedAst.rootBlock.children.find((b) => b.type === 'paragraph'); + if (firstP) { + firstP.content += `\n\n> 🤖 **智能体填报生效**:参谋小队已结合当前项目基线与权威法规知识库完成编制,骨架模板已平滑升级为受控草稿。`; + } + } + + setDeliverables((prev) => + prev.map((d) => + d.docId === matchedTemplate.docId + ? { + ...d, + isTemplateOnly: false, + lifecycleState: 'drafting', + isClaimed: true, + summary: updatedAst.summary, + updatedAt: '刚刚', + } + : d + ) + ); + setDocCache((prev) => ({ ...prev, [matchedTemplate.docId]: updatedAst })); + setActiveDocId(matchedTemplate.docId); + setActiveCategory('workflow'); + + setMessages((prev) => [ + ...prev, + { + id: 'ast-fill-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + authorName: '水木东方协同参谋', + intentTag: '🎯 模板填报升级', + content: `收到!参谋小队已依据您的指令完成《${matchedTemplate.title}》的填报。\n\n该文档已成功从【📄 初始模板】平滑升格为【✍️ 编制中 (受控草稿)】,右侧工作台已为您实时呈现填报正文,请审阅。`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + return; + } + setMessages((prev) => [ ...prev, { @@ -1069,26 +1169,45 @@ export default function App() { onSelectSession={setActiveSessionId} onNewSession={() => { const newSessId = 'sess-' + Date.now(); + const newProjId = Date.now(); + const newProjectTitle = '新建医疗器械研发项目'; + const newDeviceType = '通用医疗器械'; + const newSess: ProjectSession = { sessionId: newSessId, - projectId: Date.now(), - title: '新建医疗器械研发项目', - deviceType: '通用医疗器械', + projectId: newProjId, + title: newProjectTitle, + deviceType: newDeviceType, stage: 'V0.1', stageLabel: 'V0.1 需求摸底', messageCount: 1, updatedAt: '刚刚', }; + const initMsg: ChatMessage = { id: 'm-init-' + Date.now(), sessionId: newSessId, role: 'system', - content: '水木东方协同参谋小队已就位,当前新项目进入【V0.1 需求摸底与产品定义】阶段。请在此输入您的器械研发构想。', + content: '水木东方协同参谋小队已就位,当前新项目进入【V0.1 需求摸底与产品定义】阶段。全套研发工作流与阶段文档模板已克隆就绪,请在右侧查阅或在此输入您的研发构想。', timestamp: new Date().toTimeString().substring(0, 5), }; + + // 即时克隆全生命周期 7 份受控模板副本 + const { deliverables: clonedDocs, docASTMap: clonedAsts } = createProjectTemplateDeliverables({ + projectId: newProjId, + projectName: newProjectTitle, + deviceType: newDeviceType, + creatorId: currentUser.id, + creatorName: currentUser.name, + }); + setSessions((prev) => [newSess, ...prev]); setMessages((prev) => [...prev, initMsg]); + setDeliverables((prev) => [...prev, ...clonedDocs]); + setDocCache((prev) => ({ ...prev, ...clonedAsts })); setActiveSessionId(newSessId); + setActiveDocId(clonedDocs[0].docId); + setActiveCategory('workflow'); }} currentUser={currentUser} currentRole={currentDocRole}