annotation-view.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /**
  2. * AnnotationView Component
  3. *
  4. * Annotation interface with LabelStudio editor integration.
  5. * Requirements: 3.2, 8.1, 8.2, 8.3, 10.4
  6. */
  7. import React, { useEffect, useState, useRef } from 'react';
  8. import { useParams, useNavigate } from 'react-router-dom';
  9. import { useAtom } from 'jotai';
  10. import {
  11. Button,
  12. IconArrowLeft,
  13. IconCheck,
  14. IconForward,
  15. } from '@humansignal/ui';
  16. import { getTask, getProject, createAnnotation, updateTask } from '../../services/api';
  17. import { currentTaskAtom } from '../../atoms/task-atoms';
  18. import { currentProjectAtom } from '../../atoms/project-atoms';
  19. export const AnnotationView: React.FC = () => {
  20. const { id } = useParams<{ id: string }>();
  21. const navigate = useNavigate();
  22. const [currentTask, setCurrentTask] = useAtom(currentTaskAtom);
  23. const [currentProject, setCurrentProject] = useAtom(currentProjectAtom);
  24. const [loading, setLoading] = useState(true);
  25. const [error, setError] = useState<string | null>(null);
  26. const [isSaving, setIsSaving] = useState(false);
  27. const editorContainerRef = useRef<HTMLDivElement>(null);
  28. // Load task and project data
  29. useEffect(() => {
  30. if (!id) return;
  31. const loadData = async () => {
  32. try {
  33. setLoading(true);
  34. setError(null);
  35. // Load task details
  36. const taskData = await getTask(id);
  37. setCurrentTask(taskData);
  38. // Load project details to get annotation config
  39. const projectData = await getProject(taskData.project_id);
  40. setCurrentProject(projectData);
  41. } catch (err: any) {
  42. setError(err.message || '加载任务失败');
  43. } finally {
  44. setLoading(false);
  45. }
  46. };
  47. loadData();
  48. }, [id]);
  49. const handleSave = async () => {
  50. if (!currentTask || !id) return;
  51. try {
  52. setIsSaving(true);
  53. // TODO: Get annotation result from LabelStudio editor
  54. const annotationResult = {};
  55. // Create annotation
  56. await createAnnotation({
  57. task_id: id,
  58. user_id: 'current_user', // TODO: Get from auth context
  59. result: annotationResult,
  60. });
  61. // Update task status to in_progress or completed
  62. await updateTask(id, {
  63. status: 'in_progress',
  64. });
  65. // Navigate back to tasks list
  66. navigate('/tasks');
  67. } catch (err: any) {
  68. setError(err.message || '保存标注失败');
  69. } finally {
  70. setIsSaving(false);
  71. }
  72. };
  73. const handleSkip = () => {
  74. // Navigate back to tasks list without saving
  75. navigate('/tasks');
  76. };
  77. if (loading) {
  78. return (
  79. <div className="flex items-center justify-center h-full">
  80. <div className="text-center">
  81. <p className="text-body-medium text-secondary-foreground">加载中...</p>
  82. </div>
  83. </div>
  84. );
  85. }
  86. if (error) {
  87. return (
  88. <div className="flex flex-col gap-comfortable h-full">
  89. <div className="flex items-center gap-comfortable pb-comfortable border-b border-neutral-border">
  90. <Button
  91. variant="neutral"
  92. look="string"
  93. size="small"
  94. onClick={() => navigate('/tasks')}
  95. leading={<IconArrowLeft className="size-4" />}
  96. >
  97. 返回
  98. </Button>
  99. </div>
  100. <div className="flex-1 flex items-center justify-center">
  101. <div className="bg-error-background text-error-foreground p-comfortable rounded-lg border border-error-border max-w-md">
  102. <div className="flex flex-col gap-tight">
  103. <span className="text-body-medium font-semibold">加载失败</span>
  104. <span className="text-body-medium">{error}</span>
  105. </div>
  106. </div>
  107. </div>
  108. </div>
  109. );
  110. }
  111. if (!currentTask || !currentProject) {
  112. return (
  113. <div className="flex items-center justify-center h-full">
  114. <div className="text-center">
  115. <p className="text-body-medium text-secondary-foreground">任务不存在</p>
  116. </div>
  117. </div>
  118. );
  119. }
  120. return (
  121. <div className="flex flex-col h-full">
  122. {/* Header */}
  123. <div className="flex items-center justify-between p-comfortable border-b border-neutral-border bg-primary-background">
  124. <div className="flex items-center gap-comfortable">
  125. <Button
  126. variant="neutral"
  127. look="string"
  128. size="small"
  129. onClick={() => navigate('/tasks')}
  130. leading={<IconArrowLeft className="size-4" />}
  131. >
  132. 返回
  133. </Button>
  134. <div>
  135. <h1 className="text-heading-medium font-bold text-primary-foreground">
  136. {currentTask.name}
  137. </h1>
  138. <p className="text-body-small text-secondary-foreground">
  139. 项目: {currentProject.name} | 进度: {currentTask.progress}%
  140. </p>
  141. </div>
  142. </div>
  143. <div className="flex items-center gap-tight">
  144. <Button
  145. variant="neutral"
  146. size="medium"
  147. onClick={handleSkip}
  148. disabled={isSaving}
  149. leading={<IconForward className="size-4" />}
  150. >
  151. 跳过
  152. </Button>
  153. <Button
  154. variant="primary"
  155. size="medium"
  156. onClick={handleSave}
  157. disabled={isSaving}
  158. leading={<IconCheck className="size-4" />}
  159. >
  160. {isSaving ? '保存中...' : '保存'}
  161. </Button>
  162. </div>
  163. </div>
  164. {/* Editor Container */}
  165. <div className="flex-1 overflow-hidden bg-secondary-background">
  166. <div
  167. ref={editorContainerRef}
  168. className="w-full h-full"
  169. id="label-studio-editor"
  170. >
  171. {/* LabelStudio editor will be mounted here */}
  172. <div className="flex items-center justify-center h-full">
  173. <div className="text-center">
  174. <h2 className="text-heading-medium font-semibold text-primary-foreground mb-tight">
  175. 标注编辑器
  176. </h2>
  177. <p className="text-body-medium text-secondary-foreground mb-comfortable">
  178. LabelStudio 编辑器将在此处加载
  179. </p>
  180. <div className="bg-primary-background border border-neutral-border rounded-lg p-comfortable max-w-2xl mx-auto">
  181. <h3 className="text-body-medium font-semibold text-primary-foreground mb-tight">
  182. 任务数据:
  183. </h3>
  184. <pre className="text-body-small font-mono text-primary-foreground text-left bg-secondary-background p-tight rounded border border-neutral-border overflow-auto">
  185. {JSON.stringify(currentTask.data, null, 2)}
  186. </pre>
  187. </div>
  188. </div>
  189. </div>
  190. </div>
  191. </div>
  192. </div>
  193. );
  194. };