Files
AIclinicalresearch/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx
HaHafeng 5523ef36ea feat(admin): Complete Phase 3.5.1-3.5.4 Prompt Management System (83%)
Summary:
- Implement Prompt management infrastructure and core services
- Build admin portal frontend with light theme
- Integrate CodeMirror 6 editor for non-technical users

Phase 3.5.1: Infrastructure Setup
- Create capability_schema for Prompt storage
- Add prompt_templates and prompt_versions tables
- Add prompt:view/edit/debug/publish permissions
- Migrate RVW prompts to database (RVW_EDITORIAL, RVW_METHODOLOGY)

Phase 3.5.2: PromptService Core
- Implement gray preview logic (DRAFT for debuggers, ACTIVE for users)
- Module-level debug control (setDebugMode)
- Handlebars template rendering
- Variable extraction and validation (extractVariables, validateVariables)
- Three-level disaster recovery (database -> cache -> hardcoded fallback)

Phase 3.5.3: Management API
- 8 RESTful endpoints (/api/admin/prompts/*)
- Permission control (PROMPT_ENGINEER can edit, SUPER_ADMIN can publish)

Phase 3.5.4: Frontend Management UI
- Build admin portal architecture (AdminLayout, OrgLayout)
- Add route system (/admin/*, /org/*)
- Implement PromptListPage (filter, search, debug switch)
- Implement PromptEditor (CodeMirror 6 simplified for clinical users)
- Implement PromptEditorPage (edit, save, publish, test, version history)

Technical Details:
- Backend: 6 files, ~2044 lines (prompt.service.ts 596 lines)
- Frontend: 9 files, ~1735 lines (PromptEditorPage.tsx 399 lines)
- CodeMirror 6: Line numbers, auto-wrap, variable highlight, search, undo/redo
- Chinese-friendly: 15px font, 1.8 line-height, system fonts

Next Step: Phase 3.5.5 - Integrate RVW module with PromptService

Tested: Backend API tests passed (8/8), Frontend pending user testing
Status: Ready for Phase 3.5.5 RVW integration
2026-01-11 21:25:16 +08:00

346 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
import React, { useState, useEffect } from 'react';
import { Modal, Radio, Button, Slider, Alert, App, Statistic, Row, Col, Checkbox } from 'antd';
import { Trash2, AlertTriangle } from 'lucide-react';
interface Props {
visible: boolean;
onClose: () => void;
onApply: (newData: any[]) => void;
columns: Array<{ id: string; name: string }>;
data: any[];
sessionId: string | null;
}
const DropnaDialog: React.FC<Props> = ({
visible,
onClose,
onApply,
columns,
data,
sessionId,
}) => {
const { message } = App.useApp();
const [method, setMethod] = useState<'row' | 'column' | 'both'>('row');
const [threshold, setThreshold] = useState<number>(50); // 百分比
const [selectedColumns, setSelectedColumns] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
// 计算缺失值统计
const [missingStats, setMissingStats] = useState<{
totalMissing: number;
rowsWithMissing: number;
colsWithMissing: number;
}>({ totalMissing: 0, rowsWithMissing: 0, colsWithMissing: 0 });
useEffect(() => {
if (visible && data.length > 0) {
// 计算缺失值
let totalMissing = 0;
let rowsWithMissing = 0;
const colsMissing: Record<string, number> = {};
data.forEach(row => {
let rowHasMissing = false;
Object.keys(row).forEach(key => {
const value = row[key];
if (value === null || value === undefined || value === '') {
totalMissing++;
colsMissing[key] = (colsMissing[key] || 0) + 1;
rowHasMissing = true;
}
});
if (rowHasMissing) {
rowsWithMissing++;
}
});
setMissingStats({
totalMissing,
rowsWithMissing,
colsWithMissing: Object.keys(colsMissing).length,
});
}
}, [visible, data]);
// 执行
const handleApply = async () => {
if (!sessionId) {
message.error('Session ID不存在');
return;
}
// 验证
if (method === 'row' && selectedColumns.length === 0) {
// 删除所有行:不需要指定列
}
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: 'dropna',
params: {
method,
threshold: threshold / 100, // 转换为0-1
subset: selectedColumns.length > 0 ? selectedColumns : undefined,
},
}),
});
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);
}
};
return (
<Modal
title={
<div className="flex items-center gap-2">
<Trash2 size={20} className="text-red-500" />
<span></span>
</div>
}
open={visible}
onCancel={onClose}
width={700}
footer={[
<Button key="cancel" onClick={onClose}>
</Button>,
<Button
key="apply"
type="primary"
danger
onClick={handleApply}
loading={loading}
icon={<Trash2 size={16} />}
>
</Button>,
]}
>
<div className="space-y-4">
{/* 缺失值统计 */}
<Alert
message="当前数据缺失值统计"
description={
<Row gutter={16} className="mt-3">
<Col span={8}>
<Statistic
title="总缺失值"
value={missingStats.totalMissing}
suffix={`/ ${data.length * columns.length} 单元格`}
/>
</Col>
<Col span={8}>
<Statistic
title="含缺失值的行"
value={missingStats.rowsWithMissing}
suffix={`/ ${data.length}`}
/>
</Col>
<Col span={8}>
<Statistic
title="含缺失值的列"
value={missingStats.colsWithMissing}
suffix={`/ ${columns.length}`}
/>
</Col>
</Row>
}
type="info"
showIcon
className="mb-4"
/>
{/* 删除方式 */}
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">
</label>
<Radio.Group
value={method}
onChange={(e) => setMethod(e.target.value)}
className="w-full"
>
<div className="space-y-3">
<div className="border rounded-lg p-3 hover:border-blue-400 transition-colors">
<Radio value="row">
<div className="ml-2">
<div className="font-medium"></div>
<div className="text-xs text-slate-500 mt-1">
</div>
</div>
</Radio>
</div>
<div className="border rounded-lg p-3 hover:border-blue-400 transition-colors">
<Radio value="column">
<div className="ml-2">
<div className="font-medium"></div>
<div className="text-xs text-slate-500 mt-1">
</div>
</div>
</Radio>
</div>
<div className="border rounded-lg p-3 hover:border-blue-400 transition-colors">
<Radio value="both">
<div className="ml-2">
<div className="font-medium"></div>
<div className="text-xs text-slate-500 mt-1">
</div>
</div>
</Radio>
</div>
</div>
</Radio.Group>
</div>
{/* 缺失率阈值(仅对按列删除和先删列再删行有效) */}
{(method === 'column' || method === 'both') && (
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">
{threshold}%
</label>
<Slider
min={0}
max={100}
value={threshold}
onChange={setThreshold}
marks={{
0: '0%',
30: '30%',
50: '50%',
70: '70%',
100: '100%',
}}
/>
<div className="text-xs text-slate-500 mt-2">
{threshold}%
</div>
</div>
)}
{/* 仅检查指定列(仅对按行删除有效) */}
{method === 'row' && (
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">
</label>
<div className="border rounded-lg p-3 max-h-48 overflow-y-auto">
<Checkbox.Group
value={selectedColumns}
onChange={setSelectedColumns}
className="flex flex-col gap-2"
>
{columns.map((col) => (
<Checkbox key={col.id} value={col.id}>
{col.name}
</Checkbox>
))}
</Checkbox.Group>
</div>
<div className="text-xs text-slate-500 mt-2">
</div>
</div>
)}
{/* 警告提示 */}
<Alert
message={
<div className="flex items-center gap-2">
<AlertTriangle size={16} />
<span className="font-semibold"></span>
</div>
}
description={
<div className="text-xs space-y-1 mt-1">
<div> </div>
<div> 使AI对话功能探索数据</div>
<div> </div>
<div> 使"多重插补"</div>
</div>
}
type="warning"
showIcon={false}
className="bg-orange-50 border-orange-200"
/>
</div>
</Modal>
);
};
export default DropnaDialog;