diff --git a/deployment/helm/label-studio/templates/_helpers.tpl b/deployment/helm/label-studio/templates/_helpers.tpl index 0e6b7695..04d95dd9 100644 --- a/deployment/helm/label-studio/templates/_helpers.tpl +++ b/deployment/helm/label-studio/templates/_helpers.tpl @@ -2,7 +2,7 @@ Expand the name of the chart. */}} {{- define "label-studio.name" -}} -{{- default .Chart.name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- default .Values.nameOverride .Chart.Name | trunc 63 | trimSuffix "-" -}} {{- end -}} {{/* @@ -12,7 +12,7 @@ Create a default fully qualified app name. {{- if .Values.fullnameOverride -}} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} {{- else -}} -{{- $name := default .Chart.name .Values.nameOverride -}} +{{- $name := default .Values.nameOverride .Chart.Name -}} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} {{- end -}} {{- end -}} @@ -21,6 +21,5 @@ Create a default fully qualified app name. Create chart name and version as used by the chart label. */}} {{- 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 -}} - diff --git a/deployment/helm/label-studio/values.yaml b/deployment/helm/label-studio/values.yaml index 347be476..696e5a40 100644 --- a/deployment/helm/label-studio/values.yaml +++ b/deployment/helm/label-studio/values.yaml @@ -1,5 +1,6 @@ # Default values for label-studio Helm chart. # This mirrors the configuration from deployment/docker/label-studio/docker-compose.yml +fullnameOverride: label-studio replicaCount: 1 @@ -24,9 +25,9 @@ postgres: size: 10Gi service: - type: ClusterIP + type: NodePort port: 8000 - nodePort: null + nodePort: 30001 # Corresponds to docker-compose port mapping 30001:8000 ingress: @@ -54,7 +55,7 @@ env: POSTGRE_USER: "postgres" POSTGRE_PASSWORD: "" POSTGRE_PORT: 5432 - POSTGRE_HOST: "db" + POSTGRE_HOST: "label-studio-postgres" LABEL_STUDIO_HOST: "" # can be overridden LOCAL_FILES_SERVING_ENABLED: "true" 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. datasetVolume: 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 + diff --git a/frontend/src/pages/SynthesisTask/CreateTask.tsx b/frontend/src/pages/SynthesisTask/CreateTask.tsx index 357f75fb..5c00a2ce 100644 --- a/frontend/src/pages/SynthesisTask/CreateTask.tsx +++ b/frontend/src/pages/SynthesisTask/CreateTask.tsx @@ -146,9 +146,8 @@ export default function SynthesisTaskCreate() { } // 构造后端要求的参数格式 - const payload = { - name: values.name || form.getFieldValue("name"), // 必选,确保传递 - description: values.description ?? "", // 可选,始终传递 + const payload: Record = { + name: values.name || form.getFieldValue("name"), model_id: selectedModel, source_file_id: selectedFiles, text_split_config: { @@ -161,10 +160,14 @@ export default function SynthesisTaskCreate() { 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); - const res = (await createSynthesisTaskUsingPost( - payload as unknown as Record - )) as CreateTaskApiResponse; + const res = (await createSynthesisTaskUsingPost(payload)) as CreateTaskApiResponse; const ok = res?.success === true || diff --git a/frontend/src/pages/SynthesisTask/SynthDataDetail.tsx b/frontend/src/pages/SynthesisTask/SynthDataDetail.tsx index eae232b0..bfc364b1 100644 --- a/frontend/src/pages/SynthesisTask/SynthDataDetail.tsx +++ b/frontend/src/pages/SynthesisTask/SynthDataDetail.tsx @@ -1,6 +1,20 @@ import { useEffect, useMemo, useState } from "react"; -import { useLocation, useNavigate, useParams } from "react-router"; -import { Badge, Button, Empty, List, Pagination, Spin, Typography, Popconfirm, message, Dropdown, Input } from "antd"; +import { useLocation, useNavigate, useParams, Link } from "react-router"; +import { + Badge, + Empty, + List, + Pagination, + Spin, + Typography, + Popconfirm, + message, + Dropdown, + Input, + Breadcrumb, + Button, + Tag, +} from "antd"; import type { PaginationProps } from "antd"; import { MoreOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons"; import { @@ -69,6 +83,8 @@ export default function SynthDataDetail() { const [chunkLoading, setChunkLoading] = useState(false); const [dataLoading, setDataLoading] = useState(false); const [synthDataList, setSynthDataList] = useState([]); + const [chunkConfirmVisibleId, setChunkConfirmVisibleId] = useState(null); + const [dataConfirmVisibleId, setDataConfirmVisibleId] = useState(null); // 加载任务信息(用于顶部展示) useEffect(() => { @@ -237,291 +253,296 @@ export default function SynthDataDetail() { } }; + const breadItems = [ + { + title: 合成任务, + }, + { + title: state.taskId ? ( + {taskInfo?.name || "任务详情"} + ) : ( + taskInfo?.name || "任务详情" + ), + }, + { + title: state.fileName || "文件详情", + }, + ]; + + const showChunkConfirm = (id: string) => setChunkConfirmVisibleId(id); + const hideChunkConfirm = () => setChunkConfirmVisibleId(null); + + const showDataConfirm = (id: string) => setDataConfirmVisibleId(id); + const hideDataConfirm = () => setDataConfirmVisibleId(null); + return ( -
- {/* 顶部信息和返回 */} -
-
-
- - 合成数据详情 - - {state.fileName && ( - - 文件:{state.fileName} - - )} -
- {taskInfo && ( -
- - 任务:{taskInfo.name} - - - 类型: - {taskInfo.synthesis_type === "QA" - ? "问答对生成" - : taskInfo.synthesis_type === "COT" - ? "链式推理生成" - : taskInfo.synthesis_type} - - - 创建时间:{formatDateTime(taskInfo.created_at)} - - 模型ID:{taskInfo.model_id} + <> + + {/* 全局删除确认遮罩:Chunk */} + {chunkConfirmVisibleId && ( +
+
+
确认删除该 Chunk 及其合成数据?
+
+ ID: {chunkConfirmVisibleId}
- )} +
+ + +
+
- -
+ )} - {/* 主体左右布局 */} -
- {/* 左侧 Chunk 列表:占比 2/5 */} -
-
- Chunk 列表 -
-
- {chunkLoading ? ( -
- -
- ) : chunks.length === 0 ? ( - - ) : ( - +
+
确认删除该条合成数据?
+
+ ID: {dataConfirmVisibleId} +
+
+ +
-
- - ); + type="primary" + danger + onClick={async () => { + await handleDeleteSingleSynthesisData(dataConfirmVisibleId); + hideDataConfirm(); }} - /> - )} -
-
- `共 ${total} 条`} - /> + > + 删除 + +
+ )} - {/* 右侧合成数据展示:占比 3/5 */} -
-
- 合成数据 - {currentChunk && ( - - 当前 Chunk #{currentChunk.chunk_index} - - )} -
-
- {dataLoading ? ( -
- +
+
+ {/* 左侧 Chunk 列表 */} +
+
+
+ Chunk 列表 + {chunkPagination.total ? ( + 共 {chunkPagination.total} 条 + ) : null}
- ) : !selectedChunkId ? ( - - ) : synthDataList.length === 0 ? ( - - ) : ( -
- {synthDataList.map((item, index) => { - const isEditing = editingId === item.id; - return ( -
-
- 记录 {index + 1} - ID:{item.id} -
- - {/* 右下角更多操作按钮:编辑 & 删除 */} -
- - - 编辑 - - ), - onClick: (info) => { - info.domEvent.stopPropagation(); - startEdit(item); - }, - }, - { - key: "delete-data", - danger: true, - label: ( - { - e?.stopPropagation(); - handleDeleteSingleSynthesisData(item.id); - }} - okText="删除" - cancelText="取消" - > - - - 删除 - - - ), - }, - ], - }} - trigger={["click"]} +
+ {chunkLoading ? ( +
+ +
+ ) : chunks.length === 0 ? ( + + ) : ( + { + const active = item.id === selectedChunkId; + return ( + setSelectedChunkId(item.id)} > -
- - {/* 表格形式的 key-value 展示 + 可编辑 value */} -
- {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 ( -
-
- {key} +
+
+
+ + Chunk #{item.chunk_index} + +
+
+ {/* 右侧显示 Chunk ID,完整展示 */} + + ID: {item.id} + +
-
- {isEditing ? ( - { - const v = e.target.value; - setEditingMap((prev) => ({ ...prev, [key]: v })); +
+
+ {item.chunk_content} +
+
+ + ); + }} + /> + )} +
+ {chunkPagination.total ? ( +
+ `共 ${total} 条`} + /> +
+ ) : null} +
+
+ + {/* 右侧合成数据展示 */} +
+
+
+ 合成数据 + {currentChunk && ( + + 当前 Chunk #{currentChunk.chunk_index} + + )} +
+
+ {dataLoading ? ( +
+ +
+ ) : !selectedChunkId ? ( + + ) : synthDataList.length === 0 ? ( + + ) : ( +
+ {synthDataList.map((item, index) => { + const isEditing = editingId === item.id; + return ( +
+
+ 记录 {index + 1} +
+ ID:{item.id} + {!isEditing && ( + <> +
+ + )}
- ); - })} -
- - {isEditing && ( -
- - +
+ + {/* key-value 展示区域:不再截断,完整展示 */} +
+ {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 ( +
+
+ {key} +
+
+ {isEditing ? ( + { + const v = e.target.value; + setEditingMap((prev) => ({ ...prev, [key]: v })); + }} + autoSize={{ minRows: 1, maxRows: 6 }} + /> + ) : ( + displayValue + )} +
+
+ ); + })} +
+ + {isEditing && ( +
+ + +
+ )}
- )} -
- ); - })} + ); + })} +
+ )}
- )} +
-
+ ); } diff --git a/frontend/src/pages/SynthesisTask/SynthFileTask.tsx b/frontend/src/pages/SynthesisTask/SynthFileTask.tsx index a71f84bf..7fc9c12d 100644 --- a/frontend/src/pages/SynthesisTask/SynthFileTask.tsx +++ b/frontend/src/pages/SynthesisTask/SynthFileTask.tsx @@ -1,10 +1,19 @@ +import { App } from "antd"; import { useEffect, useState } from "react"; -import { useParams, useNavigate } from "react-router"; -import { Table, Badge, Button } from "antd"; -import type { ColumnsType, TablePaginationConfig } from "antd/es/table"; -import { querySynthesisFileTasksUsingGet, querySynthesisTaskByIdUsingGet } from "@/pages/SynthesisTask/synthesis-api"; +import { Link, useNavigate, useParams } from "react-router"; +import { DeleteOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Badge, Breadcrumb, Button, Table, Tabs, Progress, Tooltip } 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 { Folder, Sparkles, Trash2 } from "lucide-react"; interface SynthesisFileTaskItem { id: string; @@ -33,12 +42,18 @@ interface SynthesisTaskInfo { synthesis_type: string; status: string; created_at: string; + updated_at?: string; model_id: string; + total_files?: number; + total_synthesis_data?: number; + description?: string; } export default function SynthFileTask() { const { id: taskId = "" } = useParams(); const navigate = useNavigate(); + const { message } = App.useApp(); + const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [pagination, setPagination] = useState({ @@ -47,17 +62,34 @@ export default function SynthFileTask() { total: 0, }); const [taskInfo, setTaskInfo] = useState(null); + const [activeTab, setActiveTab] = useState("files"); // 查询总任务详情 - useEffect(() => { + const fetchTaskDetail = async () => { if (!taskId) return; - querySynthesisTaskByIdUsingGet(taskId).then((res) => { - setTaskInfo(res?.data?.data || null); - }); + try { + 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]); - const fetchData = async (page = 1, pageSize = 10) => { + const fetchData = async (page = 1, pageSize = 10, withTopLoading = false) => { if (!taskId) return; + + if (withTopLoading) { + window.dispatchEvent(new Event("loading:show")); + } + setLoading(true); try { const res = await querySynthesisFileTasksUsingGet(taskId, { @@ -80,30 +112,41 @@ export default function SynthFileTask() { }); } finally { setLoading(false); + if (withTopLoading) { + window.dispatchEvent(new Event("loading:hide")); + } } }; useEffect(() => { - fetchData(1, pagination.pageSize || 10); + // 首次进入或任务切换时,不触发顶部 loading,只用表格自带的 loading + fetchData(1, pagination.pageSize || 10, false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [taskId]); const handleTableChange = (pag: TablePaginationConfig) => { - fetchData(pag.current || 1, pag.pageSize || 10); + // 分页切换时,也只用表格 loading,不闪顶部条 + fetchData(pag.current || 1, pag.pageSize || 10, false); }; const columns: ColumnsType = [ { - title: "文件名", - dataIndex: "file_name", - key: "file_name", - render: (text: string, record) => ( - + title: "文件", + key: "file", + render: (_text, record) => ( +
+ + +
), }, { @@ -113,13 +156,13 @@ export default function SynthFileTask() { render: (status?: string) => { let badgeStatus: BadgeProps["status"] = "default"; let text = status || "未知"; - if (status === "pending" || status === "processing") { + if (status === "pending" || status === "PROCESSING" || status === "processing") { badgeStatus = "processing"; text = "处理中"; - } else if (status === "completed") { + } else if (status === "COMPLETED" || status === "completed") { badgeStatus = "success"; text = "已完成"; - } else if (status === "failed") { + } else if (status === "FAILED" || status === "failed") { badgeStatus = "error"; text = "失败"; } @@ -127,19 +170,28 @@ export default function SynthFileTask() { }, }, { - title: "切片进度", - key: "chunks", - render: (_text, record) => ( - - {record.processed_chunks}/{record.total_chunks} - - ), + title: "切片总数", + dataIndex: "total_chunks", + key: "total_chunks", }, { - title: "目标文件路径", - dataIndex: "target_file_location", - key: "target_file_location", - ellipsis: true, + title: "处理进度", + key: "progress", + render: (_text, record) => { + 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 ( +
+ `${processed}/${total}`} + /> +
+ ); + }, }, { title: "创建时间", @@ -153,42 +205,138 @@ export default function SynthFileTask() { key: "updated_at", render: (val?: string) => (val ? formatDateTime(val) : "-"), }, + { + title: "操作", + key: "actions", + render: () => ( + +
- {/* 文件任务表格 */} - - rowKey="id" - loading={loading} - dataSource={data} - columns={columns} - pagination={pagination} - onChange={handleTableChange} - /> -
+ ); } diff --git a/frontend/src/pages/SynthesisTask/components/SynthesisTaskTab.tsx b/frontend/src/pages/SynthesisTask/components/SynthesisTaskTab.tsx index 832fa323..01a0b538 100644 --- a/frontend/src/pages/SynthesisTask/components/SynthesisTaskTab.tsx +++ b/frontend/src/pages/SynthesisTask/components/SynthesisTaskTab.tsx @@ -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 { Plus, @@ -50,11 +50,6 @@ interface SynthesisTask { updated_by?: string; } -interface SynthesisDataItem { - id: string; - [key: string]: any; -} - export default function SynthesisTaskTab() { const navigate = useNavigate(); const [searchQuery, setSearchQuery] = useState(""); @@ -74,11 +69,6 @@ export default function SynthesisTaskTab() { const [evalForm] = Form.useForm(); - // 合成数据相关状态 - const [activeChunkId, setActiveChunkId] = useState(null); - const [synthesisData, setSynthesisData] = useState([]); - const [selectedDataIds, setSelectedDataIds] = useState([]); - // 获取任务列表 const loadTasks = async () => { 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 = [ + { title: (