| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- /**
- * AnnotationView Component
- *
- * Annotation interface with LabelStudio editor integration.
- * Requirements: 3.2, 8.1, 8.2, 8.3, 10.4
- */
- import React, { useEffect, useState, useRef } from 'react';
- import { useParams, useNavigate } from 'react-router-dom';
- import { useAtom } from 'jotai';
- import {
- Button,
- IconArrowLeft,
- IconCheck,
- IconForward,
- } from '@humansignal/ui';
- import { getTask, getProject, createAnnotation, updateTask } from '../../services/api';
- import { currentTaskAtom } from '../../atoms/task-atoms';
- import { currentProjectAtom } from '../../atoms/project-atoms';
- export const AnnotationView: React.FC = () => {
- const { id } = useParams<{ id: string }>();
- const navigate = useNavigate();
-
- const [currentTask, setCurrentTask] = useAtom(currentTaskAtom);
- const [currentProject, setCurrentProject] = useAtom(currentProjectAtom);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const [isSaving, setIsSaving] = useState(false);
-
- const editorContainerRef = useRef<HTMLDivElement>(null);
- // Load task and project data
- useEffect(() => {
- if (!id) return;
- const loadData = async () => {
- try {
- setLoading(true);
- setError(null);
-
- // Load task details
- const taskData = await getTask(id);
- setCurrentTask(taskData);
-
- // Load project details to get annotation config
- const projectData = await getProject(taskData.project_id);
- setCurrentProject(projectData);
- } catch (err: any) {
- setError(err.message || '加载任务失败');
- } finally {
- setLoading(false);
- }
- };
- loadData();
- }, [id]);
- const handleSave = async () => {
- if (!currentTask || !id) return;
- try {
- setIsSaving(true);
-
- // TODO: Get annotation result from LabelStudio editor
- const annotationResult = {};
-
- // Create annotation
- await createAnnotation({
- task_id: id,
- user_id: 'current_user', // TODO: Get from auth context
- result: annotationResult,
- });
-
- // Update task status to in_progress or completed
- await updateTask(id, {
- status: 'in_progress',
- });
-
- // Navigate back to tasks list
- navigate('/tasks');
- } catch (err: any) {
- setError(err.message || '保存标注失败');
- } finally {
- setIsSaving(false);
- }
- };
- const handleSkip = () => {
- // Navigate back to tasks list without saving
- navigate('/tasks');
- };
- if (loading) {
- return (
- <div className="flex items-center justify-center h-full">
- <div className="text-center">
- <p className="text-body-medium text-secondary-foreground">加载中...</p>
- </div>
- </div>
- );
- }
- if (error) {
- return (
- <div className="flex flex-col gap-comfortable h-full">
- <div className="flex items-center gap-comfortable pb-comfortable border-b border-neutral-border">
- <Button
- variant="neutral"
- look="string"
- size="small"
- onClick={() => navigate('/tasks')}
- leading={<IconArrowLeft className="size-4" />}
- >
- 返回
- </Button>
- </div>
- <div className="flex-1 flex items-center justify-center">
- <div className="bg-error-background text-error-foreground p-comfortable rounded-lg border border-error-border max-w-md">
- <div className="flex flex-col gap-tight">
- <span className="text-body-medium font-semibold">加载失败</span>
- <span className="text-body-medium">{error}</span>
- </div>
- </div>
- </div>
- </div>
- );
- }
- if (!currentTask || !currentProject) {
- return (
- <div className="flex items-center justify-center h-full">
- <div className="text-center">
- <p className="text-body-medium text-secondary-foreground">任务不存在</p>
- </div>
- </div>
- );
- }
- return (
- <div className="flex flex-col h-full">
- {/* Header */}
- <div className="flex items-center justify-between p-comfortable border-b border-neutral-border bg-primary-background">
- <div className="flex items-center gap-comfortable">
- <Button
- variant="neutral"
- look="string"
- size="small"
- onClick={() => navigate('/tasks')}
- leading={<IconArrowLeft className="size-4" />}
- >
- 返回
- </Button>
- <div>
- <h1 className="text-heading-medium font-bold text-primary-foreground">
- {currentTask.name}
- </h1>
- <p className="text-body-small text-secondary-foreground">
- 项目: {currentProject.name} | 进度: {currentTask.progress}%
- </p>
- </div>
- </div>
- <div className="flex items-center gap-tight">
- <Button
- variant="neutral"
- size="medium"
- onClick={handleSkip}
- disabled={isSaving}
- leading={<IconForward className="size-4" />}
- >
- 跳过
- </Button>
- <Button
- variant="primary"
- size="medium"
- onClick={handleSave}
- disabled={isSaving}
- leading={<IconCheck className="size-4" />}
- >
- {isSaving ? '保存中...' : '保存'}
- </Button>
- </div>
- </div>
- {/* Editor Container */}
- <div className="flex-1 overflow-hidden bg-secondary-background">
- <div
- ref={editorContainerRef}
- className="w-full h-full"
- id="label-studio-editor"
- >
- {/* LabelStudio editor will be mounted here */}
- <div className="flex items-center justify-center h-full">
- <div className="text-center">
- <h2 className="text-heading-medium font-semibold text-primary-foreground mb-tight">
- 标注编辑器
- </h2>
- <p className="text-body-medium text-secondary-foreground mb-comfortable">
- LabelStudio 编辑器将在此处加载
- </p>
- <div className="bg-primary-background border border-neutral-border rounded-lg p-comfortable max-w-2xl mx-auto">
- <h3 className="text-body-medium font-semibold text-primary-foreground mb-tight">
- 任务数据:
- </h3>
- <pre className="text-body-small font-mono text-primary-foreground text-left bg-secondary-background p-tight rounded border border-neutral-border overflow-auto">
- {JSON.stringify(currentTask.data, null, 2)}
- </pre>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- };
|