Skip to content

Commit 977d163

Browse files
authored
feat(synthesis): add chunk-level synthesis data detail page & refine APIs/routing (#130)
* feat: implement synthesis data detail view with chunk selection and data display
1 parent 7012a9a commit 977d163

File tree

4 files changed

+330
-6
lines changed

4 files changed

+330
-6
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
import { useEffect, useMemo, useState } from "react";
2+
import { useLocation, useNavigate, useParams } from "react-router";
3+
import { Badge, Button, Empty, List, Pagination, Spin, Typography } from "antd";
4+
import type { PaginationProps } from "antd";
5+
import { queryChunksByFileUsingGet, querySynthesisDataByChunkUsingGet, querySynthesisTaskByIdUsingGet } from "@/pages/SynthesisTask/synthesis-api";
6+
import { formatDateTime } from "@/utils/unit";
7+
8+
interface LocationState {
9+
fileName?: string;
10+
taskId?: string;
11+
}
12+
13+
interface ChunkItem {
14+
id: string;
15+
synthesis_file_instance_id: string;
16+
chunk_index: number;
17+
chunk_content: string;
18+
chunk_metadata?: Record<string, unknown>;
19+
}
20+
21+
interface PagedChunkResponse {
22+
content: ChunkItem[];
23+
totalElements: number;
24+
totalPages: number;
25+
page: number;
26+
size: number;
27+
}
28+
29+
interface SynthesisDataItem {
30+
id: string;
31+
data: Record<string, unknown>;
32+
synthesis_file_instance_id: string;
33+
chunk_instance_id: string;
34+
}
35+
36+
interface SynthesisTaskInfo {
37+
id: string;
38+
name: string;
39+
synthesis_type: string;
40+
status: string;
41+
created_at: string;
42+
model_id: string;
43+
}
44+
45+
const { Title, Text } = Typography;
46+
47+
export default function SynthDataDetail() {
48+
const { id: fileId = "" } = useParams();
49+
const navigate = useNavigate();
50+
const location = useLocation();
51+
const state = (location.state || {}) as LocationState;
52+
53+
const [taskInfo, setTaskInfo] = useState<SynthesisTaskInfo | null>(null);
54+
const [chunks, setChunks] = useState<ChunkItem[]>([]);
55+
const [chunkPagination, setChunkPagination] = useState<{
56+
page: number;
57+
size: number;
58+
total: number;
59+
}>({ page: 1, size: 10, total: 0 });
60+
const [selectedChunkId, setSelectedChunkId] = useState<string | null>(null);
61+
const [chunkLoading, setChunkLoading] = useState(false);
62+
const [dataLoading, setDataLoading] = useState(false);
63+
const [synthDataList, setSynthDataList] = useState<SynthesisDataItem[]>([]);
64+
65+
// 加载任务信息(用于顶部展示)
66+
useEffect(() => {
67+
if (!state.taskId) return;
68+
querySynthesisTaskByIdUsingGet(state.taskId).then((res) => {
69+
setTaskInfo(res?.data?.data || null);
70+
});
71+
}, [state.taskId]);
72+
73+
const fetchChunks = async (page = 1, size = 10) => {
74+
if (!fileId) return;
75+
setChunkLoading(true);
76+
try {
77+
const res = await queryChunksByFileUsingGet(fileId, { page, page_size: size });
78+
const payload: PagedChunkResponse =
79+
res?.data?.data ?? res?.data ?? {
80+
content: [],
81+
totalElements: 0,
82+
totalPages: 0,
83+
page,
84+
size,
85+
};
86+
setChunks(payload.content || []);
87+
setChunkPagination({
88+
page: payload.page ?? page,
89+
size: payload.size ?? size,
90+
total: payload.totalElements ?? payload.content?.length ?? 0,
91+
});
92+
// 默认选中第一个 chunk
93+
if (!selectedChunkId && payload.content && payload.content.length > 0) {
94+
setSelectedChunkId(payload.content[0].id);
95+
}
96+
} finally {
97+
setChunkLoading(false);
98+
}
99+
};
100+
101+
useEffect(() => {
102+
fetchChunks(1, chunkPagination.size);
103+
// eslint-disable-next-line react-hooks/exhaustive-deps
104+
}, [fileId]);
105+
106+
const handleChunkPageChange: PaginationProps["onChange"] = (page, pageSize) => {
107+
fetchChunks(page, pageSize || 10);
108+
};
109+
110+
// 加载选中 chunk 的所有合成数据
111+
const fetchSynthData = async (chunkId: string) => {
112+
setDataLoading(true);
113+
try {
114+
const res = await querySynthesisDataByChunkUsingGet(chunkId);
115+
const list: SynthesisDataItem[] = res?.data?.data ?? res?.data ?? [];
116+
setSynthDataList(list || []);
117+
} finally {
118+
setDataLoading(false);
119+
}
120+
};
121+
122+
useEffect(() => {
123+
if (selectedChunkId) {
124+
fetchSynthData(selectedChunkId);
125+
} else {
126+
setSynthDataList([]);
127+
}
128+
}, [selectedChunkId]);
129+
130+
const currentChunk = useMemo(
131+
() => chunks.find((c) => c.id === selectedChunkId) || null,
132+
[chunks, selectedChunkId]
133+
);
134+
135+
// 将合成数据的 data 转换成键值对数组,方便以表格形式展示
136+
const getDataEntries = (data: Record<string, unknown>) => {
137+
return Object.entries(data || {});
138+
};
139+
140+
return (
141+
<div className="p-4 bg-white rounded-lg h-full flex flex-col overflow-hidden">
142+
{/* 顶部信息和返回 */}
143+
<div className="flex items-center justify-between mb-4">
144+
<div className="space-y-1">
145+
<div className="flex items-center gap-2">
146+
<Title level={4} style={{ margin: 0 }}>
147+
合成数据详情
148+
</Title>
149+
{state.fileName && (
150+
<Text type="secondary" className="!text-xs">
151+
文件:{state.fileName}
152+
</Text>
153+
)}
154+
</div>
155+
{taskInfo && (
156+
<div className="text-xs text-gray-500 flex gap-4">
157+
<span>
158+
任务:{taskInfo.name}
159+
</span>
160+
<span>
161+
类型:
162+
{taskInfo.synthesis_type === "QA"
163+
? "问答对生成"
164+
: taskInfo.synthesis_type === "COT"
165+
? "链式推理生成"
166+
: taskInfo.synthesis_type}
167+
</span>
168+
<span>
169+
创建时间:{formatDateTime(taskInfo.created_at)}
170+
</span>
171+
<span>模型ID:{taskInfo.model_id}</span>
172+
</div>
173+
)}
174+
</div>
175+
<Button onClick={() => navigate(-1)}>返回</Button>
176+
</div>
177+
178+
{/* 主体左右布局 */}
179+
<div className="flex flex-1 min-h-0 gap-4">
180+
{/* 左侧 Chunk 列表:占比 2/5 */}
181+
<div className="basis-2/5 max-w-[40%] border rounded-lg flex flex-col overflow-hidden">
182+
<div className="px-3 py-2 border-b text-sm font-medium bg-gray-50">
183+
Chunk 列表
184+
</div>
185+
<div className="flex-1 overflow-auto">
186+
{chunkLoading ? (
187+
<div className="h-full flex items-center justify-center">
188+
<Spin />
189+
</div>
190+
) : chunks.length === 0 ? (
191+
<Empty description="暂无 Chunk" style={{ marginTop: 40 }} />
192+
) : (
193+
<List
194+
size="small"
195+
dataSource={chunks}
196+
renderItem={(item) => {
197+
const active = item.id === selectedChunkId;
198+
return (
199+
<List.Item
200+
className={
201+
"cursor-pointer px-3 py-2 !border-0 " +
202+
(active ? "bg-blue-50" : "hover:bg-gray-50")
203+
}
204+
onClick={() => setSelectedChunkId(item.id)}
205+
>
206+
<div className="flex flex-col gap-1 w-full">
207+
<div className="flex items-center justify-between text-xs">
208+
<span className="font-medium">Chunk #{item.chunk_index}</span>
209+
<Badge
210+
color={active ? "blue" : "default"}
211+
text={active ? "当前" : ""}
212+
/>
213+
</div>
214+
{/* 展示 chunk 全部内容,不截断 */}
215+
<div className="text-xs text-gray-600 whitespace-pre-wrap break-words">
216+
{item.chunk_content}
217+
</div>
218+
</div>
219+
</List.Item>
220+
);
221+
}}
222+
/>
223+
)}
224+
</div>
225+
<div className="border-t px-2 py-1 flex justify-end bg-white">
226+
<Pagination
227+
size="small"
228+
current={chunkPagination.page}
229+
pageSize={chunkPagination.size}
230+
total={chunkPagination.total}
231+
onChange={handleChunkPageChange}
232+
showSizeChanger
233+
showTotal={(total) => `共 ${total} 条`}
234+
/>
235+
</div>
236+
</div>
237+
238+
{/* 右侧合成数据展示:占比 3/5 */}
239+
<div className="basis-3/5 max-w-[60%] border rounded-lg flex flex-col min-w-0 overflow-hidden">
240+
<div className="px-3 py-2 border-b flex items-center justify-between bg-gray-50 text-sm font-medium">
241+
<span>合成数据</span>
242+
{currentChunk && (
243+
<span className="text-xs text-gray-500">
244+
当前 Chunk #{currentChunk.chunk_index}
245+
</span>
246+
)}
247+
</div>
248+
<div className="flex-1 overflow-auto p-3">
249+
{dataLoading ? (
250+
<div className="h-full flex items-center justify-center">
251+
<Spin />
252+
</div>
253+
) : !selectedChunkId ? (
254+
<Empty description="请选择左侧 Chunk" style={{ marginTop: 40 }} />
255+
) : synthDataList.length === 0 ? (
256+
<Empty description="该 Chunk 暂无合成数据" style={{ marginTop: 40 }} />
257+
) : (
258+
<div className="space-y-4">
259+
{synthDataList.map((item, index) => (
260+
<div
261+
key={item.id || index}
262+
className="border border-gray-100 rounded-md p-3 bg-white shadow-sm/50"
263+
>
264+
<div className="mb-2 text-xs text-gray-500 flex justify-between">
265+
<span>记录 {index + 1}</span>
266+
<span>ID:{item.id}</span>
267+
</div>
268+
{/* 淡化表格样式的 key-value 展示 */}
269+
<div className="w-full border border-gray-100 rounded-md overflow-hidden">
270+
{getDataEntries(item.data).map(([key, value], rowIdx) => (
271+
<div
272+
key={key + rowIdx}
273+
className={
274+
"grid grid-cols-[120px,1fr] text-xs " +
275+
(rowIdx % 2 === 0 ? "bg-gray-50/60" : "bg-white")
276+
}
277+
>
278+
<div className="px-3 py-2 border-r border-gray-100 font-medium text-gray-600 break-words">
279+
{key}
280+
</div>
281+
<div className="px-3 py-2 text-gray-700 whitespace-pre-wrap break-words">
282+
{typeof value === "string" || typeof value === "number"
283+
? String(value)
284+
: JSON.stringify(value, null, 2)}
285+
</div>
286+
</div>
287+
))}
288+
</div>
289+
</div>
290+
))}
291+
</div>
292+
)}
293+
</div>
294+
</div>
295+
</div>
296+
</div>
297+
);
298+
}

frontend/src/pages/SynthesisTask/SynthFileTask.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ export default function SynthFileTask() {
9797
title: "文件名",
9898
dataIndex: "file_name",
9999
key: "file_name",
100+
render: (text: string, record) => (
101+
<Button
102+
type="link"
103+
onClick={() => navigate(`/data/synthesis/task/file/${record.id}/detail`, { state: { fileName: record.file_name, taskId } })}
104+
>
105+
{text}
106+
</Button>
107+
),
100108
},
101109
{
102110
title: "状态",

frontend/src/pages/SynthesisTask/synthesis-api.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { get, post, del } from "@/utils/request";
22

33
// 创建数据合成任务
4-
export function createSynthesisTaskUsingPost(data: unknown) {
5-
return post("/api/synthesis/gen/task", data);
4+
export function createSynthesisTaskUsingPost(data: Record<string, unknown>) {
5+
return post("/api/synthesis/gen/task", data as unknown as Record<string, never>);
66
}
77

88
// 获取数据合成任务详情
@@ -18,7 +18,7 @@ export function querySynthesisTasksUsingGet(params: {
1818
status?: string;
1919
name?: string;
2020
}) {
21-
return get(`/api/synthesis/gen/tasks`, params as any);
21+
return get(`/api/synthesis/gen/tasks`, params);
2222
}
2323

2424
// 删除整个数据合成任务
@@ -28,10 +28,20 @@ export function deleteSynthesisTaskByIdUsingDelete(taskId: string) {
2828

2929
// 分页查询某个任务下的文件任务列表
3030
export function querySynthesisFileTasksUsingGet(taskId: string, params: { page?: number; page_size?: number }) {
31-
return get(`/api/synthesis/gen/task/${taskId}/files`, params as any);
31+
return get(`/api/synthesis/gen/task/${taskId}/files`, params);
32+
}
33+
34+
// 根据文件任务 ID 分页查询 chunk 记录
35+
export function queryChunksByFileUsingGet(fileId: string, params: { page?: number; page_size?: number }) {
36+
return get(`/api/synthesis/gen/file/${fileId}/chunks`, params);
37+
}
38+
39+
// 根据 chunk ID 查询所有合成结果数据
40+
export function querySynthesisDataByChunkUsingGet(chunkId: string) {
41+
return get(`/api/synthesis/gen/chunk/${chunkId}/data`);
3242
}
3343

3444
// 获取不同合成类型对应的 Prompt
3545
export function getPromptByTypeUsingGet(synthType: string) {
36-
return get(`/api/synthesis/gen/prompt`, { synth_type: synthType } as any);
46+
return get(`/api/synthesis/gen/prompt`, { synth_type: synthType });
3747
}

frontend/src/routes/routes.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import RatioTaskDetail from "@/pages/RatioTask/Detail/RatioTaskDetail";
4242
import CleansingTemplateDetail from "@/pages/DataCleansing/Detail/TemplateDetail";
4343
import SynthFileTask from "@/pages/SynthesisTask/SynthFileTask.tsx";
4444
import EvaluationDetailPage from "@/pages/DataEvaluation/Detail/TaskDetail.tsx";
45+
import SynthDataDetail from "@/pages/SynthesisTask/SynthDataDetail.tsx";
4546

4647
const router = createBrowserRouter([
4748
{
@@ -161,7 +162,14 @@ const router = createBrowserRouter([
161162
path: "create",
162163
Component: SynthesisTaskCreate,
163164
},
164-
{path: ":id", Component: SynthFileTask},
165+
{
166+
path: ":id",
167+
Component: SynthFileTask
168+
},
169+
{
170+
path: "file/:id/detail",
171+
Component: SynthDataDetail,
172+
}
165173
],
166174
},
167175
{

0 commit comments

Comments
 (0)