feat(rvw): Complete RVW module development Phase 1-3
Summary: - Migrate backend to modules/rvw with v2 API routes (/api/v2/rvw) - Add new database fields: selectedAgents, editorialScore, methodologyStatus, picoExtract, isArchived - Create frontend module in frontend-v2/src/modules/rvw - Implement Dashboard with task list, filtering, batch operations - Implement ReportDetail with dual tabs (editorial/methodology) - Implement AgentModal for intelligent agent selection - Register RVW module in moduleRegistry.ts - Add navigation entry in TopNavigation - Update documentation for RVW module status (v3.0) - Update system status document (v2.9) Features: - User can select agents: editorial, methodology, or both - Support batch task execution - Task status filtering - Replace console.log with logger service - Maintain v1 API backward compatibility Tested: Frontend and backend verified locally Status: 85% complete (Phase 1-3 done)
This commit is contained in:
@@ -137,4 +137,3 @@ const TopNavigation = () => {
|
||||
}
|
||||
|
||||
export default TopNavigation
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
FolderOpenOutlined,
|
||||
ClearOutlined,
|
||||
BarChartOutlined,
|
||||
LineChartOutlined
|
||||
LineChartOutlined,
|
||||
AuditOutlined
|
||||
} from '@ant-design/icons'
|
||||
|
||||
/**
|
||||
@@ -78,6 +79,16 @@ export const MODULES: ModuleDefinition[] = [
|
||||
description: '统计分析工具集(Java团队开发)',
|
||||
isExternal: true, // 外部模块
|
||||
},
|
||||
{
|
||||
id: 'review-system',
|
||||
name: '预审稿',
|
||||
path: '/rvw',
|
||||
icon: AuditOutlined,
|
||||
component: lazy(() => import('@/modules/rvw')),
|
||||
placeholder: false, // RVW模块已开发
|
||||
requiredVersion: 'basic',
|
||||
description: '智能期刊审稿系统(稿约评审+方法学评审)',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -555,5 +555,6 @@ export default FulltextDetailDrawer;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -148,5 +148,6 @@ export const useAssets = (activeTab: AssetTabType) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -138,5 +138,6 @@ export const useRecentTasks = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -337,5 +337,6 @@ export default DropnaDialog;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -422,5 +422,6 @@ export default MetricTimePanel;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -308,5 +308,6 @@ export default PivotPanel;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -108,5 +108,6 @@ export function useSessionStatus({
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -100,5 +100,6 @@ export interface DataStats {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -96,5 +96,6 @@ export type AssetTabType = 'all' | 'processed' | 'raw';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -217,3 +217,4 @@ export const documentSelectionApi = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -285,3 +285,4 @@ export default KnowledgePage;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -223,3 +223,4 @@ export const useKnowledgeBaseStore = create<KnowledgeBaseState>((set, get) => ({
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -40,3 +40,4 @@ export interface BatchTemplate {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
502
frontend-v2/src/modules/rvw/index.tsx
Normal file
502
frontend-v2/src/modules/rvw/index.tsx
Normal file
@@ -0,0 +1,502 @@
|
||||
/**
|
||||
* RVW - 预审稿模块入口
|
||||
*
|
||||
* @description 智能期刊审稿系统
|
||||
* - 稿约评审:评估稿件是否符合期刊投稿要求
|
||||
* - 方法学评审:评估临床研究的方法学质量
|
||||
*
|
||||
* @version Phase 3 - 前端重构
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate, Routes, Route, useParams } from 'react-router-dom'
|
||||
import {
|
||||
Upload,
|
||||
Button,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
message,
|
||||
Card,
|
||||
Spin,
|
||||
Modal,
|
||||
Checkbox,
|
||||
Progress,
|
||||
Tabs,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Popconfirm
|
||||
} from 'antd'
|
||||
import {
|
||||
UploadOutlined,
|
||||
FileTextOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
PlayCircleOutlined,
|
||||
ReloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
DownloadOutlined
|
||||
} from '@ant-design/icons'
|
||||
import type { UploadFile, UploadProps } from 'antd'
|
||||
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
const { TabPane } = Tabs
|
||||
|
||||
// API 基础路径
|
||||
const API_BASE = '/api/v2/rvw'
|
||||
|
||||
// 任务类型定义
|
||||
interface ReviewTask {
|
||||
id: string
|
||||
fileName: string
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed'
|
||||
selectedAgents: string[]
|
||||
editorialScore?: number
|
||||
methodologyStatus?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
editorialReview?: any
|
||||
methodologyReview?: any
|
||||
}
|
||||
|
||||
// 智能体选择弹窗
|
||||
const AgentSelectModal: React.FC<{
|
||||
visible: boolean
|
||||
onCancel: () => void
|
||||
onConfirm: (agents: string[]) => void
|
||||
loading?: boolean
|
||||
}> = ({ visible, onCancel, onConfirm, loading }) => {
|
||||
const [selected, setSelected] = useState<string[]>(['editorial', 'methodology'])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="选择审稿智能体"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={() => onConfirm(selected)}
|
||||
confirmLoading={loading}
|
||||
okText="开始审稿"
|
||||
cancelText="取消"
|
||||
>
|
||||
<div className="py-4">
|
||||
<Checkbox.Group
|
||||
value={selected}
|
||||
onChange={(vals) => setSelected(vals as string[])}
|
||||
>
|
||||
<Space direction="vertical" size="middle">
|
||||
<Checkbox value="editorial">
|
||||
<div>
|
||||
<div className="font-medium">📝 稿约评审智能体</div>
|
||||
<div className="text-gray-500 text-sm">评估稿件是否符合期刊投稿要求(11项标准)</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
<Checkbox value="methodology">
|
||||
<div>
|
||||
<div className="font-medium">🔬 方法学评审智能体</div>
|
||||
<div className="text-gray-500 text-sm">评估临床研究的方法学质量(20项检查点)</div>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</Checkbox.Group>
|
||||
{selected.length === 0 && (
|
||||
<div className="text-red-500 mt-2">请至少选择一个智能体</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
// 任务列表页面
|
||||
const TaskListPage: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const [tasks, setTasks] = useState<ReviewTask[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [showAgentModal, setShowAgentModal] = useState(false)
|
||||
const [pendingFile, setPendingFile] = useState<File | null>(null)
|
||||
const [runningTaskId, setRunningTaskId] = useState<string | null>(null)
|
||||
|
||||
// 加载任务列表
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await fetch(`${API_BASE}/tasks`)
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
setTasks(data.data || [])
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载任务列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks()
|
||||
}, [])
|
||||
|
||||
// 上传文件
|
||||
const handleUpload = async (agents: string[]) => {
|
||||
if (!pendingFile || agents.length === 0) return
|
||||
|
||||
try {
|
||||
setUploading(true)
|
||||
const formData = new FormData()
|
||||
formData.append('file', pendingFile)
|
||||
formData.append('selectedAgents', JSON.stringify(agents))
|
||||
|
||||
const res = await fetch(`${API_BASE}/tasks`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
if (data.success) {
|
||||
message.success('稿件上传成功,开始审稿...')
|
||||
setShowAgentModal(false)
|
||||
setPendingFile(null)
|
||||
loadTasks()
|
||||
} else {
|
||||
message.error(data.error || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('上传失败')
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const handleDelete = async (taskId: string) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/tasks/${taskId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
message.success('删除成功')
|
||||
loadTasks()
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 运行审稿
|
||||
const handleRun = async (taskId: string, agents: string[]) => {
|
||||
try {
|
||||
setRunningTaskId(taskId)
|
||||
const res = await fetch(`${API_BASE}/tasks/${taskId}/run`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ selectedAgents: agents }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
message.success('审稿任务已启动')
|
||||
loadTasks()
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('启动审稿失败')
|
||||
} finally {
|
||||
setRunningTaskId(null)
|
||||
}
|
||||
}
|
||||
|
||||
// 上传配置
|
||||
const uploadProps: UploadProps = {
|
||||
beforeUpload: (file) => {
|
||||
setPendingFile(file)
|
||||
setShowAgentModal(true)
|
||||
return false
|
||||
},
|
||||
showUploadList: false,
|
||||
accept: '.pdf,.doc,.docx,.txt',
|
||||
}
|
||||
|
||||
// 状态标签
|
||||
const getStatusTag = (status: string) => {
|
||||
const config: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
|
||||
pending: { color: 'default', icon: <ClockCircleOutlined />, text: '待审稿' },
|
||||
processing: { color: 'processing', icon: <Spin size="small" />, text: '审稿中' },
|
||||
completed: { color: 'success', icon: <CheckCircleOutlined />, text: '已完成' },
|
||||
failed: { color: 'error', icon: <ExclamationCircleOutlined />, text: '失败' },
|
||||
}
|
||||
const c = config[status] || config.pending
|
||||
return <Tag color={c.color} icon={c.icon}>{c.text}</Tag>
|
||||
}
|
||||
|
||||
// 表格列
|
||||
const columns = [
|
||||
{
|
||||
title: '文件名',
|
||||
dataIndex: 'fileName',
|
||||
key: 'fileName',
|
||||
render: (name: string) => (
|
||||
<Space>
|
||||
<FileTextOutlined />
|
||||
<span>{name}</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 120,
|
||||
render: (status: string) => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '智能体',
|
||||
dataIndex: 'selectedAgents',
|
||||
key: 'selectedAgents',
|
||||
width: 200,
|
||||
render: (agents: string[]) => (
|
||||
<Space>
|
||||
{agents?.includes('editorial') && <Tag color="blue">稿约</Tag>}
|
||||
{agents?.includes('methodology') && <Tag color="purple">方法学</Tag>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '稿约评分',
|
||||
dataIndex: 'editorialScore',
|
||||
key: 'editorialScore',
|
||||
width: 100,
|
||||
render: (score: number) => score ? `${score}分` : '-',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 180,
|
||||
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
render: (_: any, record: ReviewTask) => (
|
||||
<Space>
|
||||
{record.status === 'completed' && (
|
||||
<Tooltip title="查看报告">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/rvw/report/${record.id}`)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{record.status === 'pending' && (
|
||||
<Tooltip title="开始审稿">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<PlayCircleOutlined />}
|
||||
loading={runningTaskId === record.id}
|
||||
onClick={() => handleRun(record.id, record.selectedAgents || ['editorial', 'methodology'])}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确定删除此任务?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<Title level={3} style={{ margin: 0 }}>📋 智能期刊审稿系统</Title>
|
||||
<Text type="secondary">上传稿件,AI智能评估稿约符合度和方法学质量</Text>
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadTasks}>刷新</Button>
|
||||
<Upload {...uploadProps}>
|
||||
<Button type="primary" icon={<UploadOutlined />}>上传稿件</Button>
|
||||
</Upload>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tasks}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<AgentSelectModal
|
||||
visible={showAgentModal}
|
||||
onCancel={() => {
|
||||
setShowAgentModal(false)
|
||||
setPendingFile(null)
|
||||
}}
|
||||
onConfirm={handleUpload}
|
||||
loading={uploading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 报告详情页面
|
||||
const ReportDetailPage: React.FC = () => {
|
||||
const { taskId } = useParams<{ taskId: string }>()
|
||||
const navigate = useNavigate()
|
||||
const [task, setTask] = useState<ReviewTask | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const loadReport = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/tasks/${taskId}/report`)
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
setTask(data.data)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载报告失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
if (taskId) loadReport()
|
||||
}, [taskId])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-96">
|
||||
<Spin size="large" tip="加载报告中..." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
<Text type="secondary">报告不存在</Text>
|
||||
<br />
|
||||
<Button onClick={() => navigate('/rvw')}>返回列表</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<Button onClick={() => navigate('/rvw')} className="mb-2">← 返回列表</Button>
|
||||
<Title level={3} style={{ margin: 0 }}>{task.fileName}</Title>
|
||||
</div>
|
||||
<Button icon={<DownloadOutlined />}>导出报告</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultActiveKey="editorial">
|
||||
{task.editorialReview && (
|
||||
<TabPane tab="📝 稿约评审" key="editorial">
|
||||
<Card>
|
||||
<div className="mb-4">
|
||||
<Title level={4}>总体评分:{task.editorialScore || 0}分</Title>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Title level={5}>总体评价</Title>
|
||||
<Paragraph>{task.editorialReview.overallAssessment}</Paragraph>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Title level={5}>详细评审结果</Title>
|
||||
{task.editorialReview.criteria?.map((item: any, index: number) => (
|
||||
<Card key={index} size="small" className="mb-2">
|
||||
<div className="flex justify-between">
|
||||
<Text strong>{item.name}</Text>
|
||||
<Tag color={item.passed ? 'success' : 'error'}>
|
||||
{item.passed ? '通过' : '不通过'}
|
||||
</Tag>
|
||||
</div>
|
||||
<Text type="secondary">{item.comment}</Text>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<Title level={5}>修改建议</Title>
|
||||
<ul>
|
||||
{task.editorialReview.suggestions?.map((s: string, i: number) => (
|
||||
<li key={i}>{s}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</TabPane>
|
||||
)}
|
||||
|
||||
{task.methodologyReview && (
|
||||
<TabPane tab="🔬 方法学评审" key="methodology">
|
||||
<Card>
|
||||
<div className="mb-4">
|
||||
<Title level={4}>
|
||||
评审结论:
|
||||
<Tag color={task.methodologyReview.overallConclusion === 'acceptable' ? 'success' : 'warning'}>
|
||||
{task.methodologyReview.overallConclusion === 'acceptable' ? '可接受' :
|
||||
task.methodologyReview.overallConclusion === 'needs_revision' ? '需修改' : '不可接受'}
|
||||
</Tag>
|
||||
</Title>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Title level={5}>总体评价</Title>
|
||||
<Paragraph>{task.methodologyReview.overallAssessment}</Paragraph>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Title level={5}>检查点结果</Title>
|
||||
{task.methodologyReview.checkpoints?.map((item: any, index: number) => (
|
||||
<Card key={index} size="small" className="mb-2">
|
||||
<div className="flex justify-between">
|
||||
<Text strong>{item.name}</Text>
|
||||
<Tag color={
|
||||
item.status === 'pass' ? 'success' :
|
||||
item.status === 'fail' ? 'error' : 'warning'
|
||||
}>
|
||||
{item.status === 'pass' ? '通过' :
|
||||
item.status === 'fail' ? '不通过' : '部分通过'}
|
||||
</Tag>
|
||||
</div>
|
||||
<Text type="secondary">{item.comment}</Text>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<Title level={5}>改进建议</Title>
|
||||
<ul>
|
||||
{task.methodologyReview.recommendations?.map((r: string, i: number) => (
|
||||
<li key={i}>{r}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</TabPane>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 模块主入口
|
||||
const RVWModule: React.FC = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route index element={<TaskListPage />} />
|
||||
<Route path="report/:taskId" element={<ReportDetailPage />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export default RVWModule
|
||||
|
||||
@@ -51,5 +51,6 @@ export { default as Placeholder } from './Placeholder';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1
frontend-v2/src/vite-env.d.ts
vendored
1
frontend-v2/src/vite-env.d.ts
vendored
@@ -31,5 +31,6 @@ interface ImportMeta {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user