feat(iit): V3.2 data consistency + project isolation + admin config redesign + Chinese labels
Summary: - Refactor timeline API to read from qc_field_status (SSOT) instead of qc_logs - Add field-issues paginated API with severity/dimension/recordId filters - Add LEFT JOIN field_metadata + qc_event_status for Chinese display names - Implement per-project ChatOrchestrator cache and SessionMemory isolation - Redesign admin IIT config tabs (REDCap -> Fields -> KB -> Rules -> Members) - Add AI-powered QC rule generation (D3 programmatic + D1/D5/D6 LLM-based) - Add clickable warning/critical detail Modal in ReportsPage - Auto-dispatch eQuery after batch QC via DailyQcOrchestrator - Update module status documentation to v3.2 Backend changes: - iitQcCockpitController: rewrite getTimeline from qc_field_status, add getFieldIssues - iitQcCockpitRoutes: add field-issues route - ChatOrchestrator: per-projectId cached instances - SessionMemory: keyed by userId::projectId - WechatCallbackController: resolve projectId from iitUserMapping - iitRuleSuggestionService: dimension-based suggest + generateD3Rules - iitBatchController: call DailyQcOrchestrator after batch QC Frontend changes: - AiStreamPage: adapt to new timeline structure with dimension tags - ReportsPage: clickable stats cards with issue detail Modal - IitProjectDetailPage: reorder tabs, add AI rule generation UI - iitProjectApi: add TimelineIssue, FieldIssueItem types and APIs Status: TypeScript compilation verified, no new lint errors Made-with: Cursor
This commit is contained in:
@@ -8,9 +8,7 @@
|
||||
import { Layout, Menu } from 'antd';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
FileTextOutlined,
|
||||
SearchOutlined,
|
||||
FolderOpenOutlined,
|
||||
FilterOutlined,
|
||||
FileSearchOutlined,
|
||||
DatabaseOutlined,
|
||||
@@ -32,29 +30,15 @@ const ASLLayout = () => {
|
||||
|
||||
// 菜单项配置
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
key: 'research-plan',
|
||||
icon: <FileTextOutlined />,
|
||||
label: '1. 研究方案生成',
|
||||
disabled: true,
|
||||
title: '敬请期待'
|
||||
},
|
||||
{
|
||||
key: '/literature/research/deep',
|
||||
icon: <SearchOutlined />,
|
||||
label: '2. 智能文献检索',
|
||||
},
|
||||
{
|
||||
key: 'literature-management',
|
||||
icon: <FolderOpenOutlined />,
|
||||
label: '3. 文献管理',
|
||||
disabled: true,
|
||||
title: '敬请期待'
|
||||
label: '1. 智能文献检索',
|
||||
},
|
||||
{
|
||||
key: 'title-screening',
|
||||
icon: <FilterOutlined />,
|
||||
label: '4. 标题摘要初筛',
|
||||
label: '2. 标题摘要初筛',
|
||||
children: [
|
||||
{
|
||||
key: '/literature/screening/title/settings',
|
||||
@@ -76,7 +60,7 @@ const ASLLayout = () => {
|
||||
{
|
||||
key: 'fulltext-screening',
|
||||
icon: <FileSearchOutlined />,
|
||||
label: '5. 全文复筛',
|
||||
label: '3. 全文复筛',
|
||||
children: [
|
||||
{
|
||||
key: '/literature/screening/fulltext/settings',
|
||||
@@ -98,7 +82,7 @@ const ASLLayout = () => {
|
||||
{
|
||||
key: 'extraction',
|
||||
icon: <DatabaseOutlined />,
|
||||
label: '6. 全文智能提取',
|
||||
label: '4. 全文智能提取',
|
||||
children: [
|
||||
{
|
||||
key: '/literature/extraction/setup',
|
||||
@@ -115,12 +99,12 @@ const ASLLayout = () => {
|
||||
{
|
||||
key: '/literature/charting',
|
||||
icon: <ApartmentOutlined />,
|
||||
label: '7. SR 图表生成器',
|
||||
label: '5. SR 图表生成器',
|
||||
},
|
||||
{
|
||||
key: '/literature/meta-analysis',
|
||||
icon: <BarChartOutlined />,
|
||||
label: '8. Meta 分析引擎',
|
||||
label: '6. Meta 分析引擎',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -6,16 +6,13 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, Checkbox, Button, Input, Select, Spin, Divider, Typography, Tag } from 'antd';
|
||||
import { Card, Button, Input, Typography, Tag } from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
ThunderboltOutlined,
|
||||
GlobalOutlined,
|
||||
EditOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { aslApi } from '../../api';
|
||||
import type { DataSourceConfig } from '../../types/deepResearch';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -43,24 +40,11 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
|
||||
initialQuery, onSubmit, onBack, loading, collapsed, onExpand,
|
||||
}) => {
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [dataSources, setDataSources] = useState<DataSourceConfig[]>([]);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [yearRange, setYearRange] = useState<string>('近5年');
|
||||
const [targetCount, setTargetCount] = useState<string>('全面检索');
|
||||
const [loadingSources, setLoadingSources] = useState(true);
|
||||
const [loadingTextIdx, setLoadingTextIdx] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
aslApi.getDeepResearchDataSources().then(res => {
|
||||
const sources = res.data || [];
|
||||
setDataSources(sources);
|
||||
setSelectedIds(sources.filter((s: DataSourceConfig) => s.defaultChecked).map((s: DataSourceConfig) => s.id));
|
||||
}).catch(() => {
|
||||
setDataSources([]);
|
||||
}).finally(() => {
|
||||
setLoadingSources(false);
|
||||
});
|
||||
}, []);
|
||||
// 默认使用 PubMed + 近5年 + 全面检索(数据源/年限/篇数 UI 暂时隐藏)
|
||||
const yearRange = '近5年';
|
||||
const targetCount = '全面检索';
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) { setLoadingTextIdx(0); return; }
|
||||
@@ -70,21 +54,10 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
|
||||
return () => clearInterval(timer);
|
||||
}, [loading]);
|
||||
|
||||
const handleToggle = (id: string) => {
|
||||
setSelectedIds(prev =>
|
||||
prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const domains = dataSources
|
||||
.filter(s => selectedIds.includes(s.id))
|
||||
.map(s => s.domainScope);
|
||||
onSubmit(query, domains, { yearRange, targetCount });
|
||||
onSubmit(query, ['PubMed'], { yearRange, targetCount });
|
||||
};
|
||||
|
||||
const selectedNames = dataSources.filter(s => selectedIds.includes(s.id)).map(s => s.label);
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
<Card size="small" className="!bg-white">
|
||||
@@ -94,9 +67,7 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
|
||||
<div className="min-w-0">
|
||||
<Text strong className="block truncate">{query}</Text>
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
{selectedNames.map(name => (
|
||||
<Tag key={name} className="!text-xs !m-0">{name}</Tag>
|
||||
))}
|
||||
<Tag className="!text-xs !m-0">PubMed</Tag>
|
||||
<Text type="secondary" className="text-xs">{yearRange} · {targetCount}</Text>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,9 +82,6 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const englishSources = dataSources.filter(s => s.category === 'english');
|
||||
const chineseSources = dataSources.filter(s => s.category === 'chinese');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
@@ -133,77 +101,7 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-4" size="small" title={<><GlobalOutlined className="mr-2" />选择数据源</>}>
|
||||
{loadingSources ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<>
|
||||
<Text type="secondary" className="block mb-3 text-xs">英文数据库</Text>
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
{englishSources.map(ds => (
|
||||
<Checkbox
|
||||
key={ds.id}
|
||||
checked={selectedIds.includes(ds.id)}
|
||||
onChange={() => handleToggle(ds.id)}
|
||||
>
|
||||
<span className="font-medium">{ds.label}</span>
|
||||
<span className="text-gray-400 text-xs ml-2">{ds.domainScope}</span>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Divider className="!my-3" />
|
||||
|
||||
<Text type="secondary" className="block mb-3 text-xs">中文数据库</Text>
|
||||
<div className="flex flex-col gap-2">
|
||||
{chineseSources.map(ds => (
|
||||
<Checkbox
|
||||
key={ds.id}
|
||||
checked={selectedIds.includes(ds.id)}
|
||||
onChange={() => handleToggle(ds.id)}
|
||||
>
|
||||
<span className="font-medium">{ds.label}</span>
|
||||
<span className="text-gray-400 text-xs ml-2">{ds.domainScope}</span>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card className="mb-6" size="small" title="高级筛选">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<Text type="secondary" className="block mb-1 text-xs">时间范围</Text>
|
||||
<Select
|
||||
value={yearRange}
|
||||
onChange={setYearRange}
|
||||
className="w-full"
|
||||
options={[
|
||||
{ value: '不限', label: '不限' },
|
||||
{ value: '近1年', label: '近1年' },
|
||||
{ value: '近3年', label: '近3年' },
|
||||
{ value: '近5年', label: '近5年' },
|
||||
{ value: '近10年', label: '近10年' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Text type="secondary" className="block mb-1 text-xs">目标数量</Text>
|
||||
<Select
|
||||
value={targetCount}
|
||||
onChange={setTargetCount}
|
||||
className="w-full"
|
||||
options={[
|
||||
{ value: '全面检索', label: '全面检索' },
|
||||
{ value: '约20篇', label: '约20篇' },
|
||||
{ value: '约50篇', label: '约50篇' },
|
||||
{ value: '约100篇', label: '约100篇' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* 数据源/年限/篇数暂时隐藏,默认 PubMed + 近5年 + 全面检索 */}
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -212,7 +110,7 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
|
||||
block
|
||||
onClick={handleSubmit}
|
||||
loading={loading}
|
||||
disabled={!query.trim() || selectedIds.length === 0}
|
||||
disabled={!query.trim()}
|
||||
>
|
||||
{loading ? LOADING_TEXTS[loadingTextIdx] : '生成检索需求书'}
|
||||
</Button>
|
||||
|
||||
@@ -47,7 +47,7 @@ const ASLModule = () => {
|
||||
>
|
||||
<Routes>
|
||||
<Route path="" element={<ASLLayout />}>
|
||||
<Route index element={<Navigate to="screening/title/settings" replace />} />
|
||||
<Route index element={<Navigate to="research/deep" replace />} />
|
||||
|
||||
{/* 智能文献检索 V1.x(保留兼容) */}
|
||||
<Route path="research/search" element={<ResearchSearch />} />
|
||||
|
||||
Reference in New Issue
Block a user