From fa160164d2cc49015a6e7d048371658aacfabbe4 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 26 Jan 2026 11:44:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(annotation):=20=E6=B7=BB=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E6=A0=87=E6=B3=A8=E7=BC=96=E8=BE=91=E5=99=A8=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=AE=B5=E8=90=BD=E6=A0=91=E5=AF=BC=E8=88=AA=E5=92=8C?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B7=B3=E8=BD=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 Tree 和 Empty 组件用于段落导航展示 - 实现分段树形结构数据生成和展示功能 - 添加自动跳转到下一个待标注文件或段落的功能 - 优化文件选择逻辑,优先选择未标注的文件 - 实现段落切换时的状态管理和依赖更新 - 添加段落树节点选中和展开状态控制 - 优化界面布局和滚动区域的高度计算 --- .../Annotate/LabelStudioTextEditor.tsx | 199 ++++++++++++++---- 1 file changed, 157 insertions(+), 42 deletions(-) diff --git a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx index 0196dfa..3fa53a5 100644 --- a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx +++ b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { App, Button, Card, List, Spin, Typography, Tag, Switch } from "antd"; +import { App, Button, Card, List, Spin, Typography, Tag, Switch, Tree, Empty } from "antd"; import { LeftOutlined, ReloadOutlined, SaveOutlined, MenuFoldOutlined, MenuUnfoldOutlined, CheckOutlined } from "@ant-design/icons"; import { useNavigate, useParams } from "react-router"; @@ -151,6 +151,7 @@ export default function LabelStudioTextEditor() { } | null>(null); const exportCheckSeqRef = useRef(0); const savedSnapshotsRef = useRef>({}); + const pendingAutoAdvanceRef = useRef(false); const [loadingProject, setLoadingProject] = useState(true); const [loadingTasks, setLoadingTasks] = useState(false); @@ -208,9 +209,12 @@ export default function LabelStudioTextEditor() { const content = resp?.data?.content || []; const items = Array.isArray(content) ? content : []; setTasks(items); - if (items.length > 0) { - setSelectedFileId((prev) => prev || (items[0]?.fileId ?? "")); - } + const defaultFileId = + items.find((item) => !item.hasAnnotation)?.fileId || items[0]?.fileId || ""; + setSelectedFileId((prev) => { + if (prev && items.some((item) => item.fileId === prev)) return prev; + return defaultFileId; + }); } catch (e) { console.error(e); if (!silent) message.error("获取文件列表失败"); @@ -318,7 +322,46 @@ export default function LabelStudioTextEditor() { } }, [iframeReady, message, postToIframe, project, projectId]); - const saveFromExport = useCallback(async (payload?: ExportPayload | null) => { + const advanceAfterSave = useCallback(async (fileId: string, segmentIndex?: number) => { + if (!fileId) return; + if (segmented && segments.length > 0) { + const sortedSegmentIndices = segments + .map((seg) => seg.idx) + .sort((a, b) => a - b); + const baseIndex = segmentIndex ?? currentSegmentIndex; + const currentPos = sortedSegmentIndices.indexOf(baseIndex); + const nextSegmentIndex = + currentPos >= 0 ? sortedSegmentIndices[currentPos + 1] : sortedSegmentIndices[0]; + if (nextSegmentIndex !== undefined) { + await initEditorForFile(fileId, nextSegmentIndex); + return; + } + } + + if (tasks.length === 0) { + message.info("暂无可跳转的数据"); + return; + } + const currentFileIndex = tasks.findIndex((item) => item.fileId === fileId); + const nextTask = currentFileIndex >= 0 ? tasks[currentFileIndex + 1] : tasks[0]; + if (nextTask?.fileId) { + setSelectedFileId(nextTask.fileId); + return; + } + message.info("已是最后一个数据"); + }, [ + currentSegmentIndex, + initEditorForFile, + message, + segmented, + segments, + tasks, + ]); + + const saveFromExport = useCallback(async ( + payload?: ExportPayload | null, + options?: { autoAdvance?: boolean } + ) => { const payloadTaskId = payload?.taskId; if (expectedTaskIdRef.current && payloadTaskId) { if (Number(payloadTaskId) !== expectedTaskIdRef.current) { @@ -363,6 +406,9 @@ export default function LabelStudioTextEditor() { ) ); } + if (options?.autoAdvance) { + await advanceAfterSave(String(fileId), segmentIndex); + } return true; } catch (e) { console.error(e); @@ -372,6 +418,7 @@ export default function LabelStudioTextEditor() { setSaving(false); } }, [ + advanceAfterSave, currentSegmentIndex, loadTasks, message, @@ -442,11 +489,12 @@ export default function LabelStudioTextEditor() { message.warning("请先选择文件"); return; } + pendingAutoAdvanceRef.current = true; postToIframe("LS_EXPORT", {}); }; // 段落切换处理 - const handleSegmentChange = async (newIndex: number) => { + const handleSegmentChange = useCallback(async (newIndex: number) => { if (newIndex === currentSegmentIndex) return; if (segmentSwitching || saving || loadingTaskDetail) return; if (!iframeReady || !lsReady) { @@ -504,7 +552,22 @@ export default function LabelStudioTextEditor() { } finally { setSegmentSwitching(false); } - }; + }, [ + autoSaveOnSwitch, + confirmSaveBeforeSwitch, + currentSegmentIndex, + iframeReady, + initEditorForFile, + loadingTaskDetail, + lsReady, + message, + requestExportForCheck, + saveFromExport, + segmented, + selectedFileId, + segmentSwitching, + saving, + ]); useEffect(() => { setIframeReady(false); @@ -537,6 +600,51 @@ export default function LabelStudioTextEditor() { initEditorForFile(selectedFileId); }, [selectedFileId, iframeReady, initEditorForFile]); + const segmentTreeData = useMemo(() => { + if (!segmented || segments.length === 0) return []; + const lineMap = new Map(); + segments.forEach((seg) => { + const list = lineMap.get(seg.lineIndex) || []; + list.push(seg); + lineMap.set(seg.lineIndex, list); + }); + return Array.from(lineMap.entries()) + .sort((a, b) => a[0] - b[0]) + .map(([lineIndex, lineSegments]) => ({ + key: `line-${lineIndex}`, + title: `第${lineIndex + 1}行`, + selectable: false, + children: lineSegments + .sort((a, b) => a.chunkIndex - b.chunkIndex) + .map((seg) => ({ + key: `seg-${seg.idx}`, + title: ( + + {`片${seg.chunkIndex + 1}`} + {seg.hasAnnotation && ( + + )} + + ), + })), + })); + }, [segmented, segments]); + + const segmentLineKeys = useMemo( + () => segmentTreeData.map((item) => String(item.key)), + [segmentTreeData] + ); + + const handleSegmentSelect = useCallback((keys: Array) => { + const [first] = keys; + if (first === undefined || first === null) return; + const key = String(first); + if (!key.startsWith("seg-")) return; + const nextIndex = Number(key.replace("seg-", "")); + if (!Number.isFinite(nextIndex)) return; + handleSegmentChange(nextIndex); + }, [handleSegmentChange]); + useEffect(() => { const handler = (event: MessageEvent) => { if (event.origin !== origin) return; @@ -560,7 +668,9 @@ export default function LabelStudioTextEditor() { } if (msg.type === "LS_EXPORT_RESULT") { - saveFromExport(payload); + const shouldAutoAdvance = pendingAutoAdvanceRef.current; + pendingAutoAdvanceRef.current = false; + saveFromExport(payload, { autoAdvance: shouldAutoAdvance }); return; } @@ -579,7 +689,7 @@ export default function LabelStudioTextEditor() { // 兼容 iframe 内部在 submit 时直接上报(若启用) if (msg.type === "LS_SUBMIT") { - saveFromExport(payload); + saveFromExport(payload, { autoAdvance: false }); return; } @@ -587,6 +697,7 @@ export default function LabelStudioTextEditor() { const payloadMessage = resolvePayloadMessage(msg.payload); message.error(payloadMessage || "编辑器发生错误"); setLsReady(false); + pendingAutoAdvanceRef.current = false; } }; @@ -668,13 +779,13 @@ export default function LabelStudioTextEditor() {
{/* 左侧文件列表 - 可折叠 */}
文件列表
-
+
-
- - {/* 右侧编辑器 - Label Studio iframe */} -
- {/* 段落导航栏 */} - {segmented && segments.length > 0 && ( -
- - 段落: - -
-
- {segments.map((seg) => ( - - ))} -
+ {segmented && ( +
+
+ 段落/分段 + + {currentSegmentIndex + 1} / {segments.length} +
-
- {currentSegmentIndex + 1} / {segments.length} - 切段自动保存 +
+ {segments.length > 0 ? ( + + ) : ( +
+ +
+ )} +
+
+ + 切段自动保存 +
)} +
+ {/* 右侧编辑器 - Label Studio iframe */} +
{/* 编辑器区域 */}
{(!iframeReady || loadingTaskDetail || (selectedFileId && !lsReady)) && (