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)}
+ />
+ )}
+
+)
+
+ );
+}