You've already forked DataMate
feat(data-annotation): 添加任务预加载功能以提升用户体验
- 引入 UpsertAnnotationResponse 类型定义用于处理标注更新响应 - 移除废弃的 mergeTaskPages 函数并优化任务列表合并逻辑 - 新增 prefetchSeqRef 和 prefetching 状态管理预加载过程 - 实现 startPrefetchTasks 函数用于后台预加载剩余页的任务数据 - 更新 loadTasks 函数移除 refresh 模式并集成预加载机制 - 修改标注保存逻辑直接更新本地任务状态而非重新加载全部数据 - 在加载按钮中显示预加载状态提示用户当前操作进度 - 项目切换时重置预加载序列号确保状态一致性
This commit is contained in:
@@ -69,6 +69,11 @@ type EditorTaskListResponse = {
|
|||||||
size?: number;
|
size?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UpsertAnnotationResponse = {
|
||||||
|
annotationId?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type ExportPayload = {
|
type ExportPayload = {
|
||||||
taskId?: number | string | null;
|
taskId?: number | string | null;
|
||||||
fileId?: string | null;
|
fileId?: string | null;
|
||||||
@@ -161,9 +166,6 @@ const mergeTaskItems = (base: EditorTaskListItem[], next: EditorTaskListItem[])
|
|||||||
return merged;
|
return merged;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mergeTaskPages = (pages: EditorTaskListItem[][]) =>
|
|
||||||
pages.reduce((acc, page) => mergeTaskItems(acc, page), []);
|
|
||||||
|
|
||||||
const normalizeTaskListResponse = (
|
const normalizeTaskListResponse = (
|
||||||
response: ApiResponse<EditorTaskListResponse> | null | undefined,
|
response: ApiResponse<EditorTaskListResponse> | null | undefined,
|
||||||
fallbackPage: number,
|
fallbackPage: number,
|
||||||
@@ -193,6 +195,7 @@ export default function LabelStudioTextEditor() {
|
|||||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||||
const initSeqRef = useRef(0);
|
const initSeqRef = useRef(0);
|
||||||
const expectedTaskIdRef = useRef<number | null>(null);
|
const expectedTaskIdRef = useRef<number | null>(null);
|
||||||
|
const prefetchSeqRef = useRef(0);
|
||||||
const exportCheckRef = useRef<{
|
const exportCheckRef = useRef<{
|
||||||
requestId: string;
|
requestId: string;
|
||||||
resolve: (payload?: ExportPayload) => void;
|
resolve: (payload?: ExportPayload) => void;
|
||||||
@@ -216,6 +219,7 @@ export default function LabelStudioTextEditor() {
|
|||||||
const [taskTotal, setTaskTotal] = useState(0);
|
const [taskTotal, setTaskTotal] = useState(0);
|
||||||
const [taskTotalPages, setTaskTotalPages] = useState(0);
|
const [taskTotalPages, setTaskTotalPages] = useState(0);
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [prefetching, setPrefetching] = useState(false);
|
||||||
const [selectedFileId, setSelectedFileId] = useState<string>("");
|
const [selectedFileId, setSelectedFileId] = useState<string>("");
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [autoSaveOnSwitch, setAutoSaveOnSwitch] = useState(false);
|
const [autoSaveOnSwitch, setAutoSaveOnSwitch] = useState(false);
|
||||||
@@ -267,8 +271,41 @@ export default function LabelStudioTextEditor() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const startPrefetchTasks = useCallback((startPage: number, totalPages: number) => {
|
||||||
|
if (!projectId) return;
|
||||||
|
if (startPage >= totalPages) {
|
||||||
|
setPrefetching(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const seq = ++prefetchSeqRef.current;
|
||||||
|
setPrefetching(true);
|
||||||
|
const run = async () => {
|
||||||
|
for (let page = startPage; page < totalPages; page += 1) {
|
||||||
|
if (prefetchSeqRef.current !== seq) return;
|
||||||
|
try {
|
||||||
|
const resp = (await listEditorTasksUsingGet(projectId, {
|
||||||
|
page,
|
||||||
|
size: TASK_PAGE_SIZE,
|
||||||
|
})) as ApiResponse<EditorTaskListResponse>;
|
||||||
|
const normalized = normalizeTaskListResponse(resp, page);
|
||||||
|
setTasks((prev) => mergeTaskItems(prev, normalized.items));
|
||||||
|
setTaskPage((prev) => Math.max(prev, normalized.page));
|
||||||
|
setTaskTotal(normalized.total);
|
||||||
|
setTaskTotalPages(normalized.totalPages);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prefetchSeqRef.current === seq) {
|
||||||
|
setPrefetching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void run();
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
const loadTasks = useCallback(async (options?: {
|
const loadTasks = useCallback(async (options?: {
|
||||||
mode?: "reset" | "append" | "refresh";
|
mode?: "reset" | "append";
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
@@ -277,53 +314,33 @@ export default function LabelStudioTextEditor() {
|
|||||||
if (mode === "append" && taskTotalPages > 0 && taskPage + 1 >= taskTotalPages) {
|
if (mode === "append" && taskTotalPages > 0 && taskPage + 1 >= taskTotalPages) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mode === "reset") {
|
||||||
|
prefetchSeqRef.current += 1;
|
||||||
|
setPrefetching(false);
|
||||||
|
}
|
||||||
if (mode === "append") {
|
if (mode === "append") {
|
||||||
setLoadingMore(true);
|
setLoadingMore(true);
|
||||||
} else if (!silent) {
|
} else if (!silent) {
|
||||||
setLoadingTasks(true);
|
setLoadingTasks(true);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (mode === "refresh") {
|
|
||||||
const firstResp = (await listEditorTasksUsingGet(projectId, {
|
|
||||||
page: TASK_PAGE_START,
|
|
||||||
size: TASK_PAGE_SIZE,
|
|
||||||
})) as ApiResponse<EditorTaskListResponse>;
|
|
||||||
const firstNormalized = normalizeTaskListResponse(firstResp, TASK_PAGE_START);
|
|
||||||
const lastPage = Math.min(
|
|
||||||
taskPage,
|
|
||||||
Math.max(firstNormalized.totalPages - 1, TASK_PAGE_START),
|
|
||||||
);
|
|
||||||
const pages: EditorTaskListItem[][] = [firstNormalized.items];
|
|
||||||
for (let page = TASK_PAGE_START + 1; page <= lastPage; page += 1) {
|
|
||||||
const resp = (await listEditorTasksUsingGet(projectId, {
|
|
||||||
page,
|
|
||||||
size: TASK_PAGE_SIZE,
|
|
||||||
})) as ApiResponse<EditorTaskListResponse>;
|
|
||||||
const normalized = normalizeTaskListResponse(resp, page);
|
|
||||||
pages.push(normalized.items);
|
|
||||||
}
|
|
||||||
const mergedItems = mergeTaskPages(pages);
|
|
||||||
setTasks(mergedItems);
|
|
||||||
setTaskPage(lastPage);
|
|
||||||
setTaskTotal(firstNormalized.total);
|
|
||||||
setTaskTotalPages(firstNormalized.totalPages);
|
|
||||||
updateTaskSelection(mergedItems);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextPage = mode === "append" ? taskPage + 1 : TASK_PAGE_START;
|
const nextPage = mode === "append" ? taskPage + 1 : TASK_PAGE_START;
|
||||||
const resp = (await listEditorTasksUsingGet(projectId, {
|
const resp = (await listEditorTasksUsingGet(projectId, {
|
||||||
page: nextPage,
|
page: nextPage,
|
||||||
size: TASK_PAGE_SIZE,
|
size: TASK_PAGE_SIZE,
|
||||||
})) as ApiResponse<EditorTaskListResponse>;
|
})) as ApiResponse<EditorTaskListResponse>;
|
||||||
const normalized = normalizeTaskListResponse(resp, nextPage);
|
const normalized = normalizeTaskListResponse(resp, nextPage);
|
||||||
const nextItems =
|
if (mode === "append") {
|
||||||
mode === "append" ? mergeTaskItems(tasks, normalized.items) : normalized.items;
|
setTasks((prev) => mergeTaskItems(prev, normalized.items));
|
||||||
setTasks(nextItems);
|
setTaskPage((prev) => Math.max(prev, normalized.page));
|
||||||
|
} else {
|
||||||
|
setTasks(normalized.items);
|
||||||
setTaskPage(normalized.page);
|
setTaskPage(normalized.page);
|
||||||
|
updateTaskSelection(normalized.items);
|
||||||
|
}
|
||||||
setTaskTotal(normalized.total);
|
setTaskTotal(normalized.total);
|
||||||
setTaskTotalPages(normalized.totalPages);
|
setTaskTotalPages(normalized.totalPages);
|
||||||
updateTaskSelection(nextItems);
|
startPrefetchTasks(normalized.page + 1, normalized.totalPages);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
if (!silent && mode !== "append") message.error("获取文件列表失败");
|
if (!silent && mode !== "append") message.error("获取文件列表失败");
|
||||||
@@ -340,7 +357,7 @@ export default function LabelStudioTextEditor() {
|
|||||||
setLoadingTasks(false);
|
setLoadingTasks(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [message, projectId, taskPage, taskTotalPages, tasks, updateTaskSelection]);
|
}, [message, projectId, startPrefetchTasks, taskPage, taskTotalPages, updateTaskSelection]);
|
||||||
|
|
||||||
const initEditorForFile = useCallback(async (fileId: string, segmentIdx?: number) => {
|
const initEditorForFile = useCallback(async (fileId: string, segmentIdx?: number) => {
|
||||||
if (!project?.supported) return;
|
if (!project?.supported) return;
|
||||||
@@ -503,12 +520,23 @@ export default function LabelStudioTextEditor() {
|
|||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await upsertEditorAnnotationUsingPut(projectId, String(fileId), {
|
const resp = (await upsertEditorAnnotationUsingPut(projectId, String(fileId), {
|
||||||
annotation,
|
annotation,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
});
|
})) as ApiResponse<UpsertAnnotationResponse>;
|
||||||
|
const updatedAt = resp?.data?.updatedAt;
|
||||||
message.success("标注已保存");
|
message.success("标注已保存");
|
||||||
await loadTasks({ mode: "refresh", silent: true });
|
setTasks((prev) =>
|
||||||
|
prev.map((item) =>
|
||||||
|
item.fileId === String(fileId)
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
hasAnnotation: true,
|
||||||
|
annotationUpdatedAt: updatedAt || item.annotationUpdatedAt,
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const snapshotKey = buildSnapshotKey(String(fileId), segmentIndex);
|
const snapshotKey = buildSnapshotKey(String(fileId), segmentIndex);
|
||||||
const snapshot = buildAnnotationSnapshot(isRecord(annotation) ? annotation : undefined);
|
const snapshot = buildAnnotationSnapshot(isRecord(annotation) ? annotation : undefined);
|
||||||
@@ -538,7 +566,6 @@ export default function LabelStudioTextEditor() {
|
|||||||
}, [
|
}, [
|
||||||
advanceAfterSave,
|
advanceAfterSave,
|
||||||
currentSegmentIndex,
|
currentSegmentIndex,
|
||||||
loadTasks,
|
|
||||||
message,
|
message,
|
||||||
projectId,
|
projectId,
|
||||||
segmented,
|
segmented,
|
||||||
@@ -695,6 +722,8 @@ export default function LabelStudioTextEditor() {
|
|||||||
setTaskTotal(0);
|
setTaskTotal(0);
|
||||||
setTaskTotalPages(0);
|
setTaskTotalPages(0);
|
||||||
setLoadingMore(false);
|
setLoadingMore(false);
|
||||||
|
prefetchSeqRef.current += 1;
|
||||||
|
setPrefetching(false);
|
||||||
setSelectedFileId("");
|
setSelectedFileId("");
|
||||||
initSeqRef.current = 0;
|
initSeqRef.current = 0;
|
||||||
setLsReady(false);
|
setLsReady(false);
|
||||||
@@ -852,11 +881,16 @@ export default function LabelStudioTextEditor() {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
loading={loadingMore}
|
loading={loadingMore}
|
||||||
disabled={loadingTasks}
|
disabled={loadingTasks || prefetching}
|
||||||
onClick={() => loadTasks({ mode: "append" })}
|
onClick={() => loadTasks({ mode: "append" })}
|
||||||
>
|
>
|
||||||
加载更多
|
加载更多
|
||||||
</Button>
|
</Button>
|
||||||
|
{prefetching && (
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 10, marginLeft: 8 }}>
|
||||||
|
后台加载中...
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
{taskTotal > 0 && (
|
{taskTotal > 0 && (
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 10, marginLeft: 8 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 10, marginLeft: 8 }}>
|
||||||
已加载 {tasks.length}/{taskTotal}
|
已加载 {tasks.length}/{taskTotal}
|
||||||
|
|||||||
Reference in New Issue
Block a user