You've already forked DataMate
feat(frontend): 增强Synthesis Data Detail页面UX体验 (#163)
* fix(chart): update Helm chart helpers and values for improved configuration * feat(SynthesisTaskTab): enhance task table with tooltip support and improved column widths * feat(CreateTask, SynthFileTask): improve task creation and detail view with enhanced payload handling and UI updates * feat(SynthFileTask): enhance file display with progress tracking and delete action * feat(SynthFileTask): enhance file display with progress tracking and delete action * feat(SynthDataDetail): add delete action for chunks with confirmation prompt * feat(SynthDataDetail): update edit and delete buttons to icon-only format * feat(SynthDataDetail): add confirmation modals for chunk and synthesis data deletion
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
Expand the name of the chart.
|
Expand the name of the chart.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "label-studio.name" -}}
|
{{- define "label-studio.name" -}}
|
||||||
{{- default .Chart.name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
{{- default .Values.nameOverride .Chart.Name | trunc 63 | trimSuffix "-" -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
@@ -12,7 +12,7 @@ Create a default fully qualified app name.
|
|||||||
{{- if .Values.fullnameOverride -}}
|
{{- if .Values.fullnameOverride -}}
|
||||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
{{- $name := default .Chart.name .Values.nameOverride -}}
|
{{- $name := default .Values.nameOverride .Chart.Name -}}
|
||||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
@@ -21,6 +21,5 @@ Create a default fully qualified app name.
|
|||||||
Create chart name and version as used by the chart label.
|
Create chart name and version as used by the chart label.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "label-studio.chart" -}}
|
{{- define "label-studio.chart" -}}
|
||||||
{{- printf "%s-%s" .Chart.name .Chart.version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Default values for label-studio Helm chart.
|
# Default values for label-studio Helm chart.
|
||||||
# This mirrors the configuration from deployment/docker/label-studio/docker-compose.yml
|
# This mirrors the configuration from deployment/docker/label-studio/docker-compose.yml
|
||||||
|
fullnameOverride: label-studio
|
||||||
|
|
||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
|
|
||||||
@@ -24,9 +25,9 @@ postgres:
|
|||||||
size: 10Gi
|
size: 10Gi
|
||||||
|
|
||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: NodePort
|
||||||
port: 8000
|
port: 8000
|
||||||
nodePort: null
|
nodePort: 30001
|
||||||
|
|
||||||
# Corresponds to docker-compose port mapping 30001:8000
|
# Corresponds to docker-compose port mapping 30001:8000
|
||||||
ingress:
|
ingress:
|
||||||
@@ -54,7 +55,7 @@ env:
|
|||||||
POSTGRE_USER: "postgres"
|
POSTGRE_USER: "postgres"
|
||||||
POSTGRE_PASSWORD: ""
|
POSTGRE_PASSWORD: ""
|
||||||
POSTGRE_PORT: 5432
|
POSTGRE_PORT: 5432
|
||||||
POSTGRE_HOST: "db"
|
POSTGRE_HOST: "label-studio-postgres"
|
||||||
LABEL_STUDIO_HOST: "" # can be overridden
|
LABEL_STUDIO_HOST: "" # can be overridden
|
||||||
LOCAL_FILES_SERVING_ENABLED: "true"
|
LOCAL_FILES_SERVING_ENABLED: "true"
|
||||||
LOCAL_FILES_DOCUMENT_ROOT: "/label-studio/local"
|
LOCAL_FILES_DOCUMENT_ROOT: "/label-studio/local"
|
||||||
@@ -75,5 +76,6 @@ persistence:
|
|||||||
# If not set and persistence.enabled=true, a PVC will be created automatically.
|
# If not set and persistence.enabled=true, a PVC will be created automatically.
|
||||||
datasetVolume:
|
datasetVolume:
|
||||||
enabled: true
|
enabled: true
|
||||||
claimName: "" # if empty, uses same PVC as persistence or creates a dedicated one
|
claimName: datamate-dataset-pvc # if empty, uses same PVC as persistence or creates a dedicated one
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -146,9 +146,8 @@ export default function SynthesisTaskCreate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构造后端要求的参数格式
|
// 构造后端要求的参数格式
|
||||||
const payload = {
|
const payload: Record<string, unknown> = {
|
||||||
name: values.name || form.getFieldValue("name"), // 必选,确保传递
|
name: values.name || form.getFieldValue("name"),
|
||||||
description: values.description ?? "", // 可选,始终传递
|
|
||||||
model_id: selectedModel,
|
model_id: selectedModel,
|
||||||
source_file_id: selectedFiles,
|
source_file_id: selectedFiles,
|
||||||
text_split_config: {
|
text_split_config: {
|
||||||
@@ -161,10 +160,14 @@ export default function SynthesisTaskCreate() {
|
|||||||
synthesis_type: taskType === "qa" ? "QA" : "COT",
|
synthesis_type: taskType === "qa" ? "QA" : "COT",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 只有在有真实内容时携带 description,避免强制传空字符串
|
||||||
|
const desc = values.description ?? form.getFieldValue("description");
|
||||||
|
if (typeof desc === "string" && desc.trim().length > 0) {
|
||||||
|
payload.description = desc.trim();
|
||||||
|
}
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const res = (await createSynthesisTaskUsingPost(
|
const res = (await createSynthesisTaskUsingPost(payload)) as CreateTaskApiResponse;
|
||||||
payload as unknown as Record<string, unknown>
|
|
||||||
)) as CreateTaskApiResponse;
|
|
||||||
|
|
||||||
const ok =
|
const ok =
|
||||||
res?.success === true ||
|
res?.success === true ||
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router";
|
import { useLocation, useNavigate, useParams, Link } from "react-router";
|
||||||
import { Badge, Button, Empty, List, Pagination, Spin, Typography, Popconfirm, message, Dropdown, Input } from "antd";
|
import {
|
||||||
|
Badge,
|
||||||
|
Empty,
|
||||||
|
List,
|
||||||
|
Pagination,
|
||||||
|
Spin,
|
||||||
|
Typography,
|
||||||
|
Popconfirm,
|
||||||
|
message,
|
||||||
|
Dropdown,
|
||||||
|
Input,
|
||||||
|
Breadcrumb,
|
||||||
|
Button,
|
||||||
|
Tag,
|
||||||
|
} from "antd";
|
||||||
import type { PaginationProps } from "antd";
|
import type { PaginationProps } from "antd";
|
||||||
import { MoreOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons";
|
import { MoreOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +83,8 @@ export default function SynthDataDetail() {
|
|||||||
const [chunkLoading, setChunkLoading] = useState(false);
|
const [chunkLoading, setChunkLoading] = useState(false);
|
||||||
const [dataLoading, setDataLoading] = useState(false);
|
const [dataLoading, setDataLoading] = useState(false);
|
||||||
const [synthDataList, setSynthDataList] = useState<SynthesisDataItem[]>([]);
|
const [synthDataList, setSynthDataList] = useState<SynthesisDataItem[]>([]);
|
||||||
|
const [chunkConfirmVisibleId, setChunkConfirmVisibleId] = useState<string | null>(null);
|
||||||
|
const [dataConfirmVisibleId, setDataConfirmVisibleId] = useState<string | null>(null);
|
||||||
|
|
||||||
// 加载任务信息(用于顶部展示)
|
// 加载任务信息(用于顶部展示)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -237,123 +253,150 @@ export default function SynthDataDetail() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const breadItems = [
|
||||||
<div className="p-4 bg-white rounded-lg h-full flex flex-col overflow-hidden">
|
{
|
||||||
{/* 顶部信息和返回 */}
|
title: <Link to="/data/synthesis/task">合成任务</Link>,
|
||||||
<div className="flex items-center justify-between mb-4">
|
},
|
||||||
<div className="space-y-1">
|
{
|
||||||
<div className="flex items-center gap-2">
|
title: state.taskId ? (
|
||||||
<Title level={4} style={{ margin: 0 }}>
|
<Link to={`/data/synthesis/task/${state.taskId}`}>{taskInfo?.name || "任务详情"}</Link>
|
||||||
合成数据详情
|
) : (
|
||||||
</Title>
|
taskInfo?.name || "任务详情"
|
||||||
{state.fileName && (
|
),
|
||||||
<Text type="secondary" className="!text-xs">
|
},
|
||||||
文件:{state.fileName}
|
{
|
||||||
</Text>
|
title: state.fileName || "文件详情",
|
||||||
)}
|
},
|
||||||
</div>
|
];
|
||||||
{taskInfo && (
|
|
||||||
<div className="text-xs text-gray-500 flex gap-4">
|
|
||||||
<span>
|
|
||||||
任务:{taskInfo.name}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
类型:
|
|
||||||
{taskInfo.synthesis_type === "QA"
|
|
||||||
? "问答对生成"
|
|
||||||
: taskInfo.synthesis_type === "COT"
|
|
||||||
? "链式推理生成"
|
|
||||||
: taskInfo.synthesis_type}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
创建时间:{formatDateTime(taskInfo.created_at)}
|
|
||||||
</span>
|
|
||||||
<span>模型ID:{taskInfo.model_id}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button onClick={() => navigate(-1)}>返回</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 主体左右布局 */}
|
const showChunkConfirm = (id: string) => setChunkConfirmVisibleId(id);
|
||||||
|
const hideChunkConfirm = () => setChunkConfirmVisibleId(null);
|
||||||
|
|
||||||
|
const showDataConfirm = (id: string) => setDataConfirmVisibleId(id);
|
||||||
|
const hideDataConfirm = () => setDataConfirmVisibleId(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb items={breadItems} />
|
||||||
|
{/* 全局删除确认遮罩:Chunk */}
|
||||||
|
{chunkConfirmVisibleId && (
|
||||||
|
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black/30">
|
||||||
|
<div className="bg-white rounded-lg px-6 py-4 shadow-lg min-w-[320px] max-w-[420px]">
|
||||||
|
<div className="text-sm font-medium mb-2">确认删除该 Chunk 及其合成数据?</div>
|
||||||
|
<div className="text-xs text-gray-500 mb-4 break-all">
|
||||||
|
ID: {chunkConfirmVisibleId}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 text-sm">
|
||||||
|
<Button size="small" onClick={hideChunkConfirm}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
danger
|
||||||
|
onClick={async () => {
|
||||||
|
setSelectedChunkId(chunkConfirmVisibleId);
|
||||||
|
await handleDeleteCurrentChunk();
|
||||||
|
hideChunkConfirm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 全局删除确认遮罩:合成数据 */}
|
||||||
|
{dataConfirmVisibleId && (
|
||||||
|
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black/30">
|
||||||
|
<div className="bg-white rounded-lg px-6 py-4 shadow-lg min-w-[320px] max-w-[480px]">
|
||||||
|
<div className="text-sm font-medium mb-2">确认删除该条合成数据?</div>
|
||||||
|
<div className="text-xs text-gray-500 mb-4 break-all">
|
||||||
|
ID: {dataConfirmVisibleId}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 text-sm">
|
||||||
|
<Button size="small" onClick={hideDataConfirm}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
danger
|
||||||
|
onClick={async () => {
|
||||||
|
await handleDeleteSingleSynthesisData(dataConfirmVisibleId);
|
||||||
|
hideDataConfirm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-1 flex-col overflow-hidden rounded-lg bg-transparent">
|
||||||
<div className="flex flex-1 min-h-0 gap-4">
|
<div className="flex flex-1 min-h-0 gap-4">
|
||||||
{/* 左侧 Chunk 列表:占比 2/5 */}
|
{/* 左侧 Chunk 列表 */}
|
||||||
<div className="basis-2/5 max-w-[40%] border rounded-lg flex flex-col overflow-hidden">
|
<div className="basis-2/5 max-w-[40%] flex flex-col min-w-0">
|
||||||
<div className="px-3 py-2 border-b text-sm font-medium bg-gray-50">
|
<div className="rounded-lg border border-gray-100 bg-white shadow-sm flex flex-col overflow-hidden h-full">
|
||||||
Chunk 列表
|
<div className="px-4 py-3 border-b border-gray-100 text-sm font-medium bg-gray-50/80 flex items-center justify-between">
|
||||||
|
<span>Chunk 列表</span>
|
||||||
|
{chunkPagination.total ? (
|
||||||
|
<span className="text-xs text-gray-400">共 {chunkPagination.total} 条</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{chunkLoading ? (
|
{chunkLoading ? (
|
||||||
<div className="h-full flex items-center justify-center">
|
<div className="h-full flex items-center justify-center">
|
||||||
<Spin />
|
<Spin size="small" />
|
||||||
</div>
|
</div>
|
||||||
) : chunks.length === 0 ? (
|
) : chunks.length === 0 ? (
|
||||||
<Empty description="暂无 Chunk" style={{ marginTop: 40 }} />
|
<Empty description="暂无 Chunk" style={{ marginTop: 40 }} />
|
||||||
) : (
|
) : (
|
||||||
<List
|
<List
|
||||||
size="small"
|
size="small"
|
||||||
|
className="!border-0"
|
||||||
dataSource={chunks}
|
dataSource={chunks}
|
||||||
renderItem={(item) => {
|
renderItem={(item) => {
|
||||||
const active = item.id === selectedChunkId;
|
const active = item.id === selectedChunkId;
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
className={
|
className={
|
||||||
"px-3 py-2 !border-0 " +
|
"!border-0 px-4 py-3 transition-colors rounded-none " +
|
||||||
(active ? "bg-blue-50" : "hover:bg-gray-50")
|
(active
|
||||||
|
? "bg-blue-200 hover:bg-blue-300"
|
||||||
|
: "hover:bg-blue-50")
|
||||||
}
|
}
|
||||||
|
onClick={() => setSelectedChunkId(item.id)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1 w-full">
|
<div className="flex flex-col gap-1 w-full">
|
||||||
<div
|
<div className="flex items-center justify-between text-[12px] text-gray-500">
|
||||||
className="flex items-center justify-between text-xs cursor-pointer"
|
<div className="flex items-center gap-2">
|
||||||
onClick={() => setSelectedChunkId(item.id)}
|
<span className="text-xs font-medium text-gray-800">
|
||||||
>
|
Chunk #{item.chunk_index}
|
||||||
<span className="font-medium">Chunk #{item.chunk_index}</span>
|
</span>
|
||||||
<Badge
|
</div>
|
||||||
color={active ? "blue" : "default"}
|
<div className="flex items-center gap-2">
|
||||||
text={active ? "当前" : ""}
|
{/* 右侧显示 Chunk ID,完整展示 */}
|
||||||
/>
|
<span className="text-[11px] text-gray-400" title={item.id}>
|
||||||
</div>
|
ID: {item.id}
|
||||||
<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>
|
</span>
|
||||||
</Popconfirm>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
trigger={["click"]}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
|
||||||
type="text"
|
type="text"
|
||||||
|
size="small"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
icon={<MoreOutlined />}
|
danger
|
||||||
|
icon={<DeleteOutlined className="text-[12px]" />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
showChunkConfirm(item.id);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-600 whitespace-pre-wrap break-words leading-relaxed">
|
||||||
|
{item.chunk_content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
@@ -362,7 +405,8 @@ export default function SynthDataDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t px-2 py-1 flex justify-end bg-white">
|
{chunkPagination.total ? (
|
||||||
|
<div className="border-t border-gray-100 px-3 py-2 flex justify-end bg-white">
|
||||||
<Pagination
|
<Pagination
|
||||||
size="small"
|
size="small"
|
||||||
current={chunkPagination.page}
|
current={chunkPagination.page}
|
||||||
@@ -373,22 +417,25 @@ export default function SynthDataDetail() {
|
|||||||
showTotal={(total) => `共 ${total} 条`}
|
showTotal={(total) => `共 ${total} 条`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧合成数据展示:占比 3/5 */}
|
{/* 右侧合成数据展示 */}
|
||||||
<div className="basis-3/5 max-w-[60%] border rounded-lg flex flex-col min-w-0 overflow-hidden">
|
<div className="basis-3/5 max-w-[60%] flex flex-col min-w-0">
|
||||||
<div className="px-3 py-2 border-b flex items-center justify-between bg-gray-50 text-sm font-medium">
|
<div className="rounded-lg border border-gray-100 bg-white shadow-sm flex flex-col overflow-hidden h-full">
|
||||||
|
<div className="px-4 py-3 border-b border-gray-100 flex items-center justify-between bg-gray-50/80 text-sm font-medium">
|
||||||
<span>合成数据</span>
|
<span>合成数据</span>
|
||||||
{currentChunk && (
|
{currentChunk && (
|
||||||
<span className="text-xs text-gray-500">
|
<Tag color="blue" className="text-xs px-2 py-0.5 m-0 rounded-full border-none bg-blue-100 text-blue-200">
|
||||||
当前 Chunk #{currentChunk.chunk_index}
|
当前 Chunk #{currentChunk.chunk_index}
|
||||||
</span>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto p-3">
|
<div className="flex-1 overflow-auto p-4">
|
||||||
{dataLoading ? (
|
{dataLoading ? (
|
||||||
<div className="h-full flex items-center justify-center">
|
<div className="h-full flex items-center justify-center">
|
||||||
<Spin />
|
<Spin size="small" />
|
||||||
</div>
|
</div>
|
||||||
) : !selectedChunkId ? (
|
) : !selectedChunkId ? (
|
||||||
<Empty description="请选择左侧 Chunk" style={{ marginTop: 40 }} />
|
<Empty description="请选择左侧 Chunk" style={{ marginTop: 40 }} />
|
||||||
@@ -401,67 +448,39 @@ export default function SynthDataDetail() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.id || index}
|
key={item.id || index}
|
||||||
className="border border-gray-100 rounded-md p-3 bg-white shadow-sm/50 relative"
|
className="border border-gray-100 rounded-md p-3 bg-white hover:bg-blue-50/80 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="mb-2 text-xs text-gray-500 flex justify-between">
|
<div className="mb-2 text-[12px] text-gray-500 flex justify-between items-center">
|
||||||
<span>记录 {index + 1}</span>
|
<span>记录 {index + 1}</span>
|
||||||
<span>ID:{item.id}</span>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<span title={item.id}>ID:{item.id}</span>
|
||||||
|
{!isEditing && (
|
||||||
{/* 右下角更多操作按钮:编辑 & 删除 */}
|
<>
|
||||||
<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
|
<Button
|
||||||
size="small"
|
|
||||||
type="text"
|
type="text"
|
||||||
|
size="small"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
icon={<MoreOutlined />}
|
icon={<EditOutlined className="text-[13px]" />}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={() => startEdit(item)}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
shape="circle"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined className="text-[13px]" />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
showDataConfirm(item.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 表格形式的 key-value 展示 + 可编辑 value */}
|
{/* key-value 展示区域:不再截断,完整展示 */}
|
||||||
<div className="w-full border border-gray-100 rounded-md overflow-hidden mt-2">
|
<div className="border border-gray-100 rounded-md overflow-hidden">
|
||||||
{getDataEntries(item.data).map(([key, value], rowIdx) => {
|
{getDataEntries(item.data).map(([key, value], rowIdx) => {
|
||||||
const displayValue =
|
const displayValue =
|
||||||
typeof value === "string" ||
|
typeof value === "string" ||
|
||||||
@@ -474,8 +493,8 @@ export default function SynthDataDetail() {
|
|||||||
<div
|
<div
|
||||||
key={key + rowIdx}
|
key={key + rowIdx}
|
||||||
className={
|
className={
|
||||||
"grid grid-cols-[120px,1fr] text-xs " +
|
"grid grid-cols-[120px,1fr] text-[12px] " +
|
||||||
(rowIdx % 2 === 0 ? "bg-gray-50/60" : "bg-white")
|
(rowIdx % 2 === 0 ? "bg-white" : "bg-gray-50/80")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="px-3 py-2 border-r border-gray-100 font-medium text-gray-600 break-words">
|
<div className="px-3 py-2 border-r border-gray-100 font-medium text-gray-600 break-words">
|
||||||
@@ -489,7 +508,7 @@ export default function SynthDataDetail() {
|
|||||||
const v = e.target.value;
|
const v = e.target.value;
|
||||||
setEditingMap((prev) => ({ ...prev, [key]: v }));
|
setEditingMap((prev) => ({ ...prev, [key]: v }));
|
||||||
}}
|
}}
|
||||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
autoSize={{ minRows: 1, maxRows: 6 }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
displayValue
|
displayValue
|
||||||
@@ -523,5 +542,7 @@ export default function SynthDataDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
|
import { App } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams, useNavigate } from "react-router";
|
import { Link, useNavigate, useParams } from "react-router";
|
||||||
import { Table, Badge, Button } from "antd";
|
import { DeleteOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||||
import type { ColumnsType, TablePaginationConfig } from "antd/es/table";
|
import { Badge, Breadcrumb, Button, Table, Tabs, Progress, Tooltip } from "antd";
|
||||||
import { querySynthesisFileTasksUsingGet, querySynthesisTaskByIdUsingGet } from "@/pages/SynthesisTask/synthesis-api";
|
|
||||||
import type { BadgeProps } from "antd";
|
import type { BadgeProps } from "antd";
|
||||||
|
import type { ColumnsType, TablePaginationConfig } from "antd/es/table";
|
||||||
|
|
||||||
|
import DetailHeader from "@/components/DetailHeader";
|
||||||
|
import {
|
||||||
|
querySynthesisFileTasksUsingGet,
|
||||||
|
querySynthesisTaskByIdUsingGet,
|
||||||
|
deleteSynthesisTaskByIdUsingDelete,
|
||||||
|
} from "@/pages/SynthesisTask/synthesis-api";
|
||||||
import { formatDateTime } from "@/utils/unit";
|
import { formatDateTime } from "@/utils/unit";
|
||||||
|
import { Folder, Sparkles, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
interface SynthesisFileTaskItem {
|
interface SynthesisFileTaskItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -33,12 +42,18 @@ interface SynthesisTaskInfo {
|
|||||||
synthesis_type: string;
|
synthesis_type: string;
|
||||||
status: string;
|
status: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
updated_at?: string;
|
||||||
model_id: string;
|
model_id: string;
|
||||||
|
total_files?: number;
|
||||||
|
total_synthesis_data?: number;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SynthFileTask() {
|
export default function SynthFileTask() {
|
||||||
const { id: taskId = "" } = useParams();
|
const { id: taskId = "" } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState<SynthesisFileTaskItem[]>([]);
|
const [data, setData] = useState<SynthesisFileTaskItem[]>([]);
|
||||||
const [pagination, setPagination] = useState<TablePaginationConfig>({
|
const [pagination, setPagination] = useState<TablePaginationConfig>({
|
||||||
@@ -47,17 +62,34 @@ export default function SynthFileTask() {
|
|||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
const [taskInfo, setTaskInfo] = useState<SynthesisTaskInfo | null>(null);
|
const [taskInfo, setTaskInfo] = useState<SynthesisTaskInfo | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState("files");
|
||||||
|
|
||||||
// 查询总任务详情
|
// 查询总任务详情
|
||||||
useEffect(() => {
|
const fetchTaskDetail = async () => {
|
||||||
if (!taskId) return;
|
if (!taskId) return;
|
||||||
querySynthesisTaskByIdUsingGet(taskId).then((res) => {
|
try {
|
||||||
setTaskInfo(res?.data?.data || null);
|
const res = await querySynthesisTaskByIdUsingGet(taskId);
|
||||||
});
|
const raw = res?.data?.data ?? res?.data;
|
||||||
|
if (!raw) return;
|
||||||
|
setTaskInfo(raw);
|
||||||
|
} catch {
|
||||||
|
message.error("获取合成任务详情失败");
|
||||||
|
navigate("/data/synthesis/task");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTaskDetail();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [taskId]);
|
}, [taskId]);
|
||||||
|
|
||||||
const fetchData = async (page = 1, pageSize = 10) => {
|
const fetchData = async (page = 1, pageSize = 10, withTopLoading = false) => {
|
||||||
if (!taskId) return;
|
if (!taskId) return;
|
||||||
|
|
||||||
|
if (withTopLoading) {
|
||||||
|
window.dispatchEvent(new Event("loading:show"));
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await querySynthesisFileTasksUsingGet(taskId, {
|
const res = await querySynthesisFileTasksUsingGet(taskId, {
|
||||||
@@ -80,30 +112,41 @@ export default function SynthFileTask() {
|
|||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
if (withTopLoading) {
|
||||||
|
window.dispatchEvent(new Event("loading:hide"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData(1, pagination.pageSize || 10);
|
// 首次进入或任务切换时,不触发顶部 loading,只用表格自带的 loading
|
||||||
|
fetchData(1, pagination.pageSize || 10, false);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [taskId]);
|
}, [taskId]);
|
||||||
|
|
||||||
const handleTableChange = (pag: TablePaginationConfig) => {
|
const handleTableChange = (pag: TablePaginationConfig) => {
|
||||||
fetchData(pag.current || 1, pag.pageSize || 10);
|
// 分页切换时,也只用表格 loading,不闪顶部条
|
||||||
|
fetchData(pag.current || 1, pag.pageSize || 10, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnsType<SynthesisFileTaskItem> = [
|
const columns: ColumnsType<SynthesisFileTaskItem> = [
|
||||||
{
|
{
|
||||||
title: "文件名",
|
title: "文件",
|
||||||
dataIndex: "file_name",
|
key: "file",
|
||||||
key: "file_name",
|
render: (_text, record) => (
|
||||||
render: (text: string, record) => (
|
<div className="flex items-center gap-2">
|
||||||
|
<Folder className="w-4 h-4 text-blue-500" />
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
onClick={() => navigate(`/data/synthesis/task/file/${record.id}/detail`, { state: { fileName: record.file_name, taskId } })}
|
onClick={() =>
|
||||||
|
navigate(`/data/synthesis/task/file/${record.id}/detail`, {
|
||||||
|
state: { fileName: record.file_name, taskId },
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{text}
|
{record.file_name}
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -113,13 +156,13 @@ export default function SynthFileTask() {
|
|||||||
render: (status?: string) => {
|
render: (status?: string) => {
|
||||||
let badgeStatus: BadgeProps["status"] = "default";
|
let badgeStatus: BadgeProps["status"] = "default";
|
||||||
let text = status || "未知";
|
let text = status || "未知";
|
||||||
if (status === "pending" || status === "processing") {
|
if (status === "pending" || status === "PROCESSING" || status === "processing") {
|
||||||
badgeStatus = "processing";
|
badgeStatus = "processing";
|
||||||
text = "处理中";
|
text = "处理中";
|
||||||
} else if (status === "completed") {
|
} else if (status === "COMPLETED" || status === "completed") {
|
||||||
badgeStatus = "success";
|
badgeStatus = "success";
|
||||||
text = "已完成";
|
text = "已完成";
|
||||||
} else if (status === "failed") {
|
} else if (status === "FAILED" || status === "failed") {
|
||||||
badgeStatus = "error";
|
badgeStatus = "error";
|
||||||
text = "失败";
|
text = "失败";
|
||||||
}
|
}
|
||||||
@@ -127,19 +170,28 @@ export default function SynthFileTask() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "切片进度",
|
title: "切片总数",
|
||||||
key: "chunks",
|
dataIndex: "total_chunks",
|
||||||
render: (_text, record) => (
|
key: "total_chunks",
|
||||||
<span>
|
|
||||||
{record.processed_chunks}/{record.total_chunks}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "目标文件路径",
|
title: "处理进度",
|
||||||
dataIndex: "target_file_location",
|
key: "progress",
|
||||||
key: "target_file_location",
|
render: (_text, record) => {
|
||||||
ellipsis: true,
|
const total = record.total_chunks || 0;
|
||||||
|
const processed = record.processed_chunks || 0;
|
||||||
|
const percent = total > 0 ? Math.min(100, Math.round((processed / total) * 100)) : 0;
|
||||||
|
return (
|
||||||
|
<div style={{ minWidth: 160 }}>
|
||||||
|
<Progress
|
||||||
|
percent={percent}
|
||||||
|
size="small"
|
||||||
|
status={percent === 100 ? "success" : undefined}
|
||||||
|
format={() => `${processed}/${total}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
@@ -153,34 +205,127 @@ export default function SynthFileTask() {
|
|||||||
key: "updated_at",
|
key: "updated_at",
|
||||||
render: (val?: string) => (val ? formatDateTime(val) : "-"),
|
render: (val?: string) => (val ? formatDateTime(val) : "-"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "actions",
|
||||||
|
render: () => (
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
disabled
|
||||||
|
icon={<Trash2 className="w-4 h-4" />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
// 刷新按钮:明确触发一次顶部 loading,让用户看到“闪一下”的效果
|
||||||
|
window.dispatchEvent(new Event("loading:show"));
|
||||||
|
try {
|
||||||
|
await fetchTaskDetail();
|
||||||
|
await fetchData(pagination.current || 1, pagination.pageSize || 10, false);
|
||||||
|
} finally {
|
||||||
|
window.dispatchEvent(new Event("loading:hide"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!taskId) return;
|
||||||
|
try {
|
||||||
|
await deleteSynthesisTaskByIdUsingDelete(taskId);
|
||||||
|
message.success("合成任务已删除");
|
||||||
|
navigate("/data/synthesis/task");
|
||||||
|
} catch {
|
||||||
|
message.error("删除合成任务失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 头部统计与操作
|
||||||
|
const headerData: Record<string, unknown> = taskInfo
|
||||||
|
? {
|
||||||
|
name: taskInfo.name,
|
||||||
|
id: taskInfo.id,
|
||||||
|
icon: <Sparkles className="w-8 h-8" />,
|
||||||
|
description: taskInfo.description,
|
||||||
|
createdAt: taskInfo.created_at ? formatDateTime(taskInfo.created_at) : "--",
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const statistics = [
|
||||||
|
{
|
||||||
|
key: "type",
|
||||||
|
icon: <Sparkles className="w-4 h-4 text-blue-500" />,
|
||||||
|
label: "类型",
|
||||||
|
value:
|
||||||
|
taskInfo?.synthesis_type === "QA"
|
||||||
|
? "问答对生成"
|
||||||
|
: taskInfo?.synthesis_type === "COT"
|
||||||
|
? "链式推理生成"
|
||||||
|
: taskInfo?.synthesis_type || "--",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "fileCount",
|
||||||
|
icon: <Folder className="w-4 h-4 text-purple-500" />,
|
||||||
|
label: "文件数",
|
||||||
|
value: taskInfo?.total_files ?? "--",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const operations = [
|
||||||
|
{
|
||||||
|
key: "refresh",
|
||||||
|
label: "刷新",
|
||||||
|
icon: <ReloadOutlined className="w-4 h-4" />,
|
||||||
|
onClick: handleRefresh,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "delete",
|
||||||
|
label: "删除任务",
|
||||||
|
icon: <DeleteOutlined className="w-4 h-4" />,
|
||||||
|
danger: true,
|
||||||
|
confirm: {
|
||||||
|
title: "确认删除该合成任务?",
|
||||||
|
description: "删除后将无法恢复,请谨慎操作。",
|
||||||
|
okText: "确认删除",
|
||||||
|
cancelText: "取消",
|
||||||
|
onConfirm: handleDelete,
|
||||||
|
placement: "top",
|
||||||
|
overlayStyle: {
|
||||||
|
marginTop: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tabList = [
|
||||||
|
{
|
||||||
|
key: "files",
|
||||||
|
label: "处理文件",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const breadItems = [
|
||||||
|
{
|
||||||
|
title: <Link to="/data/synthesis/task">合成任务</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: taskInfo?.name || "任务详情",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-white rounded-lg h-full flex flex-col">
|
|
||||||
{/* 顶部任务信息和返回按钮 */}
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
{taskInfo && (
|
|
||||||
<>
|
<>
|
||||||
<div className="text-lg font-medium flex items-center gap-2">
|
<Breadcrumb items={breadItems} />
|
||||||
<span>{taskInfo.name}</span>
|
<div className="mb-4 mt-4">
|
||||||
<span className="text-xs px-2 py-0.5 rounded bg-blue-50 text-blue-700 border border-blue-200">
|
<DetailHeader data={headerData} statistics={statistics} operations={operations} />
|
||||||
{taskInfo.synthesis_type === "QA" ? "问答对生成" : taskInfo.synthesis_type === "COT" ? "链式推理生成" : taskInfo.synthesis_type}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded bg-gray-50 text-gray-700 border border-gray-200">
|
|
||||||
状态:{taskInfo.status === "pending" ? "等待中" : taskInfo.status === "completed" ? "已完成" : taskInfo.status === "failed" ? "失败" : taskInfo.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 flex gap-4">
|
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
|
||||||
<span>创建时间:{formatDateTime(taskInfo.created_at)}</span>
|
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
|
||||||
<span>模型ID:{taskInfo.model_id}</span>
|
<div className="h-full flex-1 overflow-auto">
|
||||||
</div>
|
{activeTab === "files" && (
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button type="default" onClick={() => navigate("/data/synthesis/task")}>返回任务首页</Button>
|
|
||||||
</div>
|
|
||||||
{/* 文件任务表格 */}
|
|
||||||
<Table<SynthesisFileTaskItem>
|
<Table<SynthesisFileTaskItem>
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -189,6 +334,9 @@ export default function SynthFileTask() {
|
|||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Card, Button, Table, Modal, message, Tooltip, Form, Input, Select } from "antd";
|
import { Card, Button, Table, Modal, message, Tooltip, Form, Input, Select } from "antd";
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
@@ -50,11 +50,6 @@ interface SynthesisTask {
|
|||||||
updated_by?: string;
|
updated_by?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SynthesisDataItem {
|
|
||||||
id: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SynthesisTaskTab() {
|
export default function SynthesisTaskTab() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
@@ -74,11 +69,6 @@ export default function SynthesisTaskTab() {
|
|||||||
|
|
||||||
const [evalForm] = Form.useForm();
|
const [evalForm] = Form.useForm();
|
||||||
|
|
||||||
// 合成数据相关状态
|
|
||||||
const [activeChunkId, setActiveChunkId] = useState<string | null>(null);
|
|
||||||
const [synthesisData, setSynthesisData] = useState<SynthesisDataItem[]>([]);
|
|
||||||
const [selectedDataIds, setSelectedDataIds] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// 获取任务列表
|
// 获取任务列表
|
||||||
const loadTasks = async () => {
|
const loadTasks = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -118,7 +108,16 @@ export default function SynthesisTaskTab() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 表格列
|
// 表格列
|
||||||
|
const ellipsisStyle = {
|
||||||
|
maxWidth: 100,
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
display: "inline-block",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
};
|
||||||
const taskColumns = [
|
const taskColumns = [
|
||||||
|
|
||||||
{
|
{
|
||||||
title: (
|
title: (
|
||||||
<Button
|
<Button
|
||||||
@@ -145,6 +144,7 @@ export default function SynthesisTaskTab() {
|
|||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
key: "name",
|
key: "name",
|
||||||
fixed: "left" as const,
|
fixed: "left" as const,
|
||||||
|
width: 160,
|
||||||
render: (_: unknown, task: SynthesisTask) => (
|
render: (_: unknown, task: SynthesisTask) => (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center shadow-sm">
|
<div className="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center shadow-sm">
|
||||||
@@ -152,34 +152,63 @@ export default function SynthesisTaskTab() {
|
|||||||
{task.synthesis_type?.toUpperCase()?.slice(0, 1) || "T"}
|
{task.synthesis_type?.toUpperCase()?.slice(0, 1) || "T"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<Tooltip title={task.name} placement="top">
|
||||||
|
<div style={{ ...ellipsisStyle, maxWidth: 100 }}>
|
||||||
<Link to={`/data/synthesis/task/${task.id}`}>{task.name}</Link>
|
<Link to={`/data/synthesis/task/${task.id}`}>{task.name}</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "任务ID",
|
||||||
|
dataIndex: "id",
|
||||||
|
key: "id",
|
||||||
|
width: 140,
|
||||||
|
render: (id: string) => (
|
||||||
|
<Tooltip title={id} placement="top">
|
||||||
|
<span style={{ ...ellipsisStyle, maxWidth: 140 }}>{id}</span>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "类型",
|
title: "类型",
|
||||||
dataIndex: "synthesis_type",
|
dataIndex: "synthesis_type",
|
||||||
key: "synthesis_type",
|
key: "synthesis_type",
|
||||||
render: (type: string) => typeMap[type] || type,
|
width: 100,
|
||||||
|
render: (type: string) => (
|
||||||
|
<Tooltip title={typeMap[type] || type} placement="top">
|
||||||
|
<span style={{ ...ellipsisStyle, maxWidth: 100 }}>{typeMap[type] || type}</span>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "文件数",
|
title: "文件数",
|
||||||
dataIndex: "total_files",
|
dataIndex: "total_files",
|
||||||
key: "total_files",
|
key: "total_files",
|
||||||
render: (num: number, task: SynthesisTask) => <span>{num ?? (task.source_file_id?.length ?? 0)}</span>,
|
width: 70,
|
||||||
|
render: (num: number, task: SynthesisTask) => (
|
||||||
|
<Tooltip title={num ?? (task.source_file_id?.length ?? 0)} placement="top">
|
||||||
|
<span style={{ ...ellipsisStyle, maxWidth: 70 }}>{num ?? (task.source_file_id?.length ?? 0)}</span>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
dataIndex: "created_at",
|
dataIndex: "created_at",
|
||||||
key: "created_at",
|
key: "created_at",
|
||||||
render: (val: string) => formatDateTime(val),
|
width: 200,
|
||||||
|
render: (val: string) => (
|
||||||
|
<Tooltip title={formatDateTime(val)} placement="top">
|
||||||
|
<span style={{ ...ellipsisStyle, maxWidth: 200 }}>{formatDateTime(val)}</span>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
fixed: "right" as const,
|
fixed: "right" as const,
|
||||||
|
width: 120,
|
||||||
render: (_: unknown, task: SynthesisTask) => (
|
render: (_: unknown, task: SynthesisTask) => (
|
||||||
<div className="flex items-center justify-start gap-1">
|
<div className="flex items-center justify-start gap-1">
|
||||||
<Tooltip title="查看详情">
|
<Tooltip title="查看详情">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import datetime
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
class TextSplitConfig(BaseModel):
|
class TextSplitConfig(BaseModel):
|
||||||
@@ -27,13 +27,21 @@ class SynthesisType(Enum):
|
|||||||
class CreateSynthesisTaskRequest(BaseModel):
|
class CreateSynthesisTaskRequest(BaseModel):
|
||||||
"""创建数据合成任务请求"""
|
"""创建数据合成任务请求"""
|
||||||
name: str = Field(..., description="合成任务名称")
|
name: str = Field(..., description="合成任务名称")
|
||||||
description: str = Field(None, description="合成任务描述")
|
description: Optional[str] = Field(None, description="合成任务描述")
|
||||||
model_id: str = Field(..., description="模型ID")
|
model_id: str = Field(..., description="模型ID")
|
||||||
source_file_id: list[str] = Field(..., description="原始文件ID列表")
|
source_file_id: list[str] = Field(..., description="原始文件ID列表")
|
||||||
text_split_config: TextSplitConfig = Field(None, description="文本切片配置")
|
text_split_config: TextSplitConfig = Field(None, description="文本切片配置")
|
||||||
synthesis_config: SynthesisConfig = Field(..., description="合成配置")
|
synthesis_config: SynthesisConfig = Field(..., description="合成配置")
|
||||||
synthesis_type: SynthesisType = Field(..., description="合成类型")
|
synthesis_type: SynthesisType = Field(..., description="合成类型")
|
||||||
|
|
||||||
|
@field_validator("description")
|
||||||
|
@classmethod
|
||||||
|
def empty_string_to_none(cls, v: Optional[str]) -> Optional[str]:
|
||||||
|
"""前端如果传入空字符串,将其统一转换为 None,避免存库时看起来像有描述但实际上为空。"""
|
||||||
|
if isinstance(v, str) and v.strip() == "":
|
||||||
|
return None
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
class DataSynthesisTaskItem(BaseModel):
|
class DataSynthesisTaskItem(BaseModel):
|
||||||
"""数据合成任务列表/详情项"""
|
"""数据合成任务列表/详情项"""
|
||||||
|
|||||||
Reference in New Issue
Block a user