-
💬
-
- 与AI自由对话
-
-
- 直接提问,或使用@知识库引用文献
+ try {
+ // Phase 2: 逐篇精读模式 - 限定文档范围
+ let fullContent = ''
+
+ await chatApi.sendMessageStream(
+ {
+ content: `[当前文献: ${deepReadHook.currentDoc.filename}]\n\n${content}`,
+ modelType: selectedModel,
+ knowledgeBaseIds: modeState.selectedKbId ? [modeState.selectedKbId] : [],
+ documentIds: [deepReadHook.currentDoc.id], // ✅ 只检索当前文档
+ },
+ (chunk) => {
+ fullContent += chunk
+ setStreamingContent(fullContent)
+ },
+ () => {
+ const assistantMsg: ChatMessage = {
+ id: `temp-assistant-${Date.now()}`,
+ role: 'assistant',
+ content: fullContent,
+ timestamp: new Date(),
+ }
+ deepReadHook.addMessage(assistantMsg)
+ setStreamingContent('')
+ setSending(false)
+ },
+ (error) => {
+ console.error('Send failed:', error)
+ antdMessage.error('发送消息失败')
+ setStreamingContent('')
+ setSending(false)
+ }
+ )
+ } catch (error) {
+ console.error('Send error:', error)
+ antdMessage.error('发送消息失败')
+ setSending(false)
+ }
+ }
+
+ // 渲染内容区域
+ const renderContent = () => {
+ const kb = knowledgeBases.find(k => k.id === modeState.selectedKbId)
+ const kbName = kb?.name || '未选择'
+
+ // 通用对话模式
+ if (modeState.baseMode === 'general') {
+ return (
+
+
+ {generalMessages.length === 0 && !sending ? (
+
+
+
💬
+
+ 与AI自由对话
+
+
+ 直接提问,或使用@知识库引用文献
+
-
- ) : (
-
+ ) : (
+
+ )}
+
+
+
+
+ )
+ }
+
+ // 全文阅读模式
+ if (modeState.baseMode === 'knowledge_base' && modeState.kbMode === 'full_text') {
+ if (!modeState.fullTextState) {
+ return (
+
+ )
+ }
+
+ return (
+
+ handleSendGeneralMessage(content, [modeState.selectedKbId!])}
+ messages={generalMessages}
+ loading={sending}
+ streamingContent={streamingContent}
+ />
+
+ )
+ }
+
+ // 逐篇精读模式
+ if (modeState.baseMode === 'knowledge_base' && modeState.kbMode === 'deep_read') {
+ if (!modeState.deepReadState || !deepReadHook.selectedDocs.length) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+ )
+ }
+
+ // Phase 3: 批处理模式
+ if (modeState.baseMode === 'knowledge_base' && modeState.kbMode === 'batch') {
+ return (
+
+
+
+ )
+ }
+
+ return null
+ }
+
+ return (
+
+ {/* 左侧模式选择器 */}
+
+
+ {/* 主内容区 */}
+
+ {/* 顶部工具栏 */}
+
+ {/* 用量说明按钮(仅在全文阅读模式显示) */}
+ {modeState.baseMode === 'knowledge_base' && modeState.kbMode === 'full_text' && modeState.fullTextState && (
+
+ }
+ onClick={() => setShowUsageModal(true)}
+ >
+ 用量说明
+
+
)}
+
+ {/* 模型选择器 */}
+
- {/* 消息输入 */}
-
+ {/* 内容区域 */}
+ {renderContent()}
+
+ {/* 文献选择器弹窗 */}
+ {showDocSelector && (
+
setShowDocSelector(false)}
+ />
+ )}
+
+ {/* 用量说明模态框 */}
+ {showUsageModal && modeState.fullTextState && (
+ setShowUsageModal(false)}
+ state={modeState.fullTextState}
+ kbName={knowledgeBases.find(k => k.id === modeState.selectedKbId)?.name || '未知知识库'}
+ />
+ )}
)
}
export default ChatPage
+
diff --git a/frontend/src/pages/ReviewPage.css b/frontend/src/pages/ReviewPage.css
new file mode 100644
index 00000000..8f0cdd8e
--- /dev/null
+++ b/frontend/src/pages/ReviewPage.css
@@ -0,0 +1,121 @@
+/* 打印样式优化 */
+@media print {
+ /* 隐藏不需要打印的元素 */
+ .ant-layout-sider,
+ .ant-layout-header,
+ .ant-btn,
+ .ant-dropdown,
+ .ant-steps,
+ .no-print {
+ display: none !important;
+ }
+
+ /* 页面布局优化 */
+ body {
+ margin: 0;
+ padding: 20mm;
+ }
+
+ /* 卡片样式调整 */
+ .ant-card {
+ border: none !important;
+ box-shadow: none !important;
+ page-break-inside: avoid;
+ }
+
+ /* 标题优化 */
+ .ant-typography {
+ color: #000 !important;
+ }
+
+ /* 进度条隐藏 */
+ .ant-progress {
+ display: none !important;
+ }
+
+ /* 折叠面板展开 */
+ .ant-collapse-content {
+ display: block !important;
+ }
+
+ .ant-collapse-item {
+ page-break-inside: avoid;
+ }
+
+ /* Tabs切换隐藏,显示所有内容 */
+ .ant-tabs-nav {
+ display: none !important;
+ }
+
+ .ant-tabs-content {
+ display: block !important;
+ }
+
+ .ant-tabs-tabpane {
+ display: block !important;
+ page-break-before: always;
+ }
+
+ .ant-tabs-tabpane:first-child {
+ page-break-before: auto;
+ }
+
+ /* 分页控制 */
+ h1, h2, h3, h4, h5, h6 {
+ page-break-after: avoid;
+ }
+
+ /* 背景色移除(节省墨水) */
+ * {
+ background: white !important;
+ color: black !important;
+ }
+
+ /* Tag样式调整 */
+ .ant-tag {
+ border: 1px solid #d9d9d9 !important;
+ }
+
+ /* Alert样式调整 */
+ .ant-alert {
+ border: 1px solid #d9d9d9 !important;
+ }
+}
+
+/* 屏幕显示样式 */
+@media screen {
+ .print-only {
+ display: none;
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/ReviewPage.tsx b/frontend/src/pages/ReviewPage.tsx
new file mode 100644
index 00000000..e05f429b
--- /dev/null
+++ b/frontend/src/pages/ReviewPage.tsx
@@ -0,0 +1,624 @@
+import { useState } from 'react';
+import {
+ Card,
+ Upload,
+ Button,
+ Select,
+ Steps,
+ Progress,
+ message,
+ Typography,
+ Space,
+ Alert,
+ Spin,
+ Empty,
+ Tag,
+ Divider,
+ Tabs,
+ Dropdown,
+} from 'antd';
+import type { MenuProps } from 'antd';
+import './ReviewPage.css';
+import {
+ UploadOutlined,
+ FileTextOutlined,
+ CheckCircleOutlined,
+ CloseCircleOutlined,
+ LoadingOutlined,
+ RocketOutlined,
+ DownloadOutlined,
+ PrinterOutlined,
+ CopyOutlined,
+} from '@ant-design/icons';
+import type { UploadProps } from 'antd';
+import {
+ uploadManuscript,
+ pollTaskUntilComplete,
+ getTaskReport,
+ getStatusText,
+ getStatusColor,
+ formatFileSize,
+ formatDuration,
+ type ReviewTask,
+ type ReviewReport,
+ type ReviewTaskStatus,
+} from '../api/reviewApi';
+import ScoreCard from '../components/review/ScoreCard';
+import EditorialReview from '../components/review/EditorialReview';
+import MethodologyReview from '../components/review/MethodologyReview';
+
+const { Title, Paragraph, Text } = Typography;
+const { Dragger } = Upload;
+
+/**
+ * 稿件审查页面
+ */
+const ReviewPage = () => {
+ // ==================== State ====================
+ const [modelType, setModelType] = useState<'deepseek-v3' | 'qwen3-72b' | 'qwen-long'>('deepseek-v3');
+ const [selectedFile, setSelectedFile] = useState
(null);
+ const [uploading, setUploading] = useState(false);
+ const [currentTask, setCurrentTask] = useState(null);
+ const [report, setReport] = useState(null);
+ const [error, setError] = useState(null);
+
+ // ==================== 上传配置 ====================
+ const uploadProps: UploadProps = {
+ name: 'file',
+ multiple: false,
+ maxCount: 1,
+ accept: '.doc,.docx',
+ beforeUpload: (file) => {
+ // 验证文件类型
+ const isDoc = file.name.endsWith('.doc') || file.name.endsWith('.docx');
+ if (!isDoc) {
+ message.error('只支持Word文档(.doc或.docx)!');
+ return Upload.LIST_IGNORE;
+ }
+
+ // 验证文件大小(5MB)
+ const isLt5M = file.size / 1024 / 1024 < 5;
+ if (!isLt5M) {
+ message.error('文件大小不能超过5MB!');
+ return Upload.LIST_IGNORE;
+ }
+
+ setSelectedFile(file);
+ return false; // 阻止自动上传
+ },
+ onRemove: () => {
+ setSelectedFile(null);
+ },
+ };
+
+ // ==================== 开始审查 ====================
+ const handleStartReview = async () => {
+ if (!selectedFile) {
+ message.warning('请先选择稿件文件!');
+ return;
+ }
+
+ setUploading(true);
+ setError(null);
+ setReport(null);
+
+ try {
+ // 1. 上传稿件
+ message.loading({ content: '正在上传稿件...', key: 'upload' });
+ const result = await uploadManuscript({
+ file: selectedFile,
+ modelType,
+ });
+
+ message.success({ content: '上传成功!开始审查...', key: 'upload', duration: 2 });
+
+ // 2. 轮询任务状态
+ const task = await pollTaskUntilComplete(
+ result.taskId,
+ (updatedTask) => {
+ setCurrentTask(updatedTask);
+ console.log('任务状态:', updatedTask.status);
+ },
+ 60 // 最多5分钟
+ );
+
+ // 3. 任务完成,获取报告
+ if (task.status === 'completed') {
+ const fullReport = await getTaskReport(task.id);
+ setReport(fullReport);
+ message.success('审查完成!');
+ } else if (task.status === 'failed') {
+ setError(task.errorMessage || '审查失败');
+ message.error('审查失败:' + (task.errorMessage || '未知错误'));
+ }
+ } catch (err: any) {
+ console.error('审查失败:', err);
+ setError(err.message || '审查过程出错');
+ message.error('审查失败:' + (err.message || '未知错误'));
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ // ==================== 重新审查 ====================
+ const handleReset = () => {
+ setSelectedFile(null);
+ setCurrentTask(null);
+ setReport(null);
+ setError(null);
+ };
+
+ // ==================== 导出报告 ====================
+ const handleExportPDF = async () => {
+ if (!report) return;
+
+ try {
+ message.loading({ content: '正在生成PDF...', key: 'pdf', duration: 0 });
+
+ // 动态导入依赖
+ const html2canvas = (await import('html2canvas')).default;
+ const jsPDF = (await import('jspdf')).default;
+
+ // 获取报告内容的DOM元素
+ const reportElement = document.getElementById('report-content');
+ if (!reportElement) {
+ message.error('无法找到报告内容');
+ return;
+ }
+
+ // 1. 确保所有Tabs内容都显示
+ const tabPanes = reportElement.querySelectorAll('.ant-tabs-tabpane');
+ const originalTabDisplay: string[] = [];
+ tabPanes.forEach((pane, index) => {
+ const htmlPane = pane as HTMLElement;
+ originalTabDisplay[index] = htmlPane.style.display;
+ htmlPane.style.display = 'block'; // 强制显示所有Tab内容
+ });
+
+ // 2. 隐藏不需要导出的元素
+ const elementsToHide = reportElement.querySelectorAll('.no-print, .ant-btn, .ant-dropdown, .ant-tabs-nav');
+ elementsToHide.forEach((el) => {
+ (el as HTMLElement).style.display = 'none';
+ });
+
+ // 3. 等待DOM更新和渲染
+ await new Promise(resolve => setTimeout(resolve, 300));
+
+ // 4. 将HTML转换为Canvas
+ const canvas = await html2canvas(reportElement, {
+ scale: 2, // 提高清晰度
+ useCORS: true,
+ logging: false,
+ backgroundColor: '#ffffff',
+ windowWidth: reportElement.scrollWidth,
+ windowHeight: reportElement.scrollHeight,
+ });
+
+ // 5. 恢复原始状态
+ elementsToHide.forEach((el) => {
+ (el as HTMLElement).style.display = '';
+ });
+
+ tabPanes.forEach((pane, index) => {
+ (pane as HTMLElement).style.display = originalTabDisplay[index] || '';
+ });
+
+ // 6. 创建PDF
+ const imgWidth = 210; // A4纸宽度(mm)
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
+ const pageHeight = 297; // A4纸高度(mm)
+
+ const pdf = new jsPDF('p', 'mm', 'a4');
+ const imgData = canvas.toDataURL('image/png');
+
+ let heightLeft = imgHeight;
+ let position = 0;
+
+ // 添加第一页
+ pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+ heightLeft -= pageHeight;
+
+ // 如果内容超过一页,添加更多页
+ while (heightLeft > 0) {
+ position = heightLeft - imgHeight;
+ pdf.addPage();
+ pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+ heightLeft -= pageHeight;
+ }
+
+ // 7. 下载PDF
+ const fileName = `稿件审查报告-${report.fileName.replace('.docx', '').replace('.doc', '')}-${new Date().toLocaleDateString('zh-CN').replace(/\//g, '-')}.pdf`;
+ pdf.save(fileName);
+
+ message.success({ content: 'PDF已生成并下载!', key: 'pdf', duration: 2 });
+ } catch (error) {
+ console.error('PDF生成失败:', error);
+ message.error({ content: 'PDF生成失败,请重试', key: 'pdf', duration: 3 });
+ }
+ };
+
+ const handleCopyReport = () => {
+ if (!report) return;
+
+ // 构建文本格式的报告
+ let reportText = `稿件审查报告\n`;
+ reportText += `${'='.repeat(60)}\n\n`;
+ reportText += `文件名: ${report.fileName}\n`;
+ reportText += `字数: ${report.wordCount} 字符\n`;
+ reportText += `使用模型: ${report.modelUsed}\n`;
+ reportText += `评估时间: ${report.completedAt ? new Date(report.completedAt).toLocaleString('zh-CN') : 'N/A'}\n`;
+ reportText += `耗时: ${report.durationSeconds ? formatDuration(report.durationSeconds) : 'N/A'}\n\n`;
+
+ reportText += `总体评分: ${report.overallScore?.toFixed(1) || 'N/A'} / 100\n`;
+ reportText += `${'='.repeat(60)}\n\n`;
+
+ // 稿约规范性评估
+ if (report.editorialReview) {
+ reportText += `一、稿约规范性评估\n`;
+ reportText += `-`.repeat(60) + '\n\n';
+ reportText += `总分: ${report.editorialReview.overall_score} / 100\n\n`;
+ reportText += `总结: ${report.editorialReview.summary}\n\n`;
+
+ report.editorialReview.items.forEach((item, index) => {
+ reportText += `${index + 1}. ${item.criterion}\n`;
+ reportText += ` 状态: ${item.status === 'pass' ? '✓ 通过' : item.status === 'warning' ? '⚠ 警告' : '✗ 不通过'}\n`;
+ reportText += ` 评分: ${item.score} / 100\n`;
+
+ if (item.issues && item.issues.length > 0) {
+ reportText += ` 问题:\n`;
+ item.issues.forEach(issue => {
+ reportText += ` - ${issue}\n`;
+ });
+ }
+
+ if (item.suggestions && item.suggestions.length > 0) {
+ reportText += ` 建议:\n`;
+ item.suggestions.forEach(suggestion => {
+ reportText += ` - ${suggestion}\n`;
+ });
+ }
+ reportText += '\n';
+ });
+ }
+
+ // 方法学评估
+ if (report.methodologyReview) {
+ reportText += `\n二、方法学评估\n`;
+ reportText += `-`.repeat(60) + '\n\n';
+ reportText += `总分: ${report.methodologyReview.overall_score} / 100\n\n`;
+ reportText += `总结: ${report.methodologyReview.summary}\n\n`;
+
+ report.methodologyReview.parts.forEach((part, partIndex) => {
+ reportText += `${partIndex + 1}. ${part.part}\n`;
+ reportText += ` 评分: ${part.score} / 100\n`;
+
+ if (part.issues && part.issues.length > 0) {
+ reportText += ` 发现的问题:\n`;
+ part.issues.forEach((issue, issueIndex) => {
+ reportText += ` ${issueIndex + 1}) ${issue.type} [${issue.severity === 'major' ? '严重' : '轻微'}]\n`;
+ reportText += ` 描述: ${issue.description}\n`;
+ reportText += ` 位置: ${issue.location}\n`;
+ reportText += ` 建议: ${issue.suggestion}\n`;
+ });
+ } else {
+ reportText += ` ✓ 未发现问题\n`;
+ }
+ reportText += '\n';
+ });
+ }
+
+ reportText += `${'='.repeat(60)}\n`;
+ reportText += `报告生成时间: ${new Date().toLocaleString('zh-CN')}\n`;
+
+ // 复制到剪贴板
+ navigator.clipboard.writeText(reportText).then(() => {
+ message.success('报告已复制到剪贴板!可粘贴到Word或其他文档中');
+ }).catch(() => {
+ message.error('复制失败,请手动选择文本复制');
+ });
+ };
+
+ // ==================== 获取当前步骤 ====================
+ const getCurrentStep = (status: ReviewTaskStatus): number => {
+ const stepMap: Record = {
+ pending: 0,
+ extracting: 1,
+ reviewing_editorial: 2,
+ reviewing_methodology: 3,
+ completed: 4,
+ failed: 4,
+ };
+ return stepMap[status] || 0;
+ };
+
+ // ==================== 渲染 ====================
+ return (
+
+
+ {/* 标题 */}
+
+
+
+ 稿件审查
+
+
+ 智能评估稿件的规范性和方法学质量,提供详细的改进建议
+
+
+
+
+ {/* 主内容 */}
+ {!currentTask && !report && !error && (
+ <>
+ {/* 上传区域 */}
+
+
+
+
+
+
+ 点击或拖拽Word文档到此区域
+
+ 仅支持 .doc 或 .docx 格式,文件大小不超过5MB
+
+
+
+ {selectedFile && (
+
+ 文件名:{selectedFile.name}
+ 大小:{formatFileSize(selectedFile.size)}
+
+ }
+ type="success"
+ showIcon
+ />
+ )}
+
+
+
+ {/* 模型选择 */}
+
+
+
+ ),
+ },
+ {
+ value: 'qwen3-72b',
+ label: (
+
+ Qwen3-72B - 阿里云千问大模型
+
+ ),
+ },
+ {
+ value: 'qwen-long',
+ label: (
+
+ Qwen-Long - 超长上下文(1M tokens)
+
+ ),
+ },
+ ]}
+ />
+
+
+ DeepSeek-V3:性能强大,速度快,推荐使用
+ Qwen3-72B:阿里云千问大模型,表现稳定
+ Qwen-Long:支持超长文本,适合特别长的稿件
+
+ }
+ type="info"
+ />
+
+
+
+ {/* 开始按钮 */}
+
+ }
+ onClick={handleStartReview}
+ disabled={!selectedFile || uploading}
+ loading={uploading}
+ block
+ >
+ {uploading ? '审查中...' : '开始审查'}
+
+
+ >
+ )}
+
+ {/* 进度展示 */}
+ {currentTask && !report && !error && (
+
+
+
+ } />
+
+ 正在审查中...
+
+
+ {getStatusText(currentTask.status)}
+
+
+
+ },
+ { title: '提取文本', icon: getCurrentStep(currentTask.status) >= 1 ? : },
+ { title: '稿约评估', icon: getCurrentStep(currentTask.status) >= 2 ? : },
+ { title: '方法学评估', icon: getCurrentStep(currentTask.status) >= 3 ? : },
+ { title: '生成报告', icon: getCurrentStep(currentTask.status) >= 4 ? : },
+ ]}
+ />
+
+ {currentTask.wordCount && (
+
+ )}
+
+
+
+
+ )}
+
+ {/* 报告展示(完整版) */}
+ {report && (
+
+ {/* 成功提示 */}
+
+
+
+
+ 审查完成!
+
+
+ 已完成稿约规范性评估和方法学评估
+
+
+
+
+ {/* 总体评分 */}
+
+
+ {/* 基本信息 */}
+
+
+ 文件名:{report.fileName}
+ 字数:{report.wordCount} 字符
+ 使用模型:{report.modelUsed}
+ 耗时:{report.durationSeconds ? formatDuration(report.durationSeconds) : 'N/A'}
+
+
+
+ {/* 详细评估报告(Tabs) */}
+
+ ) : (
+
+ ),
+ },
+ {
+ key: 'methodology',
+ label: `方法学评估(${report.methodologyReview?.overall_score || 'N/A'}分)`,
+ children: report.methodologyReview ? (
+
+ ) : (
+
+ ),
+ },
+ ]}
+ />
+
+ {/* 操作按钮 */}
+
+
+ }
+ onClick={handleReset}
+ block
+ >
+ 审查新稿件
+
+ ,
+ onClick: handleExportPDF,
+ },
+ {
+ key: 'copy',
+ label: '复制报告内容',
+ icon: ,
+ onClick: handleCopyReport,
+ },
+ ],
+ }}
+ placement="bottomLeft"
+ >
+ }
+ block
+ >
+ 导出报告
+
+
+
+
+
+ )}
+
+ {/* 错误展示 */}
+ {error && (
+
+
+
+
+ 审查失败
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default ReviewPage;
+
diff --git a/frontend/src/types/chat.ts b/frontend/src/types/chat.ts
new file mode 100644
index 00000000..5e877ba7
--- /dev/null
+++ b/frontend/src/types/chat.ts
@@ -0,0 +1,91 @@
+/**
+ * Phase 2: 聊天模式相关类型定义
+ */
+
+import { Document } from './index'
+
+/**
+ * 聊天基础模式
+ */
+export type ChatBaseMode = 'general' | 'knowledge_base'
+
+/**
+ * 知识库工作模式
+ */
+export type KnowledgeBaseMode = 'full_text' | 'deep_read' | 'batch'
+
+/**
+ * 聊天页面状态
+ */
+export interface ChatPageState {
+ // 基础模式
+ baseMode: ChatBaseMode
+
+ // 知识库配置
+ selectedKbId?: string
+ kbMode?: KnowledgeBaseMode
+
+ // 全文阅读模式状态
+ fullTextState?: FullTextModeState
+
+ // 逐篇精读模式状态
+ deepReadState?: DeepReadModeState
+}
+
+/**
+ * 全文阅读模式状态
+ */
+export interface FullTextModeState {
+ loadedDocs: Document[]
+ totalFiles: number
+ selectedFiles: number
+ totalTokens: number
+ usedTokens: number
+ availableTokens: number
+ reason: 'all_included' | 'file_limit' | 'token_limit'
+}
+
+/**
+ * 逐篇精读模式状态
+ */
+export interface DeepReadModeState {
+ selectedDocs: Document[]
+ currentDocId: string
+ currentDoc?: Document
+ currentConversation: ChatMessage[]
+ conversationPerDoc: Map