@taskforcehq/taskforce 0.3.169 → 0.3.300
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -21
- package/dist/Taskforce.module.css +3394 -5981
- package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
- package/dist/TaskforceCore.filter-persistence.test.js +402 -0
- package/dist/TaskforceCore.js +388 -71
- package/dist/TaskforceCore.routing.test.js +311 -0
- package/dist/TaskforceCore.test.js +3344 -615
- package/dist/TaskforceWidget.js +3 -2
- package/dist/__tests__/localizationCatalogs.test.js +48 -31
- package/dist/__tests__/workflowSimulation.test.js +2 -2
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
- package/dist/annotatedAttachments/payload.d.ts +2 -0
- package/dist/annotatedAttachments/payload.js +54 -0
- package/dist/annotatedAttachments/payload.test.js +67 -0
- package/dist/annotatedAttachments/service.d.ts +49 -0
- package/dist/annotatedAttachments/service.js +121 -0
- package/dist/annotatedAttachments/types.d.ts +86 -0
- package/dist/cli.js +74 -25
- package/dist/compat/deploymentCompat.d.ts +12 -0
- package/dist/compat/deploymentCompat.js +43 -0
- package/dist/compat/workspaceSyncCompat.d.ts +6 -0
- package/dist/compat/workspaceSyncCompat.js +79 -0
- package/dist/compat/workspaceSyncCompat.test.js +87 -0
- package/dist/components/features/AgentsModule.d.ts +5 -0
- package/dist/components/features/AgentsModule.js +131 -0
- package/dist/components/features/AgentsModule.test.js +102 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
- package/dist/components/features/DocumentGenerateModal.js +8 -16
- package/dist/components/features/DocumentIndex.d.ts +3 -4
- package/dist/components/features/DocumentIndex.js +22 -10
- package/dist/components/features/DocumentViewer.d.ts +6 -2
- package/dist/components/features/DocumentViewer.js +1000 -18
- package/dist/components/features/DocumentWorkspace.d.ts +40 -2
- package/dist/components/features/DocumentWorkspace.js +96 -16
- package/dist/components/features/DocumentWorkspace.test.js +1202 -0
- package/dist/components/features/InitiativesModule.d.ts +22 -0
- package/dist/components/features/InitiativesModule.js +33 -0
- package/dist/components/features/TaskSettings.d.ts +41 -32
- package/dist/components/features/TaskSettings.js +798 -313
- package/dist/components/features/TaskSettings.test.js +1221 -47
- package/dist/components/features/WorkflowsModule.d.ts +27 -0
- package/dist/components/features/WorkflowsModule.js +21 -0
- package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
- package/dist/components/features/documentWorkspaceFilters.js +11 -0
- package/dist/components/features/settings/CategoryManager.d.ts +2 -0
- package/dist/components/features/settings/CategoryManager.js +15 -8
- package/dist/components/features/settings/PriorityManager.d.ts +18 -0
- package/dist/components/features/settings/PriorityManager.js +51 -0
- package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
- package/dist/components/features/settings/TaxonomyEditor.js +41 -18
- package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
- package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
- package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
- package/dist/components/features/settings/TaxonomyManager.js +59 -27
- package/dist/components/features/settings/TypeManager.d.ts +11 -0
- package/dist/components/features/settings/TypeManager.js +48 -11
- package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
- package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
- package/dist/components/task/TaskActionHeader.d.ts +12 -2
- package/dist/components/task/TaskActionHeader.js +64 -8
- package/dist/components/task/TaskActionHeader.test.js +73 -0
- package/dist/components/task/TaskCard.d.ts +14 -16
- package/dist/components/task/TaskCard.js +144 -174
- package/dist/components/task/TaskCard.test.js +416 -64
- package/dist/components/task/TaskContextUpload.d.ts +6 -5
- package/dist/components/task/TaskContextUpload.js +199 -64
- package/dist/components/task/TaskContextUpload.test.js +331 -14
- package/dist/components/task/TaskForm.d.ts +23 -29
- package/dist/components/task/TaskForm.js +348 -169
- package/dist/components/task/TaskForm.test.js +419 -23
- package/dist/components/task/TaskKanban.d.ts +20 -21
- package/dist/components/task/TaskKanban.js +176 -316
- package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
- package/dist/components/task/TaskList.d.ts +22 -11
- package/dist/components/task/TaskList.js +98 -12
- package/dist/components/task/TaskList.test.js +206 -7
- package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
- package/dist/components/task/TaskReferenceBadge.js +5 -0
- package/dist/components/task/TaskStatusActions.d.ts +12 -0
- package/dist/components/task/TaskStatusActions.js +18 -0
- package/dist/components/task/TaskStatusActions.test.js +45 -0
- package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
- package/dist/components/ui/ImageReferenceBadge.js +5 -0
- package/dist/components/ui/LevelSelector.d.ts +2 -1
- package/dist/components/ui/LevelSelector.js +2 -2
- package/dist/components/ui/Markdown.d.ts +1 -2
- package/dist/components/ui/Markdown.js +2 -13
- package/dist/components/ui/Modal.d.ts +3 -2
- package/dist/components/ui/Modal.js +4 -3
- package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
- package/dist/components/ui/MultiSelectFilter.js +2 -2
- package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
- package/dist/components/ui/ReferenceBadgeButton.js +6 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
- package/dist/components/ui/TaxonomyDropdown.js +58 -23
- package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
- package/dist/components/ui/TopNoticeLayer.js +14 -0
- package/dist/components/ui/TopNoticeLayer.test.js +25 -0
- package/dist/components/views/AppShellHeader.d.ts +13 -0
- package/dist/components/views/AppShellHeader.js +19 -0
- package/dist/components/views/AppShellHeader.test.js +21 -0
- package/dist/components/views/PlanComparisonPage.d.ts +73 -0
- package/dist/components/views/PlanComparisonPage.js +196 -0
- package/dist/components/views/PlanComparisonPage.test.js +251 -0
- package/dist/components/views/PlansPage.d.ts +14 -0
- package/dist/components/views/PlansPage.js +355 -0
- package/dist/components/views/StandaloneLayout.d.ts +2 -2
- package/dist/components/views/StandaloneLayout.js +2212 -1194
- package/dist/components/views/WidgetView.d.ts +2 -2
- package/dist/components/views/WidgetView.js +149 -43
- package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
- package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
- package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
- package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
- package/dist/components/views/chrome/workspaceChrome.js +108 -0
- package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
- package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
- package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
- package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
- package/dist/components/views/panels/FilterToolbar.d.ts +66 -0
- package/dist/components/views/panels/FilterToolbar.js +21 -0
- package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbar.test.js +45 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +46 -0
- package/dist/components/views/panels/FilterToolbarControls.js +61 -0
- package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
- package/dist/components/views/panels/PlanningDrawer.js +220 -0
- package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
- package/dist/components/views/panels/ScheduleSidebar.js +127 -0
- package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
- package/dist/components/views/panels/scheduleUtils.js +79 -0
- package/dist/components/views/teamManagementAccess.d.ts +6 -0
- package/dist/components/views/teamManagementAccess.js +18 -0
- package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
- package/dist/components/views/teamManagementAccess.test.js +44 -0
- package/dist/config/clientDeployment.d.ts +2 -0
- package/dist/config/clientDeployment.js +13 -0
- package/dist/config/cloudEnvironment.d.ts +15 -0
- package/dist/config/cloudEnvironment.js +60 -0
- package/dist/config/deployment.d.ts +20 -0
- package/dist/config/deployment.js +67 -0
- package/dist/config/deployment.test.d.ts +1 -0
- package/dist/config/deployment.test.js +88 -0
- package/dist/config/envSchema.js +36 -8
- package/dist/config/envSchema.test.js +28 -0
- package/dist/core/AiProfileService.d.ts +66 -0
- package/dist/core/AiProfileService.js +320 -0
- package/dist/core/AiProfileService.test.d.ts +1 -0
- package/dist/core/AiProfileService.test.js +164 -0
- package/dist/core/AiProfiles.test.d.ts +1 -0
- package/dist/core/AiProfiles.test.js +449 -0
- package/dist/core/AttachmentLinkService.d.ts +15 -0
- package/dist/core/AttachmentLinkService.js +157 -0
- package/dist/core/AuthTokenService.d.ts +44 -0
- package/dist/core/AuthTokenService.js +207 -0
- package/dist/core/CreatedBy.test.js +134 -6
- package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
- package/dist/core/DeletedTaskLifecycleService.js +158 -0
- package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
- package/dist/core/DeletedTaskTrash.test.js +104 -0
- package/dist/core/EntitlementsPolicy.test.js +78 -5
- package/dist/core/GlobalSettingsService.d.ts +16 -0
- package/dist/core/GlobalSettingsService.js +370 -0
- package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
- package/dist/core/JsonBackupRuntime.test.js +155 -0
- package/dist/core/McpTokenService.d.ts +118 -0
- package/dist/core/McpTokenService.js +921 -0
- package/dist/core/PlanEntitlementService.d.ts +201 -0
- package/dist/core/PlanEntitlementService.js +1442 -0
- package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
- package/dist/core/PlanVersionPolicy.test.js +178 -0
- package/dist/core/PlanningEntities.test.d.ts +1 -0
- package/dist/core/PlanningEntities.test.js +95 -0
- package/dist/core/ScheduleFields.test.js +15 -19
- package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
- package/dist/core/SignupMonetizationSettings.test.js +159 -0
- package/dist/core/Stats.test.d.ts +1 -0
- package/dist/core/Stats.test.js +36 -0
- package/dist/core/SyncReconciliationService.d.ts +93 -0
- package/dist/core/SyncReconciliationService.js +406 -0
- package/dist/core/SystemAdmin.test.js +578 -13
- package/dist/core/TaskActivityService.d.ts +21 -0
- package/dist/core/TaskActivityService.js +130 -0
- package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
- package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
- package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
- package/dist/core/TaskAuditTimeline.test.js +132 -0
- package/dist/core/TaskTaxonomyValidation.test.js +338 -15
- package/dist/core/Taskforce.d.ts +930 -183
- package/dist/core/Taskforce.ids.test.js +252 -0
- package/dist/core/Taskforce.js +3848 -2532
- package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
- package/dist/core/Taskforce.mcpAuth.test.js +343 -0
- package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
- package/dist/core/TaxonomySettingsService.d.ts +18 -0
- package/dist/core/TaxonomySettingsService.js +162 -0
- package/dist/core/WorkspaceDelete.test.js +32 -13
- package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
- package/dist/core/WorkspaceLifecycleService.js +88 -0
- package/dist/core/WorkspacePermissions.test.js +95 -0
- package/dist/core/WorkspacePlanMode.test.js +114 -1
- package/dist/core/WorkspacePreferenceService.d.ts +41 -0
- package/dist/core/WorkspacePreferenceService.js +246 -0
- package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
- package/dist/core/WorkspaceSyncSettings.test.js +65 -0
- package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
- package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
- package/dist/core/shared.d.ts +36 -0
- package/dist/core/shared.js +128 -0
- package/dist/core/types.d.ts +614 -0
- package/dist/core/types.js +27 -0
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
- package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
- package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
- package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
- package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
- package/dist/documentReviews/service.d.ts +68 -0
- package/dist/documentReviews/service.js +210 -0
- package/dist/documentReviews/types.d.ts +58 -0
- package/dist/documentReviews/types.js +1 -0
- package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
- package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
- package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
- package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
- package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
- package/dist/hooks/tasks/useTaskActions.js +2 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
- package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFilters.js +2 -0
- package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
- package/dist/hooks/tasks/useTaskFormActions.js +365 -0
- package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
- package/dist/hooks/tasks/useTaskRelationships.js +64 -0
- package/dist/hooks/ui/useResourceExport.d.ts +19 -0
- package/dist/hooks/ui/useResourceExport.js +84 -0
- package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
- package/dist/hooks/ui/useSettingsModel.js +125 -0
- package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
- package/dist/hooks/ui/useSettingsPersistence.js +254 -0
- package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
- package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
- package/dist/hooks/ui/useUiNotice.d.ts +1 -0
- package/dist/hooks/ui/useUiNotice.js +1 -0
- package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
- package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
- package/dist/hooks/ui/useZenMode.d.ts +5 -0
- package/dist/hooks/ui/useZenMode.js +32 -0
- package/dist/hooks/useFilterSort.d.ts +60 -0
- package/dist/hooks/useFilterSort.js +362 -0
- package/dist/hooks/useFilterSort.test.d.ts +1 -0
- package/dist/hooks/useFilterSort.test.js +333 -0
- package/dist/hooks/useRealtimeSync.d.ts +1 -0
- package/dist/hooks/useRealtimeSync.js +24 -9
- package/dist/hooks/useRealtimeSync.test.js +28 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
- package/dist/hooks/useSyncOrchestrator.js +1538 -670
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
- package/dist/hooks/useTaskData.d.ts +35 -0
- package/dist/hooks/useTaskData.js +239 -0
- package/dist/hooks/useTaskData.test.d.ts +1 -0
- package/dist/hooks/useTaskData.test.js +119 -0
- package/dist/hooks/useTaskMutations.d.ts +46 -0
- package/dist/hooks/useTaskMutations.js +472 -0
- package/dist/hooks/useTaskMutations.test.d.ts +1 -0
- package/dist/hooks/useTaskMutations.test.js +80 -0
- package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.contract.test.js +7 -0
- package/dist/hooks/useTaskforce.d.ts +260 -295
- package/dist/hooks/useTaskforce.js +1954 -2623
- package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
- package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
- package/dist/hooks/useUiNotice.d.ts +16 -0
- package/dist/hooks/useUiNotice.js +49 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
- package/dist/hooks/useWorkspaceSyncController.js +280 -461
- package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
- package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
- package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
- package/dist/hooks/workspace/workspaceLocalState.js +101 -0
- package/dist/localization/locales/en-US.d.ts +1 -8
- package/dist/localization/locales/en-US.js +4 -11
- package/dist/localization/locales/es-419.js +2 -9
- package/dist/localization/locales/pt-BR.js +2 -9
- package/dist/localization/localizationCatalogs.test.d.ts +1 -0
- package/dist/localization/localizationCatalogs.test.js +37 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +9 -15
- package/dist/mcp/clientIntegrations.d.ts +33 -0
- package/dist/mcp/clientIntegrations.js +199 -0
- package/dist/mcp/clientIntegrations.test.d.ts +1 -0
- package/dist/mcp/clientIntegrations.test.js +110 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/optionResolution.test.js +6 -2
- package/dist/mcp/runtime.d.ts +120 -0
- package/dist/mcp/runtime.js +5668 -0
- package/dist/mcp/runtime.test.d.ts +1 -0
- package/dist/mcp/runtime.test.js +3293 -0
- package/dist/mcp/server.js +2 -1793
- package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
- package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
- package/dist/mcp/structuredDocValidation.js +1 -2
- package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
- package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
- package/dist/mcp/toolRegistry.d.ts +570 -0
- package/dist/mcp/toolRegistry.js +90 -0
- package/dist/mcp/toolRegistry.test.d.ts +1 -0
- package/dist/mcp/toolRegistry.test.js +30 -0
- package/dist/migrations/billingSchemaParity.test.js +111 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
- package/dist/migrations/taskSchemaMigrations.js +681 -39
- package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
- package/dist/migrations/taskSchemaMigrations.test.js +210 -0
- package/dist/plugins/vite.js +13 -7
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
- package/dist/public/archive/phoenix/favicon.ico +0 -0
- package/dist/public/archive/phoenix/site.webmanifest +1 -0
- package/dist/public/favicon-96x96.png +0 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/favicon.svg +17 -0
- package/dist/public/site.webmanifest +21 -1
- package/dist/public/web-app-manifest-192x192.png +0 -0
- package/dist/public/web-app-manifest-512x512.png +0 -0
- package/dist/releaseVersion.test.d.ts +1 -0
- package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
- package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
- package/dist/resources/templates/workflows/collaborate.yaml +11 -5
- package/dist/resources/templates/workflows/do.yaml +8 -2
- package/dist/resources/templates/workflows/evaluate.yaml +13 -7
- package/dist/resources/templates/workflows/execute.yaml +40 -23
- package/dist/resources/templates/workflows/plan.yaml +15 -8
- package/dist/resources/templates/workflows/review.yaml +14 -8
- package/dist/runtime/appGateDefinitions.d.ts +32 -0
- package/dist/runtime/appGateDefinitions.js +32 -0
- package/dist/runtime/appGates.d.ts +29 -0
- package/dist/runtime/appGates.js +53 -0
- package/dist/runtime/appGates.test.d.ts +1 -0
- package/dist/runtime/appGates.test.js +42 -0
- package/dist/runtime/migrationFlags.d.ts +1 -0
- package/dist/runtime/migrationFlags.js +1 -0
- package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
- package/dist/server/auth.js +14 -2
- package/dist/server/auth.test.js +21 -1
- package/dist/server/cors.js +11 -5
- package/dist/server/cors.test.js +6 -0
- package/dist/server/documentReviewRoutes.test.d.ts +1 -0
- package/dist/server/documentReviewRoutes.test.js +157 -0
- package/dist/server/index.cookieProxy.test.js +46 -1
- package/dist/server/index.d.ts +20 -0
- package/dist/server/index.js +168 -41
- package/dist/server/index.rateLimit.test.js +6 -0
- package/dist/server/index.test.js +133 -2
- package/dist/server/mockStripe.d.ts +72 -0
- package/dist/server/mockStripe.js +357 -0
- package/dist/server/realtimeSyncWs.js +4 -0
- package/dist/server/routes/admin.d.ts +114 -0
- package/dist/server/routes/admin.js +2226 -0
- package/dist/server/routes/annotatedAttachments.d.ts +24 -0
- package/dist/server/routes/annotatedAttachments.js +235 -0
- package/dist/server/routes/auth.d.ts +61 -0
- package/dist/server/routes/auth.js +1296 -0
- package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
- package/dist/server/routes/billing.js +2001 -0
- package/dist/server/routes/billing.test.d.ts +1 -0
- package/dist/server/routes/billing.test.js +1631 -0
- package/dist/server/routes/documentReviews.d.ts +11 -0
- package/dist/server/routes/documentReviews.js +191 -0
- package/dist/server/routes/documents.d.ts +118 -0
- package/dist/server/routes/documents.js +1113 -0
- package/dist/server/routes/resources.d.ts +25 -0
- package/dist/server/routes/resources.js +225 -0
- package/dist/server/routes/shared.d.ts +35 -0
- package/dist/server/routes/shared.js +305 -0
- package/dist/server/routes/shared.test.d.ts +1 -0
- package/dist/server/routes/shared.test.js +32 -0
- package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
- package/dist/server/routes/sync.integration.test.d.ts +1 -0
- package/dist/server/routes/sync.integration.test.js +2003 -0
- package/dist/server/routes/sync.js +3143 -0
- package/dist/server/routes/tasks.d.ts +72 -0
- package/dist/server/routes/tasks.js +1252 -0
- package/dist/server/routes/workspaces.d.ts +32 -0
- package/dist/server/routes/workspaces.js +307 -0
- package/dist/server/routes.d.ts +8 -9
- package/dist/server/routes.js +968 -4092
- package/dist/server/routes.test.js +9043 -2415
- package/dist/server/runtimeMode.d.ts +1 -1
- package/dist/server/runtimeMode.js +3 -18
- package/dist/server/runtimeMode.test.js +3 -3
- package/dist/server/securityHeaders.d.ts +1 -1
- package/dist/server/securityHeaders.js +65 -6
- package/dist/server/securityHeaders.test.js +40 -2
- package/dist/services/templateEngine.test.d.ts +1 -0
- package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
- package/dist/services/templates.test.d.ts +1 -0
- package/dist/{__tests__ → services}/templates.test.js +1 -1
- package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
- package/dist/services/workflowExportMatrix.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
- package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
- package/dist/services/workflowGeneration.test.d.ts +1 -0
- package/dist/services/workflowSimulation.test.d.ts +1 -0
- package/dist/services/workflowSimulation.test.js +113 -0
- package/dist/services/workflowV2Policy.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
- package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
- package/dist/shared/aiProfileSurfaceType.js +49 -0
- package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
- package/dist/shared/customTaxonomyLibrary.js +276 -0
- package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
- package/dist/shared/customTaxonomyLibrary.test.js +20 -0
- package/dist/shared/taxonomyCatalog.d.ts +19 -0
- package/dist/shared/taxonomyCatalog.js +36 -0
- package/dist/shared/taxonomyDefaults.d.ts +29 -0
- package/dist/shared/taxonomyDefaults.js +26 -0
- package/dist/shared/taxonomyLibrary.d.ts +15 -0
- package/dist/shared/taxonomyLibrary.js +98 -0
- package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
- package/dist/shared/taxonomyLibrary.test.js +26 -0
- package/dist/shared/taxonomyState.d.ts +36 -0
- package/dist/shared/taxonomyState.js +73 -0
- package/dist/shared/taxonomyState.test.d.ts +1 -0
- package/dist/shared/taxonomyState.test.js +71 -0
- package/dist/shared/workflowDefaults.d.ts +9 -0
- package/dist/shared/workflowDefaults.js +11 -0
- package/dist/storage/documentIntegrity.d.ts +2 -0
- package/dist/storage/documentIntegrity.js +34 -17
- package/dist/storage/documentPurge.d.ts +2 -1
- package/dist/storage/documentPurge.js +35 -8
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/objectStorage.d.ts +1 -1
- package/dist/storage/objectStorageClient.d.ts +6 -4
- package/dist/storage/objectStorageClient.js +77 -29
- package/dist/storage/objectStorageClient.test.d.ts +1 -0
- package/dist/storage/objectStorageClient.test.js +57 -0
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.d.ts +1 -0
- package/dist/storage/objectStorageConfig.test.js +38 -0
- package/dist/storage/postgresAdapter.d.ts +5 -0
- package/dist/storage/postgresAdapter.js +2 -2
- package/dist/storage/postgresAdapter.test.d.ts +1 -0
- package/dist/storage/postgresAdapter.test.js +29 -0
- package/dist/storage/taskAssetStore.d.ts +7 -4
- package/dist/storage/taskAssetStore.js +65 -60
- package/dist/storage/workspaceAssetStore.d.ts +122 -0
- package/dist/storage/workspaceAssetStore.js +496 -0
- package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
- package/dist/storage/workspaceAssetStore.test.js +501 -0
- package/dist/sync/cloudSyncApi.d.ts +33 -2
- package/dist/sync/cloudSyncApi.js +37 -15
- package/dist/sync/cloudSyncApi.test.js +60 -3
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +3 -0
- package/dist/sync/syncService.d.ts +25 -0
- package/dist/sync/syncService.js +184 -16
- package/dist/sync/syncService.test.js +289 -25
- package/dist/sync/workspaceRepair.d.ts +114 -0
- package/dist/sync/workspaceRepair.js +566 -0
- package/dist/sync/workspaceSyncModel.d.ts +230 -2
- package/dist/sync/workspaceSyncModel.js +456 -17
- package/dist/sync/workspaceSyncModel.test.js +642 -0
- package/dist/sync/workspaceSyncState.d.ts +12 -0
- package/dist/sync/workspaceSyncState.js +12 -0
- package/dist/sync/workspaceSyncSurface.d.ts +33 -0
- package/dist/sync/workspaceSyncSurface.js +74 -0
- package/dist/types.d.ts +143 -18
- package/dist/types.js +9 -24
- package/dist/ui/assets/AgentsModule-TVmCgizS.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CAyhnPeR.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-BOCw5e4D.js +250 -0
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
- package/dist/ui/assets/InitiativesModule-CN0r02Oi.js +1 -0
- package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
- package/dist/ui/assets/PlansPage-FcQSNLZd.js +1 -0
- package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
- package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +8 -0
- package/dist/ui/assets/WorkflowsModule-CY9R2zPB.js +5 -0
- package/dist/ui/assets/index-Bzqy5QGm.js +6 -0
- package/dist/ui/assets/index-DxRiI3tY.css +1 -0
- package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
- package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
- package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
- package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
- package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
- package/dist/ui/branding/logos/logo.png +0 -0
- package/dist/ui/branding/logos/logo_dark.svg +27 -0
- package/dist/ui/branding/logos/logo_favicon.svg +35 -0
- package/dist/ui/branding/logos/logo_light.svg +27 -0
- package/dist/ui/favicon/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
- package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
- package/dist/ui/favicon/favicon-96x96.png +0 -0
- package/dist/ui/favicon/favicon.ico +0 -0
- package/dist/ui/favicon/favicon.svg +17 -0
- package/dist/ui/favicon/site.webmanifest +21 -1
- package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
- package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
- package/dist/ui/index.html +10 -7
- package/dist/ui/product/screen_01.png +0 -0
- package/dist/ui/product/screen_02.png +0 -0
- package/dist/ui/product/screen_03.png +0 -0
- package/dist/utils/annotatedAttachments.d.ts +20 -0
- package/dist/utils/annotatedAttachments.js +74 -0
- package/dist/utils/annotatedAttachments.test.d.ts +1 -0
- package/dist/utils/annotatedAttachments.test.js +108 -0
- package/dist/utils/assignees.d.ts +26 -0
- package/dist/utils/assignees.js +132 -0
- package/dist/utils/canonicalAssetKind.d.ts +16 -0
- package/dist/utils/canonicalAssetKind.js +39 -0
- package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
- package/dist/utils/canonicalAssetKind.test.js +24 -0
- package/dist/utils/coalescedRefresh.d.ts +5 -0
- package/dist/utils/coalescedRefresh.js +16 -0
- package/dist/utils/coalescedRefresh.test.d.ts +1 -0
- package/dist/utils/coalescedRefresh.test.js +30 -0
- package/dist/utils/constants.d.ts +16 -3
- package/dist/utils/constants.icons.test.js +2 -1
- package/dist/utils/constants.js +70 -13
- package/dist/utils/contextAssetEvents.d.ts +7 -0
- package/dist/utils/contextAssetEvents.js +6 -0
- package/dist/utils/contextFiles.d.ts +14 -3
- package/dist/utils/contextFiles.js +40 -8
- package/dist/utils/documentManagerApi.d.ts +6 -0
- package/dist/utils/documentManagerApi.js +19 -0
- package/dist/utils/documentNames.d.ts +24 -0
- package/dist/utils/documentNames.js +91 -0
- package/dist/utils/documentReferences.d.ts +9 -0
- package/dist/utils/documentReferences.js +11 -0
- package/dist/utils/entityReferences.d.ts +9 -0
- package/dist/utils/entityReferences.js +32 -0
- package/dist/utils/imageReferences.d.ts +9 -0
- package/dist/utils/imageReferences.js +11 -0
- package/dist/utils/planParser.d.ts +2 -2
- package/dist/utils/planParser.js +11 -11
- package/dist/utils/planParser.test.d.ts +1 -0
- package/dist/{__tests__ → utils}/planParser.test.js +22 -22
- package/dist/utils/planningReferences.d.ts +17 -0
- package/dist/utils/planningReferences.js +30 -0
- package/dist/utils/planningReferences.test.d.ts +1 -0
- package/dist/utils/planningReferences.test.js +17 -0
- package/dist/utils/taskActivity.d.ts +9 -0
- package/dist/utils/taskActivity.js +147 -0
- package/dist/utils/taskActivity.test.d.ts +1 -0
- package/dist/utils/taskActivity.test.js +34 -0
- package/dist/utils/taskDraftDefaults.d.ts +21 -0
- package/dist/utils/taskDraftDefaults.js +40 -0
- package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
- package/dist/utils/taskDraftDefaults.test.js +34 -0
- package/dist/utils/taskNormalization.d.ts +25 -0
- package/dist/utils/taskNormalization.js +161 -0
- package/dist/utils/taskNormalization.test.d.ts +1 -0
- package/dist/utils/taskNormalization.test.js +158 -0
- package/dist/utils/taskReferences.d.ts +5 -0
- package/dist/utils/taskReferences.js +11 -0
- package/dist/utils/taskforceApiClient.d.ts +2 -0
- package/dist/utils/taskforceApiClient.js +101 -3
- package/dist/utils/taskforceApiClient.test.d.ts +1 -0
- package/dist/utils/taskforceApiClient.test.js +44 -0
- package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
- package/dist/utils/taxonomyFilterOptions.js +17 -0
- package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
- package/dist/utils/taxonomyFormDefaults.js +28 -0
- package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
- package/dist/utils/taxonomyFormDefaults.test.js +63 -0
- package/dist/utils/taxonomyLifecycle.d.ts +3 -0
- package/dist/utils/taxonomyLifecycle.js +17 -0
- package/dist/utils/taxonomySorting.d.ts +7 -0
- package/dist/utils/taxonomySorting.js +74 -0
- package/dist/utils/theme.d.ts +15 -0
- package/dist/utils/theme.js +23 -0
- package/dist/utils/uiNotice.d.ts +7 -0
- package/dist/utils/uiNotice.js +6 -0
- package/dist/utils/uiNotice.test.d.ts +1 -0
- package/dist/utils/uiNotice.test.js +18 -0
- package/dist/utils/workspaceIdentity.d.ts +14 -0
- package/dist/utils/workspaceIdentity.js +21 -0
- package/dist/utils/workspaceIdentity.test.js +30 -1
- package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
- package/dist/utils/workspaceSyncPresentation.js +21 -0
- package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
- package/dist/utils/workspaceSyncPresentation.test.js +36 -0
- package/package.json +28 -23
- package/scripts/bootstrap-playwright-auth.mjs +155 -0
- package/scripts/check-compat-boundaries.mjs +41 -0
- package/scripts/check-env.ts +9 -2
- package/scripts/debug-login.mjs +32 -0
- package/scripts/migrate-planning-model.ts +233 -0
- package/scripts/migrate-task-data-assets.mjs +408 -0
- package/scripts/playwright-auth.sh +11 -0
- package/scripts/run-dev-proxy.sh +2 -0
- package/scripts/update-homebrew-formula.sh +20 -3
- package/scripts/with-esbuild.sh +11 -0
- package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
- package/src/resources/templates/workflows/collaborate.yaml +11 -5
- package/src/resources/templates/workflows/do.yaml +8 -2
- package/src/resources/templates/workflows/evaluate.yaml +13 -7
- package/src/resources/templates/workflows/execute.yaml +40 -23
- package/src/resources/templates/workflows/plan.yaml +15 -8
- package/src/resources/templates/workflows/review.yaml +14 -8
- package/dist/TaskforceScreenshot.d.ts +0 -11
- package/dist/TaskforceScreenshot.js +0 -113
- package/dist/compat/priorityCompat.d.ts +0 -2
- package/dist/compat/priorityCompat.js +0 -24
- package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
- package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
- package/dist/components/task/TaskKanban 2.d.ts +0 -76
- package/dist/components/task/TaskKanban 2.js +0 -1004
- package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
- package/dist/components/task/TaskScreenshotSection.js +0 -22
- package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
- package/dist/components/views/StandaloneLayout 2.js +0 -1660
- package/dist/core/InitiativeTemplates.test.js +0 -50
- package/dist/core/ParentLifecycle.test.js +0 -98
- package/dist/migrations/clientMigrations.d.ts +0 -9
- package/dist/migrations/clientMigrations.js +0 -74
- package/dist/migrations/flags.d.ts +0 -6
- package/dist/migrations/flags.js +0 -8
- package/dist/migrations/taskMigrations.d.ts +0 -71
- package/dist/migrations/taskMigrations.js +0 -236
- package/dist/migrations/taskMigrations.test.js +0 -34
- package/dist/server/routes.billing.js +0 -991
- package/dist/server/routes.billing.test.js +0 -387
- package/dist/server/routes.sync.integration.test.js +0 -470
- package/dist/server/routes.sync.js +0 -1141
- package/dist/soak/syncSoakHarness.d.ts +0 -6
- package/dist/soak/syncSoakHarness.js +0 -10
- package/dist/ui/assets/index-0W5vyCpn.css +0 -1
- package/dist/ui/assets/index-MCZuNNfn.js +0 -23
- package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
- package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
- package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
- package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
- package/dist/utils/taskHierarchy.d.ts +0 -4
- package/dist/utils/taskHierarchy.js +0 -69
- package/dist/utils/taskHierarchy.test.js +0 -38
- package/dist/utils/taskParenting.d.ts +0 -3
- package/dist/utils/taskParenting.js +0 -24
- package/dist/utils/taskParenting.test.js +0 -21
- package/dist/utils/taskRelations.d.ts +0 -6
- package/dist/utils/taskRelations.js +0 -24
- package/dist/utils/taskRelations.test.js +0 -34
- package/scripts/check-compat-boundaries.js +0 -48
- /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
- /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
- /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
- /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
- /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
- /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
- /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
- /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
- /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
- /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
- /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
- /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
- /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
- /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
- /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
- /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
- /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
- /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
- /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
- /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
- /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
- /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
- /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
- /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
- /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
- /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
|
@@ -0,0 +1,1942 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { ArrowDown, ArrowLeft, ArrowUpLeft, ArrowUp, Check, ClipboardPaste, Copy, FolderOpen, Hand, HelpCircle, Loader2, MessageSquareCode, MessageSquareText, MousePointer2, Pin, Plus, RotateCcw, Save, SquareMousePointer, Trash2, ZoomIn, ZoomOut, } from 'lucide-react';
|
|
4
|
+
import sharedStyles from '../../Taskforce.module.css';
|
|
5
|
+
import { fetchTaskforceApi } from '../../utils/taskforceApiClient';
|
|
6
|
+
import { logBootstrapDebug } from '../../hooks/workspace/workspaceLocalState';
|
|
7
|
+
import { Modal } from '../ui/Modal';
|
|
8
|
+
import { ImageReferenceBadge } from '../ui/ImageReferenceBadge';
|
|
9
|
+
import { TaskReferenceBadge } from '../task/TaskReferenceBadge';
|
|
10
|
+
import styles from './AnnotatedAttachmentWorkspace.module.css';
|
|
11
|
+
const MARKER_TYPE_OPTIONS = [
|
|
12
|
+
{ value: 'review', label: 'Review' },
|
|
13
|
+
{ value: 'change', label: 'Change' },
|
|
14
|
+
{ value: 'question', label: 'Question' },
|
|
15
|
+
];
|
|
16
|
+
const TOOL_OPTIONS = [
|
|
17
|
+
{ value: 'select', label: 'Select', icon: MousePointer2 },
|
|
18
|
+
{ value: 'pin', label: 'Pin', icon: Pin },
|
|
19
|
+
{ value: 'box', label: 'Box', icon: SquareMousePointer },
|
|
20
|
+
{ value: 'arrow', label: 'Arrow', icon: ArrowUpLeft },
|
|
21
|
+
{ value: 'text-note', label: 'Note', icon: MessageSquareText },
|
|
22
|
+
];
|
|
23
|
+
const ANNOTATION_KIND_ICON = {
|
|
24
|
+
pin: Pin,
|
|
25
|
+
box: SquareMousePointer,
|
|
26
|
+
arrow: ArrowUpLeft,
|
|
27
|
+
'text-note': MessageSquareText,
|
|
28
|
+
};
|
|
29
|
+
const ANNOTATION_KIND_LABELS = {
|
|
30
|
+
pin: 'Pin',
|
|
31
|
+
box: 'Box',
|
|
32
|
+
arrow: 'Arrow',
|
|
33
|
+
'text-note': 'Note',
|
|
34
|
+
};
|
|
35
|
+
const MARKER_TYPE_ICON = {
|
|
36
|
+
review: MessageSquareCode,
|
|
37
|
+
change: Check,
|
|
38
|
+
question: HelpCircle,
|
|
39
|
+
issue: HelpCircle,
|
|
40
|
+
idea: MessageSquareCode,
|
|
41
|
+
};
|
|
42
|
+
const TOOL_HELP_COPY = {
|
|
43
|
+
select: {
|
|
44
|
+
short: 'Select and edit existing markers.',
|
|
45
|
+
detail: 'Use Select to click, drag, reorder, resize, and update markers that are already on the image.',
|
|
46
|
+
example: 'Example: move an existing marker after the screenshot changes.',
|
|
47
|
+
},
|
|
48
|
+
pin: {
|
|
49
|
+
short: 'Mark a precise spot.',
|
|
50
|
+
detail: 'Use Pin when feedback points to one exact location instead of a broader area.',
|
|
51
|
+
example: 'Example: "This icon is misaligned by 2px."',
|
|
52
|
+
},
|
|
53
|
+
box: {
|
|
54
|
+
short: 'Mark an area or component.',
|
|
55
|
+
detail: 'Use Box when the feedback applies to a whole region, card, panel, or bounded UI block.',
|
|
56
|
+
example: 'Example: "This whole card needs tighter padding and a stronger border."',
|
|
57
|
+
},
|
|
58
|
+
arrow: {
|
|
59
|
+
short: 'Show direction or relationship.',
|
|
60
|
+
detail: 'Use Arrow when you need to show movement, attachment, flow, or source-to-target intent.',
|
|
61
|
+
example: 'Example: "This tooltip should anchor to this button, not the panel."',
|
|
62
|
+
},
|
|
63
|
+
'text-note': {
|
|
64
|
+
short: 'Add a comment-style point marker.',
|
|
65
|
+
detail: 'Use Note when you want a point marker that reads more like a comment or open question.',
|
|
66
|
+
example: 'Example: "Ask design whether this badge should stay."',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const ANNOTATION_COLORS = {
|
|
70
|
+
question: '#0f766e',
|
|
71
|
+
change: '#2563eb',
|
|
72
|
+
issue: '#dc2626',
|
|
73
|
+
idea: '#d97706',
|
|
74
|
+
review: '#7c3aed',
|
|
75
|
+
};
|
|
76
|
+
const MARKER_COLOR_SWATCHES = [
|
|
77
|
+
'#7c3aed',
|
|
78
|
+
'#2563eb',
|
|
79
|
+
'#0f766e',
|
|
80
|
+
'#dc2626',
|
|
81
|
+
'#d97706',
|
|
82
|
+
'#111827',
|
|
83
|
+
];
|
|
84
|
+
const DEFAULT_MARKER_TYPE = 'review';
|
|
85
|
+
function buildSessionTitle(target) {
|
|
86
|
+
const displayName = String(target.displayName || '').trim();
|
|
87
|
+
return displayName ? `${displayName} review` : 'Annotated session';
|
|
88
|
+
}
|
|
89
|
+
function buildAnnotationId() {
|
|
90
|
+
return `annotation-${Math.random().toString(36).slice(2, 10)}`;
|
|
91
|
+
}
|
|
92
|
+
function clamp01(value) {
|
|
93
|
+
if (!Number.isFinite(value))
|
|
94
|
+
return 0;
|
|
95
|
+
if (value <= 0)
|
|
96
|
+
return 0;
|
|
97
|
+
if (value >= 1)
|
|
98
|
+
return 1;
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
function clampSize(value) {
|
|
102
|
+
return clamp01(Math.max(0.02, value));
|
|
103
|
+
}
|
|
104
|
+
function getAnnotatedTargetKey(target) {
|
|
105
|
+
if (!target)
|
|
106
|
+
return '';
|
|
107
|
+
return [
|
|
108
|
+
String(target.taskId || '').trim(),
|
|
109
|
+
String(target.assetId || '').trim(),
|
|
110
|
+
String(target.path || '').trim(),
|
|
111
|
+
].join('::');
|
|
112
|
+
}
|
|
113
|
+
function isEditableKeyboardTarget(target) {
|
|
114
|
+
if (!(target instanceof HTMLElement))
|
|
115
|
+
return false;
|
|
116
|
+
const tagName = target.tagName.toLowerCase();
|
|
117
|
+
if (target.isContentEditable)
|
|
118
|
+
return true;
|
|
119
|
+
return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
|
|
120
|
+
}
|
|
121
|
+
function normalizeOrder(annotations) {
|
|
122
|
+
return annotations.map((annotation, index) => ({
|
|
123
|
+
...annotation,
|
|
124
|
+
order: index,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
function formatTimestamp(value) {
|
|
128
|
+
if (!value)
|
|
129
|
+
return 'Unsaved';
|
|
130
|
+
const parsed = new Date(value);
|
|
131
|
+
if (Number.isNaN(parsed.getTime()))
|
|
132
|
+
return 'Unsaved';
|
|
133
|
+
return parsed.toLocaleString();
|
|
134
|
+
}
|
|
135
|
+
function getSessionCreatorLabel(session) {
|
|
136
|
+
const createdByLabel = String(session.createdByActor?.label || '').trim();
|
|
137
|
+
if (createdByLabel)
|
|
138
|
+
return createdByLabel;
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function sessionInstructionPreview(value) {
|
|
142
|
+
const normalized = String(value || '').trim().replace(/\s+/g, ' ');
|
|
143
|
+
if (!normalized)
|
|
144
|
+
return '';
|
|
145
|
+
return normalized.length > 110 ? `${normalized.slice(0, 107)}...` : normalized;
|
|
146
|
+
}
|
|
147
|
+
function annotationSummary(annotation) {
|
|
148
|
+
if (annotation.kind === 'pin' || annotation.kind === 'text-note') {
|
|
149
|
+
return `${Math.round(annotation.x * 100)}%, ${Math.round(annotation.y * 100)}%`;
|
|
150
|
+
}
|
|
151
|
+
if (annotation.kind === 'box') {
|
|
152
|
+
return `${Math.round(annotation.width * 100)}% × ${Math.round(annotation.height * 100)}%`;
|
|
153
|
+
}
|
|
154
|
+
return `${Math.round(Math.abs(annotation.x2 - annotation.x) * 100)}% span`;
|
|
155
|
+
}
|
|
156
|
+
function createAnnotation(kind, start, end, color = ANNOTATION_COLORS[DEFAULT_MARKER_TYPE]) {
|
|
157
|
+
const base = {
|
|
158
|
+
id: buildAnnotationId(),
|
|
159
|
+
order: 0,
|
|
160
|
+
instruction: '',
|
|
161
|
+
markerType: DEFAULT_MARKER_TYPE,
|
|
162
|
+
color,
|
|
163
|
+
};
|
|
164
|
+
if (kind === 'pin') {
|
|
165
|
+
return { ...base, kind, x: start.x, y: start.y };
|
|
166
|
+
}
|
|
167
|
+
if (kind === 'text-note') {
|
|
168
|
+
return { ...base, kind, x: start.x, y: start.y };
|
|
169
|
+
}
|
|
170
|
+
if (kind === 'box') {
|
|
171
|
+
const endPoint = end || start;
|
|
172
|
+
return {
|
|
173
|
+
...base,
|
|
174
|
+
kind,
|
|
175
|
+
x: clamp01(Math.min(start.x, endPoint.x)),
|
|
176
|
+
y: clamp01(Math.min(start.y, endPoint.y)),
|
|
177
|
+
width: clampSize(Math.abs(endPoint.x - start.x)),
|
|
178
|
+
height: clampSize(Math.abs(endPoint.y - start.y)),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const endPoint = end || start;
|
|
182
|
+
return {
|
|
183
|
+
...base,
|
|
184
|
+
kind: 'arrow',
|
|
185
|
+
x: start.x,
|
|
186
|
+
y: start.y,
|
|
187
|
+
x2: endPoint.x,
|
|
188
|
+
y2: endPoint.y,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function mapMarkerColor(annotation) {
|
|
192
|
+
if (annotation.color)
|
|
193
|
+
return annotation.color;
|
|
194
|
+
return ANNOTATION_COLORS[annotation.markerType || DEFAULT_MARKER_TYPE];
|
|
195
|
+
}
|
|
196
|
+
function getArrowGeometry(annotation, dimensions) {
|
|
197
|
+
const width = Math.max(dimensions.width, 1);
|
|
198
|
+
const height = Math.max(dimensions.height, 1);
|
|
199
|
+
const tailX = annotation.x * width;
|
|
200
|
+
const tailY = annotation.y * height;
|
|
201
|
+
const tipX = annotation.x2 * width;
|
|
202
|
+
const tipY = annotation.y2 * height;
|
|
203
|
+
const dx = tipX - tailX;
|
|
204
|
+
const dy = tipY - tailY;
|
|
205
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
206
|
+
const unitX = dx / length;
|
|
207
|
+
const unitY = dy / length;
|
|
208
|
+
const headLength = Math.max(10, Math.min(16, length - 2));
|
|
209
|
+
const halfHeadWidth = headLength * 0.62;
|
|
210
|
+
const baseX = tipX - unitX * headLength;
|
|
211
|
+
const baseY = tipY - unitY * headLength;
|
|
212
|
+
const perpX = -unitY;
|
|
213
|
+
const perpY = unitX;
|
|
214
|
+
return {
|
|
215
|
+
shaftX1: tailX,
|
|
216
|
+
shaftY1: tailY,
|
|
217
|
+
shaftX2: baseX,
|
|
218
|
+
shaftY2: baseY,
|
|
219
|
+
headPoints: [
|
|
220
|
+
`${tipX},${tipY}`,
|
|
221
|
+
`${baseX + perpX * halfHeadWidth},${baseY + perpY * halfHeadWidth}`,
|
|
222
|
+
`${baseX - perpX * halfHeadWidth},${baseY - perpY * halfHeadWidth}`,
|
|
223
|
+
].join(' '),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function updateMarkerType(annotation, markerType) {
|
|
227
|
+
return {
|
|
228
|
+
...annotation,
|
|
229
|
+
markerType,
|
|
230
|
+
color: annotation.color || ANNOTATION_COLORS[markerType],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function cloneAnnotation(annotation, order) {
|
|
234
|
+
return {
|
|
235
|
+
...annotation,
|
|
236
|
+
id: buildAnnotationId(),
|
|
237
|
+
order,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function buildDuplicateSessionTitle(title, fallbackTarget) {
|
|
241
|
+
const baseTitle = String(title || '').trim() || (fallbackTarget ? buildSessionTitle(fallbackTarget) : 'Annotated session');
|
|
242
|
+
if (/\bcopy$/i.test(baseTitle)) {
|
|
243
|
+
return `${baseTitle} 2`;
|
|
244
|
+
}
|
|
245
|
+
return `${baseTitle} copy`;
|
|
246
|
+
}
|
|
247
|
+
function moveAnnotation(annotations, fromIndex, toIndex) {
|
|
248
|
+
if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= annotations.length || toIndex >= annotations.length) {
|
|
249
|
+
return annotations;
|
|
250
|
+
}
|
|
251
|
+
const next = [...annotations];
|
|
252
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
253
|
+
if (!moved)
|
|
254
|
+
return annotations;
|
|
255
|
+
next.splice(toIndex, 0, moved);
|
|
256
|
+
return normalizeOrder(next);
|
|
257
|
+
}
|
|
258
|
+
function toPercentField(value) {
|
|
259
|
+
return String(Math.round(clamp01(value) * 1000) / 10);
|
|
260
|
+
}
|
|
261
|
+
function parsePercentField(value) {
|
|
262
|
+
const numeric = Number.parseFloat(value);
|
|
263
|
+
if (!Number.isFinite(numeric))
|
|
264
|
+
return null;
|
|
265
|
+
return clamp01(numeric / 100);
|
|
266
|
+
}
|
|
267
|
+
function updateAnnotationGeometryField(annotation, field, value) {
|
|
268
|
+
if (annotation.kind === 'pin' || annotation.kind === 'text-note') {
|
|
269
|
+
if (field === 'x' || field === 'y') {
|
|
270
|
+
return { ...annotation, [field]: clamp01(value) };
|
|
271
|
+
}
|
|
272
|
+
return annotation;
|
|
273
|
+
}
|
|
274
|
+
if (annotation.kind === 'box') {
|
|
275
|
+
if (field === 'x' || field === 'y') {
|
|
276
|
+
return { ...annotation, [field]: clamp01(value) };
|
|
277
|
+
}
|
|
278
|
+
if (field === 'width' || field === 'height') {
|
|
279
|
+
return { ...annotation, [field]: clampSize(value) };
|
|
280
|
+
}
|
|
281
|
+
return annotation;
|
|
282
|
+
}
|
|
283
|
+
if (field === 'x' || field === 'y' || field === 'x2' || field === 'y2') {
|
|
284
|
+
return { ...annotation, [field]: clamp01(value) };
|
|
285
|
+
}
|
|
286
|
+
return annotation;
|
|
287
|
+
}
|
|
288
|
+
function getGeometryFields(annotation) {
|
|
289
|
+
if (annotation.kind === 'pin' || annotation.kind === 'text-note') {
|
|
290
|
+
return [
|
|
291
|
+
{ key: 'x', label: 'X', value: toPercentField(annotation.x) },
|
|
292
|
+
{ key: 'y', label: 'Y', value: toPercentField(annotation.y) },
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
if (annotation.kind === 'box') {
|
|
296
|
+
return [
|
|
297
|
+
{ key: 'x', label: 'X', value: toPercentField(annotation.x) },
|
|
298
|
+
{ key: 'y', label: 'Y', value: toPercentField(annotation.y) },
|
|
299
|
+
{ key: 'width', label: 'Width', value: toPercentField(annotation.width) },
|
|
300
|
+
{ key: 'height', label: 'Height', value: toPercentField(annotation.height) },
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
return [
|
|
304
|
+
{ key: 'x', label: 'Start X', value: toPercentField(annotation.x) },
|
|
305
|
+
{ key: 'y', label: 'Start Y', value: toPercentField(annotation.y) },
|
|
306
|
+
{ key: 'x2', label: 'End X', value: toPercentField(annotation.x2) },
|
|
307
|
+
{ key: 'y2', label: 'End Y', value: toPercentField(annotation.y2) },
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
function formatCropHint(cropHint) {
|
|
311
|
+
if (!cropHint)
|
|
312
|
+
return null;
|
|
313
|
+
const left = Math.round(cropHint.x * 100);
|
|
314
|
+
const top = Math.round(cropHint.y * 100);
|
|
315
|
+
const width = Math.round(cropHint.width * 100);
|
|
316
|
+
const height = Math.round(cropHint.height * 100);
|
|
317
|
+
return `crop ${left}%, ${top}% size ${width}% x ${height}%`;
|
|
318
|
+
}
|
|
319
|
+
function clampZoom(value) {
|
|
320
|
+
if (!Number.isFinite(value))
|
|
321
|
+
return 1;
|
|
322
|
+
return Math.min(4, Math.max(0.25, Number(value.toFixed(2))));
|
|
323
|
+
}
|
|
324
|
+
function buildPayloadBrief(payload) {
|
|
325
|
+
const lines = [
|
|
326
|
+
`Annotated attachment: ${payload.title || payload.image.displayName}`,
|
|
327
|
+
`Image: ${payload.image.displayName}`,
|
|
328
|
+
`Image Reference: ${payload.image.referenceLabel || payload.image.assetId}`,
|
|
329
|
+
`Task ID: ${payload.taskId}`,
|
|
330
|
+
payload.globalInstruction
|
|
331
|
+
? `Global instruction: ${payload.globalInstruction}`
|
|
332
|
+
: 'Global instruction: None provided.',
|
|
333
|
+
'Markers:'
|
|
334
|
+
];
|
|
335
|
+
if (payload.annotations.length === 0) {
|
|
336
|
+
lines.push('0. No markers.');
|
|
337
|
+
return lines.join('\n');
|
|
338
|
+
}
|
|
339
|
+
payload.annotations.forEach((annotation, index) => {
|
|
340
|
+
const markerType = annotation.markerType || DEFAULT_MARKER_TYPE;
|
|
341
|
+
const cropText = formatCropHint(annotation.cropHint);
|
|
342
|
+
lines.push(`${index + 1}. ${annotation.kind} (${markerType})`);
|
|
343
|
+
lines.push(`Instruction: ${annotation.instruction || 'No marker instruction.'}`);
|
|
344
|
+
if (cropText)
|
|
345
|
+
lines.push(`Region: ${cropText}`);
|
|
346
|
+
});
|
|
347
|
+
return lines.join('\n');
|
|
348
|
+
}
|
|
349
|
+
function normalizeDraftPayload(payload) {
|
|
350
|
+
return {
|
|
351
|
+
title: payload.title,
|
|
352
|
+
globalInstruction: payload.globalInstruction,
|
|
353
|
+
annotations: normalizeOrder(payload.annotations),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function serializeDraftPayload(payload) {
|
|
357
|
+
return JSON.stringify(normalizeDraftPayload(payload));
|
|
358
|
+
}
|
|
359
|
+
function parseDraftSnapshot(snapshot) {
|
|
360
|
+
const parsed = JSON.parse(snapshot);
|
|
361
|
+
return normalizeDraftPayload({
|
|
362
|
+
title: String(parsed?.title || ''),
|
|
363
|
+
globalInstruction: String(parsed?.globalInstruction || ''),
|
|
364
|
+
annotations: Array.isArray(parsed?.annotations) ? parsed.annotations : [],
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function buildDraftPayloadFromSession(session, fallbackTitle) {
|
|
368
|
+
return normalizeDraftPayload({
|
|
369
|
+
title: session?.title || fallbackTitle,
|
|
370
|
+
globalInstruction: session?.globalInstruction || '',
|
|
371
|
+
annotations: session?.annotations || [],
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', workspaceId = 'default', sessionLoadReady = true, requestedTarget = null, requestedSessionId = null, requestedOpenVersion = 0, resolveTaskReferenceLabel, resolveImageReferenceLabel, onRequestedTargetHandled, onOpenTarget, onContextChange, onBackToTask, }) {
|
|
375
|
+
const resolvedApiBase = runtimeMode === 'cloud'
|
|
376
|
+
? (cloudAuthBaseUrl || apiBaseUrl || '')
|
|
377
|
+
: '';
|
|
378
|
+
const [uncontrolledTarget, setUncontrolledTarget] = useState(requestedTarget);
|
|
379
|
+
const [sessions, setSessions] = useState([]);
|
|
380
|
+
const [selectedSessionId, setSelectedSessionId] = useState(null);
|
|
381
|
+
const [sessionCardEditing, setSessionCardEditing] = useState(false);
|
|
382
|
+
const [draftTitle, setDraftTitle] = useState('');
|
|
383
|
+
const [draftGlobalInstruction, setDraftGlobalInstruction] = useState('');
|
|
384
|
+
const [draftAnnotations, setDraftAnnotations] = useState([]);
|
|
385
|
+
const [selectedAnnotationId, setSelectedAnnotationId] = useState(null);
|
|
386
|
+
const [defaultMarkerColor, setDefaultMarkerColor] = useState(ANNOTATION_COLORS[DEFAULT_MARKER_TYPE]);
|
|
387
|
+
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
|
388
|
+
const [toolMode, setToolMode] = useState('select');
|
|
389
|
+
const [showCreateSessionPrompt, setShowCreateSessionPrompt] = useState(false);
|
|
390
|
+
const [loading, setLoading] = useState(false);
|
|
391
|
+
const [saving, setSaving] = useState(false);
|
|
392
|
+
const [error, setError] = useState(null);
|
|
393
|
+
const [saveState, setSaveState] = useState('saved');
|
|
394
|
+
const [lastSaveError, setLastSaveError] = useState(null);
|
|
395
|
+
const [payloadLoading, setPayloadLoading] = useState(false);
|
|
396
|
+
const [payloadPreview, setPayloadPreview] = useState(null);
|
|
397
|
+
const [payloadCopied, setPayloadCopied] = useState(false);
|
|
398
|
+
const [payloadModalOpen, setPayloadModalOpen] = useState(false);
|
|
399
|
+
const [payloadPreviewMode, setPayloadPreviewMode] = useState('json');
|
|
400
|
+
const [taskIdCopied, setTaskIdCopied] = useState(false);
|
|
401
|
+
const [imageIdCopied, setImageIdCopied] = useState(false);
|
|
402
|
+
const [toolHelpModalOpen, setToolHelpModalOpen] = useState(false);
|
|
403
|
+
const [openImageModalOpen, setOpenImageModalOpen] = useState(false);
|
|
404
|
+
const [openImageReference, setOpenImageReference] = useState('');
|
|
405
|
+
const [openingImage, setOpeningImage] = useState(false);
|
|
406
|
+
const [imageLoading, setImageLoading] = useState(false);
|
|
407
|
+
const [imageLoadError, setImageLoadError] = useState(false);
|
|
408
|
+
const [zoomLevel, setZoomLevel] = useState(1);
|
|
409
|
+
const [panMode, setPanMode] = useState(false);
|
|
410
|
+
const [pastingImage, setPastingImage] = useState(false);
|
|
411
|
+
const [naturalImageSize, setNaturalImageSize] = useState({ width: 0, height: 0 });
|
|
412
|
+
const overlayRef = useRef(null);
|
|
413
|
+
const canvasScrollerRef = useRef(null);
|
|
414
|
+
const imageRef = useRef(null);
|
|
415
|
+
const selectedInstructionTextareaRef = useRef(null);
|
|
416
|
+
const pendingRequestedSessionIdRef = useRef(requestedSessionId);
|
|
417
|
+
const lastLoadedTargetKeyRef = useRef('');
|
|
418
|
+
const lastHandledRequestedOpenVersionRef = useRef(0);
|
|
419
|
+
const deferredTargetKeyRef = useRef('');
|
|
420
|
+
const latestLoadRequestIdRef = useRef(0);
|
|
421
|
+
const activeLoadKeyRef = useRef(null);
|
|
422
|
+
const suppressVisibilityReloadUntilRef = useRef(0);
|
|
423
|
+
const suppressAutoSaveRef = useRef(false);
|
|
424
|
+
const debounceTimerRef = useRef(null);
|
|
425
|
+
const pendingDebounceMsRef = useRef(null);
|
|
426
|
+
const selectedSessionIdRef = useRef(null);
|
|
427
|
+
const currentDraftSnapshotRef = useRef('');
|
|
428
|
+
const lastSavedSnapshotRef = useRef('');
|
|
429
|
+
const inFlightSnapshotRef = useRef(null);
|
|
430
|
+
const inFlightPromiseRef = useRef(null);
|
|
431
|
+
const hasPendingSaveAfterFlightRef = useRef(false);
|
|
432
|
+
const activeSaveSessionIdRef = useRef(null);
|
|
433
|
+
const autoRetrySnapshotRef = useRef(null);
|
|
434
|
+
const retryTimerRef = useRef(null);
|
|
435
|
+
const gestureStartRef = useRef(null);
|
|
436
|
+
const dragRef = useRef(null);
|
|
437
|
+
const resizeRef = useRef(null);
|
|
438
|
+
const arrowHandleRef = useRef(null);
|
|
439
|
+
const creatingAnnotationRef = useRef(null);
|
|
440
|
+
const panRef = useRef(null);
|
|
441
|
+
const colorPickerRef = useRef(null);
|
|
442
|
+
const sessionCardRef = useRef(null);
|
|
443
|
+
const selectedSession = useMemo(() => sessions.find((session) => session.id === selectedSessionId) || null, [selectedSessionId, sessions]);
|
|
444
|
+
const workspaceQuery = useMemo(() => `workspaceId=${encodeURIComponent(String(workspaceId || 'default').trim() || 'default')}`, [workspaceId]);
|
|
445
|
+
const requestHeaders = useMemo(() => ({ 'x-taskforce-workspace-id': String(workspaceId || 'default').trim() || 'default' }), [workspaceId]);
|
|
446
|
+
const selectedAnnotation = useMemo(() => draftAnnotations.find((annotation) => annotation.id === selectedAnnotationId) || null, [draftAnnotations, selectedAnnotationId]);
|
|
447
|
+
const requestedTargetKey = useMemo(() => getAnnotatedTargetKey(requestedTarget), [requestedTarget]);
|
|
448
|
+
const isTargetControlled = typeof onOpenTarget === 'function';
|
|
449
|
+
const activeTarget = isTargetControlled ? requestedTarget : uncontrolledTarget;
|
|
450
|
+
const activeTaskReferenceLabel = useMemo(() => {
|
|
451
|
+
const taskId = String(activeTarget?.taskId || '').trim();
|
|
452
|
+
if (!taskId)
|
|
453
|
+
return '';
|
|
454
|
+
const resolvedLabel = resolveTaskReferenceLabel?.(taskId).trim() || '';
|
|
455
|
+
if (resolvedLabel)
|
|
456
|
+
return resolvedLabel;
|
|
457
|
+
const explicitLabel = String(activeTarget?.taskReferenceLabel || '').trim();
|
|
458
|
+
if (explicitLabel && explicitLabel !== taskId)
|
|
459
|
+
return explicitLabel;
|
|
460
|
+
return resolvedLabel || taskId;
|
|
461
|
+
}, [activeTarget?.taskId, activeTarget?.taskReferenceLabel, resolveTaskReferenceLabel]);
|
|
462
|
+
const activeImageReferenceLabel = useMemo(() => {
|
|
463
|
+
const assetId = String(activeTarget?.assetId || '').trim();
|
|
464
|
+
if (!assetId)
|
|
465
|
+
return '';
|
|
466
|
+
const resolvedLabel = resolveImageReferenceLabel?.(assetId).trim() || '';
|
|
467
|
+
if (resolvedLabel)
|
|
468
|
+
return resolvedLabel;
|
|
469
|
+
return String(activeTarget?.imageReferenceLabel || '').trim();
|
|
470
|
+
}, [activeTarget?.assetId, activeTarget?.imageReferenceLabel, resolveImageReferenceLabel]);
|
|
471
|
+
const selectedAnnotationIndex = useMemo(() => draftAnnotations.findIndex((annotation) => annotation.id === selectedAnnotationId), [draftAnnotations, selectedAnnotationId]);
|
|
472
|
+
const selectedMarkerColor = selectedAnnotation?.color || defaultMarkerColor;
|
|
473
|
+
const currentDraftPayload = useMemo(() => normalizeDraftPayload({
|
|
474
|
+
title: draftTitle,
|
|
475
|
+
globalInstruction: draftGlobalInstruction,
|
|
476
|
+
annotations: draftAnnotations,
|
|
477
|
+
}), [draftAnnotations, draftGlobalInstruction, draftTitle]);
|
|
478
|
+
const currentDraftSnapshot = useMemo(() => serializeDraftPayload(currentDraftPayload), [currentDraftPayload]);
|
|
479
|
+
const hasUnsavedChanges = currentDraftSnapshot !== lastSavedSnapshotRef.current;
|
|
480
|
+
const canvasMediaSize = useMemo(() => ({
|
|
481
|
+
width: Math.max(naturalImageSize.width * zoomLevel, 0),
|
|
482
|
+
height: Math.max(naturalImageSize.height * zoomLevel, 0),
|
|
483
|
+
}), [naturalImageSize.height, naturalImageSize.width, zoomLevel]);
|
|
484
|
+
const arrowHandleGeometry = useMemo(() => ({
|
|
485
|
+
visibleRadius: 7,
|
|
486
|
+
hitRadius: 11,
|
|
487
|
+
}), []);
|
|
488
|
+
const arrowOverlayViewBox = useMemo(() => `0 0 ${Math.max(canvasMediaSize.width, 1)} ${Math.max(canvasMediaSize.height, 1)}`, [canvasMediaSize.height, canvasMediaSize.width]);
|
|
489
|
+
const canPan = useMemo(() => {
|
|
490
|
+
const scroller = canvasScrollerRef.current;
|
|
491
|
+
if (!scroller)
|
|
492
|
+
return zoomLevel > 1;
|
|
493
|
+
return (zoomLevel > 1
|
|
494
|
+
|| (canvasMediaSize.width > (scroller.clientWidth + 1)
|
|
495
|
+
|| canvasMediaSize.height > (scroller.clientHeight + 1)));
|
|
496
|
+
}, [canvasMediaSize.height, canvasMediaSize.width, zoomLevel]);
|
|
497
|
+
const zoomLabel = `${Math.round(zoomLevel * 100)}%`;
|
|
498
|
+
const annotationOrderNumbers = useMemo(() => {
|
|
499
|
+
const orderMap = new Map();
|
|
500
|
+
draftAnnotations.forEach((annotation, index) => {
|
|
501
|
+
orderMap.set(annotation.id, index + 1);
|
|
502
|
+
});
|
|
503
|
+
return orderMap;
|
|
504
|
+
}, [draftAnnotations]);
|
|
505
|
+
useEffect(() => {
|
|
506
|
+
pendingRequestedSessionIdRef.current = requestedSessionId;
|
|
507
|
+
}, [requestedSessionId]);
|
|
508
|
+
useEffect(() => {
|
|
509
|
+
const textarea = selectedInstructionTextareaRef.current;
|
|
510
|
+
if (!textarea)
|
|
511
|
+
return;
|
|
512
|
+
if (!selectedAnnotationId)
|
|
513
|
+
return;
|
|
514
|
+
textarea.focus();
|
|
515
|
+
const end = textarea.value.length;
|
|
516
|
+
textarea.setSelectionRange(end, end);
|
|
517
|
+
}, [selectedAnnotationId]);
|
|
518
|
+
useEffect(() => {
|
|
519
|
+
selectedSessionIdRef.current = selectedSessionId;
|
|
520
|
+
}, [selectedSessionId]);
|
|
521
|
+
useEffect(() => {
|
|
522
|
+
setSessionCardEditing(false);
|
|
523
|
+
}, [selectedSessionId]);
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
if (!selectedAnnotation) {
|
|
526
|
+
setColorPickerOpen(false);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
setDefaultMarkerColor(selectedAnnotation.color || ANNOTATION_COLORS[selectedAnnotation.markerType || DEFAULT_MARKER_TYPE]);
|
|
530
|
+
}, [selectedAnnotation]);
|
|
531
|
+
const createSessionForTarget = useCallback(async (target) => {
|
|
532
|
+
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
533
|
+
const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions?${workspaceQuery}`, {
|
|
534
|
+
method: 'POST',
|
|
535
|
+
credentials: 'include',
|
|
536
|
+
headers: { 'Content-Type': 'application/json', ...requestHeaders },
|
|
537
|
+
body: JSON.stringify({
|
|
538
|
+
taskId: target.taskId,
|
|
539
|
+
baseImageAssetId: target.assetId,
|
|
540
|
+
title: buildSessionTitle(target),
|
|
541
|
+
globalInstruction: '',
|
|
542
|
+
annotations: [],
|
|
543
|
+
}),
|
|
544
|
+
}, resolvedApiBase);
|
|
545
|
+
const data = await res.json().catch(() => ({}));
|
|
546
|
+
if (!res.ok) {
|
|
547
|
+
throw new Error(String(data?.error || 'Failed to create session.'));
|
|
548
|
+
}
|
|
549
|
+
const session = data?.session;
|
|
550
|
+
if (!session?.id) {
|
|
551
|
+
throw new Error('Failed to create session.');
|
|
552
|
+
}
|
|
553
|
+
logBootstrapDebug('annotated_session_create_completed', {
|
|
554
|
+
assetId: target.assetId,
|
|
555
|
+
taskId: target.taskId || null,
|
|
556
|
+
sessionId: session.id,
|
|
557
|
+
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt),
|
|
558
|
+
debugTimings: data?.debugTimings || null,
|
|
559
|
+
serverTiming: typeof res.headers?.get === 'function'
|
|
560
|
+
? res.headers.get('server-timing') || null
|
|
561
|
+
: null,
|
|
562
|
+
});
|
|
563
|
+
return session;
|
|
564
|
+
}, [requestHeaders, resolvedApiBase, workspaceQuery]);
|
|
565
|
+
const readFileAsDataUrl = useCallback((blob) => new Promise((resolve, reject) => {
|
|
566
|
+
const reader = new FileReader();
|
|
567
|
+
reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
|
|
568
|
+
reader.onerror = () => reject(reader.error || new Error('Failed to read clipboard image.'));
|
|
569
|
+
reader.readAsDataURL(blob);
|
|
570
|
+
}), []);
|
|
571
|
+
const clearPendingAutoSave = useCallback(() => {
|
|
572
|
+
if (debounceTimerRef.current !== null) {
|
|
573
|
+
window.clearTimeout(debounceTimerRef.current);
|
|
574
|
+
debounceTimerRef.current = null;
|
|
575
|
+
}
|
|
576
|
+
if (retryTimerRef.current !== null) {
|
|
577
|
+
window.clearTimeout(retryTimerRef.current);
|
|
578
|
+
retryTimerRef.current = null;
|
|
579
|
+
}
|
|
580
|
+
}, []);
|
|
581
|
+
const applyDraftPayload = useCallback((payload) => {
|
|
582
|
+
setDraftTitle(payload.title);
|
|
583
|
+
setDraftGlobalInstruction(payload.globalInstruction);
|
|
584
|
+
setDraftAnnotations(payload.annotations);
|
|
585
|
+
setSelectedAnnotationId((current) => (current && payload.annotations.some((annotation) => annotation.id === current)
|
|
586
|
+
? current
|
|
587
|
+
: (payload.annotations[0]?.id || null)));
|
|
588
|
+
}, []);
|
|
589
|
+
const syncDraftFromSession = useCallback((session, target) => {
|
|
590
|
+
const fallbackTitle = buildSessionTitle(target || activeTarget || {
|
|
591
|
+
taskId: '',
|
|
592
|
+
assetId: session.baseImageAssetId,
|
|
593
|
+
path: '',
|
|
594
|
+
displayName: 'Annotated session',
|
|
595
|
+
});
|
|
596
|
+
const payload = buildDraftPayloadFromSession(session, fallbackTitle);
|
|
597
|
+
const snapshot = serializeDraftPayload(payload);
|
|
598
|
+
suppressAutoSaveRef.current = true;
|
|
599
|
+
clearPendingAutoSave();
|
|
600
|
+
applyDraftPayload(payload);
|
|
601
|
+
lastSavedSnapshotRef.current = snapshot;
|
|
602
|
+
currentDraftSnapshotRef.current = snapshot;
|
|
603
|
+
inFlightSnapshotRef.current = null;
|
|
604
|
+
hasPendingSaveAfterFlightRef.current = false;
|
|
605
|
+
activeSaveSessionIdRef.current = null;
|
|
606
|
+
autoRetrySnapshotRef.current = null;
|
|
607
|
+
setLastSaveError(null);
|
|
608
|
+
setSaveState('saved');
|
|
609
|
+
setSaving(false);
|
|
610
|
+
}, [activeTarget, applyDraftPayload, clearPendingAutoSave]);
|
|
611
|
+
const resetForTargetLoad = useCallback((target) => {
|
|
612
|
+
suppressAutoSaveRef.current = true;
|
|
613
|
+
clearPendingAutoSave();
|
|
614
|
+
setSessions([]);
|
|
615
|
+
setSelectedSessionId(null);
|
|
616
|
+
setShowCreateSessionPrompt(false);
|
|
617
|
+
setSessionCardEditing(false);
|
|
618
|
+
setDraftTitle(buildSessionTitle(target));
|
|
619
|
+
setDraftGlobalInstruction('');
|
|
620
|
+
setDraftAnnotations([]);
|
|
621
|
+
setSelectedAnnotationId(null);
|
|
622
|
+
lastSavedSnapshotRef.current = '';
|
|
623
|
+
currentDraftSnapshotRef.current = '';
|
|
624
|
+
inFlightSnapshotRef.current = null;
|
|
625
|
+
hasPendingSaveAfterFlightRef.current = false;
|
|
626
|
+
activeSaveSessionIdRef.current = null;
|
|
627
|
+
autoRetrySnapshotRef.current = null;
|
|
628
|
+
setPayloadPreview(null);
|
|
629
|
+
setPayloadModalOpen(false);
|
|
630
|
+
setLastSaveError(null);
|
|
631
|
+
setSaveState('saved');
|
|
632
|
+
setSaving(false);
|
|
633
|
+
}, [clearPendingAutoSave]);
|
|
634
|
+
const saveSnapshot = useCallback(async (snapshot, trigger, options) => {
|
|
635
|
+
const sessionId = selectedSessionIdRef.current;
|
|
636
|
+
if (!sessionId)
|
|
637
|
+
return true;
|
|
638
|
+
if (snapshot === lastSavedSnapshotRef.current) {
|
|
639
|
+
if (!inFlightPromiseRef.current) {
|
|
640
|
+
setLastSaveError(null);
|
|
641
|
+
setSaveState('saved');
|
|
642
|
+
}
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
if (inFlightPromiseRef.current) {
|
|
646
|
+
hasPendingSaveAfterFlightRef.current = true;
|
|
647
|
+
if (!options?.waitForInFlight)
|
|
648
|
+
return false;
|
|
649
|
+
const inFlightResult = await inFlightPromiseRef.current;
|
|
650
|
+
if (!inFlightResult)
|
|
651
|
+
return false;
|
|
652
|
+
const latestSnapshot = currentDraftSnapshotRef.current;
|
|
653
|
+
if (latestSnapshot === lastSavedSnapshotRef.current)
|
|
654
|
+
return true;
|
|
655
|
+
return saveSnapshot(latestSnapshot, trigger, options);
|
|
656
|
+
}
|
|
657
|
+
clearPendingAutoSave();
|
|
658
|
+
const payload = parseDraftSnapshot(snapshot);
|
|
659
|
+
activeSaveSessionIdRef.current = sessionId;
|
|
660
|
+
inFlightSnapshotRef.current = snapshot;
|
|
661
|
+
setSaving(true);
|
|
662
|
+
setError(null);
|
|
663
|
+
setLastSaveError(null);
|
|
664
|
+
setSaveState('saving');
|
|
665
|
+
const request = (async () => {
|
|
666
|
+
try {
|
|
667
|
+
const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions/${encodeURIComponent(sessionId)}?${workspaceQuery}`, {
|
|
668
|
+
method: 'PATCH',
|
|
669
|
+
credentials: 'include',
|
|
670
|
+
headers: { 'Content-Type': 'application/json', ...requestHeaders },
|
|
671
|
+
body: JSON.stringify(payload),
|
|
672
|
+
}, resolvedApiBase);
|
|
673
|
+
const data = await res.json().catch(() => ({}));
|
|
674
|
+
if (!res.ok) {
|
|
675
|
+
throw new Error(String(data?.error || 'Failed to save session.'));
|
|
676
|
+
}
|
|
677
|
+
const session = data?.session;
|
|
678
|
+
if (!session?.id)
|
|
679
|
+
throw new Error('Failed to save session.');
|
|
680
|
+
setSessions((current) => {
|
|
681
|
+
const existingIndex = current.findIndex((item) => item.id === session.id);
|
|
682
|
+
if (existingIndex === -1)
|
|
683
|
+
return [session, ...current];
|
|
684
|
+
const next = [...current];
|
|
685
|
+
next[existingIndex] = session;
|
|
686
|
+
return next;
|
|
687
|
+
});
|
|
688
|
+
lastSavedSnapshotRef.current = snapshot;
|
|
689
|
+
autoRetrySnapshotRef.current = null;
|
|
690
|
+
setLastSaveError(null);
|
|
691
|
+
const stillCurrentSession = selectedSessionIdRef.current === session.id;
|
|
692
|
+
const latestSnapshot = currentDraftSnapshotRef.current;
|
|
693
|
+
const hasNewerLocalChanges = latestSnapshot !== snapshot;
|
|
694
|
+
const shouldSaveAgain = hasPendingSaveAfterFlightRef.current || hasNewerLocalChanges;
|
|
695
|
+
hasPendingSaveAfterFlightRef.current = false;
|
|
696
|
+
if (stillCurrentSession && !shouldSaveAgain) {
|
|
697
|
+
suppressAutoSaveRef.current = true;
|
|
698
|
+
applyDraftPayload(payload);
|
|
699
|
+
setSaveState('saved');
|
|
700
|
+
}
|
|
701
|
+
else if (!shouldSaveAgain && latestSnapshot === lastSavedSnapshotRef.current) {
|
|
702
|
+
setSaveState('saved');
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
setSaveState('pending');
|
|
706
|
+
}
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
catch (requestError) {
|
|
710
|
+
const message = requestError instanceof Error ? requestError.message : 'Failed to save session.';
|
|
711
|
+
setError(message);
|
|
712
|
+
setLastSaveError(message);
|
|
713
|
+
setSaveState('error');
|
|
714
|
+
if (autoRetrySnapshotRef.current !== snapshot
|
|
715
|
+
&& selectedSessionIdRef.current === sessionId
|
|
716
|
+
&& currentDraftSnapshotRef.current === snapshot) {
|
|
717
|
+
autoRetrySnapshotRef.current = snapshot;
|
|
718
|
+
retryTimerRef.current = window.setTimeout(() => {
|
|
719
|
+
retryTimerRef.current = null;
|
|
720
|
+
if (selectedSessionIdRef.current !== sessionId
|
|
721
|
+
|| currentDraftSnapshotRef.current !== snapshot) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
void saveSnapshot(snapshot, trigger, { waitForInFlight: true });
|
|
725
|
+
}, 1500);
|
|
726
|
+
}
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
finally {
|
|
730
|
+
inFlightSnapshotRef.current = null;
|
|
731
|
+
inFlightPromiseRef.current = null;
|
|
732
|
+
activeSaveSessionIdRef.current = null;
|
|
733
|
+
setSaving(false);
|
|
734
|
+
}
|
|
735
|
+
})();
|
|
736
|
+
inFlightPromiseRef.current = request;
|
|
737
|
+
const success = await request;
|
|
738
|
+
if (success) {
|
|
739
|
+
const latestSnapshot = currentDraftSnapshotRef.current;
|
|
740
|
+
if (latestSnapshot !== lastSavedSnapshotRef.current) {
|
|
741
|
+
return saveSnapshot(latestSnapshot, trigger, options);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return success;
|
|
745
|
+
}, [applyDraftPayload, clearPendingAutoSave, requestHeaders, resolvedApiBase, workspaceQuery]);
|
|
746
|
+
const scheduleAutoSave = useCallback((delayMs) => {
|
|
747
|
+
if (!selectedSessionIdRef.current)
|
|
748
|
+
return;
|
|
749
|
+
if (currentDraftSnapshotRef.current === lastSavedSnapshotRef.current) {
|
|
750
|
+
setLastSaveError(null);
|
|
751
|
+
if (!inFlightPromiseRef.current)
|
|
752
|
+
setSaveState('saved');
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (saveState !== 'saving')
|
|
756
|
+
setSaveState('pending');
|
|
757
|
+
clearPendingAutoSave();
|
|
758
|
+
debounceTimerRef.current = window.setTimeout(() => {
|
|
759
|
+
debounceTimerRef.current = null;
|
|
760
|
+
void saveSnapshot(currentDraftSnapshotRef.current, 'structure');
|
|
761
|
+
}, delayMs);
|
|
762
|
+
}, [clearPendingAutoSave, saveSnapshot, saveState]);
|
|
763
|
+
const markDraftChanged = useCallback((delayMs) => {
|
|
764
|
+
pendingDebounceMsRef.current = delayMs;
|
|
765
|
+
}, []);
|
|
766
|
+
const flushPendingDraft = useCallback(async (trigger) => {
|
|
767
|
+
clearPendingAutoSave();
|
|
768
|
+
const currentSnapshot = currentDraftSnapshotRef.current;
|
|
769
|
+
if (!selectedSessionIdRef.current || currentSnapshot === lastSavedSnapshotRef.current) {
|
|
770
|
+
setLastSaveError(null);
|
|
771
|
+
if (!inFlightPromiseRef.current)
|
|
772
|
+
setSaveState('saved');
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
return saveSnapshot(currentSnapshot, trigger, { waitForInFlight: true });
|
|
776
|
+
}, [clearPendingAutoSave, saveSnapshot]);
|
|
777
|
+
const restoreLastSavedDraft = useCallback(() => {
|
|
778
|
+
const snapshot = lastSavedSnapshotRef.current;
|
|
779
|
+
if (!snapshot)
|
|
780
|
+
return;
|
|
781
|
+
clearPendingAutoSave();
|
|
782
|
+
suppressAutoSaveRef.current = true;
|
|
783
|
+
applyDraftPayload(parseDraftSnapshot(snapshot));
|
|
784
|
+
setLastSaveError(null);
|
|
785
|
+
setSaveState('saved');
|
|
786
|
+
setError(null);
|
|
787
|
+
}, [applyDraftPayload, clearPendingAutoSave]);
|
|
788
|
+
const loadSessions = useCallback(async (target, options) => {
|
|
789
|
+
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
790
|
+
const loadKey = JSON.stringify({
|
|
791
|
+
targetKey: getAnnotatedTargetKey(target),
|
|
792
|
+
requestedSessionId: options?.requestedSessionId ?? null,
|
|
793
|
+
autoCreateIfEmpty: options?.autoCreateIfEmpty === true,
|
|
794
|
+
});
|
|
795
|
+
if (activeLoadKeyRef.current === loadKey)
|
|
796
|
+
return;
|
|
797
|
+
const requestId = latestLoadRequestIdRef.current + 1;
|
|
798
|
+
latestLoadRequestIdRef.current = requestId;
|
|
799
|
+
activeLoadKeyRef.current = loadKey;
|
|
800
|
+
setLoading(true);
|
|
801
|
+
setError(null);
|
|
802
|
+
try {
|
|
803
|
+
const params = new URLSearchParams();
|
|
804
|
+
if (workspaceId)
|
|
805
|
+
params.set('workspaceId', workspaceId);
|
|
806
|
+
if (target.taskId)
|
|
807
|
+
params.set('taskId', target.taskId);
|
|
808
|
+
params.set('imageAssetId', target.assetId);
|
|
809
|
+
const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions?${params.toString()}`, { credentials: 'include', headers: requestHeaders, cache: 'no-store' }, resolvedApiBase);
|
|
810
|
+
const data = await res.json().catch(() => ({}));
|
|
811
|
+
if (!res.ok) {
|
|
812
|
+
throw new Error(String(data?.error || 'Failed to load annotated attachment sessions.'));
|
|
813
|
+
}
|
|
814
|
+
const nextSessions = Array.isArray(data?.sessions) ? data.sessions : [];
|
|
815
|
+
logBootstrapDebug('annotated_sessions_loaded', {
|
|
816
|
+
assetId: target.assetId,
|
|
817
|
+
taskId: target.taskId || null,
|
|
818
|
+
requestedSessionId: options?.requestedSessionId ?? null,
|
|
819
|
+
autoCreateIfEmpty: options?.autoCreateIfEmpty === true,
|
|
820
|
+
sessionCount: nextSessions.length,
|
|
821
|
+
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt),
|
|
822
|
+
serverTiming: typeof res.headers?.get === 'function'
|
|
823
|
+
? res.headers.get('server-timing') || null
|
|
824
|
+
: null,
|
|
825
|
+
});
|
|
826
|
+
if (nextSessions.length === 0
|
|
827
|
+
&& options?.autoCreateIfEmpty
|
|
828
|
+
&& String(target.assetId || '').trim()) {
|
|
829
|
+
const requestedSessionIdValue = options?.requestedSessionId ?? pendingRequestedSessionIdRef.current;
|
|
830
|
+
pendingRequestedSessionIdRef.current = null;
|
|
831
|
+
if (requestedSessionIdValue) {
|
|
832
|
+
setError('The previously selected annotation session could not be restored.');
|
|
833
|
+
}
|
|
834
|
+
if (latestLoadRequestIdRef.current !== requestId)
|
|
835
|
+
return;
|
|
836
|
+
const session = await createSessionForTarget(target);
|
|
837
|
+
logBootstrapDebug('annotated_sessions_auto_created_after_empty_load', {
|
|
838
|
+
assetId: target.assetId,
|
|
839
|
+
taskId: target.taskId || null,
|
|
840
|
+
sessionId: session.id,
|
|
841
|
+
totalDurationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt),
|
|
842
|
+
});
|
|
843
|
+
if (latestLoadRequestIdRef.current !== requestId)
|
|
844
|
+
return;
|
|
845
|
+
setSessions([session]);
|
|
846
|
+
setSelectedSessionId(session.id);
|
|
847
|
+
setShowCreateSessionPrompt(false);
|
|
848
|
+
syncDraftFromSession(session, target);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (latestLoadRequestIdRef.current !== requestId)
|
|
852
|
+
return;
|
|
853
|
+
setSessions(nextSessions);
|
|
854
|
+
setShowCreateSessionPrompt(nextSessions.length === 0);
|
|
855
|
+
const requestedSessionIdValue = options?.requestedSessionId ?? pendingRequestedSessionIdRef.current;
|
|
856
|
+
pendingRequestedSessionIdRef.current = null;
|
|
857
|
+
const nextSelected = nextSessions.find((session) => session.id === requestedSessionIdValue) || nextSessions[0] || null;
|
|
858
|
+
if (requestedSessionIdValue && !nextSelected) {
|
|
859
|
+
setError('The previously selected annotation session could not be restored.');
|
|
860
|
+
}
|
|
861
|
+
setSelectedSessionId(nextSelected?.id || null);
|
|
862
|
+
if (nextSelected) {
|
|
863
|
+
syncDraftFromSession(nextSelected, target);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
suppressAutoSaveRef.current = true;
|
|
867
|
+
clearPendingAutoSave();
|
|
868
|
+
setDraftTitle(buildSessionTitle(target));
|
|
869
|
+
setDraftGlobalInstruction('');
|
|
870
|
+
setDraftAnnotations([]);
|
|
871
|
+
setSelectedAnnotationId(null);
|
|
872
|
+
lastSavedSnapshotRef.current = '';
|
|
873
|
+
currentDraftSnapshotRef.current = '';
|
|
874
|
+
setLastSaveError(null);
|
|
875
|
+
setSaveState('saved');
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch (requestError) {
|
|
879
|
+
if (latestLoadRequestIdRef.current !== requestId)
|
|
880
|
+
return;
|
|
881
|
+
setError(requestError instanceof Error ? requestError.message : 'Failed to load sessions.');
|
|
882
|
+
}
|
|
883
|
+
finally {
|
|
884
|
+
if (activeLoadKeyRef.current === loadKey) {
|
|
885
|
+
activeLoadKeyRef.current = null;
|
|
886
|
+
}
|
|
887
|
+
if (latestLoadRequestIdRef.current === requestId) {
|
|
888
|
+
setLoading(false);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}, [clearPendingAutoSave, createSessionForTarget, requestHeaders, resolvedApiBase, syncDraftFromSession, workspaceId]);
|
|
892
|
+
const pasteImageFromClipboard = useCallback(async () => {
|
|
893
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard || typeof navigator.clipboard.read !== 'function') {
|
|
894
|
+
setError('Clipboard image paste is not supported in this environment.');
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
setPastingImage(true);
|
|
898
|
+
setError(null);
|
|
899
|
+
suppressVisibilityReloadUntilRef.current = Date.now() + 2000;
|
|
900
|
+
try {
|
|
901
|
+
const items = await navigator.clipboard.read();
|
|
902
|
+
const imageItem = items.find((item) => item.types.some((type) => type.startsWith('image/')));
|
|
903
|
+
const imageType = imageItem?.types.find((type) => type.startsWith('image/')) || '';
|
|
904
|
+
if (!imageItem || !imageType) {
|
|
905
|
+
throw new Error('No image found on the clipboard.');
|
|
906
|
+
}
|
|
907
|
+
const blob = await imageItem.getType(imageType);
|
|
908
|
+
const encoded = await readFileAsDataUrl(blob);
|
|
909
|
+
if (!encoded) {
|
|
910
|
+
throw new Error('Failed to read clipboard image.');
|
|
911
|
+
}
|
|
912
|
+
const extension = imageType === 'image/jpeg'
|
|
913
|
+
? 'jpg'
|
|
914
|
+
: imageType === 'image/webp'
|
|
915
|
+
? 'webp'
|
|
916
|
+
: imageType === 'image/gif'
|
|
917
|
+
? 'gif'
|
|
918
|
+
: 'png';
|
|
919
|
+
const response = await fetch('/api/taskforce/context-upload', {
|
|
920
|
+
method: 'POST',
|
|
921
|
+
credentials: 'include',
|
|
922
|
+
headers: {
|
|
923
|
+
'Content-Type': 'application/json',
|
|
924
|
+
...requestHeaders,
|
|
925
|
+
},
|
|
926
|
+
body: JSON.stringify({
|
|
927
|
+
file: encoded,
|
|
928
|
+
originalName: `pasted-image.${extension}`,
|
|
929
|
+
workspaceId: String(workspaceId || 'default').trim() || 'default',
|
|
930
|
+
}),
|
|
931
|
+
});
|
|
932
|
+
const data = await response.json().catch(() => ({}));
|
|
933
|
+
if (!response.ok || !data?.success || typeof data?.assetId !== 'string' || typeof data?.path !== 'string') {
|
|
934
|
+
throw new Error(String(data?.error || 'Failed to paste image into Image Notes.'));
|
|
935
|
+
}
|
|
936
|
+
const target = {
|
|
937
|
+
assetId: data.assetId,
|
|
938
|
+
imageReferenceLabel: typeof data?.referenceLabel === 'string' ? data.referenceLabel : undefined,
|
|
939
|
+
path: data.path,
|
|
940
|
+
displayName: typeof data?.displayName === 'string' && data.displayName.trim().length > 0
|
|
941
|
+
? data.displayName.trim()
|
|
942
|
+
: 'Pasted image',
|
|
943
|
+
};
|
|
944
|
+
pendingRequestedSessionIdRef.current = null;
|
|
945
|
+
if (isTargetControlled) {
|
|
946
|
+
lastLoadedTargetKeyRef.current = '';
|
|
947
|
+
onOpenTarget?.(target, { sessionId: null });
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
setUncontrolledTarget(target);
|
|
951
|
+
lastLoadedTargetKeyRef.current = getAnnotatedTargetKey(target);
|
|
952
|
+
void loadSessions(target, { autoCreateIfEmpty: true });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
catch (pasteError) {
|
|
956
|
+
setError(pasteError instanceof Error ? pasteError.message : 'Failed to paste image.');
|
|
957
|
+
}
|
|
958
|
+
finally {
|
|
959
|
+
setPastingImage(false);
|
|
960
|
+
}
|
|
961
|
+
}, [isTargetControlled, loadSessions, onOpenTarget, readFileAsDataUrl, requestHeaders, workspaceId]);
|
|
962
|
+
const openImageByReference = useCallback(async () => {
|
|
963
|
+
const normalizedReference = openImageReference.trim();
|
|
964
|
+
if (!normalizedReference) {
|
|
965
|
+
setError('Enter an image reference to open.');
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
setOpeningImage(true);
|
|
969
|
+
setError(null);
|
|
970
|
+
try {
|
|
971
|
+
const workspaceQuery = new URLSearchParams({
|
|
972
|
+
workspaceId: String(workspaceId || 'default').trim() || 'default',
|
|
973
|
+
});
|
|
974
|
+
const response = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/images/${encodeURIComponent(normalizedReference)}?${workspaceQuery.toString()}`, {
|
|
975
|
+
method: 'GET',
|
|
976
|
+
credentials: 'include',
|
|
977
|
+
headers: requestHeaders,
|
|
978
|
+
});
|
|
979
|
+
const data = await response.json().catch(() => ({}));
|
|
980
|
+
if (!response.ok || !data?.target || typeof data.target.assetId !== 'string' || typeof data.target.path !== 'string') {
|
|
981
|
+
throw new Error(String(data?.error || 'Failed to open image reference.'));
|
|
982
|
+
}
|
|
983
|
+
const target = {
|
|
984
|
+
assetId: data.target.assetId,
|
|
985
|
+
path: data.target.path,
|
|
986
|
+
displayName: typeof data.target.displayName === 'string' && data.target.displayName.trim().length > 0
|
|
987
|
+
? data.target.displayName.trim()
|
|
988
|
+
: 'Image attachment',
|
|
989
|
+
taskId: typeof data.target.taskId === 'string' && data.target.taskId.trim().length > 0
|
|
990
|
+
? data.target.taskId.trim()
|
|
991
|
+
: undefined,
|
|
992
|
+
taskReferenceLabel: typeof data.target.taskReferenceLabel === 'string' && data.target.taskReferenceLabel.trim().length > 0
|
|
993
|
+
? data.target.taskReferenceLabel.trim()
|
|
994
|
+
: undefined,
|
|
995
|
+
imageReferenceLabel: typeof data.target.imageReferenceLabel === 'string' && data.target.imageReferenceLabel.trim().length > 0
|
|
996
|
+
? data.target.imageReferenceLabel.trim()
|
|
997
|
+
: undefined,
|
|
998
|
+
};
|
|
999
|
+
pendingRequestedSessionIdRef.current = null;
|
|
1000
|
+
setOpenImageModalOpen(false);
|
|
1001
|
+
setOpenImageReference('');
|
|
1002
|
+
if (isTargetControlled) {
|
|
1003
|
+
lastLoadedTargetKeyRef.current = '';
|
|
1004
|
+
onOpenTarget?.(target, { sessionId: null });
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
setUncontrolledTarget(target);
|
|
1008
|
+
lastLoadedTargetKeyRef.current = getAnnotatedTargetKey(target);
|
|
1009
|
+
void loadSessions(target, { autoCreateIfEmpty: true });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
catch (openError) {
|
|
1013
|
+
setError(openError instanceof Error ? openError.message : 'Failed to open image reference.');
|
|
1014
|
+
}
|
|
1015
|
+
finally {
|
|
1016
|
+
setOpeningImage(false);
|
|
1017
|
+
}
|
|
1018
|
+
}, [isTargetControlled, loadSessions, onOpenTarget, openImageReference, requestHeaders, workspaceId]);
|
|
1019
|
+
const applySessionLocally = useCallback((session) => {
|
|
1020
|
+
setSessions((current) => {
|
|
1021
|
+
const existingIndex = current.findIndex((item) => item.id === session.id);
|
|
1022
|
+
if (existingIndex === -1)
|
|
1023
|
+
return [session, ...current];
|
|
1024
|
+
const next = [...current];
|
|
1025
|
+
next[existingIndex] = session;
|
|
1026
|
+
return next;
|
|
1027
|
+
});
|
|
1028
|
+
setSelectedSessionId(session.id);
|
|
1029
|
+
syncDraftFromSession(session, activeTarget);
|
|
1030
|
+
}, [activeTarget, syncDraftFromSession]);
|
|
1031
|
+
const removeSessionLocally = useCallback((sessionId) => {
|
|
1032
|
+
setSessions((current) => {
|
|
1033
|
+
const remaining = current.filter((session) => session.id !== sessionId);
|
|
1034
|
+
const nextSelected = remaining[0] || null;
|
|
1035
|
+
setSelectedSessionId(nextSelected?.id || null);
|
|
1036
|
+
if (nextSelected) {
|
|
1037
|
+
syncDraftFromSession(nextSelected, activeTarget);
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
suppressAutoSaveRef.current = true;
|
|
1041
|
+
clearPendingAutoSave();
|
|
1042
|
+
setDraftTitle(buildSessionTitle(activeTarget || {
|
|
1043
|
+
taskId: '',
|
|
1044
|
+
assetId: '',
|
|
1045
|
+
path: '',
|
|
1046
|
+
displayName: 'Annotated session',
|
|
1047
|
+
}));
|
|
1048
|
+
setDraftGlobalInstruction('');
|
|
1049
|
+
setDraftAnnotations([]);
|
|
1050
|
+
setSelectedAnnotationId(null);
|
|
1051
|
+
lastSavedSnapshotRef.current = '';
|
|
1052
|
+
currentDraftSnapshotRef.current = '';
|
|
1053
|
+
setLastSaveError(null);
|
|
1054
|
+
setSaveState('saved');
|
|
1055
|
+
}
|
|
1056
|
+
return remaining;
|
|
1057
|
+
});
|
|
1058
|
+
}, [activeTarget, clearPendingAutoSave, syncDraftFromSession]);
|
|
1059
|
+
useEffect(() => {
|
|
1060
|
+
if (!requestedTarget)
|
|
1061
|
+
return;
|
|
1062
|
+
if (!isTargetControlled) {
|
|
1063
|
+
setUncontrolledTarget((current) => {
|
|
1064
|
+
if (current
|
|
1065
|
+
&& getAnnotatedTargetKey(current) === requestedTargetKey
|
|
1066
|
+
&& current.taskReferenceLabel === requestedTarget.taskReferenceLabel
|
|
1067
|
+
&& current.imageReferenceLabel === requestedTarget.imageReferenceLabel
|
|
1068
|
+
&& current.displayName === requestedTarget.displayName) {
|
|
1069
|
+
return current;
|
|
1070
|
+
}
|
|
1071
|
+
return requestedTarget;
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
if (!sessionLoadReady) {
|
|
1075
|
+
const shouldDeferRequestedTarget = Boolean(requestedTargetKey
|
|
1076
|
+
&& (requestedTargetKey !== lastLoadedTargetKeyRef.current
|
|
1077
|
+
|| requestedOpenVersion !== lastHandledRequestedOpenVersionRef.current)
|
|
1078
|
+
&& requestedTargetKey !== deferredTargetKeyRef.current);
|
|
1079
|
+
if (shouldDeferRequestedTarget) {
|
|
1080
|
+
resetForTargetLoad(requestedTarget);
|
|
1081
|
+
deferredTargetKeyRef.current = requestedTargetKey;
|
|
1082
|
+
lastHandledRequestedOpenVersionRef.current = requestedOpenVersion;
|
|
1083
|
+
logBootstrapDebug('annotated_sessions_load_deferred', {
|
|
1084
|
+
assetId: requestedTarget.assetId,
|
|
1085
|
+
taskId: requestedTarget.taskId || null,
|
|
1086
|
+
requestedTargetKey,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
onRequestedTargetHandled?.();
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
const shouldReloadRequestedTarget = Boolean(requestedTargetKey
|
|
1093
|
+
&& (requestedTargetKey !== lastLoadedTargetKeyRef.current
|
|
1094
|
+
|| requestedOpenVersion !== lastHandledRequestedOpenVersionRef.current));
|
|
1095
|
+
if (shouldReloadRequestedTarget) {
|
|
1096
|
+
resetForTargetLoad(requestedTarget);
|
|
1097
|
+
lastLoadedTargetKeyRef.current = requestedTargetKey;
|
|
1098
|
+
lastHandledRequestedOpenVersionRef.current = requestedOpenVersion;
|
|
1099
|
+
deferredTargetKeyRef.current = '';
|
|
1100
|
+
void loadSessions(requestedTarget, { autoCreateIfEmpty: true });
|
|
1101
|
+
}
|
|
1102
|
+
onRequestedTargetHandled?.();
|
|
1103
|
+
}, [isTargetControlled, loadSessions, onRequestedTargetHandled, requestedOpenVersion, requestedTarget, requestedTargetKey, resetForTargetLoad, requestedSessionId, sessionLoadReady]);
|
|
1104
|
+
useEffect(() => {
|
|
1105
|
+
if (!activeTarget)
|
|
1106
|
+
return;
|
|
1107
|
+
onContextChange?.({ target: activeTarget, sessionId: selectedSessionId });
|
|
1108
|
+
}, [activeTarget, onContextChange, selectedSessionId]);
|
|
1109
|
+
useEffect(() => {
|
|
1110
|
+
if (!activeTarget || typeof document === 'undefined' || !sessionLoadReady)
|
|
1111
|
+
return undefined;
|
|
1112
|
+
const reloadIfIdle = () => {
|
|
1113
|
+
if (Date.now() < suppressVisibilityReloadUntilRef.current)
|
|
1114
|
+
return;
|
|
1115
|
+
if (currentDraftSnapshotRef.current !== lastSavedSnapshotRef.current || saving || loading)
|
|
1116
|
+
return;
|
|
1117
|
+
void loadSessions(activeTarget, { requestedSessionId: selectedSessionId });
|
|
1118
|
+
};
|
|
1119
|
+
const handleVisibilityChange = () => {
|
|
1120
|
+
if (document.visibilityState !== 'visible')
|
|
1121
|
+
return;
|
|
1122
|
+
reloadIfIdle();
|
|
1123
|
+
};
|
|
1124
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
1125
|
+
return () => {
|
|
1126
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
1127
|
+
};
|
|
1128
|
+
}, [activeTarget, loadSessions, loading, saving, selectedSessionId, sessionLoadReady]);
|
|
1129
|
+
useEffect(() => {
|
|
1130
|
+
if (!activeTarget?.path) {
|
|
1131
|
+
setImageLoading(false);
|
|
1132
|
+
setImageLoadError(false);
|
|
1133
|
+
setNaturalImageSize({ width: 0, height: 0 });
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
setImageLoading(true);
|
|
1137
|
+
setImageLoadError(false);
|
|
1138
|
+
setNaturalImageSize({ width: 0, height: 0 });
|
|
1139
|
+
setZoomLevel(1);
|
|
1140
|
+
setPanMode(false);
|
|
1141
|
+
}, [activeTarget?.path]);
|
|
1142
|
+
useEffect(() => {
|
|
1143
|
+
if (!imageLoading)
|
|
1144
|
+
return;
|
|
1145
|
+
const image = imageRef.current;
|
|
1146
|
+
if (!image || !image.complete || image.naturalWidth <= 0 || image.naturalHeight <= 0)
|
|
1147
|
+
return;
|
|
1148
|
+
setNaturalImageSize({
|
|
1149
|
+
width: image.naturalWidth,
|
|
1150
|
+
height: image.naturalHeight,
|
|
1151
|
+
});
|
|
1152
|
+
setImageLoading(false);
|
|
1153
|
+
setImageLoadError(false);
|
|
1154
|
+
}, [activeTarget?.path, imageLoading]);
|
|
1155
|
+
useEffect(() => {
|
|
1156
|
+
if (canPan)
|
|
1157
|
+
return;
|
|
1158
|
+
setPanMode(false);
|
|
1159
|
+
}, [canPan]);
|
|
1160
|
+
useEffect(() => {
|
|
1161
|
+
setPayloadPreview(null);
|
|
1162
|
+
}, [selectedSessionId]);
|
|
1163
|
+
useEffect(() => {
|
|
1164
|
+
currentDraftSnapshotRef.current = currentDraftSnapshot;
|
|
1165
|
+
if (suppressAutoSaveRef.current) {
|
|
1166
|
+
suppressAutoSaveRef.current = false;
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (!selectedSessionId) {
|
|
1170
|
+
clearPendingAutoSave();
|
|
1171
|
+
setLastSaveError(null);
|
|
1172
|
+
setSaveState('saved');
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (currentDraftSnapshot === lastSavedSnapshotRef.current) {
|
|
1176
|
+
clearPendingAutoSave();
|
|
1177
|
+
if (!inFlightPromiseRef.current) {
|
|
1178
|
+
setLastSaveError(null);
|
|
1179
|
+
setSaveState('saved');
|
|
1180
|
+
}
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const pendingDelayMs = pendingDebounceMsRef.current;
|
|
1184
|
+
pendingDebounceMsRef.current = null;
|
|
1185
|
+
if (saveState === 'error' && pendingDelayMs === null) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const delayMs = pendingDelayMs ?? 600;
|
|
1189
|
+
if (inFlightPromiseRef.current) {
|
|
1190
|
+
hasPendingSaveAfterFlightRef.current = true;
|
|
1191
|
+
setSaveState('pending');
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
scheduleAutoSave(delayMs);
|
|
1195
|
+
}, [clearPendingAutoSave, currentDraftSnapshot, saveState, scheduleAutoSave, selectedSessionId]);
|
|
1196
|
+
useEffect(() => {
|
|
1197
|
+
if (!selectedSessionId)
|
|
1198
|
+
return;
|
|
1199
|
+
if (inFlightPromiseRef.current)
|
|
1200
|
+
return;
|
|
1201
|
+
if (currentDraftSnapshot !== lastSavedSnapshotRef.current)
|
|
1202
|
+
return;
|
|
1203
|
+
if (saveState === 'error')
|
|
1204
|
+
return;
|
|
1205
|
+
if (saveState === 'saved')
|
|
1206
|
+
return;
|
|
1207
|
+
setLastSaveError(null);
|
|
1208
|
+
setSaveState('saved');
|
|
1209
|
+
}, [currentDraftSnapshot, saveState, selectedSessionId]);
|
|
1210
|
+
useEffect(() => {
|
|
1211
|
+
if (typeof window === 'undefined')
|
|
1212
|
+
return undefined;
|
|
1213
|
+
const handleBeforeUnload = (event) => {
|
|
1214
|
+
if (currentDraftSnapshotRef.current === lastSavedSnapshotRef.current)
|
|
1215
|
+
return;
|
|
1216
|
+
event.preventDefault();
|
|
1217
|
+
event.returnValue = '';
|
|
1218
|
+
};
|
|
1219
|
+
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
1220
|
+
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
1221
|
+
}, []);
|
|
1222
|
+
useEffect(() => {
|
|
1223
|
+
if (typeof document === 'undefined')
|
|
1224
|
+
return undefined;
|
|
1225
|
+
const handleVisibilityChange = () => {
|
|
1226
|
+
if (document.visibilityState !== 'hidden')
|
|
1227
|
+
return;
|
|
1228
|
+
void flushPendingDraft('visibility-hidden');
|
|
1229
|
+
};
|
|
1230
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
1231
|
+
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
1232
|
+
}, [flushPendingDraft]);
|
|
1233
|
+
useEffect(() => {
|
|
1234
|
+
if (!colorPickerOpen || typeof document === 'undefined')
|
|
1235
|
+
return undefined;
|
|
1236
|
+
const handlePointerDown = (event) => {
|
|
1237
|
+
const target = event.target;
|
|
1238
|
+
if (!(target instanceof Node))
|
|
1239
|
+
return;
|
|
1240
|
+
if (colorPickerRef.current?.contains(target))
|
|
1241
|
+
return;
|
|
1242
|
+
setColorPickerOpen(false);
|
|
1243
|
+
};
|
|
1244
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
1245
|
+
return () => document.removeEventListener('mousedown', handlePointerDown);
|
|
1246
|
+
}, [colorPickerOpen]);
|
|
1247
|
+
useEffect(() => {
|
|
1248
|
+
if (!sessionCardEditing || typeof document === 'undefined')
|
|
1249
|
+
return undefined;
|
|
1250
|
+
const handlePointerDown = (event) => {
|
|
1251
|
+
const target = event.target;
|
|
1252
|
+
if (!(target instanceof Node))
|
|
1253
|
+
return;
|
|
1254
|
+
if (sessionCardRef.current?.contains(target))
|
|
1255
|
+
return;
|
|
1256
|
+
setSessionCardEditing(false);
|
|
1257
|
+
};
|
|
1258
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
1259
|
+
return () => document.removeEventListener('mousedown', handlePointerDown);
|
|
1260
|
+
}, [sessionCardEditing]);
|
|
1261
|
+
useEffect(() => () => {
|
|
1262
|
+
clearPendingAutoSave();
|
|
1263
|
+
}, [clearPendingAutoSave]);
|
|
1264
|
+
useEffect(() => {
|
|
1265
|
+
if (!payloadCopied)
|
|
1266
|
+
return;
|
|
1267
|
+
const timeoutId = window.setTimeout(() => setPayloadCopied(false), 2000);
|
|
1268
|
+
return () => window.clearTimeout(timeoutId);
|
|
1269
|
+
}, [payloadCopied]);
|
|
1270
|
+
useEffect(() => {
|
|
1271
|
+
if (!taskIdCopied)
|
|
1272
|
+
return;
|
|
1273
|
+
const timeoutId = window.setTimeout(() => setTaskIdCopied(false), 2000);
|
|
1274
|
+
return () => window.clearTimeout(timeoutId);
|
|
1275
|
+
}, [taskIdCopied]);
|
|
1276
|
+
useEffect(() => {
|
|
1277
|
+
if (!imageIdCopied)
|
|
1278
|
+
return;
|
|
1279
|
+
const timeoutId = window.setTimeout(() => setImageIdCopied(false), 2000);
|
|
1280
|
+
return () => window.clearTimeout(timeoutId);
|
|
1281
|
+
}, [imageIdCopied]);
|
|
1282
|
+
const updateDraftAnnotation = useCallback((annotationId, updater, delayMs = 300) => {
|
|
1283
|
+
setDraftAnnotations((current) => normalizeOrder(current.map((annotation) => (annotation.id === annotationId
|
|
1284
|
+
? updater(annotation)
|
|
1285
|
+
: annotation))));
|
|
1286
|
+
markDraftChanged(delayMs);
|
|
1287
|
+
}, [markDraftChanged]);
|
|
1288
|
+
const applyMarkerColor = useCallback((color) => {
|
|
1289
|
+
if (!selectedAnnotation)
|
|
1290
|
+
return;
|
|
1291
|
+
updateDraftAnnotation(selectedAnnotation.id, (annotation) => ({
|
|
1292
|
+
...annotation,
|
|
1293
|
+
color,
|
|
1294
|
+
}));
|
|
1295
|
+
setDefaultMarkerColor(color);
|
|
1296
|
+
setColorPickerOpen(false);
|
|
1297
|
+
}, [selectedAnnotation, updateDraftAnnotation]);
|
|
1298
|
+
const createSession = useCallback(async () => {
|
|
1299
|
+
if (!activeTarget)
|
|
1300
|
+
return;
|
|
1301
|
+
const flushed = await flushPendingDraft('session-switch');
|
|
1302
|
+
if (!flushed)
|
|
1303
|
+
return;
|
|
1304
|
+
setSaving(true);
|
|
1305
|
+
setError(null);
|
|
1306
|
+
try {
|
|
1307
|
+
const session = await createSessionForTarget(activeTarget);
|
|
1308
|
+
setShowCreateSessionPrompt(false);
|
|
1309
|
+
await loadSessions(activeTarget, { requestedSessionId: session.id, autoCreateIfEmpty: false });
|
|
1310
|
+
setToolMode('select');
|
|
1311
|
+
}
|
|
1312
|
+
catch (requestError) {
|
|
1313
|
+
setError(requestError instanceof Error ? requestError.message : 'Failed to create session.');
|
|
1314
|
+
setShowCreateSessionPrompt(true);
|
|
1315
|
+
}
|
|
1316
|
+
finally {
|
|
1317
|
+
setSaving(false);
|
|
1318
|
+
}
|
|
1319
|
+
}, [activeTarget, createSessionForTarget, flushPendingDraft, loadSessions]);
|
|
1320
|
+
const handleToolSelection = useCallback((toolValue) => {
|
|
1321
|
+
if (toolValue !== 'select' && !selectedSession) {
|
|
1322
|
+
setPanMode(false);
|
|
1323
|
+
setError('No session exists for this image yet.');
|
|
1324
|
+
setShowCreateSessionPrompt(true);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
setError(null);
|
|
1328
|
+
setShowCreateSessionPrompt(false);
|
|
1329
|
+
setPanMode(false);
|
|
1330
|
+
setToolMode(toolValue);
|
|
1331
|
+
}, [selectedSession]);
|
|
1332
|
+
const duplicateSelectedSession = useCallback(async () => {
|
|
1333
|
+
if (!activeTarget || !selectedSession)
|
|
1334
|
+
return;
|
|
1335
|
+
const flushed = await flushPendingDraft('duplicate');
|
|
1336
|
+
if (!flushed)
|
|
1337
|
+
return;
|
|
1338
|
+
setSaving(true);
|
|
1339
|
+
setError(null);
|
|
1340
|
+
try {
|
|
1341
|
+
const clonedAnnotations = normalizeOrder(draftAnnotations.map((annotation, index) => cloneAnnotation(annotation, index)));
|
|
1342
|
+
const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions?${workspaceQuery}`, {
|
|
1343
|
+
method: 'POST',
|
|
1344
|
+
credentials: 'include',
|
|
1345
|
+
headers: { 'Content-Type': 'application/json', ...requestHeaders },
|
|
1346
|
+
body: JSON.stringify({
|
|
1347
|
+
taskId: activeTarget.taskId,
|
|
1348
|
+
baseImageAssetId: activeTarget.assetId,
|
|
1349
|
+
title: buildDuplicateSessionTitle(draftTitle || selectedSession.title, activeTarget),
|
|
1350
|
+
globalInstruction: draftGlobalInstruction,
|
|
1351
|
+
annotations: clonedAnnotations,
|
|
1352
|
+
}),
|
|
1353
|
+
}, resolvedApiBase);
|
|
1354
|
+
const data = await res.json().catch(() => ({}));
|
|
1355
|
+
if (!res.ok) {
|
|
1356
|
+
throw new Error(String(data?.error || 'Failed to duplicate session.'));
|
|
1357
|
+
}
|
|
1358
|
+
const session = data?.session;
|
|
1359
|
+
if (!session?.id) {
|
|
1360
|
+
throw new Error('Failed to duplicate session.');
|
|
1361
|
+
}
|
|
1362
|
+
applySessionLocally(session);
|
|
1363
|
+
setToolMode('select');
|
|
1364
|
+
}
|
|
1365
|
+
catch (requestError) {
|
|
1366
|
+
setError(requestError instanceof Error ? requestError.message : 'Failed to duplicate session.');
|
|
1367
|
+
}
|
|
1368
|
+
finally {
|
|
1369
|
+
setSaving(false);
|
|
1370
|
+
}
|
|
1371
|
+
}, [
|
|
1372
|
+
activeTarget,
|
|
1373
|
+
draftAnnotations,
|
|
1374
|
+
draftGlobalInstruction,
|
|
1375
|
+
draftTitle,
|
|
1376
|
+
applySessionLocally,
|
|
1377
|
+
flushPendingDraft,
|
|
1378
|
+
requestHeaders,
|
|
1379
|
+
resolvedApiBase,
|
|
1380
|
+
selectedSession,
|
|
1381
|
+
workspaceQuery,
|
|
1382
|
+
]);
|
|
1383
|
+
const saveSession = useCallback(async () => {
|
|
1384
|
+
return flushPendingDraft('manual');
|
|
1385
|
+
}, [flushPendingDraft]);
|
|
1386
|
+
const loadPayloadPreview = useCallback(async () => {
|
|
1387
|
+
if (!selectedSessionId)
|
|
1388
|
+
return;
|
|
1389
|
+
setPayloadLoading(true);
|
|
1390
|
+
setError(null);
|
|
1391
|
+
try {
|
|
1392
|
+
const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions/${encodeURIComponent(selectedSessionId)}/payload?${workspaceQuery}`, {
|
|
1393
|
+
credentials: 'include',
|
|
1394
|
+
headers: requestHeaders,
|
|
1395
|
+
}, resolvedApiBase);
|
|
1396
|
+
const data = await res.json().catch(() => ({}));
|
|
1397
|
+
if (!res.ok) {
|
|
1398
|
+
throw new Error(String(data?.error || 'Failed to load payload preview.'));
|
|
1399
|
+
}
|
|
1400
|
+
setPayloadPreview(data?.payload || null);
|
|
1401
|
+
}
|
|
1402
|
+
catch (requestError) {
|
|
1403
|
+
setError(requestError instanceof Error ? requestError.message : 'Failed to load payload preview.');
|
|
1404
|
+
}
|
|
1405
|
+
finally {
|
|
1406
|
+
setPayloadLoading(false);
|
|
1407
|
+
}
|
|
1408
|
+
}, [requestHeaders, resolvedApiBase, selectedSessionId, workspaceQuery]);
|
|
1409
|
+
const openPayloadPreview = useCallback(() => {
|
|
1410
|
+
if (!selectedSessionId)
|
|
1411
|
+
return;
|
|
1412
|
+
void flushPendingDraft('payload-preview').then((saved) => {
|
|
1413
|
+
if (!saved)
|
|
1414
|
+
return;
|
|
1415
|
+
setPayloadModalOpen(true);
|
|
1416
|
+
void loadPayloadPreview();
|
|
1417
|
+
});
|
|
1418
|
+
}, [flushPendingDraft, loadPayloadPreview, selectedSessionId]);
|
|
1419
|
+
const copyPayloadContent = useCallback(async (mode) => {
|
|
1420
|
+
if (!payloadPreview || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
|
|
1421
|
+
setError('Clipboard copy is not available in this browser.');
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
try {
|
|
1425
|
+
const text = mode === 'json'
|
|
1426
|
+
? JSON.stringify(payloadPreview, null, 2)
|
|
1427
|
+
: buildPayloadBrief(payloadPreview);
|
|
1428
|
+
await navigator.clipboard.writeText(text);
|
|
1429
|
+
setPayloadCopied(mode);
|
|
1430
|
+
}
|
|
1431
|
+
catch (copyError) {
|
|
1432
|
+
setError(copyError instanceof Error ? copyError.message : 'Failed to copy payload content.');
|
|
1433
|
+
setPayloadCopied(false);
|
|
1434
|
+
}
|
|
1435
|
+
}, [payloadPreview]);
|
|
1436
|
+
const copyTaskId = useCallback(async () => {
|
|
1437
|
+
if (!activeTaskReferenceLabel || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
|
|
1438
|
+
setError('Clipboard copy is not available in this browser.');
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
try {
|
|
1442
|
+
await navigator.clipboard.writeText(activeTaskReferenceLabel);
|
|
1443
|
+
setTaskIdCopied(true);
|
|
1444
|
+
}
|
|
1445
|
+
catch (copyError) {
|
|
1446
|
+
setError(copyError instanceof Error ? copyError.message : 'Failed to copy task id.');
|
|
1447
|
+
setTaskIdCopied(false);
|
|
1448
|
+
}
|
|
1449
|
+
}, [activeTaskReferenceLabel]);
|
|
1450
|
+
const copyImageId = useCallback(async () => {
|
|
1451
|
+
if (!activeImageReferenceLabel || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
|
|
1452
|
+
setError('Clipboard copy is not available in this browser.');
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
try {
|
|
1456
|
+
await navigator.clipboard.writeText(activeImageReferenceLabel);
|
|
1457
|
+
setImageIdCopied(true);
|
|
1458
|
+
}
|
|
1459
|
+
catch (copyError) {
|
|
1460
|
+
setError(copyError instanceof Error ? copyError.message : 'Failed to copy image reference.');
|
|
1461
|
+
setImageIdCopied(false);
|
|
1462
|
+
}
|
|
1463
|
+
}, [activeImageReferenceLabel]);
|
|
1464
|
+
const deleteSelectedSession = useCallback(async () => {
|
|
1465
|
+
if (!selectedSessionId)
|
|
1466
|
+
return;
|
|
1467
|
+
const flushed = await flushPendingDraft('delete-session');
|
|
1468
|
+
if (!flushed)
|
|
1469
|
+
return;
|
|
1470
|
+
const sessionTitle = (selectedSession?.title || 'Untitled session').trim() || 'Untitled session';
|
|
1471
|
+
if (!window.confirm(`Delete the annotated attachment session "${sessionTitle}"?`))
|
|
1472
|
+
return;
|
|
1473
|
+
setSaving(true);
|
|
1474
|
+
setError(null);
|
|
1475
|
+
try {
|
|
1476
|
+
const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/sessions/${encodeURIComponent(selectedSessionId)}?${workspaceQuery}`, {
|
|
1477
|
+
method: 'DELETE',
|
|
1478
|
+
credentials: 'include',
|
|
1479
|
+
headers: requestHeaders,
|
|
1480
|
+
}, resolvedApiBase);
|
|
1481
|
+
const data = await res.json().catch(() => ({}));
|
|
1482
|
+
if (!res.ok) {
|
|
1483
|
+
throw new Error(String(data?.error || 'Failed to delete session.'));
|
|
1484
|
+
}
|
|
1485
|
+
removeSessionLocally(selectedSessionId);
|
|
1486
|
+
setToolMode('select');
|
|
1487
|
+
}
|
|
1488
|
+
catch (requestError) {
|
|
1489
|
+
setError(requestError instanceof Error ? requestError.message : 'Failed to delete session.');
|
|
1490
|
+
}
|
|
1491
|
+
finally {
|
|
1492
|
+
setSaving(false);
|
|
1493
|
+
}
|
|
1494
|
+
}, [flushPendingDraft, removeSessionLocally, requestHeaders, resolvedApiBase, selectedSession, selectedSessionId, workspaceQuery]);
|
|
1495
|
+
const deleteSelectedAnnotation = useCallback(() => {
|
|
1496
|
+
if (!selectedAnnotationId)
|
|
1497
|
+
return;
|
|
1498
|
+
setDraftAnnotations((current) => normalizeOrder(current.filter((annotation) => annotation.id !== selectedAnnotationId)));
|
|
1499
|
+
setSelectedAnnotationId(null);
|
|
1500
|
+
markDraftChanged(300);
|
|
1501
|
+
}, [markDraftChanged, selectedAnnotationId]);
|
|
1502
|
+
const applyZoom = useCallback((nextZoomValue) => {
|
|
1503
|
+
const nextZoom = clampZoom(nextZoomValue);
|
|
1504
|
+
const scroller = canvasScrollerRef.current;
|
|
1505
|
+
if (!scroller || nextZoom === zoomLevel) {
|
|
1506
|
+
setZoomLevel(nextZoom);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const nextScrollLeft = ((scroller.scrollLeft + scroller.clientWidth / 2) * (nextZoom / zoomLevel)) - scroller.clientWidth / 2;
|
|
1510
|
+
const nextScrollTop = ((scroller.scrollTop + scroller.clientHeight / 2) * (nextZoom / zoomLevel)) - scroller.clientHeight / 2;
|
|
1511
|
+
setZoomLevel(nextZoom);
|
|
1512
|
+
window.requestAnimationFrame(() => {
|
|
1513
|
+
scroller.scrollLeft = Math.max(0, nextScrollLeft);
|
|
1514
|
+
scroller.scrollTop = Math.max(0, nextScrollTop);
|
|
1515
|
+
});
|
|
1516
|
+
}, [zoomLevel]);
|
|
1517
|
+
const zoomIn = useCallback(() => {
|
|
1518
|
+
applyZoom(zoomLevel + 0.25);
|
|
1519
|
+
}, [applyZoom, zoomLevel]);
|
|
1520
|
+
const zoomOut = useCallback(() => {
|
|
1521
|
+
applyZoom(zoomLevel - 0.25);
|
|
1522
|
+
}, [applyZoom, zoomLevel]);
|
|
1523
|
+
const resetZoom = useCallback(() => {
|
|
1524
|
+
applyZoom(1);
|
|
1525
|
+
const scroller = canvasScrollerRef.current;
|
|
1526
|
+
if (!scroller)
|
|
1527
|
+
return;
|
|
1528
|
+
window.requestAnimationFrame(() => {
|
|
1529
|
+
scroller.scrollLeft = 0;
|
|
1530
|
+
scroller.scrollTop = 0;
|
|
1531
|
+
});
|
|
1532
|
+
}, [applyZoom]);
|
|
1533
|
+
const moveSelectedAnnotation = useCallback((direction) => {
|
|
1534
|
+
if (!selectedAnnotationId)
|
|
1535
|
+
return;
|
|
1536
|
+
setDraftAnnotations((current) => {
|
|
1537
|
+
const fromIndex = current.findIndex((annotation) => annotation.id === selectedAnnotationId);
|
|
1538
|
+
if (fromIndex === -1)
|
|
1539
|
+
return current;
|
|
1540
|
+
const toIndex = direction === 'up' ? fromIndex - 1 : fromIndex + 1;
|
|
1541
|
+
if (toIndex < 0 || toIndex >= current.length)
|
|
1542
|
+
return current;
|
|
1543
|
+
markDraftChanged(300);
|
|
1544
|
+
return moveAnnotation(current, fromIndex, toIndex);
|
|
1545
|
+
});
|
|
1546
|
+
}, [markDraftChanged, selectedAnnotationId]);
|
|
1547
|
+
useEffect(() => {
|
|
1548
|
+
if (!selectedAnnotationId)
|
|
1549
|
+
return;
|
|
1550
|
+
const handleKeyDown = (event) => {
|
|
1551
|
+
if (event.key !== 'Delete')
|
|
1552
|
+
return;
|
|
1553
|
+
if (isEditableKeyboardTarget(event.target))
|
|
1554
|
+
return;
|
|
1555
|
+
event.preventDefault();
|
|
1556
|
+
deleteSelectedAnnotation();
|
|
1557
|
+
};
|
|
1558
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
1559
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
1560
|
+
}, [deleteSelectedAnnotation, selectedAnnotationId]);
|
|
1561
|
+
const readPoint = useCallback((event) => {
|
|
1562
|
+
const rect = overlayRef.current?.getBoundingClientRect();
|
|
1563
|
+
if (!rect || rect.width <= 0 || rect.height <= 0)
|
|
1564
|
+
return null;
|
|
1565
|
+
return {
|
|
1566
|
+
x: clamp01((event.clientX - rect.left) / rect.width),
|
|
1567
|
+
y: clamp01((event.clientY - rect.top) / rect.height),
|
|
1568
|
+
};
|
|
1569
|
+
}, []);
|
|
1570
|
+
const handleCanvasPointerDown = useCallback((event) => {
|
|
1571
|
+
if (panMode && canPan) {
|
|
1572
|
+
const scroller = canvasScrollerRef.current;
|
|
1573
|
+
if (!scroller)
|
|
1574
|
+
return;
|
|
1575
|
+
panRef.current = {
|
|
1576
|
+
pointerId: event.pointerId,
|
|
1577
|
+
startX: event.clientX,
|
|
1578
|
+
startY: event.clientY,
|
|
1579
|
+
scrollLeft: scroller.scrollLeft,
|
|
1580
|
+
scrollTop: scroller.scrollTop,
|
|
1581
|
+
};
|
|
1582
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1583
|
+
event.preventDefault();
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (!selectedSessionId)
|
|
1587
|
+
return;
|
|
1588
|
+
if (toolMode === 'select') {
|
|
1589
|
+
setSelectedAnnotationId(null);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const point = readPoint(event);
|
|
1593
|
+
if (!point)
|
|
1594
|
+
return;
|
|
1595
|
+
if (toolMode === 'pin' || toolMode === 'text-note') {
|
|
1596
|
+
const annotation = createAnnotation(toolMode, point, point, defaultMarkerColor);
|
|
1597
|
+
setDraftAnnotations((current) => normalizeOrder([...current, annotation]));
|
|
1598
|
+
setSelectedAnnotationId(annotation.id);
|
|
1599
|
+
markDraftChanged(300);
|
|
1600
|
+
setToolMode('select');
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
gestureStartRef.current = point;
|
|
1604
|
+
const annotation = createAnnotation(toolMode, point, point, defaultMarkerColor);
|
|
1605
|
+
creatingAnnotationRef.current = {
|
|
1606
|
+
annotationId: annotation.id,
|
|
1607
|
+
kind: toolMode,
|
|
1608
|
+
};
|
|
1609
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
1610
|
+
setDraftAnnotations((current) => normalizeOrder([...current, annotation]));
|
|
1611
|
+
setSelectedAnnotationId(annotation.id);
|
|
1612
|
+
markDraftChanged(300);
|
|
1613
|
+
}, [canPan, defaultMarkerColor, markDraftChanged, panMode, readPoint, selectedSessionId, toolMode]);
|
|
1614
|
+
const handleCanvasPointerUp = useCallback((event) => {
|
|
1615
|
+
if (panRef.current?.pointerId === event.pointerId) {
|
|
1616
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
if (!gestureStartRef.current || !creatingAnnotationRef.current || (toolMode !== 'box' && toolMode !== 'arrow'))
|
|
1620
|
+
return;
|
|
1621
|
+
const end = readPoint(event);
|
|
1622
|
+
gestureStartRef.current = null;
|
|
1623
|
+
creatingAnnotationRef.current = null;
|
|
1624
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
1625
|
+
if (!end)
|
|
1626
|
+
return;
|
|
1627
|
+
setToolMode('select');
|
|
1628
|
+
}, [readPoint, toolMode]);
|
|
1629
|
+
const startDragAnnotation = useCallback((event, annotation) => {
|
|
1630
|
+
if (toolMode !== 'select' || panMode)
|
|
1631
|
+
return;
|
|
1632
|
+
const point = readPoint(event);
|
|
1633
|
+
if (!point)
|
|
1634
|
+
return;
|
|
1635
|
+
dragRef.current = {
|
|
1636
|
+
annotationId: annotation.id,
|
|
1637
|
+
originPointer: point,
|
|
1638
|
+
originAnnotation: annotation,
|
|
1639
|
+
};
|
|
1640
|
+
setSelectedAnnotationId(annotation.id);
|
|
1641
|
+
event.stopPropagation();
|
|
1642
|
+
}, [panMode, readPoint, toolMode]);
|
|
1643
|
+
const handleCanvasPointerMove = useCallback((event) => {
|
|
1644
|
+
if (panRef.current?.pointerId === event.pointerId) {
|
|
1645
|
+
const scroller = canvasScrollerRef.current;
|
|
1646
|
+
if (!scroller)
|
|
1647
|
+
return;
|
|
1648
|
+
const dx = event.clientX - panRef.current.startX;
|
|
1649
|
+
const dy = event.clientY - panRef.current.startY;
|
|
1650
|
+
scroller.scrollLeft = panRef.current.scrollLeft - dx;
|
|
1651
|
+
scroller.scrollTop = panRef.current.scrollTop - dy;
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
if (creatingAnnotationRef.current && gestureStartRef.current) {
|
|
1655
|
+
const point = readPoint(event);
|
|
1656
|
+
if (!point)
|
|
1657
|
+
return;
|
|
1658
|
+
const { annotationId, kind } = creatingAnnotationRef.current;
|
|
1659
|
+
const start = gestureStartRef.current;
|
|
1660
|
+
updateDraftAnnotation(annotationId, (annotation) => {
|
|
1661
|
+
if (kind === 'box' && annotation.kind === 'box') {
|
|
1662
|
+
return {
|
|
1663
|
+
...annotation,
|
|
1664
|
+
x: clamp01(Math.min(start.x, point.x)),
|
|
1665
|
+
y: clamp01(Math.min(start.y, point.y)),
|
|
1666
|
+
width: clampSize(Math.abs(point.x - start.x)),
|
|
1667
|
+
height: clampSize(Math.abs(point.y - start.y)),
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
if (kind === 'arrow' && annotation.kind === 'arrow') {
|
|
1671
|
+
return {
|
|
1672
|
+
...annotation,
|
|
1673
|
+
x: start.x,
|
|
1674
|
+
y: start.y,
|
|
1675
|
+
x2: point.x,
|
|
1676
|
+
y2: point.y,
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
return annotation;
|
|
1680
|
+
});
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (resizeRef.current) {
|
|
1684
|
+
const point = readPoint(event);
|
|
1685
|
+
if (!point)
|
|
1686
|
+
return;
|
|
1687
|
+
const { annotationId, originPointer, originAnnotation } = resizeRef.current;
|
|
1688
|
+
const dx = point.x - originPointer.x;
|
|
1689
|
+
const dy = point.y - originPointer.y;
|
|
1690
|
+
updateDraftAnnotation(annotationId, () => ({
|
|
1691
|
+
...originAnnotation,
|
|
1692
|
+
width: clampSize(originAnnotation.width + dx),
|
|
1693
|
+
height: clampSize(originAnnotation.height + dy),
|
|
1694
|
+
}));
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
if (arrowHandleRef.current) {
|
|
1698
|
+
const point = readPoint(event);
|
|
1699
|
+
if (!point)
|
|
1700
|
+
return;
|
|
1701
|
+
const { annotationId, endpoint, originPointer, originAnnotation } = arrowHandleRef.current;
|
|
1702
|
+
const dx = point.x - originPointer.x;
|
|
1703
|
+
const dy = point.y - originPointer.y;
|
|
1704
|
+
updateDraftAnnotation(annotationId, () => {
|
|
1705
|
+
if (endpoint === 'tail') {
|
|
1706
|
+
return {
|
|
1707
|
+
...originAnnotation,
|
|
1708
|
+
x: clamp01(originAnnotation.x + dx),
|
|
1709
|
+
y: clamp01(originAnnotation.y + dy),
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
return {
|
|
1713
|
+
...originAnnotation,
|
|
1714
|
+
x2: clamp01(originAnnotation.x2 + dx),
|
|
1715
|
+
y2: clamp01(originAnnotation.y2 + dy),
|
|
1716
|
+
};
|
|
1717
|
+
});
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
if (!dragRef.current)
|
|
1721
|
+
return;
|
|
1722
|
+
const point = readPoint(event);
|
|
1723
|
+
if (!point)
|
|
1724
|
+
return;
|
|
1725
|
+
const { annotationId, originPointer, originAnnotation } = dragRef.current;
|
|
1726
|
+
const dx = point.x - originPointer.x;
|
|
1727
|
+
const dy = point.y - originPointer.y;
|
|
1728
|
+
updateDraftAnnotation(annotationId, () => {
|
|
1729
|
+
if (originAnnotation.kind === 'pin' || originAnnotation.kind === 'text-note') {
|
|
1730
|
+
return {
|
|
1731
|
+
...originAnnotation,
|
|
1732
|
+
x: clamp01(originAnnotation.x + dx),
|
|
1733
|
+
y: clamp01(originAnnotation.y + dy),
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
if (originAnnotation.kind === 'box') {
|
|
1737
|
+
return {
|
|
1738
|
+
...originAnnotation,
|
|
1739
|
+
x: clamp01(originAnnotation.x + dx),
|
|
1740
|
+
y: clamp01(originAnnotation.y + dy),
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
return {
|
|
1744
|
+
...originAnnotation,
|
|
1745
|
+
x: clamp01(originAnnotation.x + dx),
|
|
1746
|
+
y: clamp01(originAnnotation.y + dy),
|
|
1747
|
+
x2: clamp01(originAnnotation.x2 + dx),
|
|
1748
|
+
y2: clamp01(originAnnotation.y2 + dy),
|
|
1749
|
+
};
|
|
1750
|
+
});
|
|
1751
|
+
}, [readPoint, updateDraftAnnotation]);
|
|
1752
|
+
const handleCanvasPointerLeave = useCallback(() => {
|
|
1753
|
+
gestureStartRef.current = null;
|
|
1754
|
+
dragRef.current = null;
|
|
1755
|
+
resizeRef.current = null;
|
|
1756
|
+
arrowHandleRef.current = null;
|
|
1757
|
+
creatingAnnotationRef.current = null;
|
|
1758
|
+
panRef.current = null;
|
|
1759
|
+
}, []);
|
|
1760
|
+
const handleCanvasPointerCancel = useCallback(() => {
|
|
1761
|
+
gestureStartRef.current = null;
|
|
1762
|
+
dragRef.current = null;
|
|
1763
|
+
resizeRef.current = null;
|
|
1764
|
+
arrowHandleRef.current = null;
|
|
1765
|
+
creatingAnnotationRef.current = null;
|
|
1766
|
+
panRef.current = null;
|
|
1767
|
+
}, []);
|
|
1768
|
+
const handleCanvasPointerEnd = useCallback(() => {
|
|
1769
|
+
dragRef.current = null;
|
|
1770
|
+
resizeRef.current = null;
|
|
1771
|
+
arrowHandleRef.current = null;
|
|
1772
|
+
creatingAnnotationRef.current = null;
|
|
1773
|
+
panRef.current = null;
|
|
1774
|
+
}, []);
|
|
1775
|
+
const startResizeBox = useCallback((event, annotation) => {
|
|
1776
|
+
if (toolMode !== 'select' || panMode)
|
|
1777
|
+
return;
|
|
1778
|
+
const point = readPoint(event);
|
|
1779
|
+
if (!point)
|
|
1780
|
+
return;
|
|
1781
|
+
resizeRef.current = {
|
|
1782
|
+
annotationId: annotation.id,
|
|
1783
|
+
originPointer: point,
|
|
1784
|
+
originAnnotation: annotation,
|
|
1785
|
+
};
|
|
1786
|
+
setSelectedAnnotationId(annotation.id);
|
|
1787
|
+
event.stopPropagation();
|
|
1788
|
+
}, [panMode, readPoint, toolMode]);
|
|
1789
|
+
const startArrowHandleDrag = useCallback((event, annotation, endpoint) => {
|
|
1790
|
+
if (toolMode !== 'select' || panMode)
|
|
1791
|
+
return;
|
|
1792
|
+
const point = readPoint(event);
|
|
1793
|
+
if (!point)
|
|
1794
|
+
return;
|
|
1795
|
+
arrowHandleRef.current = {
|
|
1796
|
+
annotationId: annotation.id,
|
|
1797
|
+
originPointer: point,
|
|
1798
|
+
originAnnotation: annotation,
|
|
1799
|
+
endpoint,
|
|
1800
|
+
};
|
|
1801
|
+
setSelectedAnnotationId(annotation.id);
|
|
1802
|
+
event.stopPropagation();
|
|
1803
|
+
}, [panMode, readPoint, toolMode]);
|
|
1804
|
+
const activeSessionTimestamp = selectedSession?.updatedAt ? formatTimestamp(selectedSession.updatedAt) : 'Not saved yet';
|
|
1805
|
+
return (_jsxs("div", { className: styles.shell, children: [_jsxs("section", { className: `tf-surface-panel ${styles.panel} ${styles.sessionPanel}`, children: [_jsx("div", { className: styles.sessionContextBar, children: activeTarget?.taskId ? (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.sessionContextLeft, children: onBackToTask ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.backToTaskBtn}`, onClick: () => {
|
|
1806
|
+
void flushPendingDraft('back-to-task').then((saved) => {
|
|
1807
|
+
if (!saved)
|
|
1808
|
+
return;
|
|
1809
|
+
if (activeTarget.taskId)
|
|
1810
|
+
onBackToTask(activeTarget.taskId);
|
|
1811
|
+
});
|
|
1812
|
+
}, "aria-label": "Back to task", title: "Back to task", children: _jsx(ArrowLeft, { size: 16 }) })) : _jsx("div", { className: styles.sessionContextSpacer, "aria-hidden": "true" }) }), _jsx("div", { className: styles.sessionContextRight, children: _jsx(TaskReferenceBadge, { copied: taskIdCopied, onClick: () => { void copyTaskId(); }, title: "Copy task id", ariaLabel: taskIdCopied ? 'Copied task id' : 'Copy task id', label: activeTaskReferenceLabel }) })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.sessionContextLeft, children: _jsx("span", { className: `tf-label-micro ${styles.sessionContextLabel}`, children: "Unattached Image" }) }), _jsx("div", { className: styles.sessionContextRight, children: _jsx("div", { className: styles.sessionContextSpacer, "aria-hidden": "true" }) })] })) }), _jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.panelHeaderText, children: [_jsx("div", { className: `tf-heading-card ${styles.panelTitle}`, children: "Sessions" }), !activeTarget ? (_jsx("div", { className: "tf-text-secondary", children: "Open an image attachment to begin" })) : null] }), _jsx("div", { className: styles.sessionActions, children: _jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => void createSession(), disabled: !activeTarget || saving, "aria-label": "Create session", title: "Create session", children: _jsx(Plus, { size: 16 }) }) })] }), !activeTarget ? (_jsxs("div", { className: styles.emptyState, children: [_jsx("strong", { className: "tf-empty-title", children: "No image selected" }), _jsx("p", { className: "tf-empty-copy", children: "Open an image attachment from a task to start an annotated session." })] })) : loading ? (_jsx("div", { className: styles.emptyState, children: _jsx("p", { className: "tf-empty-copy", children: "Loading sessions\u2026" }) })) : (_jsx("div", { className: `tf-scrollbar ${styles.sessionList}`, children: sessions.length === 0 ? (_jsxs("div", { className: styles.sessionEmptyState, children: [_jsx("div", { className: `tf-heading-card ${styles.sessionTitle}`, children: "No sessions yet" }), _jsx("div", { className: `tf-text-secondary ${styles.annotationSummary}`, children: "Create the first annotation session for this image." })] })) : sessions.map((session) => {
|
|
1813
|
+
const isSelected = selectedSessionId === session.id;
|
|
1814
|
+
const isEditingSelectedSession = isSelected && sessionCardEditing;
|
|
1815
|
+
const preview = sessionInstructionPreview(isSelected ? draftGlobalInstruction : session.globalInstruction);
|
|
1816
|
+
const sessionTitle = (isSelected ? draftTitle : session.title) || 'Untitled session';
|
|
1817
|
+
return (_jsxs("div", { className: `tf-surface-elevated ${styles.sessionCard} ${isSelected ? styles.sessionCardActive : ''}`, ref: isSelected ? sessionCardRef : undefined, onBlur: isEditingSelectedSession
|
|
1818
|
+
? (event) => {
|
|
1819
|
+
const nextTarget = event.relatedTarget;
|
|
1820
|
+
if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget))
|
|
1821
|
+
return;
|
|
1822
|
+
setSessionCardEditing(false);
|
|
1823
|
+
}
|
|
1824
|
+
: undefined, children: [isEditingSelectedSession ? (_jsxs("div", { className: styles.sessionCardBody, children: [_jsxs("div", { className: styles.sessionMeta, children: [_jsx("label", { className: "tf-field-label", htmlFor: "annotated-session-title", children: "Session title" }), _jsx("input", { id: "annotated-session-title", className: `tf-field-shell ${styles.textInput} ${styles.sessionTitleInput}`, value: draftTitle, onChange: (event) => {
|
|
1825
|
+
setDraftTitle(event.target.value);
|
|
1826
|
+
markDraftChanged(600);
|
|
1827
|
+
}, placeholder: "Session title" }), _jsx("span", { className: `tf-text-meta ${styles.sessionTimestamp}`, children: formatTimestamp(session.updatedAt) })] }), getSessionCreatorLabel(session) ? (_jsxs("div", { className: `tf-text-secondary ${styles.sessionCreator}`, children: ["Created by ", getSessionCreatorLabel(session)] })) : null, _jsxs("div", { className: styles.sessionInstructionEditor, children: [_jsx("label", { className: "tf-field-label", htmlFor: "annotated-session-instruction", children: "Session instruction" }), _jsx("textarea", { id: "annotated-session-instruction", className: `tf-field-shell ${styles.textArea} ${styles.sessionInstructionField}`, value: draftGlobalInstruction, onChange: (event) => {
|
|
1828
|
+
setDraftGlobalInstruction(event.target.value);
|
|
1829
|
+
markDraftChanged(600);
|
|
1830
|
+
}, placeholder: "Add overall instructions, context, or framing for this session." })] }), _jsxs("div", { className: `tf-text-secondary ${styles.annotationSummary}`, children: [session.annotations.length, " annotation", session.annotations.length === 1 ? '' : 's'] })] })) : (_jsxs("button", { type: "button", className: styles.sessionCardButton, onClick: () => {
|
|
1831
|
+
if (isSelected) {
|
|
1832
|
+
setSessionCardEditing(true);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
void flushPendingDraft('session-switch').then((saved) => {
|
|
1836
|
+
if (!saved)
|
|
1837
|
+
return;
|
|
1838
|
+
setSelectedSessionId(session.id);
|
|
1839
|
+
syncDraftFromSession(session, activeTarget);
|
|
1840
|
+
});
|
|
1841
|
+
}, "aria-pressed": isSelected, children: [_jsxs("div", { className: styles.sessionMeta, children: [_jsx("span", { className: `tf-heading-card ${styles.sessionTitle}`, children: sessionTitle }), _jsx("span", { className: `tf-text-meta ${styles.sessionTimestamp}`, children: formatTimestamp(session.updatedAt) })] }), getSessionCreatorLabel(session) ? (_jsxs("div", { className: `tf-text-secondary ${styles.sessionCreator}`, children: ["Created by ", getSessionCreatorLabel(session)] })) : null, preview ? (_jsx("div", { className: `tf-text-secondary ${styles.sessionInstructionPreview}`, children: preview })) : null, _jsxs("div", { className: `tf-text-secondary ${styles.annotationSummary}`, children: [session.annotations.length, " annotation", session.annotations.length === 1 ? '' : 's'] })] })), isSelected ? (_jsx("div", { className: styles.sessionCardFooter, children: _jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: () => void duplicateSelectedSession(), disabled: saving, "aria-label": "Duplicate session", title: `Duplicate session "${sessionTitle}"`, children: _jsx(Copy, { size: 16 }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: () => void deleteSelectedSession(), disabled: saving, "aria-label": "Delete session", title: `Delete session "${sessionTitle}"`, children: _jsx(Trash2, { size: 16 }) })] }) })) : null] }, session.id));
|
|
1842
|
+
}) }))] }), _jsxs("section", { className: `tf-surface-panel ${styles.panel} ${styles.canvasPanel}`, children: [_jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.canvasHeading, children: [_jsx("div", { className: "tf-heading-card", children: activeTarget?.displayName || 'Annotated attachment' }), _jsx("div", { className: "tf-text-secondary", children: selectedSession ? `${draftAnnotations.length} markers · ${activeSessionTimestamp}` : 'Pick or create a session' })] }), activeImageReferenceLabel ? (_jsx(ImageReferenceBadge, { copied: imageIdCopied, onClick: () => { void copyImageId(); }, title: "Copy image reference", ariaLabel: imageIdCopied ? 'Copied image reference' : 'Copy image reference', label: activeImageReferenceLabel })) : null] }), _jsxs("div", { className: styles.toolRail, children: [_jsxs("div", { className: `${styles.toolbarCluster} ${styles.toolbarViewportCluster}`, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void saveSession(), disabled: !selectedSession || saving || (!hasUnsavedChanges && saveState !== 'error'), "aria-label": saving ? 'Saving session' : 'Save session', title: saving ? 'Saving session' : 'Save session', children: _jsx(Save, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => setOpenImageModalOpen(true), disabled: openingImage, "aria-label": "Open image", title: "Open image", children: _jsx(FolderOpen, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void pasteImageFromClipboard(), disabled: pastingImage, "aria-label": pastingImage ? 'Pasting image' : 'Paste image', title: pastingImage ? 'Pasting image' : 'Paste image', children: pastingImage ? _jsx(Loader2, { size: 18, className: sharedStyles.spin }) : _jsx(ClipboardPaste, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => {
|
|
1843
|
+
if (!selectedSession)
|
|
1844
|
+
return;
|
|
1845
|
+
restoreLastSavedDraft();
|
|
1846
|
+
}, disabled: !selectedSession || !hasUnsavedChanges, "aria-label": "Reset unsaved changes", title: "Reset unsaved changes", children: _jsx(RotateCcw, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomOut, disabled: !activeTarget || imageLoading || zoomLevel <= 0.25, "aria-label": "Zoom out", children: _jsx(ZoomOut, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: resetZoom, disabled: !activeTarget || imageLoading || zoomLevel === 1, "aria-label": `Reset zoom to 100 percent (currently ${zoomLabel})`, title: `Reset zoom to 100% (currently ${zoomLabel})`, children: _jsx("span", { children: zoomLabel }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomIn, disabled: !activeTarget || imageLoading || zoomLevel >= 4, "aria-label": "Zoom in", children: _jsx(ZoomIn, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${styles.toolbarButton} ${panMode ? styles.toolBtnActive : ''}`, onClick: () => setPanMode((current) => !current), disabled: !activeTarget || imageLoading || !canPan, "aria-label": "Pan canvas", title: "Pan canvas", children: _jsx(Hand, { size: 18 }) })] }), _jsx("span", { className: styles.toolbarSeparator, "aria-hidden": "true" }), _jsxs("div", { className: styles.toolbarActions, children: [_jsx("div", { className: styles.toolbarSelectionActions, children: selectedAnnotation ? (_jsxs(_Fragment, { children: [_jsxs("div", { ref: colorPickerRef, className: styles.toolbarColorPicker, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton} ${styles.colorPickerButton}`, onClick: () => setColorPickerOpen((current) => !current), "aria-label": "Marker color", "aria-expanded": colorPickerOpen, title: "Marker color", children: _jsx("span", { className: styles.colorPickerSwatch, style: { backgroundColor: selectedMarkerColor }, "aria-hidden": "true" }) }), colorPickerOpen ? (_jsx("div", { className: styles.colorPickerPopover, role: "menu", "aria-label": "Marker color options", children: MARKER_COLOR_SWATCHES.map((color) => (_jsx("button", { type: "button", className: `${styles.colorOption} ${selectedMarkerColor === color ? styles.colorOptionActive : ''}`, style: { backgroundColor: color }, onClick: () => applyMarkerColor(color), "aria-label": `Use marker color ${color}`, "aria-pressed": selectedMarkerColor === color }, color))) })) : null] }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: deleteSelectedAnnotation, "aria-label": "Delete marker", title: "Delete selected marker", children: _jsx(Trash2, { size: 18 }) }), _jsx("div", { className: styles.toolbarGeometryFields, "aria-label": "Marker geometry percent controls", children: getGeometryFields(selectedAnnotation).map((field) => (_jsxs("label", { className: styles.toolbarGeometryField, children: [_jsx("span", { className: `tf-text-meta ${styles.toolbarGeometryLabel}`, children: field.label }), _jsx("input", { className: `tf-field-shell ${styles.toolbarGeometryInput}`, type: "number", min: 0, max: 100, step: 0.1, value: field.value, onChange: (event) => {
|
|
1847
|
+
const nextValue = parsePercentField(event.target.value);
|
|
1848
|
+
if (nextValue === null)
|
|
1849
|
+
return;
|
|
1850
|
+
updateDraftAnnotation(selectedAnnotation.id, (annotation) => (updateAnnotationGeometryField(annotation, field.key, nextValue)));
|
|
1851
|
+
}, "aria-label": `${field.label} percent` })] }, field.key))) })] })) : null }), _jsxs("div", { className: styles.toolbarUtilities, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.payloadBtn} ${styles.toolbarButton}`, onClick: openPayloadPreview, disabled: !selectedSession || payloadLoading, "aria-label": payloadLoading ? 'Loading payload preview' : 'Preview payload', title: payloadLoading ? 'Loading payload preview' : 'Preview payload', children: _jsx(MessageSquareCode, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => setToolHelpModalOpen(true), "aria-label": "How to use marker tools", title: "How to use marker tools", children: _jsx(HelpCircle, { size: 18 }) })] })] })] }), _jsxs("div", { className: styles.canvasWorkspace, children: [_jsx("div", { className: styles.canvasToolRail, "aria-label": "Annotation tools", children: TOOL_OPTIONS.map((tool) => {
|
|
1852
|
+
const Icon = tool.icon;
|
|
1853
|
+
return (_jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${toolMode === tool.value ? styles.toolBtnActive : ''}`, onClick: () => handleToolSelection(tool.value), disabled: !activeTarget, title: tool.label, "aria-label": `${tool.label} tool. ${TOOL_HELP_COPY[tool.value].short}`, children: _jsx(Icon, { size: 18 }) }, tool.value));
|
|
1854
|
+
}) }), _jsx("div", { className: styles.canvasMain, children: _jsx("div", { ref: canvasScrollerRef, className: `tf-scrollbar ${styles.canvasScroller}`, children: !activeTarget ? (_jsx("div", { className: styles.emptyState, children: _jsx("p", { className: "tf-empty-copy", children: "Select an image attachment to annotate." }) })) : (_jsxs(_Fragment, { children: [imageLoading && !imageLoadError ? (_jsx("div", { className: styles.canvasStatusOverlay, "aria-live": "polite", children: _jsxs("div", { className: styles.canvasStatusCard, children: [_jsx(Loader2, { size: 20, className: sharedStyles.spinner }), _jsx("span", { children: "Loading image\u2026" })] }) })) : null, imageLoadError ? (_jsx("div", { className: styles.canvasStatusOverlay, "aria-live": "polite", children: _jsx("div", { className: styles.canvasStatusCard, children: _jsx("span", { children: "Image failed to load." }) }) })) : null, _jsx("div", { className: styles.canvasFrame, children: _jsxs("div", { className: styles.canvasMedia, style: {
|
|
1855
|
+
width: canvasMediaSize.width ? `${canvasMediaSize.width}px` : undefined,
|
|
1856
|
+
height: canvasMediaSize.height ? `${canvasMediaSize.height}px` : undefined,
|
|
1857
|
+
}, children: [_jsx("img", { ref: imageRef, src: activeTarget.path, alt: activeTarget.displayName, className: styles.canvasImage, onLoad: () => {
|
|
1858
|
+
const image = imageRef.current;
|
|
1859
|
+
setNaturalImageSize({
|
|
1860
|
+
width: image?.naturalWidth || 0,
|
|
1861
|
+
height: image?.naturalHeight || 0,
|
|
1862
|
+
});
|
|
1863
|
+
setImageLoading(false);
|
|
1864
|
+
setImageLoadError(false);
|
|
1865
|
+
}, onError: () => {
|
|
1866
|
+
setNaturalImageSize({ width: 0, height: 0 });
|
|
1867
|
+
setImageLoading(false);
|
|
1868
|
+
setImageLoadError(true);
|
|
1869
|
+
} }), !imageLoading && !imageLoadError && selectedSession ? (_jsxs("div", { ref: overlayRef, className: `${styles.overlay} ${toolMode === 'select' ? styles.overlaySelect : ''} ${panMode && canPan ? styles.overlayPan : ''}`, onPointerDown: handleCanvasPointerDown, onPointerMove: handleCanvasPointerMove, onPointerUp: (event) => {
|
|
1870
|
+
handleCanvasPointerUp(event);
|
|
1871
|
+
handleCanvasPointerEnd();
|
|
1872
|
+
}, "data-testid": "annotated-attachment-overlay", onPointerLeave: handleCanvasPointerLeave, onPointerCancel: handleCanvasPointerCancel, children: [_jsx("svg", { className: styles.overlaySvg, viewBox: arrowOverlayViewBox, preserveAspectRatio: "none", "aria-hidden": "true", children: draftAnnotations.filter((annotation) => annotation.kind === 'arrow').map((annotation) => {
|
|
1873
|
+
const geometry = getArrowGeometry(annotation, canvasMediaSize);
|
|
1874
|
+
const color = mapMarkerColor(annotation);
|
|
1875
|
+
const isSelected = selectedAnnotationId === annotation.id;
|
|
1876
|
+
return (_jsxs(React.Fragment, { children: [isSelected ? (_jsxs(_Fragment, { children: [_jsx("line", { x1: geometry.shaftX1, y1: geometry.shaftY1, x2: geometry.shaftX2, y2: geometry.shaftY2, stroke: "color-mix(in srgb, var(--surface-elevated) 92%, transparent)", strokeWidth: 14, strokeLinecap: "round" }), _jsx("polygon", { points: geometry.headPoints, fill: color, stroke: "color-mix(in srgb, var(--surface-elevated) 92%, transparent)", strokeWidth: 4, strokeLinejoin: "round" })] })) : null, _jsx("line", { x1: geometry.shaftX1, y1: geometry.shaftY1, x2: geometry.shaftX2, y2: geometry.shaftY2, stroke: color, strokeWidth: isSelected ? 10 : 6, strokeLinecap: "round" }), _jsx("polygon", { points: geometry.headPoints, fill: color })] }, annotation.id));
|
|
1877
|
+
}) }), _jsx("svg", { className: styles.overlayHitLayer, viewBox: arrowOverlayViewBox, preserveAspectRatio: "none", children: draftAnnotations.filter((annotation) => annotation.kind === 'arrow').map((annotation) => (_jsxs(React.Fragment, { children: [_jsx("line", { x1: annotation.x * canvasMediaSize.width, y1: annotation.y * canvasMediaSize.height, x2: annotation.x2 * canvasMediaSize.width, y2: annotation.y2 * canvasMediaSize.height, className: styles.arrowHitArea, "data-testid": `annotated-arrow-hit-${annotation.id}`, "aria-label": `Arrow marker ${annotationOrderNumbers.get(annotation.id) || 0}`, onPointerDown: (event) => startDragAnnotation(event, annotation), onClick: (event) => {
|
|
1878
|
+
event.stopPropagation();
|
|
1879
|
+
setSelectedAnnotationId(annotation.id);
|
|
1880
|
+
} }), selectedAnnotationId === annotation.id ? (_jsxs(_Fragment, { children: [_jsx("circle", { cx: annotation.x * canvasMediaSize.width, cy: annotation.y * canvasMediaSize.height, r: arrowHandleGeometry.hitRadius, className: styles.canvasHandleHit, role: "button", tabIndex: 0, "aria-label": "Move arrow tail", onPointerDown: (event) => startArrowHandleDrag(event, annotation, 'tail') }), _jsx("circle", { cx: annotation.x * canvasMediaSize.width, cy: annotation.y * canvasMediaSize.height, r: arrowHandleGeometry.visibleRadius, className: styles.canvasHandleVisible, "aria-hidden": "true" }), _jsx("circle", { cx: annotation.x2 * canvasMediaSize.width, cy: annotation.y2 * canvasMediaSize.height, r: arrowHandleGeometry.hitRadius, className: styles.canvasHandleHit, role: "button", tabIndex: 0, "aria-label": "Move arrow tip", onPointerDown: (event) => startArrowHandleDrag(event, annotation, 'tip') }), _jsx("circle", { cx: annotation.x2 * canvasMediaSize.width, cy: annotation.y2 * canvasMediaSize.height, r: arrowHandleGeometry.visibleRadius, className: styles.canvasHandleVisible, "aria-hidden": "true" })] })) : null] }, `hit-${annotation.id}`))) }), draftAnnotations.filter((annotation) => annotation.kind === 'arrow').map((annotation) => (_jsx("div", { className: `${styles.annotationNumberBadge} ${styles.arrowNumberBadge}`, style: {
|
|
1881
|
+
left: `${((annotation.x + annotation.x2) / 2) * 100}%`,
|
|
1882
|
+
top: `${((annotation.y + annotation.y2) / 2) * 100}%`,
|
|
1883
|
+
backgroundColor: mapMarkerColor(annotation),
|
|
1884
|
+
}, "aria-hidden": "true", children: annotationOrderNumbers.get(annotation.id) || 0 }, `arrow-number-${annotation.id}`))), draftAnnotations.filter((annotation) => annotation.kind !== 'arrow').map((annotation) => {
|
|
1885
|
+
const color = mapMarkerColor(annotation);
|
|
1886
|
+
const annotationOrderNumber = annotationOrderNumbers.get(annotation.id) || 0;
|
|
1887
|
+
const sharedProps = {
|
|
1888
|
+
onPointerDown: (event) => startDragAnnotation(event, annotation),
|
|
1889
|
+
onClick: (event) => {
|
|
1890
|
+
event.stopPropagation();
|
|
1891
|
+
setSelectedAnnotationId(annotation.id);
|
|
1892
|
+
},
|
|
1893
|
+
};
|
|
1894
|
+
if (annotation.kind === 'pin') {
|
|
1895
|
+
return (_jsx("button", { type: "button", ...sharedProps, className: `${styles.pin} ${selectedAnnotationId === annotation.id ? styles.selected : ''}`, style: { left: `${annotation.x * 100}%`, top: `${annotation.y * 100}%`, backgroundColor: color }, children: annotationOrderNumber }, annotation.id));
|
|
1896
|
+
}
|
|
1897
|
+
if (annotation.kind === 'text-note') {
|
|
1898
|
+
return (_jsx("button", { type: "button", ...sharedProps, className: `${styles.note} ${selectedAnnotationId === annotation.id ? styles.selected : ''}`, style: { left: `${annotation.x * 100}%`, top: `${annotation.y * 100}%`, backgroundColor: color }, children: annotationOrderNumber }, annotation.id));
|
|
1899
|
+
}
|
|
1900
|
+
return (_jsxs("div", { className: `${styles.box} ${selectedAnnotationId === annotation.id ? styles.selected : ''}`, style: {
|
|
1901
|
+
left: `${annotation.x * 100}%`,
|
|
1902
|
+
top: `${annotation.y * 100}%`,
|
|
1903
|
+
width: `${annotation.width * 100}%`,
|
|
1904
|
+
height: `${annotation.height * 100}%`,
|
|
1905
|
+
borderColor: color,
|
|
1906
|
+
}, children: [_jsx("span", { className: `${styles.annotationNumberBadge} ${styles.boxNumberBadge}`, style: { backgroundColor: color }, "aria-hidden": "true", children: annotationOrderNumber }), _jsx("button", { type: "button", ...sharedProps, className: styles.boxSurface, "aria-label": `Box marker ${annotationOrderNumber}` })] }, annotation.id));
|
|
1907
|
+
}), _jsx("svg", { className: styles.overlaySvg, viewBox: arrowOverlayViewBox, preserveAspectRatio: "none", children: draftAnnotations.filter((annotation) => annotation.kind === 'box' && selectedAnnotationId === annotation.id).map((annotation) => (_jsxs(React.Fragment, { children: [_jsx("circle", { cx: (annotation.x + annotation.width) * canvasMediaSize.width, cy: (annotation.y + annotation.height) * canvasMediaSize.height, r: arrowHandleGeometry.hitRadius, className: `${styles.canvasHandleHit} ${styles.canvasResizeHandleHit}`, role: "button", tabIndex: 0, "aria-label": "Resize box marker", onPointerDown: (event) => startResizeBox(event, annotation) }), _jsx("circle", { cx: (annotation.x + annotation.width) * canvasMediaSize.width, cy: (annotation.y + annotation.height) * canvasMediaSize.height, r: arrowHandleGeometry.visibleRadius, className: styles.canvasHandleVisible, "aria-hidden": "true" })] }, `box-handle-${annotation.id}`))) })] })) : null] }) })] })) }) })] }), _jsxs("div", { className: styles.statusBar, children: [_jsx("span", { children: saveState === 'error'
|
|
1908
|
+
? 'Save failed. Retry now.'
|
|
1909
|
+
: saveState === 'saving' || saveState === 'pending'
|
|
1910
|
+
? 'Saving…'
|
|
1911
|
+
: 'Saved' }), error ? _jsx("span", { children: error }) : _jsx("span", { children: !selectedSession ? 'Create a session to begin placing markers on this image.' : panMode && canPan ? 'Drag on the image to pan.' : toolMode === 'select' ? 'Select a marker to edit it.' : `Click on the image to place a ${toolMode}.` }), activeTarget && !selectedSession && showCreateSessionPrompt ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void createSession(), disabled: saving || loading, children: saving ? 'Creating…' : 'Create one now' })) : null] })] }), _jsx("section", { className: `tf-surface-panel ${styles.panel} ${styles.detailPanel}`, children: selectedSession ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.panelHeaderText, children: [_jsx("div", { className: `tf-heading-card ${styles.panelTitle}`, children: "Markers" }), _jsxs("div", { className: "tf-text-secondary", children: [draftAnnotations.length, " in this session"] })] }), _jsxs("div", { className: styles.sessionActions, children: [_jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('up'), disabled: !selectedAnnotation || selectedAnnotationIndex <= 0, "aria-label": "Move marker up", children: _jsx(ArrowUp, { size: 16 }) }), _jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('down'), disabled: !selectedAnnotation || selectedAnnotationIndex === -1 || selectedAnnotationIndex >= draftAnnotations.length - 1, "aria-label": "Move marker down", children: _jsx(ArrowDown, { size: 16 }) })] })] }), _jsx("div", { className: `tf-scrollbar ${styles.annotationList}`, children: draftAnnotations.length === 0 ? (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Add a marker on the image to begin." }) })) : draftAnnotations.map((annotation, index) => {
|
|
1912
|
+
const AnnotationKindIcon = ANNOTATION_KIND_ICON[annotation.kind];
|
|
1913
|
+
const annotationKindLabel = ANNOTATION_KIND_LABELS[annotation.kind];
|
|
1914
|
+
const annotationSequenceLabel = `${annotationKindLabel} ${index + 1}`;
|
|
1915
|
+
const InstructionTypeIcon = MARKER_TYPE_ICON[annotation.markerType || DEFAULT_MARKER_TYPE];
|
|
1916
|
+
const instructionTypeLabel = MARKER_TYPE_OPTIONS.find((option) => option.value === (annotation.markerType || DEFAULT_MARKER_TYPE))?.label || 'Review';
|
|
1917
|
+
const isSelected = selectedAnnotationId === annotation.id;
|
|
1918
|
+
return (_jsxs("div", { className: `tf-surface-elevated ${styles.annotationCard} ${isSelected ? styles.annotationCardActive : ''}`, children: [_jsx("button", { type: "button", className: styles.annotationCardButton, onClick: () => setSelectedAnnotationId(annotation.id), "aria-label": annotationKindLabel, children: _jsxs("div", { className: styles.annotationMeta, children: [_jsx("span", { className: `tf-heading-card ${styles.annotationTitle}`, children: annotationSequenceLabel }), _jsx("span", { className: `tf-text-meta ${styles.annotationKind}`, "aria-hidden": "true", children: _jsx(AnnotationKindIcon, { size: 16 }) })] }) }), isSelected ? (_jsxs("div", { className: styles.annotationInstructionEditor, children: [_jsx("label", { className: "tf-field-label", htmlFor: "annotated-marker-instruction", children: "Marker instruction" }), _jsx("textarea", { id: "annotated-marker-instruction", ref: isSelected ? selectedInstructionTextareaRef : null, className: `tf-field-shell ${styles.textArea} ${styles.annotationInstructionField}`, value: annotation.instruction, onChange: (event) => {
|
|
1919
|
+
updateDraftAnnotation(annotation.id, (current) => ({
|
|
1920
|
+
...current,
|
|
1921
|
+
instruction: event.target.value,
|
|
1922
|
+
}), 600);
|
|
1923
|
+
}, onBlur: () => {
|
|
1924
|
+
void flushPendingDraft('text');
|
|
1925
|
+
}, placeholder: "What should the AI focus on for this marker?" }), _jsxs("div", { className: styles.annotationTypeField, children: [_jsx("label", { className: "tf-field-label", htmlFor: "annotated-marker-type", children: "Instruction type" }), _jsx("select", { id: "annotated-marker-type", className: `tf-field-shell ${styles.select}`, value: annotation.markerType || DEFAULT_MARKER_TYPE, onChange: (event) => {
|
|
1926
|
+
updateDraftAnnotation(annotation.id, (current) => updateMarkerType(current, event.target.value));
|
|
1927
|
+
}, children: MARKER_TYPE_OPTIONS.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) })] })] })) : (_jsx("button", { type: "button", className: styles.annotationInstructionButton, onClick: () => setSelectedAnnotationId(annotation.id), "aria-label": `Edit ${annotationKindLabel} instruction`, children: _jsxs("div", { className: styles.annotationInstructionEditor, children: [_jsx("div", { className: `tf-text-secondary ${styles.annotationInstructionPreview}`, children: annotation.instruction.trim() || 'No marker instruction yet.' }), _jsx("div", { className: styles.annotationPreviewFooter, children: _jsx("span", { className: `tf-text-meta ${styles.annotationInstructionTypeIcon}`, "aria-label": `Instruction type: ${instructionTypeLabel}`, title: instructionTypeLabel, children: _jsx(InstructionTypeIcon, { size: 16 }) }) })] }) }))] }, annotation.id));
|
|
1928
|
+
}) })] })) : (_jsxs("div", { className: styles.emptyState, children: [_jsx("strong", { className: "tf-empty-title", children: "No active session" }), _jsx("p", { className: "tf-empty-copy", children: "Create or select a session to edit annotations." })] })) }), _jsx(Modal, { isOpen: openImageModalOpen, onClose: () => {
|
|
1929
|
+
if (openingImage)
|
|
1930
|
+
return;
|
|
1931
|
+
setOpenImageModalOpen(false);
|
|
1932
|
+
setOpenImageReference('');
|
|
1933
|
+
}, title: "Open Image", size: "sm", children: _jsxs("form", { className: styles.openImageModalBody, onSubmit: (event) => {
|
|
1934
|
+
event.preventDefault();
|
|
1935
|
+
void openImageByReference();
|
|
1936
|
+
}, children: [_jsx("label", { className: "tf-field-label", htmlFor: "annotated-open-image-reference", children: "Image reference number" }), _jsx("input", { id: "annotated-open-image-reference", className: `tf-field-shell ${styles.textInput} ${styles.openImageField}`, value: openImageReference, onChange: (event) => setOpenImageReference(event.target.value), placeholder: "I-24", autoFocus: true }), _jsxs("div", { className: styles.openImageActions, children: [_jsx("button", { type: "button", className: "tf-button-ghost tf-button-compact", onClick: () => {
|
|
1937
|
+
setOpenImageModalOpen(false);
|
|
1938
|
+
setOpenImageReference('');
|
|
1939
|
+
}, disabled: openingImage, children: "Cancel" }), _jsx("button", { type: "submit", className: "tf-button-primary tf-button-compact", disabled: openingImage, children: openingImage ? 'Opening…' : 'Open' })] })] }) }), _jsx(Modal, { isOpen: toolHelpModalOpen, onClose: () => setToolHelpModalOpen(false), title: "How To Use Markers", size: "md", children: _jsx("div", { className: styles.markerHelpModalBody, children: TOOL_OPTIONS.filter((tool) => tool.value !== 'select').map((tool) => (_jsxs("div", { className: `tf-surface-inset ${styles.markerHelpItem}`, children: [_jsxs("div", { className: styles.markerHelpHeader, children: [_jsx("strong", { className: "tf-heading-card", children: tool.label }), _jsx("span", { className: "tf-text-meta", children: TOOL_HELP_COPY[tool.value].short })] }), _jsx("p", { className: "tf-text-secondary", children: TOOL_HELP_COPY[tool.value].detail }), _jsx("p", { className: `tf-text-body ${styles.markerHelpExample}`, children: TOOL_HELP_COPY[tool.value].example })] }, tool.value))) }) }), _jsx(Modal, { isOpen: payloadModalOpen, onClose: () => setPayloadModalOpen(false), title: "AI Payload", size: "lg", children: _jsxs("div", { className: styles.payloadModalBody, children: [_jsxs("div", { className: styles.payloadModalToolbar, children: [_jsxs("div", { className: styles.payloadViewToggle, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${payloadPreviewMode === 'json' ? styles.toolBtnActive : ''}`, onClick: () => setPayloadPreviewMode('json'), children: "JSON" }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${payloadPreviewMode === 'brief' ? styles.toolBtnActive : ''}`, onClick: () => setPayloadPreviewMode('brief'), children: "AI Brief" })] }), _jsxs("div", { className: styles.payloadModalActions, children: [_jsxs("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn}`, onClick: () => void copyPayloadContent('json'), disabled: !payloadPreview, "aria-label": payloadCopied === 'json' ? 'Copied JSON' : 'Copy JSON', title: payloadCopied === 'json' ? 'Copied JSON' : 'Copy JSON', children: [payloadCopied === 'json' ? _jsx(Check, { size: 16 }) : _jsx(Copy, { size: 16 }), _jsx("span", { children: "JSON" })] }), _jsxs("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn}`, onClick: () => void copyPayloadContent('brief'), disabled: !payloadPreview, "aria-label": payloadCopied === 'brief' ? 'Copied AI Brief' : 'Copy AI Brief', title: payloadCopied === 'brief' ? 'Copied AI Brief' : 'Copy AI Brief', children: [payloadCopied === 'brief' ? _jsx(Check, { size: 16 }) : _jsx(Copy, { size: 16 }), _jsx("span", { children: "AI Brief" })] })] })] }), payloadLoading ? (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Loading payload\u2026" }) })) : payloadPreview ? (_jsx("pre", { className: `tf-surface-inset tf-scrollbar ${styles.payloadModalPreview}`, children: payloadPreviewMode === 'json'
|
|
1940
|
+
? JSON.stringify(payloadPreview, null, 2)
|
|
1941
|
+
: buildPayloadBrief(payloadPreview) })) : (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Load a payload preview to inspect the current session contract." }) }))] }) })] }));
|
|
1942
|
+
}
|