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;
|
||||
};
|
||||
|
||||
type UpsertAnnotationResponse = {
|
||||
annotationId?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
type ExportPayload = {
|
||||
taskId?: number | string | null;
|
||||
fileId?: string | null;
|
||||
@@ -161,9 +166,6 @@ const mergeTaskItems = (base: EditorTaskListItem[], next: EditorTaskListItem[])
|
||||
return merged;
|
||||
};
|
||||
|
||||
const mergeTaskPages = (pages: EditorTaskListItem[][]) =>
|
||||
pages.reduce((acc, page) => mergeTaskItems(acc, page), []);
|
||||
|
||||
const normalizeTaskListResponse = (
|
||||
response: ApiResponse<EditorTaskListResponse> | null | undefined,
|
||||
fallbackPage: number,
|
||||
@@ -193,6 +195,7 @@ export default function LabelStudioTextEditor() {
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const initSeqRef = useRef(0);
|
||||
const expectedTaskIdRef = useRef<number | null>(null);
|
||||
const prefetchSeqRef = useRef(0);
|
||||
const exportCheckRef = useRef<{
|
||||
requestId: string;
|
||||
resolve: (payload?: ExportPayload) => void;
|
||||
@@ -216,6 +219,7 @@ export default function LabelStudioTextEditor() {
|
||||
const [taskTotal, setTaskTotal] = useState(0);
|
||||
const [taskTotalPages, setTaskTotalPages] = useState(0);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [prefetching, setPrefetching] = useState(false);
|
||||
const [selectedFileId, setSelectedFileId] = useState<string>("");
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = 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?: {
|
||||
mode?: "reset" | "append" | "refresh";
|
||||
mode?: "reset" | "append";
|
||||
silent?: boolean;
|
||||
}) => {
|
||||
if (!projectId) return;
|
||||
@@ -277,53 +314,33 @@ export default function LabelStudioTextEditor() {
|
||||
if (mode === "append" && taskTotalPages > 0 && taskPage + 1 >= taskTotalPages) {
|
||||
return;
|
||||
}
|
||||
if (mode === "reset") {
|
||||
prefetchSeqRef.current += 1;
|
||||
setPrefetching(false);
|
||||
}
|
||||
if (mode === "append") {
|
||||
setLoadingMore(true);
|
||||
} else if (!silent) {
|
||||
setLoadingTasks(true);
|
||||
}
|
||||
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 resp = (await listEditorTasksUsingGet(projectId, {
|
||||
page: nextPage,
|
||||
size: TASK_PAGE_SIZE,
|
||||
})) as ApiResponse<EditorTaskListResponse>;
|
||||
const normalized = normalizeTaskListResponse(resp, nextPage);
|
||||
const nextItems =
|
||||
mode === "append" ? mergeTaskItems(tasks, normalized.items) : normalized.items;
|
||||
setTasks(nextItems);
|
||||
setTaskPage(normalized.page);
|
||||
if (mode === "append") {
|
||||
setTasks((prev) => mergeTaskItems(prev, normalized.items));
|
||||
setTaskPage((prev) => Math.max(prev, normalized.page));
|
||||
} else {
|
||||
setTasks(normalized.items);
|
||||
setTaskPage(normalized.page);
|
||||
updateTaskSelection(normalized.items);
|
||||
}
|
||||
setTaskTotal(normalized.total);
|
||||
setTaskTotalPages(normalized.totalPages);
|
||||
updateTaskSelection(nextItems);
|
||||
startPrefetchTasks(normalized.page + 1, normalized.totalPages);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (!silent && mode !== "append") message.error("获取文件列表失败");
|
||||
@@ -340,7 +357,7 @@ export default function LabelStudioTextEditor() {
|
||||
setLoadingTasks(false);
|
||||
}
|
||||
}
|
||||
}, [message, projectId, taskPage, taskTotalPages, tasks, updateTaskSelection]);
|
||||
}, [message, projectId, startPrefetchTasks, taskPage, taskTotalPages, updateTaskSelection]);
|
||||
|
||||
const initEditorForFile = useCallback(async (fileId: string, segmentIdx?: number) => {
|
||||
if (!project?.supported) return;
|
||||
@@ -503,12 +520,23 @@ export default function LabelStudioTextEditor() {
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await upsertEditorAnnotationUsingPut(projectId, String(fileId), {
|
||||
const resp = (await upsertEditorAnnotationUsingPut(projectId, String(fileId), {
|
||||
annotation,
|
||||
segmentIndex,
|
||||
});
|
||||
})) as ApiResponse<UpsertAnnotationResponse>;
|
||||
const updatedAt = resp?.data?.updatedAt;
|
||||
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 snapshot = buildAnnotationSnapshot(isRecord(annotation) ? annotation : undefined);
|
||||
@@ -538,7 +566,6 @@ export default function LabelStudioTextEditor() {
|
||||
}, [
|
||||
advanceAfterSave,
|
||||
currentSegmentIndex,
|
||||
loadTasks,
|
||||
message,
|
||||
projectId,
|
||||
segmented,
|
||||
@@ -695,6 +722,8 @@ export default function LabelStudioTextEditor() {
|
||||
setTaskTotal(0);
|
||||
setTaskTotalPages(0);
|
||||
setLoadingMore(false);
|
||||
prefetchSeqRef.current += 1;
|
||||
setPrefetching(false);
|
||||
setSelectedFileId("");
|
||||
initSeqRef.current = 0;
|
||||
setLsReady(false);
|
||||
@@ -852,11 +881,16 @@ export default function LabelStudioTextEditor() {
|
||||
<Button
|
||||
size="small"
|
||||
loading={loadingMore}
|
||||
disabled={loadingTasks}
|
||||
disabled={loadingTasks || prefetching}
|
||||
onClick={() => loadTasks({ mode: "append" })}
|
||||
>
|
||||
加载更多
|
||||
</Button>
|
||||
{prefetching && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 10, marginLeft: 8 }}>
|
||||
后台加载中...
|
||||
</Typography.Text>
|
||||
)}
|
||||
{taskTotal > 0 && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 10, marginLeft: 8 }}>
|
||||
已加载 {tasks.length}/{taskTotal}
|
||||
|
||||
Reference in New Issue
Block a user