You've already forked DataMate
Merge branch 'editor_next' into lsf
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { App, Button, Card, List, Spin, Typography } from "antd";
|
||||
import { LeftOutlined, ReloadOutlined, SaveOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
|
||||
import { App, Button, Card, List, Spin, Typography, Tag } from "antd";
|
||||
import { LeftOutlined, ReloadOutlined, SaveOutlined, MenuFoldOutlined, MenuUnfoldOutlined, CheckOutlined } from "@ant-design/icons";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
|
||||
import {
|
||||
@@ -32,6 +32,14 @@ type LsfMessage = {
|
||||
payload?: any;
|
||||
};
|
||||
|
||||
type SegmentInfo = {
|
||||
idx: number;
|
||||
text: string;
|
||||
start: number;
|
||||
end: number;
|
||||
hasAnnotation: boolean;
|
||||
};
|
||||
|
||||
const LSF_IFRAME_SRC = "/lsf/lsf.html";
|
||||
|
||||
export default function LabelStudioTextEditor() {
|
||||
@@ -56,6 +64,11 @@ export default function LabelStudioTextEditor() {
|
||||
const [selectedFileId, setSelectedFileId] = useState<string>("");
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
|
||||
// 分段相关状态
|
||||
const [segmented, setSegmented] = useState(false);
|
||||
const [segments, setSegments] = useState<SegmentInfo[]>([]);
|
||||
const [currentSegmentIndex, setCurrentSegmentIndex] = useState(0);
|
||||
|
||||
const postToIframe = (type: string, payload?: any) => {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
if (!win) return;
|
||||
@@ -102,7 +115,7 @@ export default function LabelStudioTextEditor() {
|
||||
}
|
||||
};
|
||||
|
||||
const initEditorForFile = async (fileId: string) => {
|
||||
const initEditorForFile = async (fileId: string, segmentIdx?: number) => {
|
||||
if (!project?.supported) return;
|
||||
if (!project?.labelConfig) {
|
||||
message.error("该项目未绑定标注模板,无法加载编辑器");
|
||||
@@ -116,14 +129,28 @@ export default function LabelStudioTextEditor() {
|
||||
expectedTaskIdRef.current = null;
|
||||
|
||||
try {
|
||||
const resp = (await getEditorTaskUsingGet(projectId, fileId)) as any;
|
||||
const task = resp?.data?.task;
|
||||
const resp = (await getEditorTaskUsingGet(projectId, fileId, {
|
||||
segmentIndex: segmentIdx,
|
||||
})) as any;
|
||||
const data = resp?.data;
|
||||
const task = data?.task;
|
||||
if (!task) {
|
||||
message.error("获取任务详情失败");
|
||||
return;
|
||||
}
|
||||
if (seq !== initSeqRef.current) return;
|
||||
|
||||
// 更新分段状态
|
||||
if (data?.segmented) {
|
||||
setSegmented(true);
|
||||
setSegments(data.segments || []);
|
||||
setCurrentSegmentIndex(data.currentSegmentIndex || 0);
|
||||
} else {
|
||||
setSegmented(false);
|
||||
setSegments([]);
|
||||
setCurrentSegmentIndex(0);
|
||||
}
|
||||
|
||||
expectedTaskIdRef.current = Number(task?.id) || null;
|
||||
postToIframe("LS_INIT", {
|
||||
labelConfig: project.labelConfig,
|
||||
@@ -173,9 +200,23 @@ export default function LabelStudioTextEditor() {
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await upsertEditorAnnotationUsingPut(projectId, String(fileId), { annotation });
|
||||
await upsertEditorAnnotationUsingPut(projectId, String(fileId), {
|
||||
annotation,
|
||||
segmentIndex: segmented ? currentSegmentIndex : undefined,
|
||||
});
|
||||
message.success("标注已保存");
|
||||
await loadTasks(true);
|
||||
|
||||
// 分段模式下更新当前段落的标注状态
|
||||
if (segmented) {
|
||||
setSegments((prev) =>
|
||||
prev.map((seg) =>
|
||||
seg.idx === currentSegmentIndex
|
||||
? { ...seg, hasAnnotation: true }
|
||||
: seg
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error("保存失败");
|
||||
@@ -192,6 +233,13 @@ export default function LabelStudioTextEditor() {
|
||||
postToIframe("LS_EXPORT", {});
|
||||
};
|
||||
|
||||
// 段落切换处理
|
||||
const handleSegmentChange = async (newIndex: number) => {
|
||||
if (newIndex === currentSegmentIndex) return;
|
||||
setCurrentSegmentIndex(newIndex);
|
||||
await initEditorForFile(selectedFileId, newIndex);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIframeReady(false);
|
||||
setProject(null);
|
||||
@@ -200,6 +248,10 @@ export default function LabelStudioTextEditor() {
|
||||
initSeqRef.current = 0;
|
||||
setLsReady(false);
|
||||
expectedTaskIdRef.current = null;
|
||||
// 重置分段状态
|
||||
setSegmented(false);
|
||||
setSegments([]);
|
||||
setCurrentSegmentIndex(0);
|
||||
|
||||
if (projectId) loadProject();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -379,26 +431,55 @@ export default function LabelStudioTextEditor() {
|
||||
</div>
|
||||
|
||||
{/* 右侧编辑器 - Label Studio iframe */}
|
||||
<div className="flex-1 relative">
|
||||
{(!iframeReady || loadingTaskDetail || (selectedFileId && !lsReady)) && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/80">
|
||||
<Spin
|
||||
tip={
|
||||
!iframeReady
|
||||
? "编辑器资源加载中..."
|
||||
: loadingTaskDetail
|
||||
? "任务数据加载中..."
|
||||
: "编辑器初始化中..."
|
||||
}
|
||||
/>
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{/* 段落导航栏 */}
|
||||
{segmented && segments.length > 0 && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-gray-50 border-b border-gray-200">
|
||||
<Typography.Text style={{ fontSize: 12 }}>段落:</Typography.Text>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{segments.map((seg) => (
|
||||
<Button
|
||||
key={seg.idx}
|
||||
size="small"
|
||||
type={seg.idx === currentSegmentIndex ? "primary" : "default"}
|
||||
onClick={() => handleSegmentChange(seg.idx)}
|
||||
style={{ minWidth: 32, padding: "0 8px" }}
|
||||
>
|
||||
{seg.idx + 1}
|
||||
{seg.hasAnnotation && (
|
||||
<CheckOutlined style={{ marginLeft: 2, fontSize: 10 }} />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Tag color="blue" style={{ marginLeft: 8 }}>
|
||||
{currentSegmentIndex + 1} / {segments.length}
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Label Studio Frontend"
|
||||
src={LSF_IFRAME_SRC}
|
||||
className="w-full h-full border-0"
|
||||
/>
|
||||
|
||||
{/* 编辑器区域 */}
|
||||
<div className="flex-1 relative">
|
||||
{(!iframeReady || loadingTaskDetail || (selectedFileId && !lsReady)) && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/80">
|
||||
<Spin
|
||||
tip={
|
||||
!iframeReady
|
||||
? "编辑器资源加载中..."
|
||||
: loadingTaskDetail
|
||||
? "任务数据加载中..."
|
||||
: "编辑器初始化中..."
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Label Studio Frontend"
|
||||
src={LSF_IFRAME_SRC}
|
||||
className="w-full h-full border-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,14 +69,22 @@ export function listEditorTasksUsingGet(projectId: string, params?: any) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}/tasks`, params);
|
||||
}
|
||||
|
||||
export function getEditorTaskUsingGet(projectId: string, fileId: string) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}/tasks/${fileId}`);
|
||||
export function getEditorTaskUsingGet(
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
params?: { segmentIndex?: number }
|
||||
) {
|
||||
return get(`/api/annotation/editor/projects/${projectId}/tasks/${fileId}`, params);
|
||||
}
|
||||
|
||||
export function upsertEditorAnnotationUsingPut(
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
data: any
|
||||
data: {
|
||||
annotation: any;
|
||||
expectedUpdatedAt?: string;
|
||||
segmentIndex?: number;
|
||||
}
|
||||
) {
|
||||
return put(`/api/annotation/editor/projects/${projectId}/tasks/${fileId}/annotation`, data);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user