Files
AIclinicalresearch/frontend-v2/src/modules/dc/pages/tool-c/components/MetricTimePanel.tsx
HaHafeng 4ed67a8846 fix(admin): Fix Prompt management list not showing version info and add debug diagnostics
Summary:
- Fix Prompt list API response schema missing activeVersion and draftVersion fields
- Fastify was filtering out undefined schema fields, causing version columns to show empty
- Add detailed diagnostic logging for Prompt debug mode troubleshooting
- Verify debug mode works correctly (DRAFT version is used when debug enabled)

Changes:
- backend/src/common/prompt/prompt.routes.ts: Add activeVersion and draftVersion to response schema
- backend/src/common/prompt/prompt.service.ts: Add diagnostic logs for setDebugMode and get methods
- PKB module: Various authentication and document handling fixes from previous session

Tested: Debug mode verified working - v2 DRAFT version correctly loaded when debug enabled
2026-01-13 22:22:10 +08:00

436 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 指标-时间表转换面板
*
* 将多个时间点列转换为"指标行+时间点列"格式
* 典型场景制作临床研究Table 1横向对比同一指标的时间变化
*/
import React, { useState, useEffect } from 'react';
import { Button, Alert, Checkbox, App, Input, Spin, Tag } from 'antd';
import { ArrowLeftRight, Info, Sparkles } from 'lucide-react';
interface Props {
columns: Array<{ id: string; name: string }>;
sessionId: string | null;
onApply: (newData: any[]) => void;
onClose: () => void;
}
interface DetectedPattern {
common_prefix: string;
separator: string;
timepoints: string[];
confidence: number;
message: string;
}
const MetricTimePanel: React.FC<Props> = ({
columns,
sessionId,
onApply,
onClose,
}) => {
const { message } = App.useApp();
const [idVars, setIdVars] = useState<string[]>([]);
const [valueVars, setValueVars] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [detecting, setDetecting] = useState(false);
// 检测结果
const [pattern, setPattern] = useState<DetectedPattern | null>(null);
const [metricName, setMetricName] = useState('');
const [separator, setSeparator] = useState('');
const [timepointColName, setTimepointColName] = useState('时间点');
// 当值列变化时,自动检测模式
useEffect(() => {
if (valueVars.length >= 2) {
detectPattern();
} else {
setPattern(null);
setMetricName('');
setSeparator('');
}
}, [valueVars]);
// 自动检测模式
const detectPattern = async () => {
setDetecting(true);
try {
const response = await fetch('/api/v1/dc/tool-c/metric-time/detect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId,
valueVars,
}),
});
const result = await response.json();
if (result.success && result.pattern) {
setPattern(result.pattern);
setMetricName(result.pattern.common_prefix || '');
setSeparator(result.pattern.separator || '');
if (result.pattern.confidence >= 0.8) {
message.success(`自动检测成功!置信度: ${(result.pattern.confidence * 100).toFixed(0)}%`);
} else if (result.pattern.confidence >= 0.5) {
message.warning(`检测成功但置信度较低 (${(result.pattern.confidence * 100).toFixed(0)}%),建议检查结果`);
} else {
message.warning('检测置信度较低,建议手动调整参数');
}
} else {
message.error(result.error || '模式检测失败');
}
} catch (error: any) {
message.error('检测失败:' + error.message);
} finally {
setDetecting(false);
}
};
// 执行转换
const handleApply = async () => {
if (!sessionId) {
message.error('Session ID不存在');
return;
}
// 验证
if (idVars.length === 0) {
message.warning('请至少选择1个ID列');
return;
}
if (valueVars.length < 2) {
message.warning('请至少选择2个值列');
return;
}
// 验证ID列和值列不能重复
const overlap = idVars.filter(id => valueVars.includes(id));
if (overlap.length > 0) {
message.error(`ID列和值列不能重复${overlap.join(', ')}`);
return;
}
if (!metricName.trim()) {
message.warning('请输入指标名称');
return;
}
if (!timepointColName.trim()) {
message.warning('请输入时间点列名');
return;
}
setLoading(true);
try {
const response = await fetch('/api/v1/dc/tool-c/quick-action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId,
action: 'metric_time',
params: {
idVars,
valueVars,
metricName,
separator: separator || undefined,
timepointColName,
},
}),
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || '指标-时间表转换失败');
}
message.success('指标-时间表转换成功!');
// 更新父组件数据
if (result.data?.newDataPreview) {
onApply(result.data.newDataPreview);
}
// 成功后关闭
onClose();
} catch (error: any) {
message.error(error.message || '执行失败');
} finally {
setLoading(false);
}
};
// 获取时间点预览从pattern中
const timepoints = pattern?.timepoints || [];
const timepointsSample = timepoints.slice(0, 5);
const hasMoreTimepoints = timepoints.length > 5;
return (
<div className="space-y-4 pb-4">
{/* 说明 */}
<Alert
description={
<div className="text-xs space-y-1">
<div> "指标行+时间点列"</div>
<div> Table 1</div>
<div> FMA___基线FMA___2周 FMA+ 线 + 2</div>
</div>
}
type="info"
showIcon
icon={<Info size={16} />}
className="mb-4"
/>
{/* 第1步ID列 */}
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">
1ID列<span className="text-red-500">*</span>
</label>
<div className="border rounded-lg p-3 max-h-40 overflow-y-auto bg-slate-50">
<Checkbox.Group
value={idVars}
onChange={setIdVars}
className="flex flex-col gap-2"
>
{columns.map((col) => (
<Checkbox key={col.id} value={col.id} disabled={valueVars.includes(col.id)}>
{col.name}
</Checkbox>
))}
</Checkbox.Group>
</div>
<div className="text-xs text-slate-500 mt-1">
Record_ID
</div>
</div>
{/* 第2步值列 */}
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">
2<span className="text-red-500">*</span>
</label>
<div className="border rounded-lg p-3 max-h-40 overflow-y-auto bg-slate-50">
<Checkbox.Group
value={valueVars}
onChange={setValueVars}
className="flex flex-col gap-2"
>
{columns.map((col) => (
<Checkbox key={col.id} value={col.id} disabled={idVars.includes(col.id)}>
{col.name}
</Checkbox>
))}
</Checkbox.Group>
</div>
<div className="text-xs text-slate-500 mt-1">
2 {valueVars.length}
</div>
</div>
{/* 第3步自动检测结果 */}
{valueVars.length >= 2 && (
<div className="border-t pt-4">
<div className="flex items-center gap-2 mb-3">
<Sparkles size={16} className="text-purple-500" />
<span className="text-sm font-medium text-slate-700">3</span>
{detecting && <Spin size="small" />}
</div>
{pattern ? (
<div className="space-y-3">
{/* 检测结果 */}
<div className="p-3 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-start justify-between mb-2">
<div className="text-sm font-medium text-green-800">
</div>
<Tag color={pattern.confidence >= 0.8 ? 'green' : pattern.confidence >= 0.5 ? 'orange' : 'red'}>
: {(pattern.confidence * 100).toFixed(0)}%
</Tag>
</div>
<div className="text-xs text-green-700 space-y-1">
<div> {timepoints.length} </div>
<div> {pattern.message}</div>
</div>
</div>
{/* 指标名称(可编辑) */}
<div>
<label className="text-sm font-medium text-slate-700 mb-1 block">
<span className="text-red-500">*</span>
</label>
<Input
placeholder="如FMA总得分"
value={metricName}
onChange={(e) => setMetricName(e.target.value)}
maxLength={100}
suffix={
<span className="text-xs text-slate-400">🖊 </span>
}
/>
<div className="text-xs text-slate-500 mt-1">
</div>
</div>
{/* 分隔符(可编辑) */}
<div>
<label className="text-sm font-medium text-slate-700 mb-1 block">
</label>
<Input
placeholder="如___"
value={separator}
onChange={(e) => setSeparator(e.target.value)}
maxLength={10}
suffix={
<span className="text-xs text-slate-400">
{separator ? `"${separator}"` : '无'}
</span>
}
/>
<div className="text-xs text-slate-500 mt-1">
{separator ? `"${separator}"` : '未检测到'}
</div>
</div>
{/* 时间点列名 */}
<div>
<label className="text-sm font-medium text-slate-700 mb-1 block">
<span className="text-red-500">*</span>
</label>
<Input
placeholder="如:时间点"
value={timepointColName}
onChange={(e) => setTimepointColName(e.target.value)}
maxLength={30}
/>
<div className="text-xs text-slate-500 mt-1">
</div>
</div>
{/* 时间点预览 */}
<div className="p-3 bg-slate-50 rounded-lg border border-slate-200">
<div className="text-xs font-medium text-slate-700 mb-2">
{timepoints.length}
</div>
<div className="flex flex-wrap gap-1">
{timepointsSample.map((tp, idx) => (
<Tag key={idx} color="blue">
{tp}
</Tag>
))}
{hasMoreTimepoints && (
<Tag color="default">
... {timepoints.length - 5}
</Tag>
)}
</div>
</div>
</div>
) : (
<div className="p-3 bg-slate-50 rounded-lg text-center text-sm text-slate-500">
...
</div>
)}
</div>
)}
{/* 转换结果预览 */}
{pattern && valueVars.length >= 2 && (
<Alert
description={
<div className="text-xs space-y-1">
<div className="font-medium mb-1"></div>
<div className="pl-2 space-y-0.5">
<div> "{timepointColName}"{metricName}</div>
<div> {valueVars.length} {timepoints.length} </div>
<div> {timepointsSample.join(', ')}{hasMoreTimepoints ? '...' : ''}</div>
</div>
</div>
}
type="success"
showIcon={false}
className="bg-green-50 border-green-200"
/>
)}
{/* 警告 */}
<Alert
description={
<div className="text-xs space-y-1">
<div> </div>
<div> </div>
</div>
}
type="warning"
showIcon={false}
className="bg-orange-50 border-orange-200"
/>
{/* 底部按钮 */}
<div className="flex justify-end gap-2 pt-4 border-t">
<Button onClick={onClose}>
</Button>
<Button
type="primary"
onClick={handleApply}
loading={loading}
disabled={!pattern || valueVars.length < 2 || idVars.length === 0}
icon={<ArrowLeftRight size={16} />}
>
</Button>
</div>
</div>
);
};
export default MetricTimePanel;