diff --git a/frontend/src/components/project/workflow/CreateWorkflowWizard.vue b/frontend/src/components/project/workflow/CreateWorkflowWizard.vue index e96dfa4b..b8da5c5e 100644 --- a/frontend/src/components/project/workflow/CreateWorkflowWizard.vue +++ b/frontend/src/components/project/workflow/CreateWorkflowWizard.vue @@ -894,6 +894,9 @@ const handleSubmit = async () => { isInitialStage: true, isFinalStage: false, // Annotation stage is never final - completion stage is always final inputDataSourceId: form.annotationInputDataSourceId as number, + targetDataSourceId: form.includeRevision + ? (form.revisionInputDataSourceId as number || undefined) + : (form.completionInputDataSourceId as number || undefined), assignedProjectMemberIds: form.annotationMembers }); @@ -907,6 +910,7 @@ const handleSubmit = async () => { isInitialStage: false, isFinalStage: false, inputDataSourceId: form.revisionInputDataSourceId as number || undefined, + targetDataSourceId: form.completionInputDataSourceId as number || undefined, assignedProjectMemberIds: form.revisionMembers }); } @@ -920,6 +924,7 @@ const handleSubmit = async () => { isInitialStage: false, isFinalStage: true, inputDataSourceId: form.completionInputDataSourceId as number || undefined, + targetDataSourceId: undefined, // Final stage has no target assignedProjectMemberIds: form.completionMembers }); diff --git a/frontend/src/core/workspace/interaction/toolHandlers/boundingBoxToolHandler.ts b/frontend/src/core/workspace/interaction/toolHandlers/boundingBoxToolHandler.ts index d670833d..3029569c 100644 --- a/frontend/src/core/workspace/interaction/toolHandlers/boundingBoxToolHandler.ts +++ b/frontend/src/core/workspace/interaction/toolHandlers/boundingBoxToolHandler.ts @@ -105,6 +105,7 @@ export class BoundingBoxToolHandler implements ToolHandler { coordinates: boundingBoxCoordinates, assetId: Number(store.currentAssetId), taskId: Number(store.currentTaskId), + isGroundTruth: true }; store.addAnnotation(newAnnotation); diff --git a/frontend/src/core/workspace/interaction/toolHandlers/lineToolHandler.ts b/frontend/src/core/workspace/interaction/toolHandlers/lineToolHandler.ts index 55e961a6..fa210110 100644 --- a/frontend/src/core/workspace/interaction/toolHandlers/lineToolHandler.ts +++ b/frontend/src/core/workspace/interaction/toolHandlers/lineToolHandler.ts @@ -96,6 +96,7 @@ export class LineToolHandler implements ToolHandler { coordinates: lineCoordinates, assetId: Number(store.currentAssetId), taskId: Number(store.currentTaskId), + isGroundTruth: true }; store.addAnnotation(newAnnotation); diff --git a/frontend/src/core/workspace/interaction/toolHandlers/pointToolHandler.ts b/frontend/src/core/workspace/interaction/toolHandlers/pointToolHandler.ts index b1c60d80..db09b213 100644 --- a/frontend/src/core/workspace/interaction/toolHandlers/pointToolHandler.ts +++ b/frontend/src/core/workspace/interaction/toolHandlers/pointToolHandler.ts @@ -53,6 +53,7 @@ export class PointToolHandler implements ToolHandler { coordinates: pointCoordinates, assetId: Number(store.currentAssetId), taskId: Number(store.currentTaskId), + isGroundTruth: true }; store.addAnnotation(newAnnotation); diff --git a/frontend/src/core/workspace/interaction/toolHandlers/polygonToolHandler.ts b/frontend/src/core/workspace/interaction/toolHandlers/polygonToolHandler.ts index e0cdd26e..8c16a4cc 100644 --- a/frontend/src/core/workspace/interaction/toolHandlers/polygonToolHandler.ts +++ b/frontend/src/core/workspace/interaction/toolHandlers/polygonToolHandler.ts @@ -173,6 +173,7 @@ export class PolygonToolHandler implements ToolHandler { coordinates: polygonCoordinates, assetId: Number(store.currentAssetId), taskId: Number(store.currentTaskId), + isGroundTruth: true }; store.addAnnotation(newAnnotation); diff --git a/frontend/src/core/workspace/interaction/toolHandlers/polylineToolHandler.ts b/frontend/src/core/workspace/interaction/toolHandlers/polylineToolHandler.ts index 28c18fa3..4e923c85 100644 --- a/frontend/src/core/workspace/interaction/toolHandlers/polylineToolHandler.ts +++ b/frontend/src/core/workspace/interaction/toolHandlers/polylineToolHandler.ts @@ -164,6 +164,7 @@ export class PolylineToolHandler implements ToolHandler { coordinates: polylineCoordinates, assetId: Number(store.currentAssetId), taskId: Number(store.currentTaskId), + isGroundTruth: true }; store.addAnnotation(newAnnotation); diff --git a/frontend/src/services/project/export/export.types.ts b/frontend/src/services/project/export/export.types.ts new file mode 100644 index 00000000..b769785b --- /dev/null +++ b/frontend/src/services/project/export/export.types.ts @@ -0,0 +1,41 @@ +/** + * Export metadata response + */ +export interface ExportMetadata { + completedTasksCount: number + annotationsCount: number + annotatedAssetsCount: number + categoriesCount: number + workflowStageName: string + projectName: string + availableFormats: string[] +} + +/** + * COCO export request configuration + */ +export interface CocoExportRequest { + includeGroundTruth?: boolean + includePredictions?: boolean + fileName?: string + description?: string + contributor?: string + taskIds?: number[] + labelIds?: number[] +} + +/** + * Export format types + */ +export enum ExportFormat { + COCO = 'COCO' +} + +/** + * Export download response + */ +export interface ExportDownloadResponse { + data: Blob + filename: string + contentType: string +} \ No newline at end of file diff --git a/frontend/src/services/project/export/exportService.ts b/frontend/src/services/project/export/exportService.ts new file mode 100644 index 00000000..616cc00f --- /dev/null +++ b/frontend/src/services/project/export/exportService.ts @@ -0,0 +1,158 @@ +import { BaseProjectService } from '../baseProjectService' +import apiClient from '@/services/apiClient' +import type { + ExportMetadata, + CocoExportRequest, + ExportDownloadResponse +} from './export.types' + +/** + * Service for exporting annotated data in various formats + */ +export class ExportService extends BaseProjectService { + constructor() { + super('ExportService') + } + + /** + * Gets export metadata for a workflow stage + */ + async getExportMetadata(projectId: number, workflowStageId: number): Promise { + const url = this.buildProjectUrl(projectId, `export/workflow-stages/${workflowStageId}/metadata`) + return this.get(url) + } + + /** + * Exports workflow stage data in COCO format (simple GET request) + */ + async exportCoco( + projectId: number, + workflowStageId: number, + includeGroundTruth: boolean = true, + includePredictions: boolean = false + ): Promise { + const url = this.buildProjectUrl(projectId, `export/workflow-stages/${workflowStageId}/coco`) + const params = { + includeGroundTruth: includeGroundTruth.toString(), + includePredictions: includePredictions.toString() + } + + try { + const response = await apiClient.get(url, { + params, + responseType: 'blob' + }) + + // Extract filename from Content-Disposition header + const contentDisposition = response.headers['content-disposition'] || response.headers['Content-Disposition'] + let filename = 'coco_export.json' + + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) + if (filenameMatch && filenameMatch[1]) { + filename = filenameMatch[1].replace(/['"]/g, '') + } + } + + return { + data: response.data as Blob, + filename, + contentType: 'application/json' + } + } catch (error) { + this.logger.error('Failed to export COCO data', error) + throw error + } + } + + /** + * Exports workflow stage data in COCO format with advanced options (POST request) + */ + async exportCocoWithOptions( + projectId: number, + workflowStageId: number, + exportRequest: CocoExportRequest + ): Promise { + const url = this.buildProjectUrl(projectId, `export/workflow-stages/${workflowStageId}/coco`) + + try { + const response = await apiClient.post(url, exportRequest, { + responseType: 'blob' + }) + + // Extract filename from Content-Disposition header + const contentDisposition = response.headers['content-disposition'] || response.headers['Content-Disposition'] + let filename = exportRequest.fileName || 'coco_export.json' + + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) + if (filenameMatch && filenameMatch[1]) { + filename = filenameMatch[1].replace(/['"]/g, '') + } + } + + if (!filename.toLowerCase().endsWith('.json')) { + filename += '.json' + } + + return { + data: response.data as Blob, + filename, + contentType: 'application/json' + } + } catch (error) { + this.logger.error('Failed to export COCO data with options', error) + throw error + } + } + + /** + * Downloads a blob as a file + */ + private downloadBlob(blob: Blob, filename: string): void { + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + } + + /** + * Downloads COCO export directly (convenience method) + */ + async downloadCocoExport( + projectId: number, + workflowStageId: number, + includeGroundTruth: boolean = true, + includePredictions: boolean = false + ): Promise { + const exportResponse = await this.exportCoco( + projectId, + workflowStageId, + includeGroundTruth, + includePredictions + ) + + this.downloadBlob(exportResponse.data, exportResponse.filename) + } + + /** + * Downloads COCO export with options directly (convenience method) + */ + async downloadCocoExportWithOptions( + projectId: number, + workflowStageId: number, + exportRequest: CocoExportRequest + ): Promise { + const exportResponse = await this.exportCocoWithOptions( + projectId, + workflowStageId, + exportRequest + ) + + this.downloadBlob(exportResponse.data, exportResponse.filename) + } +} \ No newline at end of file diff --git a/frontend/src/services/project/export/index.ts b/frontend/src/services/project/export/index.ts new file mode 100644 index 00000000..b7fb5379 --- /dev/null +++ b/frontend/src/services/project/export/index.ts @@ -0,0 +1,12 @@ +import { ExportService } from './exportService' + +export { ExportService } from './exportService' +export type { + ExportMetadata, + CocoExportRequest, + ExportDownloadResponse +} from './export.types' +export { ExportFormat } from './export.types' + +// Create singleton service instance +export const exportService = new ExportService() \ No newline at end of file diff --git a/frontend/src/services/project/index.ts b/frontend/src/services/project/index.ts index 411cdce3..9e43f72a 100644 --- a/frontend/src/services/project/index.ts +++ b/frontend/src/services/project/index.ts @@ -4,6 +4,7 @@ export { annotationService } from './annotationService'; export { assetService } from './asset'; export { dashboardService } from './dashboard'; export { dataSourceService } from './dataSource'; +export { exportService } from './export'; export { labelService, labelSchemeService } from './labelScheme'; export { taskService, taskBulkOperations } from './task'; export { diff --git a/frontend/src/stores/projectStore.ts b/frontend/src/stores/projectStore.ts index 3ef92fb4..604e24ad 100644 --- a/frontend/src/stores/projectStore.ts +++ b/frontend/src/stores/projectStore.ts @@ -75,7 +75,6 @@ export const useProjectStore = defineStore("project", { // Load related data in parallel await Promise.all([ this.loadTeamMembers(projectId), - this.loadProjectLabels(projectId), ]); } catch (error) { logger.error(`Failed to load project ${projectId}`, error); @@ -102,23 +101,6 @@ export const useProjectStore = defineStore("project", { } }, - async loadProjectLabels(projectId: number): Promise { - const { handleError } = useErrorHandler(); - - this.labelsLoading = true; - try { - // TODO: This would need a proper API endpoint for project labels - // For now, we'll skip this functionality until the API is available - this.projectLabels = []; - logger.info(`Labels functionality not yet implemented for project ${projectId}`); - } catch (error) { - logger.error(`Failed to load labels for project ${projectId}`, error); - handleError(error, "Failed to load project labels"); - } finally { - this.labelsLoading = false; - } - }, - addTeamMember(member: ProjectMember): void { this.teamMembers.push(member); logger.info(`Added team member: ${member.userName || member.email}`); @@ -213,12 +195,6 @@ export const useProjectStore = defineStore("project", { } }, - async refreshProjectLabels(): Promise { - if (this.currentProjectId) { - await this.loadProjectLabels(this.currentProjectId); - } - }, - setCurrentStageType(stageType: string): void { this.currentStageType = stageType; logger.info(`Updated current stage type: ${stageType}`); diff --git a/frontend/src/stores/workspaceStore.ts b/frontend/src/stores/workspaceStore.ts index 80ba1e0e..87d83ceb 100644 --- a/frontend/src/stores/workspaceStore.ts +++ b/frontend/src/stores/workspaceStore.ts @@ -474,7 +474,7 @@ export const useWorkspaceStore = defineStore("workspace", { labelId: annotation.labelId, isPrediction: annotation.isPrediction || false, confidenceScore: annotation.confidenceScore, - isGroundTruth: annotation.isGroundTruth || false, + isGroundTruth: annotation.isGroundTruth, version: annotation.version || 1, notes: annotation.notes, annotatorEmail: annotation.annotatorEmail, diff --git a/frontend/src/views/project/TasksView.vue b/frontend/src/views/project/TasksView.vue index 519627c7..73bb7e30 100644 --- a/frontend/src/views/project/TasksView.vue +++ b/frontend/src/views/project/TasksView.vue @@ -15,6 +15,16 @@
+