diff --git a/web/src/App.tsx b/web/src/App.tsx index b2ae0c27..75ed1a64 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,20 +1,10 @@ import { useState, useMemo, useEffect, useRef, useCallback } from 'react'; import { AppShell } from './components/layout/AppShell'; +import { DocumentPane } from './components/doc/DocumentPane'; 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 { DocumentRevisionToolbar } from './components/doc/DocumentRevisionToolbar'; -import { CommentSidePanel } from './components/doc/CommentSidePanel'; -import { DocGovActionBar } from './components/doc/DocGovActionBar'; -import { FreeformDocumentActionBar } from './components/doc/FreeformDocumentActionBar'; import { fetchFreeformDoc } from './api/freeformDoc'; import type { DiffResult, OverlayMode } from './features/documentRevision/types'; -import { - documentRevisionInactiveHint, - revisionToolbarMountMode, -} from './features/documentRevision/revisionToolbarMount'; import { fetchProjectLineage, getProject } from './api/projectWorkflow'; import { listProjectMembers } from './api/projectMembers'; import { @@ -45,8 +35,6 @@ import { import { DocumentLineage } from './types/projectWorkflow'; import { StaleDocumentNotice } from './components/workflow/StaleDocumentNotice'; import { StageAdvanceBanner } from './components/workflow/StageAdvanceBanner'; -import { copyRichReplyToClipboard, copyMarkdownReply } from './features/replyExport/clipboard'; -import { downloadMarkdownReply } from './features/replyExport/download'; import { ProposalConfirmCard } from './components/doc/ProposalConfirmCard'; import { UserProjectRightsModal } from './components/user/UserProjectRightsModal'; import { UserSettingsModal } from './components/user/UserSettingsModal'; @@ -56,7 +44,6 @@ import { CreateProjectModal } from './components/project/CreateProjectModal'; import { ProjectMembersModal } from './components/project/ProjectMembersModal'; import { ProjectSettingsModal } from './components/project/ProjectSettingsModal'; import { fetchProjectSettings } from './api/projectSettings'; -import { ShieldAlert } from 'lucide-react'; import { LoginView } from './components/auth/LoginView'; import { FirstLoginPasswordModal } from './components/auth/FirstLoginPasswordModal'; import { authClient } from './api/auth'; @@ -145,14 +132,10 @@ export { MOCK_USERS, } from '@demo-seeds'; -const loadFilePreviewPanel = () => - import('./components/doc/preview/FilePreviewPanel').then((m) => ({ default: m.FilePreviewPanel })); const loadAdminConsoleView = () => import('./components/admin/console/AdminConsoleView').then((m) => ({ default: m.AdminConsoleView })); const loadChatExportModal = () => import('./components/history/ChatExportModal').then((m) => ({ default: m.ChatExportModal })); -const loadBlockEditorModal = () => - import('./components/doc/BlockEditorModal').then((m) => ({ default: m.BlockEditorModal })); export default function App() { // Wave 2:启动时幂等剔除 localStorage 中的 demo seed 残留(须先于 load*) @@ -1722,295 +1705,56 @@ export default function App() { } docContent={ - isCurrentProjectExited ? ( -
-
- -
-

- 受控文档已执行合规脱密隔离 -

-

- 您已退出该研发项目。依据医疗器械质量管理体系 (GMP/ISO 13485) 及商业保密规程,本项目技术规格书、设计输入草案与验证交付物已对您执行访问切断。历史聊天记录仅供审计留痕查阅。 -

-
- ) : ( -
d.docId === activeDocId)?.title || - visibleDeliverables.find((d) => d.docId === activeDocId)?.title || - '' - } - > - {/* 36px 极简 Tab 条 (含三分类分段切换与私密/共享识别) */} - setDeckDrawerOpen(!deckDrawerOpen)} - onExportDocx={handleExportDocx} - onExportXlsx={handleExportXlsx} - onExportMarkdown={handleExportMarkdown} - /> - {(() => { - const revDoc = - deliverables.find((d) => d.docId === activeDocId) || - visibleDeliverables.find((d) => d.docId === activeDocId); - const mode = revisionToolbarMountMode( - revDoc ?? { docId: activeDocId, isTemplateOnly: true }, - ); - if (mode === 'toolbar' && revDoc) { - return ( - - ); - } - const hint = documentRevisionInactiveHint( - revDoc ?? { docId: activeDocId, isTemplateOnly: true }, - ); - if (!hint) return null; - return ( -
- {hint} -
- ); - })()} - {docRevConflict && ( -
- {docRevConflict} - -
- )} - {docRevOverlayMode === 'manual' && docRevOverlay && docRevOverlay.hunks?.length > 0 && ( -
- 人工差异 {docRevOverlay.summary.modifies + docRevOverlay.summary.inserts + docRevOverlay.summary.deletes} 处 -
- )} - - {/* 全景大盘抽屉 (支持受控/共享/私密三板块与一键流转操作) */} - setDeckDrawerOpen(false)} - deliverables={visibleDeliverables} - activeDocId={activeDocId} - currentUserId={currentUser.id} - onSelectDoc={handleSelectDoc} - onShareToProject={handleShareToProject} - onRevokeShare={handleRevokeShare} - onDeleteDoc={handleDeleteDoc} - onOpenDocVisibility={(doc) => { - setTargetDocForVisibility(doc); - setDocVisibilityModalOpen(true); - }} - /> - - {/* 核心文档内容渲染区:用户上传文档使用高保真文件预览器,工作流及对话入库文档使用结构化 BlockRenderer */} - {(() => { - const activeDeliverable = deliverables.find((d) => d.docId === activeDocId); - const isUploadedFile = Boolean( - activeDeliverable && - (activeDeliverable.sourceType === 'uploaded_file' || - Boolean(activeDeliverable.fileMeta) || - activeDeliverable.docId.startsWith('doc-upload-') || - Boolean(activeDeliverable.title?.match(/\.(docx?|pdf|xlsx?|csv|pptx?|png|jpe?g|svg|webp|txt|md|json)$/i)) || - (activeDeliverable.taskKey === 'user_requirements' && - !activeDeliverable.isFixedWorkflow && - (activeDeliverable.category === 'private' || activeDeliverable.category === 'shared'))) - ); - if (isUploadedFile && activeDeliverable) { - return ( - { - setTargetDocForVisibility(activeDeliverable); - setDocVisibilityModalOpen(true); - }, - }} - /> - ); - } - return ( - <> - {docLoadErrors[activeDocId] ? ( -
- - {docLoadErrorInfo[activeDocId] - ? [ - docLoadErrorInfo[activeDocId].code, - docLoadErrorInfo[activeDocId].message, - docLoadErrorInfo[activeDocId].correlationId - ? `corr=${docLoadErrorInfo[activeDocId].correlationId}` - : '', - ] - .filter(Boolean) - .join(' ') - : '受控文档正文加载失败,请检查网络后重试。'} - - -
- ) : null} - { - handleSendMessage( - `针对受控文档《${docAST.title}》中的条款内容:\n\n“${snippet.slice(0, 150)}${snippet.length > 150 ? '...' : ''}”\n\n请帮我审查其合规性、技术可行性或提出修改建议。` - ); - }} - highlightedBlockId={highlightedBlockId} - /> - - ); - })()} - - {/* 底部流转动作条 (受控流程文档使用 DocGovActionBar,自由私密/共享文档使用 FreeformDocumentActionBar) */} - {docAST.governanceMode === 'freeform' || docAST.space === 'my_private' || docAST.space === 'project_shared' || docAST.category === 'private' || docAST.category === 'shared' ? ( - copyRichReplyToClipboard(blockASTToMarkdown(docAST))} - onCopyMarkdown={() => copyMarkdownReply(blockASTToMarkdown(docAST))} - onDownloadMarkdown={() => downloadMarkdownReply(blockASTToMarkdown(docAST), docAST.title)} - onDownloadDocx={handleExportDocx} - /> - ) : ( - { - const currentDoc = deliverables.find((d) => d.docId === activeDocId) || null; - setTargetDocForVisibility(currentDoc); - setDocVisibilityModalOpen(true); - }} - onClaim={handleClaim} - onSubmitReview={handleSubmitReview} - onPassReview={handlePassReview} - onRejectReview={handleRejectReview} - onApprove={handleApprove} - onRejectApprove={handleRejectApprove} - onShareToProject={handleShareToProject} - onRevokeShare={handleRevokeShare} - /> - )} - - {/* 块就地编辑弹窗 (支持富文本工具栏、多维表格与未保存拦截) */} - {editorSession && ( - - )} - - {/* 审核批注侧栏 */} - {commentingBlock && ( - setCommentingBlock(null)} - /> - )} -
- ) + } /> diff --git a/web/src/components/doc/DocumentPane.test.ts b/web/src/components/doc/DocumentPane.test.ts new file mode 100644 index 00000000..10a5e695 --- /dev/null +++ b/web/src/components/doc/DocumentPane.test.ts @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +test('active document pane lives outside the workbench shell', () => { + const app = readFileSync(new URL('../../App.tsx', import.meta.url), 'utf8'); + const pane = readFileSync(new URL('./DocumentPane.tsx', import.meta.url), 'utf8'); + assert.equal((app.match(/ + import('./preview/FilePreviewPanel').then((m) => ({ default: m.FilePreviewPanel })); +const loadBlockEditorModal = () => + import('./BlockEditorModal').then((m) => ({ default: m.BlockEditorModal })); + +export interface DocumentPaneProps { + isCurrentProjectExited: boolean; + activeDocId: string; + deliverables: DeliverableItem[]; + visibleDeliverables: DeliverableItem[]; + activeCategory: DeliverableCategory; + setActiveCategory: Dispatch>; + handleSelectDoc: (id: string) => void; + deckDrawerOpen: boolean; + setDeckDrawerOpen: Dispatch>; + handleExportDocx: () => void | Promise; + handleExportXlsx: () => void | Promise; + handleExportMarkdown: () => void; + docRevOverlayMode: OverlayMode; + setDocRevOverlayMode: Dispatch>; + setDocRevOverlay: Dispatch>; + docRevRefreshKey: number; + docRevConflict: string | null; + docRevOverlay: DiffResult | null; + currentUser: UserProfile; + handleShareToProject: (docId: string) => Promise; + handleRevokeShare: (docId: string) => Promise; + handleDeleteDoc: (docId: string) => Promise; + setTargetDocForVisibility: Dispatch>; + setDocVisibilityModalOpen: Dispatch>; + docLoadErrors: Record; + docLoadErrorInfo: Record; + retryControlledDocLoad: () => void; + docAST: DocumentAST; + currentDocRole: UserRole; + openBlockEditor: (block: DocumentBlock) => void; + handleInsertBlock: (index: number, position: 'above' | 'below') => void; + handleMoveBlock: (index: number, direction: 'up' | 'down') => void; + handleDeleteBlock: (index: number) => void; + setCommentingBlock: Dispatch>; + handleSendMessage: (text: string) => void | Promise; + highlightedBlockId: string | null; + canOverrideVisibility: boolean; + handleClaim: () => void | Promise; + handleSubmitReview: () => void | Promise; + handlePassReview: () => void | Promise; + handleRejectReview: (reason: string) => void | Promise; + handleApprove: () => void | Promise; + handleRejectApprove: (reason: string) => void | Promise; + editorSession: BlockEditorSession | null; + handleSaveBlockEdit: (updated: DocumentBlock) => void | Promise; + closeBlockEditor: () => void; + commentingBlock: DocumentBlock | null; + handleAddComment: (comment: Omit) => void; +} + +export function DocumentPane(props: DocumentPaneProps) { + const { + isCurrentProjectExited, + activeDocId, + deliverables, + visibleDeliverables, + activeCategory, + setActiveCategory, + handleSelectDoc, + deckDrawerOpen, + setDeckDrawerOpen, + handleExportDocx, + handleExportXlsx, + handleExportMarkdown, + docRevOverlayMode, + setDocRevOverlayMode, + setDocRevOverlay, + docRevRefreshKey, + docRevConflict, + docRevOverlay, + currentUser, + handleShareToProject, + handleRevokeShare, + handleDeleteDoc, + setTargetDocForVisibility, + setDocVisibilityModalOpen, + docLoadErrors, + docLoadErrorInfo, + retryControlledDocLoad, + docAST, + currentDocRole, + openBlockEditor, + handleInsertBlock, + handleMoveBlock, + handleDeleteBlock, + setCommentingBlock, + handleSendMessage, + highlightedBlockId, + canOverrideVisibility, + handleClaim, + handleSubmitReview, + handlePassReview, + handleRejectReview, + handleApprove, + handleRejectApprove, + editorSession, + handleSaveBlockEdit, + closeBlockEditor, + commentingBlock, + handleAddComment, + } = props; + + return ( +isCurrentProjectExited ? ( +
+
+ +
+

+ 受控文档已执行合规脱密隔离 +

+

+ 您已退出该研发项目。依据医疗器械质量管理体系 (GMP/ISO 13485) 及商业保密规程,本项目技术规格书、设计输入草案与验证交付物已对您执行访问切断。历史聊天记录仅供审计留痕查阅。 +

+
+) : ( +
d.docId === activeDocId)?.title || + visibleDeliverables.find((d) => d.docId === activeDocId)?.title || + '' + } +> + {/* 36px 极简 Tab 条 (含三分类分段切换与私密/共享识别) */} + setDeckDrawerOpen(!deckDrawerOpen)} + onExportDocx={handleExportDocx} + onExportXlsx={handleExportXlsx} + onExportMarkdown={handleExportMarkdown} + /> + {(() => { + const revDoc = + deliverables.find((d) => d.docId === activeDocId) || + visibleDeliverables.find((d) => d.docId === activeDocId); + const mode = revisionToolbarMountMode( + revDoc ?? { docId: activeDocId, isTemplateOnly: true }, + ); + if (mode === 'toolbar' && revDoc) { + return ( + + ); + } + const hint = documentRevisionInactiveHint( + revDoc ?? { docId: activeDocId, isTemplateOnly: true }, + ); + if (!hint) return null; + return ( +
+ {hint} +
+ ); + })()} + {docRevConflict && ( +
+ {docRevConflict} + +
+ )} + {docRevOverlayMode === 'manual' && docRevOverlay && docRevOverlay.hunks?.length > 0 && ( +
+ 人工差异 {docRevOverlay.summary.modifies + docRevOverlay.summary.inserts + docRevOverlay.summary.deletes} 处 +
+ )} + + {/* 全景大盘抽屉 (支持受控/共享/私密三板块与一键流转操作) */} + setDeckDrawerOpen(false)} + deliverables={visibleDeliverables} + activeDocId={activeDocId} + currentUserId={currentUser.id} + onSelectDoc={handleSelectDoc} + onShareToProject={handleShareToProject} + onRevokeShare={handleRevokeShare} + onDeleteDoc={handleDeleteDoc} + onOpenDocVisibility={(doc) => { + setTargetDocForVisibility(doc); + setDocVisibilityModalOpen(true); + }} + /> + + {/* 核心文档内容渲染区:用户上传文档使用高保真文件预览器,工作流及对话入库文档使用结构化 BlockRenderer */} + {(() => { + const activeDeliverable = deliverables.find((d) => d.docId === activeDocId); + const isUploadedFile = Boolean( + activeDeliverable && + (activeDeliverable.sourceType === 'uploaded_file' || + Boolean(activeDeliverable.fileMeta) || + activeDeliverable.docId.startsWith('doc-upload-') || + Boolean(activeDeliverable.title?.match(/\.(docx?|pdf|xlsx?|csv|pptx?|png|jpe?g|svg|webp|txt|md|json)$/i)) || + (activeDeliverable.taskKey === 'user_requirements' && + !activeDeliverable.isFixedWorkflow && + (activeDeliverable.category === 'private' || activeDeliverable.category === 'shared'))) + ); + if (isUploadedFile && activeDeliverable) { + return ( + { + setTargetDocForVisibility(activeDeliverable); + setDocVisibilityModalOpen(true); + }, + }} + /> + ); + } + return ( + <> + {docLoadErrors[activeDocId] ? ( +
+ + {docLoadErrorInfo[activeDocId] + ? [ + docLoadErrorInfo[activeDocId].code, + docLoadErrorInfo[activeDocId].message, + docLoadErrorInfo[activeDocId].correlationId + ? `corr=${docLoadErrorInfo[activeDocId].correlationId}` + : '', + ] + .filter(Boolean) + .join(' ') + : '受控文档正文加载失败,请检查网络后重试。'} + + +
+ ) : null} + { + handleSendMessage( + `针对受控文档《${docAST.title}》中的条款内容:\n\n“${snippet.slice(0, 150)}${snippet.length > 150 ? '...' : ''}”\n\n请帮我审查其合规性、技术可行性或提出修改建议。` + ); + }} + highlightedBlockId={highlightedBlockId} + /> + + ); + })()} + + {/* 底部流转动作条 (受控流程文档使用 DocGovActionBar,自由私密/共享文档使用 FreeformDocumentActionBar) */} + {docAST.governanceMode === 'freeform' || docAST.space === 'my_private' || docAST.space === 'project_shared' || docAST.category === 'private' || docAST.category === 'shared' ? ( + copyRichReplyToClipboard(blockASTToMarkdown(docAST))} + onCopyMarkdown={() => copyMarkdownReply(blockASTToMarkdown(docAST))} + onDownloadMarkdown={() => downloadMarkdownReply(blockASTToMarkdown(docAST), docAST.title)} + onDownloadDocx={handleExportDocx} + /> + ) : ( + { + const currentDoc = deliverables.find((d) => d.docId === activeDocId) || null; + setTargetDocForVisibility(currentDoc); + setDocVisibilityModalOpen(true); + }} + onClaim={handleClaim} + onSubmitReview={handleSubmitReview} + onPassReview={handlePassReview} + onRejectReview={handleRejectReview} + onApprove={handleApprove} + onRejectApprove={handleRejectApprove} + onShareToProject={handleShareToProject} + onRevokeShare={handleRevokeShare} + /> + )} + + {/* 块就地编辑弹窗 (支持富文本工具栏、多维表格与未保存拦截) */} + {editorSession && ( + + )} + + {/* 审核批注侧栏 */} + {commentingBlock && ( + setCommentingBlock(null)} + /> + )} +
+) + + ); +}