You've already forked DataMate
feat(SynthDataDetail): add chunk/synthesis data management with edit/delete & UI enhancements (#139)
* feat(synthesis): add evaluation task creation functionality and UI enhancements * feat(synthesis): implement synthesis data management features including loading, editing, and deleting * feat(synthesis): add endpoints for deleting and updating synthesis data and chunks * fix: Correctly extract file values from selectedFilesMap in AddDataDialog
This commit is contained in:
22
frontend/package-lock.json
generated
22
frontend/package-lock.json
generated
@@ -188,7 +188,6 @@
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -2137,7 +2136,6 @@
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
@@ -2155,7 +2153,6 @@
|
||||
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -2223,7 +2220,6 @@
|
||||
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
@@ -2522,7 +2518,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2771,7 +2766,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001733",
|
||||
"electron-to-chromium": "^1.5.199",
|
||||
@@ -3168,7 +3162,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -3257,8 +3250,7 @@
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
@@ -3485,7 +3477,6 @@
|
||||
"integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -4193,7 +4184,6 @@
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz",
|
||||
"integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
@@ -6025,7 +6015,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -6038,7 +6027,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -6058,7 +6046,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -6189,8 +6176,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -6774,7 +6760,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -6878,7 +6863,6 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7046,7 +7030,6 @@
|
||||
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.6",
|
||||
@@ -7137,7 +7120,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router";
|
||||
import { Badge, Button, Empty, List, Pagination, Spin, Typography } from "antd";
|
||||
import { Badge, Button, Empty, List, Pagination, Spin, Typography, Popconfirm, message, Dropdown, Input } from "antd";
|
||||
import type { PaginationProps } from "antd";
|
||||
import { queryChunksByFileUsingGet, querySynthesisDataByChunkUsingGet, querySynthesisTaskByIdUsingGet } from "@/pages/SynthesisTask/synthesis-api";
|
||||
import { MoreOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
queryChunksByFileUsingGet,
|
||||
querySynthesisDataByChunkUsingGet,
|
||||
querySynthesisTaskByIdUsingGet,
|
||||
deleteChunkWithDataUsingDelete,
|
||||
batchDeleteSynthesisDataUsingDelete,
|
||||
updateSynthesisDataUsingPatch,
|
||||
} from "@/pages/SynthesisTask/synthesis-api";
|
||||
import { formatDateTime } from "@/utils/unit";
|
||||
|
||||
interface LocationState {
|
||||
@@ -107,6 +115,24 @@ export default function SynthDataDetail() {
|
||||
fetchChunks(page, pageSize || 10);
|
||||
};
|
||||
|
||||
// 删除当前选中的 Chunk 及其合成数据
|
||||
const handleDeleteCurrentChunk = async () => {
|
||||
if (!selectedChunkId) return;
|
||||
try {
|
||||
const res = await deleteChunkWithDataUsingDelete(selectedChunkId);
|
||||
if (res?.data?.code === 200 || res?.code === 200) {
|
||||
message.success("删除成功");
|
||||
} else {
|
||||
message.success("删除成功");
|
||||
}
|
||||
setSelectedChunkId(null);
|
||||
fetchChunks(1, chunkPagination.size);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete chunk", error);
|
||||
message.error("删除失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 加载选中 chunk 的所有合成数据
|
||||
const fetchSynthData = async (chunkId: string) => {
|
||||
setDataLoading(true);
|
||||
@@ -137,6 +163,80 @@ export default function SynthDataDetail() {
|
||||
return Object.entries(data || {});
|
||||
};
|
||||
|
||||
// 单条合成数据删除
|
||||
const handleDeleteSingleSynthesisData = async (dataId: string) => {
|
||||
try {
|
||||
await batchDeleteSynthesisDataUsingDelete({ ids: [dataId] });
|
||||
message.success("删除成功");
|
||||
if (selectedChunkId) {
|
||||
fetchSynthData(selectedChunkId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete synthesis data", error);
|
||||
message.error("删除失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑状态:仅编辑各个 key 的 value
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editingMap, setEditingMap] = useState<Record<string, string>>({});
|
||||
|
||||
const startEdit = (item: SynthesisDataItem) => {
|
||||
setEditingId(item.id);
|
||||
const map: Record<string, string> = {};
|
||||
Object.entries(item.data || {}).forEach(([k, v]) => {
|
||||
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
||||
map[k] = String(v);
|
||||
} else {
|
||||
map[k] = JSON.stringify(v);
|
||||
}
|
||||
});
|
||||
setEditingMap(map);
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null);
|
||||
setEditingMap({});
|
||||
};
|
||||
|
||||
const handleSaveEdit = async (item: SynthesisDataItem) => {
|
||||
if (editingId !== item.id) return;
|
||||
try {
|
||||
const newData: Record<string, unknown> = { ...item.data };
|
||||
Object.entries(editingMap).forEach(([k, v]) => {
|
||||
const original = item.data?.[k];
|
||||
if (typeof original === "object" && original !== null) {
|
||||
try {
|
||||
newData[k] = JSON.parse(v);
|
||||
} catch {
|
||||
newData[k] = v;
|
||||
}
|
||||
} else if (typeof original === "number") {
|
||||
const n = Number(v);
|
||||
newData[k] = Number.isNaN(n) ? v : n;
|
||||
} else if (typeof original === "boolean") {
|
||||
if (v === "true" || v === "false") {
|
||||
newData[k] = v === "true";
|
||||
} else {
|
||||
newData[k] = v;
|
||||
}
|
||||
} else {
|
||||
newData[k] = v;
|
||||
}
|
||||
});
|
||||
|
||||
await updateSynthesisDataUsingPatch(item.id, { data: newData });
|
||||
message.success("保存成功");
|
||||
cancelEdit();
|
||||
if (selectedChunkId) {
|
||||
fetchSynthData(selectedChunkId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update synthesis data", error);
|
||||
message.error("保存失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white rounded-lg h-full flex flex-col overflow-hidden">
|
||||
{/* 顶部信息和返回 */}
|
||||
@@ -198,23 +298,63 @@ export default function SynthDataDetail() {
|
||||
return (
|
||||
<List.Item
|
||||
className={
|
||||
"cursor-pointer px-3 py-2 !border-0 " +
|
||||
"px-3 py-2 !border-0 " +
|
||||
(active ? "bg-blue-50" : "hover:bg-gray-50")
|
||||
}
|
||||
onClick={() => setSelectedChunkId(item.id)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<div
|
||||
className="flex items-center justify-between text-xs cursor-pointer"
|
||||
onClick={() => setSelectedChunkId(item.id)}
|
||||
>
|
||||
<span className="font-medium">Chunk #{item.chunk_index}</span>
|
||||
<Badge
|
||||
color={active ? "blue" : "default"}
|
||||
text={active ? "当前" : ""}
|
||||
/>
|
||||
</div>
|
||||
{/* 展示 chunk 全部内容,不截断 */}
|
||||
<div className="text-xs text-gray-600 whitespace-pre-wrap break-words">
|
||||
<div
|
||||
className="text-xs text-gray-600 whitespace-pre-wrap break-words cursor-pointer"
|
||||
onClick={() => setSelectedChunkId(item.id)}
|
||||
>
|
||||
{item.chunk_content}
|
||||
</div>
|
||||
<div className="flex justify-end mt-1">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "delete-chunk",
|
||||
danger: true,
|
||||
label: (
|
||||
<Popconfirm
|
||||
title="确认删除该 Chunk 及其合成数据?"
|
||||
onConfirm={() => {
|
||||
setSelectedChunkId(item.id);
|
||||
handleDeleteCurrentChunk();
|
||||
}}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<DeleteOutlined />
|
||||
删除该 Chunk 及合成数据
|
||||
</span>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
shape="circle"
|
||||
icon={<MoreOutlined />}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
);
|
||||
@@ -256,18 +396,81 @@ export default function SynthDataDetail() {
|
||||
<Empty description="该 Chunk 暂无合成数据" style={{ marginTop: 40 }} />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{synthDataList.map((item, index) => (
|
||||
{synthDataList.map((item, index) => {
|
||||
const isEditing = editingId === item.id;
|
||||
return (
|
||||
<div
|
||||
key={item.id || index}
|
||||
className="border border-gray-100 rounded-md p-3 bg-white shadow-sm/50"
|
||||
className="border border-gray-100 rounded-md p-3 bg-white shadow-sm/50 relative"
|
||||
>
|
||||
<div className="mb-2 text-xs text-gray-500 flex justify-between">
|
||||
<span>记录 {index + 1}</span>
|
||||
<span>ID:{item.id}</span>
|
||||
</div>
|
||||
{/* 淡化表格样式的 key-value 展示 */}
|
||||
<div className="w-full border border-gray-100 rounded-md overflow-hidden">
|
||||
{getDataEntries(item.data).map(([key, value], rowIdx) => (
|
||||
|
||||
{/* 右下角更多操作按钮:编辑 & 删除 */}
|
||||
<div className="absolute bottom-2 right-2 flex gap-1">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "edit-data",
|
||||
label: (
|
||||
<span className="flex items-center gap-1">
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</span>
|
||||
),
|
||||
onClick: (info) => {
|
||||
info.domEvent.stopPropagation();
|
||||
startEdit(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete-data",
|
||||
danger: true,
|
||||
label: (
|
||||
<Popconfirm
|
||||
title="确认删除该条合成数据?"
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
handleDeleteSingleSynthesisData(item.id);
|
||||
}}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</span>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
shape="circle"
|
||||
icon={<MoreOutlined />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{/* 表格形式的 key-value 展示 + 可编辑 value */}
|
||||
<div className="w-full border border-gray-100 rounded-md overflow-hidden mt-2">
|
||||
{getDataEntries(item.data).map(([key, value], rowIdx) => {
|
||||
const displayValue =
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean"
|
||||
? String(value)
|
||||
: JSON.stringify(value, null, 2);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key + rowIdx}
|
||||
className={
|
||||
@@ -279,15 +482,41 @@ export default function SynthDataDetail() {
|
||||
{key}
|
||||
</div>
|
||||
<div className="px-3 py-2 text-gray-700 whitespace-pre-wrap break-words">
|
||||
{typeof value === "string" || typeof value === "number"
|
||||
? String(value)
|
||||
: JSON.stringify(value, null, 2)}
|
||||
{isEditing ? (
|
||||
<Input.TextArea
|
||||
value={editingMap[key] ?? displayValue}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
setEditingMap((prev) => ({ ...prev, [key]: v }));
|
||||
}}
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
/>
|
||||
) : (
|
||||
displayValue
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="flex justify-end gap-2 mt-2">
|
||||
<Button size="small" onClick={cancelEdit}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => handleSaveEdit(item)}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { useState, useEffect, ElementType } from "react";
|
||||
import { Card, Button, Badge, Table, Modal, message, Tooltip } from "antd";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card, Button, Table, Modal, message, Tooltip, Form, Input, Select } from "antd";
|
||||
import {
|
||||
Plus,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Pause,
|
||||
Play,
|
||||
CheckCircle,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { DeleteOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
import { FolderOpenOutlined, DeleteOutlined, EyeOutlined, ExperimentOutlined } from "@ant-design/icons";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import { formatDateTime } from "@/utils/unit";
|
||||
@@ -19,6 +16,9 @@ import {
|
||||
archiveSynthesisTaskToDatasetUsingPost,
|
||||
} from "@/pages/SynthesisTask/synthesis-api";
|
||||
import { createDatasetUsingPost } from "@/pages/DataManagement/dataset.api";
|
||||
import { createEvaluationTaskUsingPost } from "@/pages/DataEvaluation/evaluation.api";
|
||||
import { queryModelListUsingGet } from "@/pages/SettingsPage/settings.apis";
|
||||
import { ModelI } from "@/pages/SettingsPage/ModelAccess";
|
||||
|
||||
interface SynthesisTask {
|
||||
id: string;
|
||||
@@ -50,6 +50,11 @@ interface SynthesisTask {
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
interface SynthesisDataItem {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default function SynthesisTaskTab() {
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -61,6 +66,18 @@ export default function SynthesisTaskTab() {
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [evalModalVisible, setEvalModalVisible] = useState(false);
|
||||
const [currentEvalTask, setCurrentEvalTask] = useState<SynthesisTask | null>(null);
|
||||
const [evalLoading, setEvalLoading] = useState(false);
|
||||
const [models, setModels] = useState<ModelI[]>([]);
|
||||
const [modelLoading, setModelLoading] = useState(false);
|
||||
|
||||
const [evalForm] = Form.useForm();
|
||||
|
||||
// 合成数据相关状态
|
||||
const [activeChunkId, setActiveChunkId] = useState<string | null>(null);
|
||||
const [synthesisData, setSynthesisData] = useState<SynthesisDataItem[]>([]);
|
||||
const [selectedDataIds, setSelectedDataIds] = useState<string[]>([]);
|
||||
|
||||
// 获取任务列表
|
||||
const loadTasks = async () => {
|
||||
@@ -94,18 +111,6 @@ export default function SynthesisTaskTab() {
|
||||
// eslint-disable-next-line
|
||||
}, [searchQuery, filterStatus, page, pageSize]);
|
||||
|
||||
// 状态徽章
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig: Record<string, { label: string; color: string; icon: ElementType }> = {
|
||||
pending: { label: "等待中", color: "#F59E0B", icon: Pause },
|
||||
running: { label: "运行中", color: "#3B82F6", icon: Play },
|
||||
completed: { label: "已完成", color: "#10B981", icon: CheckCircle },
|
||||
failed: { label: "失败", color: "#EF4444", icon: Pause },
|
||||
paused: { label: "已暂停", color: "#E5E7EB", icon: Pause },
|
||||
};
|
||||
return statusConfig[status] ?? statusConfig["pending"];
|
||||
};
|
||||
|
||||
// 类型映射
|
||||
const typeMap: Record<string, string> = {
|
||||
QA: "问答对生成",
|
||||
@@ -176,19 +181,28 @@ export default function SynthesisTaskTab() {
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
render: (_: unknown, task: SynthesisTask) => (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<Tooltip title="查看详情">
|
||||
<Button
|
||||
onClick={() => navigate(`/data/synthesis/task/${task.id}`)}
|
||||
className="hover:bg-blue-50 p-1 h-7 w-7"
|
||||
className="hover:bg-blue-50 p-1 h-7 w-7 flex items-center justify-center"
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="归档到数据集">
|
||||
<Tooltip title="立即评估">
|
||||
<Button
|
||||
type="text"
|
||||
className="hover:bg-green-50 p-1 h-7 w-7"
|
||||
className="hover:bg-purple-50 p-1 h-7 w-7 flex items-center justify-center text-purple-600"
|
||||
icon={<ExperimentOutlined />}
|
||||
onClick={() => openEvalModal(task)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="留用合成数据到数据集">
|
||||
<Button
|
||||
type="text"
|
||||
className="hover:bg-green-50 p-1 h-7 w-7 flex items-center justify-center text-green-600"
|
||||
icon={<FolderOpenOutlined />}
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: "确认归档该合成任务?",
|
||||
@@ -198,15 +212,13 @@ export default function SynthesisTaskTab() {
|
||||
onOk: () => handleArchiveTask(task),
|
||||
});
|
||||
}}
|
||||
>
|
||||
归档
|
||||
</Button>
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="删除任务">
|
||||
<Button
|
||||
danger
|
||||
type="text"
|
||||
className="hover:bg-red-50 p-1 h-7 w-7"
|
||||
className="hover:bg-red-50 p-1 h-7 w-7 flex items-center justify-center"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
@@ -237,14 +249,21 @@ export default function SynthesisTaskTab() {
|
||||
try {
|
||||
// 1. 创建目标数据集(使用简单的默认命名 + 随机后缀,可后续扩展为弹窗自定义)
|
||||
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
||||
const datasetReq = {
|
||||
const datasetReq: {
|
||||
name: string;
|
||||
description: string;
|
||||
datasetType: string;
|
||||
category: string;
|
||||
format: string;
|
||||
status: string;
|
||||
} = {
|
||||
name: `${task.name}-合成数据留用${randomSuffix}`,
|
||||
description: `由合成任务 ${task.id} 留用生成`,
|
||||
datasetType: "TEXT",
|
||||
category: "SYNTHESIS",
|
||||
format: "JSONL",
|
||||
status: "DRAFT",
|
||||
} as any;
|
||||
};
|
||||
const datasetRes = await createDatasetUsingPost(datasetReq);
|
||||
const datasetId = datasetRes?.data?.id;
|
||||
if (!datasetId) {
|
||||
@@ -264,6 +283,88 @@ export default function SynthesisTaskTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const openEvalModal = (task: SynthesisTask) => {
|
||||
setCurrentEvalTask(task);
|
||||
setEvalModalVisible(true);
|
||||
evalForm.setFieldsValue({
|
||||
name: `${task.name}-数据评估`,
|
||||
taskType: task.synthesis_type || "QA",
|
||||
evalMethod: "AUTO",
|
||||
});
|
||||
// 懒加载模型列表
|
||||
if (!models.length) {
|
||||
loadModels();
|
||||
}
|
||||
};
|
||||
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
setModelLoading(true);
|
||||
const { data } = await queryModelListUsingGet({ page: 0, size: 1000 });
|
||||
setModels(data?.content || []);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("获取模型列表失败");
|
||||
} finally {
|
||||
setModelLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const chatModelOptions = models
|
||||
.filter((m) => m.type === "CHAT")
|
||||
.map((m) => ({
|
||||
label: `${m.modelName} (${m.provider})`,
|
||||
value: m.id,
|
||||
}));
|
||||
|
||||
const handleCreateEvaluation = async () => {
|
||||
if (!currentEvalTask) return;
|
||||
try {
|
||||
const values = await evalForm.validateFields();
|
||||
setEvalLoading(true);
|
||||
const taskType = currentEvalTask.synthesis_type || "QA";
|
||||
const payload = {
|
||||
name: values.name,
|
||||
taskType,
|
||||
evalMethod: values.evalMethod,
|
||||
sourceType: "SYNTHESIS",
|
||||
sourceId: currentEvalTask.id,
|
||||
sourceName: currentEvalTask.name,
|
||||
evalConfig: {
|
||||
modelId: values.modelId,
|
||||
dimensions: [
|
||||
{
|
||||
dimension: "问题是否独立",
|
||||
description:
|
||||
"仅分析问题,问题的主体和客体都比较明确,即使有省略,也符合语言习惯。在不需要补充其他信息的情况下不会引起疑惑。",
|
||||
},
|
||||
{
|
||||
dimension: "语法是否错误",
|
||||
description:
|
||||
"问题为疑问句,答案为陈述句; 不存在词语搭配不当的情况;连接词和标点符号不存在错用情况;逻辑混乱的情况不存在;语法结构都正确且完整。",
|
||||
},
|
||||
{
|
||||
dimension: "回答是否有针对性",
|
||||
description:
|
||||
"回答应对问题中的所有疑问点提供正面、直接的回答,不应引起疑惑。同时,答案不应有任何内容的遗漏,需构成一个完整的陈述。",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
await createEvaluationTaskUsingPost(payload);
|
||||
message.success("评估任务创建成功");
|
||||
setEvalModalVisible(false);
|
||||
setCurrentEvalTask(null);
|
||||
evalForm.resetFields();
|
||||
} catch (error) {
|
||||
const err = error as { errorFields?: unknown; response?: { data?: { message?: string } } };
|
||||
if (err?.errorFields) return; // 表单校验错误
|
||||
message.error(err?.response?.data?.message || "评估任务创建失败");
|
||||
} finally {
|
||||
setEvalLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 搜索和筛选 */}
|
||||
@@ -333,6 +434,85 @@ export default function SynthesisTaskTab() {
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="创建评估任务"
|
||||
open={evalModalVisible}
|
||||
onCancel={() => {
|
||||
setEvalModalVisible(false);
|
||||
setCurrentEvalTask(null);
|
||||
evalForm.resetFields();
|
||||
}}
|
||||
onOk={handleCreateEvaluation}
|
||||
confirmLoading={evalLoading}
|
||||
okText="开始评估"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form
|
||||
form={evalForm}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
evalMethod: "AUTO",
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label="评估任务名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入评估任务名称" }]}
|
||||
>
|
||||
<Input placeholder="例如:数据评估" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="任务类型"
|
||||
name="taskType"
|
||||
>
|
||||
<Select
|
||||
disabled
|
||||
options={[
|
||||
{
|
||||
label:
|
||||
currentEvalTask?.synthesis_type === "COT"
|
||||
? "COT评估"
|
||||
: "QA评估",
|
||||
value: currentEvalTask?.synthesis_type || "QA",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="评估方式"
|
||||
name="evalMethod"
|
||||
rules={[{ required: true, message: "请选择评估方式" }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "模型自动评估", value: "AUTO" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="评估模型"
|
||||
name="modelId"
|
||||
rules={[{ required: true, message: "请选择评估模型" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={modelLoading ? "加载模型中..." : "请选择用于评估的模型"}
|
||||
loading={modelLoading}
|
||||
options={chatModelOptions}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
</Form.Item>
|
||||
{currentEvalTask && (
|
||||
<Form.Item label="评估对象">
|
||||
<div className="text-xs text-gray-500">
|
||||
源类型:合成任务(SYNTHESIS)<br />
|
||||
源名称:{currentEvalTask.name}
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,14 @@ export function querySynthesisTasksUsingGet(params: {
|
||||
status?: string;
|
||||
name?: string;
|
||||
}) {
|
||||
return get(`/api/synthesis/gen/tasks`, params);
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.page !== undefined) searchParams.append("page", String(params.page));
|
||||
if (params.page_size !== undefined) searchParams.append("page_size", String(params.page_size));
|
||||
if (params.synthesis_type) searchParams.append("synthesis_type", params.synthesis_type);
|
||||
if (params.status) searchParams.append("status", params.status);
|
||||
if (params.name) searchParams.append("name", params.name);
|
||||
const qs = searchParams.toString();
|
||||
return get(`/api/synthesis/gen/tasks${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
// 删除整个数据合成任务
|
||||
@@ -28,12 +35,20 @@ export function deleteSynthesisTaskByIdUsingDelete(taskId: string) {
|
||||
|
||||
// 分页查询某个任务下的文件任务列表
|
||||
export function querySynthesisFileTasksUsingGet(taskId: string, params: { page?: number; page_size?: number }) {
|
||||
return get(`/api/synthesis/gen/task/${taskId}/files`, params);
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.page !== undefined) searchParams.append("page", String(params.page));
|
||||
if (params.page_size !== undefined) searchParams.append("page_size", String(params.page_size));
|
||||
const qs = searchParams.toString();
|
||||
return get(`/api/synthesis/gen/task/${taskId}/files${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
// 根据文件任务 ID 分页查询 chunk 记录
|
||||
export function queryChunksByFileUsingGet(fileId: string, params: { page?: number; page_size?: number }) {
|
||||
return get(`/api/synthesis/gen/file/${fileId}/chunks`, params);
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.page !== undefined) searchParams.append("page", String(params.page));
|
||||
if (params.page_size !== undefined) searchParams.append("page_size", String(params.page_size));
|
||||
const qs = searchParams.toString();
|
||||
return get(`/api/synthesis/gen/file/${fileId}/chunks${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
// 根据 chunk ID 查询所有合成结果数据
|
||||
@@ -43,10 +58,35 @@ export function querySynthesisDataByChunkUsingGet(chunkId: string) {
|
||||
|
||||
// 获取不同合成类型对应的 Prompt
|
||||
export function getPromptByTypeUsingGet(synthType: string) {
|
||||
return get(`/api/synthesis/gen/prompt`, { synth_type: synthType });
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append("synth_type", synthType);
|
||||
const qs = searchParams.toString();
|
||||
return get(`/api/synthesis/gen/prompt${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
// 将合成任务数据归档到已存在的数据集中
|
||||
export function archiveSynthesisTaskToDatasetUsingPost(taskId: string, datasetId: string) {
|
||||
return post(`/api/synthesis/gen/task/${taskId}/export-dataset/${datasetId}`);
|
||||
}
|
||||
|
||||
// ---------------- 数据记录级别:chunk 与 synthesis data ----------------
|
||||
|
||||
// 根据 chunkId 删除单个 chunk 及其下所有合成数据
|
||||
export function deleteChunkWithDataUsingDelete(chunkId: string) {
|
||||
return del(`/api/synthesis/gen/chunk/${chunkId}`);
|
||||
}
|
||||
|
||||
// 删除某个 chunk 下的所有合成数据,返回删除条数
|
||||
export function deleteSynthesisDataByChunkUsingDelete(chunkId: string) {
|
||||
return del(`/api/synthesis/gen/chunk/${chunkId}/data`);
|
||||
}
|
||||
|
||||
// 批量删除合成数据记录
|
||||
export function batchDeleteSynthesisDataUsingDelete(body: { ids: string[] }) {
|
||||
return del(`/api/synthesis/gen/data/batch`, null, { body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
// 更新单条合成数据的完整 JSON 内容
|
||||
export function updateSynthesisDataUsingPatch(dataId: string, body: { data: Record<string, unknown> }) {
|
||||
return post(`/api/synthesis/gen/data/${dataId}`, body, { method: "PATCH" });
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ from app.module.generation.schema.generation import (
|
||||
DataSynthesisChunkItem,
|
||||
PagedDataSynthesisChunkResponse,
|
||||
SynthesisDataItem,
|
||||
SynthesisDataUpdateRequest,
|
||||
BatchDeleteSynthesisDataRequest,
|
||||
)
|
||||
from app.module.generation.service.generation_service import GenerationService
|
||||
from app.module.generation.service.prompt import get_prompt
|
||||
@@ -118,7 +120,7 @@ async def list_synthesis_tasks(
|
||||
name: str | None = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""分页列出所有数据合成任务"""
|
||||
"""分页列出所有数据合成任务,默认按创建时间倒序"""
|
||||
query = select(DataSynthesisInstance)
|
||||
if synthesis_type:
|
||||
query = query.filter(DataSynthesisInstance.synthesis_type == synthesis_type)
|
||||
@@ -127,6 +129,9 @@ async def list_synthesis_tasks(
|
||||
if name:
|
||||
query = query.filter(DataSynthesisInstance.name.like(f"%{name}%"))
|
||||
|
||||
# 默认按创建时间倒序排列
|
||||
query = query.order_by(DataSynthesisInstance.created_at.desc())
|
||||
|
||||
count_q = select(func.count()).select_from(query.subquery())
|
||||
total = (await db.execute(count_q)).scalar_one()
|
||||
|
||||
@@ -179,7 +184,7 @@ async def list_synthesis_tasks(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/task/{task_id}", response_model=StandardResponse[None])
|
||||
@router.delete("/task/{task_id}", response_model=StandardResponse)
|
||||
async def delete_synthesis_task(
|
||||
task_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -228,7 +233,7 @@ async def delete_synthesis_task(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/task/{task_id}/{file_id}", response_model=StandardResponse[None])
|
||||
@router.delete("/task/{task_id}/{file_id}", response_model=StandardResponse)
|
||||
async def delete_synthesis_file_task(
|
||||
task_id: str,
|
||||
file_id: str,
|
||||
@@ -476,3 +481,100 @@ async def export_synthesis_task_to_dataset(
|
||||
message="success",
|
||||
data=dataset.id,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/chunk/{chunk_id}", response_model=StandardResponse)
|
||||
async def delete_chunk_with_data(
|
||||
chunk_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除单条 t_data_synthesis_chunk_instances 记录及其关联的所有 t_data_synthesis_data"""
|
||||
chunk = await db.get(DataSynthesisChunkInstance, chunk_id)
|
||||
if not chunk:
|
||||
raise HTTPException(status_code=404, detail="Chunk not found")
|
||||
|
||||
# 先删除与该 chunk 关联的合成数据
|
||||
await db.execute(
|
||||
delete(SynthesisData).where(SynthesisData.chunk_instance_id == chunk_id)
|
||||
)
|
||||
|
||||
# 再删除 chunk 本身
|
||||
await db.execute(
|
||||
delete(DataSynthesisChunkInstance).where(
|
||||
DataSynthesisChunkInstance.id == chunk_id
|
||||
)
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return StandardResponse(code=200, message="success", data=None)
|
||||
|
||||
|
||||
@router.delete("/chunk/{chunk_id}/data", response_model=StandardResponse)
|
||||
async def delete_synthesis_data_by_chunk(
|
||||
chunk_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""仅删除指定 chunk 下的全部 t_data_synthesis_data 记录,返回删除条数"""
|
||||
chunk = await db.get(DataSynthesisChunkInstance, chunk_id)
|
||||
if not chunk:
|
||||
raise HTTPException(status_code=404, detail="Chunk not found")
|
||||
|
||||
result = await db.execute(
|
||||
delete(SynthesisData).where(SynthesisData.chunk_instance_id == chunk_id)
|
||||
)
|
||||
deleted = result.rowcount or 0
|
||||
|
||||
await db.commit()
|
||||
|
||||
return StandardResponse(code=200, message="success", data=deleted)
|
||||
|
||||
|
||||
@router.delete("/data/batch", response_model=StandardResponse)
|
||||
async def batch_delete_synthesis_data(
|
||||
request: BatchDeleteSynthesisDataRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""批量删除 t_data_synthesis_data 记录"""
|
||||
if not request.ids:
|
||||
return StandardResponse(code=200, message="success", data=0)
|
||||
|
||||
result = await db.execute(
|
||||
delete(SynthesisData).where(SynthesisData.id.in_(request.ids))
|
||||
)
|
||||
deleted = result.rowcount or 0
|
||||
await db.commit()
|
||||
|
||||
return StandardResponse(code=200, message="success", data=deleted)
|
||||
|
||||
|
||||
@router.patch("/data/{data_id}", response_model=StandardResponse)
|
||||
async def update_synthesis_data_field(
|
||||
data_id: str,
|
||||
body: SynthesisDataUpdateRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""修改单条 t_data_synthesis_data.data 的完整 JSON
|
||||
|
||||
前端传入完整 JSON,后端直接覆盖原有 data 字段,不做局部 merge。
|
||||
"""
|
||||
record = await db.get(SynthesisData, data_id)
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="Synthesis data not found")
|
||||
|
||||
# 直接整体覆盖 data 字段
|
||||
record.data = body.data
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(record)
|
||||
|
||||
return StandardResponse(
|
||||
code=200,
|
||||
message="success",
|
||||
data=SynthesisDataItem(
|
||||
id=record.id,
|
||||
data=record.data,
|
||||
synthesis_file_instance_id=record.synthesis_file_instance_id,
|
||||
chunk_instance_id=record.chunk_instance_id,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -135,3 +135,39 @@ class ChatRequest(BaseModel):
|
||||
"""聊天请求参数"""
|
||||
model_id: str
|
||||
prompt: str
|
||||
|
||||
|
||||
class SynthesisDataUpdateRequest(BaseModel):
|
||||
"""单条合成数据 data 字段整体更新请求(前端传入完整 JSON,后端直接覆盖)"""
|
||||
data: Dict[str, Any] = Field(..., description="新的完整 JSON 对象,将覆盖原有 data 字段")
|
||||
|
||||
|
||||
class BatchDeleteSynthesisDataRequest(BaseModel):
|
||||
"""批量删除合成数据请求"""
|
||||
ids: List[str] = Field(..., description="需要删除的合成数据 ID 列表")
|
||||
|
||||
|
||||
class BatchDeleteChunkInstancesRequest(BaseModel):
|
||||
"""批量删除分块及其关联合成数据请求"""
|
||||
chunk_ids: List[str] = Field(..., description="需要删除的 chunk 实例 ID 列表")
|
||||
|
||||
|
||||
class BatchDeleteChunkInstancesByFileRequest(BaseModel):
|
||||
"""按文件任务维度删除 chunk 及其合成数据的请求"""
|
||||
file_id: str = Field(..., description="数据合成文件任务 ID")
|
||||
|
||||
|
||||
class BatchDeleteChunkInstancesByTaskRequest(BaseModel):
|
||||
"""按任务维度删除 chunk 及其合成数据的请求"""
|
||||
task_id: str = Field(..., description="数据合成任务 ID")
|
||||
|
||||
|
||||
class SynthesisDataPatchItem(BaseModel):
|
||||
"""用于前端展示/编辑的合成数据项(包含 chunk 与文件信息,可按需扩展)"""
|
||||
id: str
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
chunk_instance_id: str
|
||||
synthesis_file_instance_id: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
Reference in New Issue
Block a user