From 93ede07a536f55acd651a12623ce319ac88c78c4 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Mon, 6 Oct 2025 10:00:31 -0400 Subject: [PATCH 01/17] chore(automation): set default title and handle empty integrations in workflow visualizer --- .../components/workflow/components/UnifiedWorkflowCard.tsx | 3 ++- .../components/workflow/workflow-visualizer-simple.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/UnifiedWorkflowCard.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/UnifiedWorkflowCard.tsx index 1744cc5c8..a5558ebb4 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/UnifiedWorkflowCard.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/UnifiedWorkflowCard.tsx @@ -49,13 +49,14 @@ export function UnifiedWorkflowCard({ steps, title, onTest, integrationsUsed }: {/* Header with integration icons */}
- {integrationsUsed.map((integration) => ( + {integrationsUsed?.map((integration) => (
{integration.link}
))} diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/workflow-visualizer-simple.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/workflow-visualizer-simple.tsx index 6b4954e07..4c9fccb5d 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/workflow-visualizer-simple.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/workflow-visualizer-simple.tsx @@ -215,9 +215,9 @@ Please fix the automation script to resolve this error.`; ) : steps.length > 0 ? ( ) : ( From 735e281ef49f2d1d219d8234a6fc8efeab0578a5 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Mon, 6 Oct 2025 14:37:20 -0400 Subject: [PATCH 02/17] chore: add ability to support multiple automations per task --- apps/api/src/app.module.ts | 3 +- .../src/automation/automation.controller.ts | 41 ++++++ apps/api/src/automation/automation.module.ts | 11 ++ apps/api/src/automation/automation.service.ts | 42 ++++++ .../dto/automation-error-responses.dto.ts | 25 ++++ .../dto/automation-responses.dto.ts | 63 +++++++++ .../automation/dto/create-automation.dto.ts | 12 ++ .../schemas/automation-operations.ts | 7 + .../schemas/create-automation.responses.ts | 71 ++++++++++ .../actions/task-automation-actions.ts | 2 +- .../automation-layout-wrapper.tsx | 0 .../automation/{ => [automationId]}/chat.tsx | 34 ++--- .../components/AutomationPageClient.tsx | 19 ++- .../components/ai-elements/conversation.tsx | 0 .../components/ai-elements/loader.tsx | 0 .../components/chat/message-part/index.tsx | 0 .../chat/message-part/prompt-info.tsx | 0 .../chat/message-part/prompt-secret.tsx | 0 .../chat/message-part/reasoning.tsx | 0 .../chat/message-part/report-errors.tsx | 0 .../chat/message-part/research-activity.tsx | 0 .../components/chat/message-part/spinner.tsx | 0 .../components/chat/message-part/text.tsx | 0 .../components/chat/message-spinner.tsx | 0 .../components/chat/message.tsx | 0 .../components/chat/tool-header.tsx | 0 .../components/chat/tool-message.tsx | 0 .../components/chat/types.tsx | 0 .../components/icons/github.tsx | 0 .../components/icons/vercel-dashed.tsx | 0 .../components/layout/panels.tsx | 0 .../components/layout/sizing.ts | 0 .../markdown-renderer/markdown-renderer.tsx | 0 .../model-selector/model-selector.tsx | 0 .../model-selector/use-available-models.tsx | 0 .../components/panels/panels.tsx | 0 .../components/preview/preview.tsx | 0 .../components/tabs/index.tsx | 0 .../components/tabs/tab-content.tsx | 0 .../components/tabs/tab-item.tsx | 0 .../components/tabs/use-tab-state.ts | 0 .../components/ui/badge.tsx | 0 .../components/ui/button.tsx | 0 .../components/ui/checkbox.tsx | 0 .../components/ui/dialog.tsx | 0 .../components/ui/input.tsx | 0 .../components/ui/label.tsx | 0 .../components/ui/popover.tsx | 0 .../components/ui/scroll-area.tsx | 0 .../components/ui/select.tsx | 0 .../components/ui/sonner.tsx | 0 .../components/ui/textarea.tsx | 0 .../workflow/components/CodeViewer.tsx | 0 .../workflow/components/ConfettiEffect.tsx | 0 .../workflow/components/EmptyState.tsx | 0 .../workflow/components/TestDialog.tsx | 0 .../workflow/components/TestResultsPanel.tsx | 0 .../components/UnifiedWorkflowCard.tsx | 0 .../workflow/components/ViewModeSwitch.tsx | 0 .../workflow/components/WorkflowSkeleton.tsx | 0 .../workflow/components/WorkflowStepCard.tsx | 0 .../components/workflow/components/index.ts | 0 .../components/workflow/types.ts | 0 .../components/workflow/workflow-loading.tsx | 0 .../workflow/workflow-visualizer-simple.tsx | 11 +- .../{ => [automationId]}/hooks/index.ts | 0 .../hooks/use-task-automation-execution.ts | 16 ++- .../hooks/use-task-automation-script.ts | 3 +- .../hooks/use-task-automation-workflow.ts | 0 .../{ => [automationId]}/layout.tsx | 0 .../{ => [automationId]}/lib/chat-context.tsx | 11 ++ .../{ => [automationId]}/lib/deferred.ts | 0 .../{ => [automationId]}/lib/index.ts | 0 .../lib/is-relative-url.ts | 0 .../lib/task-automation-api.ts | 0 .../lib/task-automation-store.ts | 0 .../lib/types/data-parts.ts | 0 .../{ => [automationId]}/lib/types/index.ts | 4 +- .../lib/types/metadata.ts | 0 .../automation/{ => [automationId]}/page.tsx | 11 +- .../{ => [automationId]}/tools/exa-search.ts | 0 .../{ => [automationId]}/tools/firecrawl.ts | 0 .../{ => [automationId]}/tools/gateway.ts | 0 .../tools/generate-files/deferred.ts | 0 .../tools/generate-files/get-contents.ts | 0 .../tools/prompt-for-info.ts | 0 .../tools/prompt-for-secret.ts | 0 .../{ => [automationId]}/tools/store-to-s3.ts | 0 .../tools/task-automation-tools.ts | 0 .../tasks/[taskId]/components/SingleTask.tsx | 20 ++- .../[taskId]/components/TaskAutomations.tsx | 117 ++++++++++++++++ .../app/(app)/[orgId]/tasks/[taskId]/page.tsx | 20 ++- apps/app/src/lib/api-client.ts | 3 - .../migration.sql | 21 +++ packages/db/prisma/schema/automation.prisma | 15 ++ packages/db/prisma/schema/organization.prisma | 1 + packages/db/prisma/schema/task.prisma | 19 +-- packages/docs/openapi.json | 128 ++++++++++++++++++ 98 files changed, 680 insertions(+), 50 deletions(-) create mode 100644 apps/api/src/automation/automation.controller.ts create mode 100644 apps/api/src/automation/automation.module.ts create mode 100644 apps/api/src/automation/automation.service.ts create mode 100644 apps/api/src/automation/dto/automation-error-responses.dto.ts create mode 100644 apps/api/src/automation/dto/automation-responses.dto.ts create mode 100644 apps/api/src/automation/dto/create-automation.dto.ts create mode 100644 apps/api/src/automation/schemas/automation-operations.ts create mode 100644 apps/api/src/automation/schemas/create-automation.responses.ts rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/actions/task-automation-actions.ts (99%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/automation-layout-wrapper.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/chat.tsx (92%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/AutomationPageClient.tsx (81%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ai-elements/conversation.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ai-elements/loader.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/message-part/index.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/message-part/prompt-info.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/message-part/prompt-secret.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/message-part/reasoning.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/message-part/report-errors.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/message-part/research-activity.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/message-part/spinner.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/message-part/text.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/message-spinner.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/message.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/tool-header.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/tool-message.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/chat/types.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/icons/github.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/icons/vercel-dashed.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/layout/panels.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/layout/sizing.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/markdown-renderer/markdown-renderer.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/model-selector/model-selector.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/model-selector/use-available-models.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/panels/panels.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/preview/preview.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/tabs/index.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/tabs/tab-content.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/tabs/tab-item.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/tabs/use-tab-state.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/badge.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/button.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/checkbox.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/dialog.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/input.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/label.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/popover.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/scroll-area.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/select.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/sonner.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/ui/textarea.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/components/CodeViewer.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/components/ConfettiEffect.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/components/EmptyState.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/components/TestDialog.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/components/TestResultsPanel.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/components/UnifiedWorkflowCard.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/components/ViewModeSwitch.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/components/WorkflowSkeleton.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/components/WorkflowStepCard.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/components/index.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/types.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/workflow-loading.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/components/workflow/workflow-visualizer-simple.tsx (96%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/hooks/index.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/hooks/use-task-automation-execution.ts (93%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/hooks/use-task-automation-script.ts (93%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/hooks/use-task-automation-workflow.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/layout.tsx (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/lib/chat-context.tsx (86%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/lib/deferred.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/lib/index.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/lib/is-relative-url.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/lib/task-automation-api.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/lib/task-automation-store.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/lib/types/data-parts.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/lib/types/index.ts (98%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/lib/types/metadata.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/page.tsx (67%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/tools/exa-search.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/tools/firecrawl.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/tools/gateway.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/tools/generate-files/deferred.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/tools/generate-files/get-contents.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/tools/prompt-for-info.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/tools/prompt-for-secret.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/tools/store-to-s3.ts (100%) rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/{ => [automationId]}/tools/task-automation-tools.ts (100%) create mode 100644 apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/TaskAutomations.tsx create mode 100644 packages/db/prisma/migrations/20251006151959_add_automation_schema/migration.sql create mode 100644 packages/db/prisma/schema/automation.prisma diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 2a1a10725..21a48835b 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -4,6 +4,7 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AttachmentsModule } from './attachments/attachments.module'; import { AuthModule } from './auth/auth.module'; +import { AutomationModule } from './automation/automation.module'; import { CommentsModule } from './comments/comments.module'; import { PeopleModule } from './people/people.module'; import { DevicesModule } from './devices/devices.module'; @@ -17,7 +18,6 @@ import { TasksModule } from './tasks/tasks.module'; import { VendorsModule } from './vendors/vendors.module'; import { ContextModule } from './context/context.module'; - @Module({ imports: [ ConfigModule.forRoot({ @@ -29,6 +29,7 @@ import { ContextModule } from './context/context.module'; }, }), AuthModule, + AutomationModule, OrganizationModule, PeopleModule, RisksModule, diff --git a/apps/api/src/automation/automation.controller.ts b/apps/api/src/automation/automation.controller.ts new file mode 100644 index 000000000..4955d8f75 --- /dev/null +++ b/apps/api/src/automation/automation.controller.ts @@ -0,0 +1,41 @@ +import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { + ApiHeader, + ApiOperation, + ApiResponse, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger'; +import { OrganizationId } from '../auth/auth-context.decorator'; +import { HybridAuthGuard } from '../auth/hybrid-auth.guard'; +import { AutomationService } from './automation.service'; +import { CreateAutomationDto } from './dto/create-automation.dto'; +import { AUTOMATION_OPERATIONS } from './schemas/automation-operations'; +import { CREATE_AUTOMATION_RESPONSES } from './schemas/create-automation.responses'; + +@ApiTags('Automations') +@Controller({ path: 'automations', version: '1' }) +@UseGuards(HybridAuthGuard) +@ApiSecurity('apikey') +@ApiHeader({ + name: 'X-Organization-Id', + description: + 'Organization ID (required for session auth, optional for API key auth)', + required: false, +}) +export class AutomationController { + constructor(private readonly automationService: AutomationService) {} + + @Post() + @ApiOperation(AUTOMATION_OPERATIONS.createAutomation) + @ApiResponse(CREATE_AUTOMATION_RESPONSES[201]) + @ApiResponse(CREATE_AUTOMATION_RESPONSES[400]) + @ApiResponse(CREATE_AUTOMATION_RESPONSES[401]) + @ApiResponse(CREATE_AUTOMATION_RESPONSES[404]) + async createAutomation( + @OrganizationId() organizationId: string, + @Body() createAutomationDto: CreateAutomationDto, + ) { + return this.automationService.create(organizationId, createAutomationDto); + } +} diff --git a/apps/api/src/automation/automation.module.ts b/apps/api/src/automation/automation.module.ts new file mode 100644 index 000000000..14d470d6d --- /dev/null +++ b/apps/api/src/automation/automation.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AuthModule } from '../auth/auth.module'; +import { AutomationController } from './automation.controller'; +import { AutomationService } from './automation.service'; + +@Module({ + imports: [AuthModule], + controllers: [AutomationController], + providers: [AutomationService], +}) +export class AutomationModule {} diff --git a/apps/api/src/automation/automation.service.ts b/apps/api/src/automation/automation.service.ts new file mode 100644 index 000000000..fcbc2fc37 --- /dev/null +++ b/apps/api/src/automation/automation.service.ts @@ -0,0 +1,42 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { db } from '@trycompai/db'; +import { CreateAutomationDto } from './dto/create-automation.dto'; + +@Injectable() +export class AutomationService { + async create( + organizationId: string, + createAutomationDto: CreateAutomationDto, + ) { + const { taskId } = createAutomationDto; + + // Verify task exists and belongs to organization + const task = await db.task.findFirst({ + where: { + id: taskId, + organizationId: organizationId, + }, + }); + + if (!task) { + throw new NotFoundException('Task not found'); + } + + // Create the automation + const automation = await db.evidenceAutomation.create({ + data: { + name: `${task.title} - AI Automation`, + taskId: taskId, + organizationId: organizationId, + }, + }); + + return { + success: true, + automation: { + id: automation.id, + name: automation.name, + }, + }; + } +} diff --git a/apps/api/src/automation/dto/automation-error-responses.dto.ts b/apps/api/src/automation/dto/automation-error-responses.dto.ts new file mode 100644 index 000000000..57c394af9 --- /dev/null +++ b/apps/api/src/automation/dto/automation-error-responses.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class BadRequestResponseDto { + @ApiProperty({ + description: 'Error message', + example: 'Invalid task ID or organization ID', + }) + message: string; +} + +export class UnauthorizedResponseDto { + @ApiProperty({ + description: 'Error message', + example: 'Unauthorized', + }) + message: string; +} + +export class TaskNotFoundResponseDto { + @ApiProperty({ + description: 'Error message', + example: 'Task not found', + }) + message: string; +} diff --git a/apps/api/src/automation/dto/automation-responses.dto.ts b/apps/api/src/automation/dto/automation-responses.dto.ts new file mode 100644 index 000000000..8c5106ea1 --- /dev/null +++ b/apps/api/src/automation/dto/automation-responses.dto.ts @@ -0,0 +1,63 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AutomationResponseDto { + @ApiProperty({ + description: 'Automation ID', + example: 'auto_abc123def456', + }) + id: string; + + @ApiProperty({ + description: 'Automation name', + example: 'Task Name - Evidence Collection', + }) + name: string; + + @ApiProperty({ + description: 'Task ID this automation belongs to', + example: 'tsk_abc123def456', + }) + taskId: string; + + @ApiProperty({ + description: 'Organization ID', + example: 'org_abc123def456', + }) + organizationId: string; + + @ApiProperty({ + description: 'Automation status', + example: 'active', + enum: ['active', 'inactive', 'draft'], + }) + status: string; + + @ApiProperty({ + description: 'Creation timestamp', + example: '2024-01-15T10:30:00Z', + }) + createdAt: Date; + + @ApiProperty({ + description: 'Last update timestamp', + example: '2024-01-15T10:30:00Z', + }) + updatedAt: Date; +} + +export class CreateAutomationResponseDto { + @ApiProperty({ + description: 'Success status', + example: true, + }) + success: boolean; + + @ApiProperty({ + description: 'Created automation details', + type: () => AutomationResponseDto, + }) + automation: { + id: string; + name: string; + }; +} diff --git a/apps/api/src/automation/dto/create-automation.dto.ts b/apps/api/src/automation/dto/create-automation.dto.ts new file mode 100644 index 000000000..3a75723f3 --- /dev/null +++ b/apps/api/src/automation/dto/create-automation.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; + +export class CreateAutomationDto { + @ApiProperty({ + description: 'Task ID', + example: 'tsk_abc123def456', + }) + @IsString() + @IsNotEmpty() + taskId: string; +} diff --git a/apps/api/src/automation/schemas/automation-operations.ts b/apps/api/src/automation/schemas/automation-operations.ts new file mode 100644 index 000000000..f16e94e8d --- /dev/null +++ b/apps/api/src/automation/schemas/automation-operations.ts @@ -0,0 +1,7 @@ +export const AUTOMATION_OPERATIONS = { + createAutomation: { + summary: 'Create a new evidence automation', + description: + 'Create an automation for collecting evidence for a specific task', + }, +}; diff --git a/apps/api/src/automation/schemas/create-automation.responses.ts b/apps/api/src/automation/schemas/create-automation.responses.ts new file mode 100644 index 000000000..5b60ccd75 --- /dev/null +++ b/apps/api/src/automation/schemas/create-automation.responses.ts @@ -0,0 +1,71 @@ +export const CREATE_AUTOMATION_RESPONSES = { + 201: { + status: 201, + description: 'Automation created successfully', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + success: { type: 'boolean', example: true }, + automation: { + type: 'object', + properties: { + id: { type: 'string', example: 'auto_abc123def456' }, + name: { + type: 'string', + example: 'Task Name - Evidence Collection', + }, + }, + }, + }, + }, + }, + }, + }, + 400: { + status: 400, + description: 'Bad request - Invalid task ID or organization ID', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { + type: 'string', + example: 'Invalid task ID or organization ID', + }, + }, + }, + }, + }, + }, + 401: { + status: 401, + description: 'Unauthorized - Invalid authentication', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string', example: 'Unauthorized' }, + }, + }, + }, + }, + }, + 404: { + status: 404, + description: 'Task not found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string', example: 'Task not found' }, + }, + }, + }, + }, + }, +}; diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/actions/task-automation-actions.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/task-automation-actions.ts similarity index 99% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/actions/task-automation-actions.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/task-automation-actions.ts index 7af234d9c..ef8e14cce 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/actions/task-automation-actions.ts +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/actions/task-automation-actions.ts @@ -182,7 +182,7 @@ export async function listAutomationScripts(orgId: string) { export async function executeAutomationScript(data: { orgId: string; taskId: string; - sandboxId?: string; + automationId: string; }) { try { const result = await callEnterpriseApi('/api/tasks-automations/trigger/execute', { diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/automation-layout-wrapper.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/automation-layout-wrapper.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/automation-layout-wrapper.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/automation-layout-wrapper.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/chat.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx similarity index 92% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/chat.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx index 8e4849449..fb3e55f31 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/chat.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx @@ -32,6 +32,7 @@ interface Props { orgId: string; taskId: string; taskName?: string; + automationId: string; } interface Example { @@ -68,7 +69,7 @@ const AUTOMATION_EXAMPLES: Example[] = [ }, ]; -export function Chat({ className, orgId, taskId, taskName }: Props) { +export function Chat({ className, orgId, taskId, taskName, automationId }: Props) { const [input, setInput] = useState(''); const { chat } = useSharedChatContext(); const { messages, sendMessage, status } = useChat({ chat }); @@ -89,12 +90,20 @@ export function Chat({ className, orgId, taskId, taskName }: Props) { if (text.trim()) { sendMessage( { text }, - { body: { modelId: 'openai/gpt-5-mini', reasoningEffort: 'medium', orgId, taskId } }, + { + body: { + modelId: 'openai/gpt-5-mini', + reasoningEffort: 'medium', + orgId, + taskId, + automationId, + }, + }, ); setInput(''); } }, - [sendMessage, setInput, orgId, taskId], + [sendMessage, setInput, orgId, taskId, automationId], ); const handleSecretAdded = useCallback( @@ -180,14 +189,9 @@ export function Chat({ className, orgId, taskId, taskName }: Props) { - - - Integration Builder - - + + Integration Builder + @@ -208,7 +212,7 @@ export function Chat({ className, orgId, taskId, taskName }: Props) {
{/* Top Section - Fixed Position */} -
+

What evidence do you want to collect?

@@ -223,10 +227,8 @@ export function Chat({ className, orgId, taskId, taskName }: Props) {
{/* Examples Section */} -
-

- Get started with examples -

+
+

Get started with examples

{/* All Examples Grid */}
diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/AutomationPageClient.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationPageClient.tsx similarity index 81% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/AutomationPageClient.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationPageClient.tsx index 4f32f535c..645c1647e 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/AutomationPageClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationPageClient.tsx @@ -11,10 +11,11 @@ import { WorkflowVisualizerSimple as WorkflowVisualizer } from './workflow/workf interface Props { orgId: string; taskId: string; + automationId: string; taskName: string; } -export function AutomationPageClient({ orgId, taskId, taskName }: Props) { +export function AutomationPageClient({ orgId, taskId, automationId, taskName }: Props) { const { scriptUrl } = useTaskAutomationStore(); const { chat } = useSharedChatContext(); const { messages } = useChat({ chat }); @@ -31,7 +32,13 @@ export function AutomationPageClient({ orgId, taskId, taskName }: Props) { {/* Mobile layout tabs taking the whole space*/}
- + @@ -45,7 +52,13 @@ export function AutomationPageClient({ orgId, taskId, taskName }: Props) { scriptUrl || hasMessages ? 'w-1/2' : 'w-full' }`} > - +
{/* Workflow panel - slides in from right */} diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ai-elements/conversation.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ai-elements/conversation.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ai-elements/conversation.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ai-elements/conversation.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ai-elements/loader.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ai-elements/loader.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ai-elements/loader.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ai-elements/loader.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/index.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/index.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/index.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/index.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/prompt-info.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/prompt-info.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/prompt-info.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/prompt-info.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/prompt-secret.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/prompt-secret.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/prompt-secret.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/prompt-secret.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/reasoning.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/reasoning.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/reasoning.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/reasoning.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/report-errors.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/report-errors.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/report-errors.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/report-errors.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/research-activity.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/research-activity.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/research-activity.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/research-activity.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/spinner.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/spinner.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/spinner.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/spinner.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/text.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/text.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-part/text.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-part/text.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-spinner.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-spinner.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message-spinner.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message-spinner.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/message.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/message.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/tool-header.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/tool-header.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/tool-header.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/tool-header.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/tool-message.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/tool-message.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/tool-message.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/tool-message.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/types.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/types.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/chat/types.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/chat/types.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/icons/github.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/icons/github.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/icons/github.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/icons/github.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/icons/vercel-dashed.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/icons/vercel-dashed.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/icons/vercel-dashed.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/icons/vercel-dashed.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/layout/panels.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/layout/panels.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/layout/panels.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/layout/panels.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/layout/sizing.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/layout/sizing.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/layout/sizing.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/layout/sizing.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/markdown-renderer/markdown-renderer.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/markdown-renderer/markdown-renderer.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/markdown-renderer/markdown-renderer.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/markdown-renderer/markdown-renderer.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/model-selector/model-selector.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/model-selector/model-selector.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/model-selector/model-selector.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/model-selector/model-selector.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/model-selector/use-available-models.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/model-selector/use-available-models.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/model-selector/use-available-models.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/model-selector/use-available-models.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/panels/panels.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/panels/panels.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/panels/panels.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/panels/panels.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/preview/preview.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/preview/preview.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/preview/preview.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/preview/preview.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/tabs/index.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/tabs/index.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/tabs/index.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/tabs/index.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/tabs/tab-content.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/tabs/tab-content.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/tabs/tab-content.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/tabs/tab-content.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/tabs/tab-item.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/tabs/tab-item.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/tabs/tab-item.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/tabs/tab-item.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/tabs/use-tab-state.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/tabs/use-tab-state.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/tabs/use-tab-state.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/tabs/use-tab-state.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/badge.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/badge.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/badge.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/badge.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/button.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/button.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/button.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/button.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/checkbox.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/checkbox.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/checkbox.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/checkbox.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/dialog.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/dialog.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/dialog.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/dialog.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/input.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/input.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/input.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/input.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/label.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/label.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/label.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/label.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/popover.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/popover.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/popover.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/popover.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/scroll-area.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/scroll-area.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/scroll-area.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/scroll-area.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/select.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/select.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/select.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/select.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/sonner.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/sonner.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/sonner.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/sonner.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/textarea.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/textarea.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/ui/textarea.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/ui/textarea.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/CodeViewer.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/CodeViewer.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/CodeViewer.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/CodeViewer.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/ConfettiEffect.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/ConfettiEffect.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/ConfettiEffect.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/ConfettiEffect.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/EmptyState.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/EmptyState.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/EmptyState.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/EmptyState.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/TestDialog.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/TestDialog.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/TestDialog.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/TestDialog.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/TestResultsPanel.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/TestResultsPanel.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/TestResultsPanel.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/TestResultsPanel.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/UnifiedWorkflowCard.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/UnifiedWorkflowCard.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/UnifiedWorkflowCard.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/UnifiedWorkflowCard.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/ViewModeSwitch.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/ViewModeSwitch.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/ViewModeSwitch.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/ViewModeSwitch.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/WorkflowSkeleton.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/WorkflowSkeleton.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/WorkflowSkeleton.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/WorkflowSkeleton.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/WorkflowStepCard.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/WorkflowStepCard.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/WorkflowStepCard.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/WorkflowStepCard.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/index.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/index.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/components/index.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/components/index.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/types.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/types.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/types.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/types.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/workflow-loading.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/workflow-loading.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/workflow-loading.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/workflow-loading.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/workflow-visualizer-simple.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/workflow-visualizer-simple.tsx similarity index 96% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/workflow-visualizer-simple.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/workflow-visualizer-simple.tsx index 4c9fccb5d..ef3ed5fc3 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/components/workflow/workflow-visualizer-simple.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/workflow/workflow-visualizer-simple.tsx @@ -31,7 +31,11 @@ interface Props { export function WorkflowVisualizerSimple({ className }: Props) { const { scriptGenerated, viewMode, setViewMode, setScriptUrl } = useTaskAutomationStore(); - const { orgId, taskId } = useParams<{ orgId: string; taskId: string }>(); + const { orgId, taskId, automationId } = useParams<{ + orgId: string; + taskId: string; + automationId: string; + }>(); const { chat } = useSharedChatContext(); const { sendMessage } = useChat({ chat }); @@ -42,7 +46,8 @@ export function WorkflowVisualizerSimple({ className }: Props) { } = useTaskAutomationScript({ orgId: orgId, taskId: taskId, - enabled: !!orgId && !!taskId, + automationId: automationId, + enabled: !!orgId && !!taskId && !!automationId, }); useEffect(() => { @@ -69,7 +74,7 @@ export function WorkflowVisualizerSimple({ className }: Props) { result: executionResult, error: executionError, reset: resetExecution, - } = useTaskAutomationExecution({ orgId: orgId, taskId: taskId }); + } = useTaskAutomationExecution(); const { steps, isAnalyzing, integrationsUsed, title } = useTaskAutomationWorkflow({ scriptContent: script?.content, diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/hooks/index.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/index.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/hooks/index.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/index.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/hooks/use-task-automation-execution.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/use-task-automation-execution.ts similarity index 93% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/hooks/use-task-automation-execution.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/use-task-automation-execution.ts index abb5acabc..9df969afb 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/hooks/use-task-automation-execution.ts +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/use-task-automation-execution.ts @@ -18,6 +18,7 @@ * ``` */ +import { useParams } from 'next/navigation'; import { useCallback, useEffect, useRef, useState } from 'react'; import { taskAutomationApi } from '../lib/task-automation-api'; import type { @@ -26,11 +27,14 @@ import type { } from '../lib/types'; export function useTaskAutomationExecution({ - orgId, - taskId, onSuccess, onError, -}: UseTaskAutomationExecutionOptions) { +}: UseTaskAutomationExecutionOptions = {}) { + const { orgId, taskId, automationId } = useParams<{ + orgId: string; + taskId: string; + automationId: string; + }>(); const [runId, setRunId] = useState(null); const [isExecuting, setIsExecuting] = useState(false); const [result, setResult] = useState(null); @@ -105,7 +109,11 @@ export function useTaskAutomationExecution({ setRunId(null); try { - const response = await taskAutomationApi.execution.executeScript({ orgId, taskId }); + const response = await taskAutomationApi.execution.executeScript({ + orgId, + taskId, + automationId, + }); // The API now returns a run ID that we can monitor if ( diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/hooks/use-task-automation-script.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/use-task-automation-script.ts similarity index 93% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/hooks/use-task-automation-script.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/use-task-automation-script.ts index e75f6bc3b..b3e7949b2 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/hooks/use-task-automation-script.ts +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/use-task-automation-script.ts @@ -20,9 +20,10 @@ import { taskAutomationApi } from '../lib/task-automation-api'; export function useTaskAutomationScript({ orgId, taskId, + automationId, enabled = true, }: UseTaskAutomationScriptOptions) { - const scriptKey = `${orgId}/${taskId}.automation.js`; + const scriptKey = `${orgId}/${taskId}/${automationId}.automation.js`; const { data, error, isLoading, mutate } = useSWR( enabled ? ['task-automation-script', scriptKey] : null, diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/hooks/use-task-automation-workflow.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/use-task-automation-workflow.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/hooks/use-task-automation-workflow.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/use-task-automation-workflow.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/layout.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/layout.tsx similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/layout.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/layout.tsx diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/chat-context.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/chat-context.tsx similarity index 86% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/chat-context.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/chat-context.tsx index 72ec20f7e..323873b9a 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/chat-context.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/chat-context.tsx @@ -2,6 +2,7 @@ import { Chat } from '@ai-sdk/react'; import { DataUIPart, DefaultChatTransport } from 'ai'; +import { useParams } from 'next/navigation'; import { createContext, useContext, useMemo, useRef, type ReactNode } from 'react'; import { toast } from 'sonner'; import { mutate } from 'swr'; @@ -19,6 +20,11 @@ export function ChatProvider({ children }: { children: ReactNode }) { const mapDataToState = useTaskAutomationDataMapper(); const mapDataToStateRef = useRef(mapDataToState); mapDataToStateRef.current = mapDataToState; + const { orgId, taskId, automationId } = useParams<{ + orgId: string; + taskId: string; + automationId: string; + }>(); const baseUrl = process.env.NEXT_PUBLIC_ENTERPRISE_API_URL; const url = `${baseUrl}/api/tasks-automations/chat`; @@ -28,6 +34,11 @@ export function ChatProvider({ children }: { children: ReactNode }) { new Chat({ transport: new DefaultChatTransport({ api: url, + body: { + orgId, + taskId, + automationId, + }, }), onToolCall: () => mutate(`/api/auth/info`), onData: (data: DataUIPart) => mapDataToStateRef.current(data), diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/deferred.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/deferred.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/deferred.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/deferred.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/index.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/index.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/index.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/index.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/is-relative-url.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/is-relative-url.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/is-relative-url.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/is-relative-url.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/task-automation-api.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/task-automation-api.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/task-automation-api.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/task-automation-api.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/task-automation-store.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/task-automation-store.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/task-automation-store.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/task-automation-store.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/types/data-parts.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/types/data-parts.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/types/data-parts.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/types/data-parts.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/types/index.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/types/index.ts similarity index 98% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/types/index.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/types/index.ts index eec4fea7e..2232dff2c 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/types/index.ts +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/types/index.ts @@ -104,6 +104,7 @@ export interface TaskAutomationUploadResponse { export interface TaskAutomationExecuteRequest { orgId: string; taskId: string; + automationId: string; } // ============================================================================ @@ -129,6 +130,7 @@ export interface TaskAutomationStoreState { export interface UseTaskAutomationScriptOptions { orgId: string; taskId: string; + automationId: string; enabled?: boolean; } @@ -138,8 +140,6 @@ export interface UseTaskAutomationScriptsListOptions { } export interface UseTaskAutomationExecutionOptions { - orgId: string; - taskId: string; onSuccess?: (result: TaskAutomationExecutionResult) => void; onError?: (error: Error) => void; } diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/types/metadata.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/types/metadata.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/lib/types/metadata.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/lib/types/metadata.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/page.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/page.tsx similarity index 67% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/page.tsx rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/page.tsx index 4d76078eb..c12e79c8c 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/page.tsx @@ -6,9 +6,9 @@ import { AutomationPageClient } from './components/AutomationPageClient'; export default async function Page({ params, }: { - params: Promise<{ taskId: string; orgId: string }>; + params: Promise<{ taskId: string; orgId: string; automationId: string }>; }) { - const { taskId, orgId } = await params; + const { taskId, orgId, automationId } = await params; const task = await db.task.findUnique({ where: { @@ -26,7 +26,12 @@ export default async function Page({ return (
- +
); diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/exa-search.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/exa-search.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/exa-search.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/exa-search.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/firecrawl.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/firecrawl.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/firecrawl.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/firecrawl.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/gateway.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/gateway.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/gateway.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/gateway.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/generate-files/deferred.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/generate-files/deferred.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/generate-files/deferred.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/generate-files/deferred.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/generate-files/get-contents.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/generate-files/get-contents.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/generate-files/get-contents.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/generate-files/get-contents.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/prompt-for-info.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/prompt-for-info.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/prompt-for-info.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/prompt-for-info.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/prompt-for-secret.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/prompt-for-secret.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/prompt-for-secret.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/prompt-for-secret.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/store-to-s3.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/store-to-s3.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/store-to-s3.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/store-to-s3.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/task-automation-tools.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/task-automation-tools.ts similarity index 100% rename from apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/tools/task-automation-tools.ts rename to apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/task-automation-tools.ts diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/SingleTask.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/SingleTask.tsx index 2fe387777..0da478c5b 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/SingleTask.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/SingleTask.tsx @@ -11,7 +11,14 @@ import { DialogHeader, DialogTitle, } from '@comp/ui/dialog'; -import { CommentEntityType, type Control, type Member, type Task, type User } from '@db'; +import { + CommentEntityType, + EvidenceAutomation, + type Control, + type Member, + type Task, + type User, +} from '@db'; import { RefreshCw, Trash2 } from 'lucide-react'; import { useAction } from 'next-safe-action/hooks'; import { useParams } from 'next/navigation'; @@ -19,6 +26,7 @@ import { useMemo, useState } from 'react'; import { toast } from 'sonner'; import { Comments } from '../../../../../../components/comments/Comments'; import { updateTask } from '../../actions/updateTask'; +import { TaskAutomations } from './TaskAutomations'; import { TaskDeleteDialog } from './TaskDeleteDialog'; import { TaskMainContent } from './TaskMainContent'; import { TaskPropertiesSidebar } from './TaskPropertiesSidebar'; @@ -26,9 +34,10 @@ import { TaskPropertiesSidebar } from './TaskPropertiesSidebar'; interface SingleTaskProps { task: Task & { fileUrls?: string[]; controls?: Control[] }; members?: (Member & { user: User })[]; + automations: EvidenceAutomation[]; } -export function SingleTask({ task, members }: SingleTaskProps) { +export function SingleTask({ task, members, automations }: SingleTaskProps) { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [isRegenerateConfirmOpen, setRegenerateConfirmOpen] = useState(false); const params = useParams<{ orgId: string }>(); @@ -108,7 +117,7 @@ export function SingleTask({ task, members }: SingleTaskProps) {
{/* Right Column - Properties (starts at top) */} -
+
@@ -144,6 +153,11 @@ export function SingleTask({ task, members }: SingleTaskProps) {
+ + {/* Automations section */} + + +
diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/TaskAutomations.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/TaskAutomations.tsx new file mode 100644 index 000000000..6cf5280fc --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/TaskAutomations.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { api } from '@/lib/api-client'; +import { Button } from '@comp/ui/button'; +import { CardContent, CardHeader, CardTitle } from '@comp/ui/card'; +import { EvidenceAutomation } from '@db'; +import { ArrowRight, Loader2, Plus, Zap } from 'lucide-react'; +import Link from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +export const TaskAutomations = ({ automations }: { automations: EvidenceAutomation[] }) => { + const { orgId, taskId } = useParams<{ orgId: string; taskId: string }>(); + const router = useRouter(); + const [isCreating, setIsCreating] = useState(false); + + const handleCreateAutomation = async () => { + setIsCreating(true); + + try { + const response = await api.post<{ + success: boolean; + automation: { + id: string; + name: string; + }; + }>('/v1/automations', { taskId }, orgId); + + if (response.error) { + throw new Error(response.error); + } + + if (!response.data?.success) { + throw new Error('Failed to create automation'); + } + + toast.success('Automation created successfully!'); + + // Keep loading state during redirect + await router.push(`/${orgId}/tasks/${taskId}/automation/${response.data.automation.id}`); + + // Don't set loading to false here - let the new page handle it + } catch (error) { + console.error('Failed to create automation:', error); + toast.error(error instanceof Error ? error.message : 'Failed to create automation'); + setIsCreating(false); // Only stop loading on error + } + }; + + return ( + <> + + + Automated Evidence + + + + + {automations.length === 0 ? ( +
+
+ +
+
+

No automations yet

+

+ Create an AI automation to collect evidence for this task +

+
+ +
+ ) : ( +
+ {automations.map((automation) => ( +
+
+

{automation.name}

+
+ +
+ ))} + + +
+ )} +
+ + ); +}; diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/page.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/page.tsx index a9607c766..45307a750 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/page.tsx @@ -22,7 +22,9 @@ export default async function TaskPage({ redirect(`/${orgId}/tasks`); } - return ; + const automations = await getAutomations(orgId, session); + + return ; } const getTask = async (taskId: string, session: Session) => { @@ -69,3 +71,19 @@ const getMembers = async (orgId: string, session: Session) => { }); return members; }; + +const getAutomations = async (orgId: string, session: Session) => { + const activeOrgId = orgId ?? session?.session.activeOrganizationId; + if (!activeOrgId) { + console.warn('Could not determine active organization ID in getAutomations'); + return []; + } + + const automations = await db.evidenceAutomation.findMany({ + where: { + organizationId: activeOrgId, + }, + }); + + return automations; +}; diff --git a/apps/app/src/lib/api-client.ts b/apps/app/src/lib/api-client.ts index 17fca5c37..0a163b889 100644 --- a/apps/app/src/lib/api-client.ts +++ b/apps/app/src/lib/api-client.ts @@ -51,9 +51,6 @@ export class ApiClient { if (token) { headers['Authorization'] = `Bearer ${token}`; - console.log('🎯 Using fresh JWT token for API authentication'); - } else { - console.log('⚠️ No JWT token available for API authentication'); } } catch (error) { console.error('❌ Error getting JWT token for API call:', error); diff --git a/packages/db/prisma/migrations/20251006151959_add_automation_schema/migration.sql b/packages/db/prisma/migrations/20251006151959_add_automation_schema/migration.sql new file mode 100644 index 000000000..eb7ec3748 --- /dev/null +++ b/packages/db/prisma/migrations/20251006151959_add_automation_schema/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "public"."EvidenceAutomation" ( + "id" TEXT NOT NULL DEFAULT generate_prefixed_cuid('aut'::text), + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastRunAt" TIMESTAMP(3), + "organizationId" TEXT NOT NULL, + "taskId" TEXT NOT NULL, + + CONSTRAINT "EvidenceAutomation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "EvidenceAutomation_organizationId_idx" ON "public"."EvidenceAutomation"("organizationId"); + +-- AddForeignKey +ALTER TABLE "public"."EvidenceAutomation" ADD CONSTRAINT "EvidenceAutomation_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "public"."Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."EvidenceAutomation" ADD CONSTRAINT "EvidenceAutomation_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema/automation.prisma b/packages/db/prisma/schema/automation.prisma new file mode 100644 index 000000000..97a86f8cd --- /dev/null +++ b/packages/db/prisma/schema/automation.prisma @@ -0,0 +1,15 @@ +model EvidenceAutomation { + id String @id @default(dbgenerated("generate_prefixed_cuid('aut'::text)")) + name String + description String? + createdAt DateTime @default(now()) + lastRunAt DateTime? + + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + taskId String + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + + @@index([organizationId]) + @@index([taskId]) +} diff --git a/packages/db/prisma/schema/organization.prisma b/packages/db/prisma/schema/organization.prisma index 30a119263..d1db5923b 100644 --- a/packages/db/prisma/schema/organization.prisma +++ b/packages/db/prisma/schema/organization.prisma @@ -31,6 +31,7 @@ model Organization { trust Trust[] context Context[] secrets Secret[] + automations EvidenceAutomation[] @@index([slug]) } diff --git a/packages/db/prisma/schema/task.prisma b/packages/db/prisma/schema/task.prisma index ef9e511bf..e633e747e 100644 --- a/packages/db/prisma/schema/task.prisma +++ b/packages/db/prisma/schema/task.prisma @@ -15,15 +15,16 @@ model Task { reviewDate DateTime? // Relationships - controls Control[] - vendors Vendor[] - risks Risk[] - assigneeId String? - assignee Member? @relation(fields: [assigneeId], references: [id]) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - taskTemplateId String? - taskTemplate FrameworkEditorTaskTemplate? @relation(fields: [taskTemplateId], references: [id]) + assigneeId String? + assignee Member? @relation(fields: [assigneeId], references: [id]) + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + taskTemplateId String? + taskTemplate FrameworkEditorTaskTemplate? @relation(fields: [taskTemplateId], references: [id]) + controls Control[] + vendors Vendor[] + risks Risk[] + evidenceAutomations EvidenceAutomation[] } enum TaskStatus { diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index e2476a8c1..ecf3f6e02 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -1,6 +1,121 @@ { "openapi": "3.0.0", "paths": { + "/v1/automations": { + "post": { + "description": "Create an automation for collecting evidence for a specific task", + "operationId": "AutomationController_createAutomation_v1", + "parameters": [ + { + "name": "X-Organization-Id", + "in": "header", + "description": "Organization ID (required for session auth, optional for API key auth)", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAutomationDto" + } + } + } + }, + "responses": { + "201": { + "description": "Automation created successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "automation": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "auto_abc123def456" + }, + "name": { + "type": "string", + "example": "Task Name - Evidence Collection" + } + } + } + } + } + } + } + }, + "400": { + "description": "Bad request - Invalid task ID or organization ID", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Invalid task ID or organization ID" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Invalid authentication", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Unauthorized" + } + } + } + } + } + }, + "404": { + "description": "Task not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Task not found" + } + } + } + } + } + } + }, + "security": [ + { + "apikey": [] + } + ], + "summary": "Create a new evidence automation", + "tags": [ + "Automations" + ] + } + }, "/v1/organization": { "get": { "description": "Returns detailed information about the authenticated organization. Supports both API key authentication (X-API-Key header) and session authentication (cookies + X-Organization-Id header).", @@ -6286,6 +6401,19 @@ } }, "schemas": { + "CreateAutomationDto": { + "type": "object", + "properties": { + "taskId": { + "type": "string", + "description": "Task ID", + "example": "tsk_abc123def456" + } + }, + "required": [ + "taskId" + ] + }, "UserResponseDto": { "type": "object", "properties": { From 1c1e9e637aadefd3dd4140493ba91de45dc1cb02 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Mon, 6 Oct 2025 16:41:35 -0400 Subject: [PATCH 03/17] chore(automation): add update and delete functionality for task automations --- apps/api/src/app.module.ts | 2 - .../src/automation/automation.controller.ts | 29 +- apps/api/src/automation/automation.service.ts | 36 ++ .../automation/dto/update-automation.dto.ts | 22 + .../schemas/automation-operations.ts | 4 + .../schemas/update-automation.responses.ts | 69 +++ .../automations/automations.controller.ts | 182 ++++++ .../tasks/automations/automations.module.ts | 13 + .../tasks/automations/automations.service.ts | 139 +++++ .../dto/automation-error-responses.dto.ts | 25 + .../dto/automation-responses.dto.ts | 63 ++ .../automations/dto/create-automation.dto.ts | 12 + .../automations/dto/update-automation.dto.ts | 22 + .../schemas/automation-operations.ts | 11 + .../schemas/create-automation.responses.ts | 71 +++ .../schemas/update-automation.responses.ts | 69 +++ apps/api/src/tasks/tasks.controller.ts | 1 + apps/api/src/tasks/tasks.module.ts | 3 +- .../automation/[automationId]/chat.tsx | 50 +- .../components/AutomationSettingsDialogs.tsx | 240 ++++++++ .../workflow/workflow-visualizer-simple.tsx | 4 +- .../automation/[automationId]/hooks/index.ts | 2 +- ...flow.ts => use-task-automation-analyze.ts} | 30 +- .../hooks/use-task-automation.ts | 66 +++ .../[automationId]/lib/types/index.ts | 20 +- .../tasks/[taskId]/components/SingleTask.tsx | 22 +- .../[taskId]/components/TaskAutomations.tsx | 35 +- .../components/TaskPropertiesSidebar.tsx | 25 - .../[taskId]/hooks/use-task-automations.ts | 58 ++ .../app/(app)/[orgId]/tasks/[taskId]/page.tsx | 5 +- apps/app/src/lib/api-client.ts | 18 + packages/docs/openapi.json | 557 ++++++++++++++---- 32 files changed, 1694 insertions(+), 211 deletions(-) create mode 100644 apps/api/src/automation/dto/update-automation.dto.ts create mode 100644 apps/api/src/automation/schemas/update-automation.responses.ts create mode 100644 apps/api/src/tasks/automations/automations.controller.ts create mode 100644 apps/api/src/tasks/automations/automations.module.ts create mode 100644 apps/api/src/tasks/automations/automations.service.ts create mode 100644 apps/api/src/tasks/automations/dto/automation-error-responses.dto.ts create mode 100644 apps/api/src/tasks/automations/dto/automation-responses.dto.ts create mode 100644 apps/api/src/tasks/automations/dto/create-automation.dto.ts create mode 100644 apps/api/src/tasks/automations/dto/update-automation.dto.ts create mode 100644 apps/api/src/tasks/automations/schemas/automation-operations.ts create mode 100644 apps/api/src/tasks/automations/schemas/create-automation.responses.ts create mode 100644 apps/api/src/tasks/automations/schemas/update-automation.responses.ts create mode 100644 apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationSettingsDialogs.tsx rename apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/{use-task-automation-workflow.ts => use-task-automation-analyze.ts} (79%) create mode 100644 apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/hooks/use-task-automation.ts create mode 100644 apps/app/src/app/(app)/[orgId]/tasks/[taskId]/hooks/use-task-automations.ts diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 21a48835b..7280e9c5a 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -4,7 +4,6 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AttachmentsModule } from './attachments/attachments.module'; import { AuthModule } from './auth/auth.module'; -import { AutomationModule } from './automation/automation.module'; import { CommentsModule } from './comments/comments.module'; import { PeopleModule } from './people/people.module'; import { DevicesModule } from './devices/devices.module'; @@ -29,7 +28,6 @@ import { ContextModule } from './context/context.module'; }, }), AuthModule, - AutomationModule, OrganizationModule, PeopleModule, RisksModule, diff --git a/apps/api/src/automation/automation.controller.ts b/apps/api/src/automation/automation.controller.ts index 4955d8f75..f78304365 100644 --- a/apps/api/src/automation/automation.controller.ts +++ b/apps/api/src/automation/automation.controller.ts @@ -1,4 +1,11 @@ -import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; import { ApiHeader, ApiOperation, @@ -10,8 +17,10 @@ import { OrganizationId } from '../auth/auth-context.decorator'; import { HybridAuthGuard } from '../auth/hybrid-auth.guard'; import { AutomationService } from './automation.service'; import { CreateAutomationDto } from './dto/create-automation.dto'; +import { UpdateAutomationDto } from './dto/update-automation.dto'; import { AUTOMATION_OPERATIONS } from './schemas/automation-operations'; import { CREATE_AUTOMATION_RESPONSES } from './schemas/create-automation.responses'; +import { UPDATE_AUTOMATION_RESPONSES } from './schemas/update-automation.responses'; @ApiTags('Automations') @Controller({ path: 'automations', version: '1' }) @@ -38,4 +47,22 @@ export class AutomationController { ) { return this.automationService.create(organizationId, createAutomationDto); } + + @Patch(':automationId') + @ApiOperation(AUTOMATION_OPERATIONS.updateAutomation) + @ApiResponse(UPDATE_AUTOMATION_RESPONSES[200]) + @ApiResponse(UPDATE_AUTOMATION_RESPONSES[400]) + @ApiResponse(UPDATE_AUTOMATION_RESPONSES[401]) + @ApiResponse(UPDATE_AUTOMATION_RESPONSES[404]) + async updateAutomation( + @OrganizationId() organizationId: string, + @Param('automationId') automationId: string, + @Body() updateAutomationDto: UpdateAutomationDto, + ) { + return this.automationService.update( + organizationId, + automationId, + updateAutomationDto, + ); + } } diff --git a/apps/api/src/automation/automation.service.ts b/apps/api/src/automation/automation.service.ts index fcbc2fc37..aab6711c1 100644 --- a/apps/api/src/automation/automation.service.ts +++ b/apps/api/src/automation/automation.service.ts @@ -1,6 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { db } from '@trycompai/db'; import { CreateAutomationDto } from './dto/create-automation.dto'; +import { UpdateAutomationDto } from './dto/update-automation.dto'; @Injectable() export class AutomationService { @@ -39,4 +40,39 @@ export class AutomationService { }, }; } + + async update( + organizationId: string, + automationId: string, + updateAutomationDto: UpdateAutomationDto, + ) { + // Verify automation exists and belongs to organization + const existingAutomation = await db.evidenceAutomation.findFirst({ + where: { + id: automationId, + organizationId: organizationId, + }, + }); + + if (!existingAutomation) { + throw new NotFoundException('Automation not found'); + } + + // Update the automation + const automation = await db.evidenceAutomation.update({ + where: { + id: automationId, + }, + data: updateAutomationDto, + }); + + return { + success: true, + automation: { + id: automation.id, + name: automation.name, + description: automation.description, + }, + }; + } } diff --git a/apps/api/src/automation/dto/update-automation.dto.ts b/apps/api/src/automation/dto/update-automation.dto.ts new file mode 100644 index 000000000..9890f98d3 --- /dev/null +++ b/apps/api/src/automation/dto/update-automation.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsOptional } from 'class-validator'; + +export class UpdateAutomationDto { + @ApiProperty({ + description: 'Automation name', + example: 'GitHub Security Check - Evidence Collection', + required: false, + }) + @IsString() + @IsOptional() + name?: string; + + @ApiProperty({ + description: 'Automation description', + example: 'Collects evidence about GitHub repository security settings', + required: false, + }) + @IsString() + @IsOptional() + description?: string; +} diff --git a/apps/api/src/automation/schemas/automation-operations.ts b/apps/api/src/automation/schemas/automation-operations.ts index f16e94e8d..b5d9056fb 100644 --- a/apps/api/src/automation/schemas/automation-operations.ts +++ b/apps/api/src/automation/schemas/automation-operations.ts @@ -4,4 +4,8 @@ export const AUTOMATION_OPERATIONS = { description: 'Create an automation for collecting evidence for a specific task', }, + updateAutomation: { + summary: 'Update an existing automation', + description: 'Update the name or description of an existing automation', + }, }; diff --git a/apps/api/src/automation/schemas/update-automation.responses.ts b/apps/api/src/automation/schemas/update-automation.responses.ts new file mode 100644 index 000000000..8a8a5ec71 --- /dev/null +++ b/apps/api/src/automation/schemas/update-automation.responses.ts @@ -0,0 +1,69 @@ +export const UPDATE_AUTOMATION_RESPONSES = { + 200: { + status: 200, + description: 'Automation updated successfully', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + success: { type: 'boolean', example: true }, + automation: { + type: 'object', + properties: { + id: { type: 'string', example: 'auto_abc123def456' }, + name: { type: 'string', example: 'Updated Automation Name' }, + description: { type: 'string', example: 'Updated description' }, + }, + }, + }, + }, + }, + }, + }, + 400: { + status: 400, + description: 'Bad request - Invalid automation ID or data', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { + type: 'string', + example: 'Invalid automation data', + }, + }, + }, + }, + }, + }, + 401: { + status: 401, + description: 'Unauthorized - Invalid authentication', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string', example: 'Unauthorized' }, + }, + }, + }, + }, + }, + 404: { + status: 404, + description: 'Automation not found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string', example: 'Automation not found' }, + }, + }, + }, + }, + }, +}; diff --git a/apps/api/src/tasks/automations/automations.controller.ts b/apps/api/src/tasks/automations/automations.controller.ts new file mode 100644 index 000000000..33e05e9b8 --- /dev/null +++ b/apps/api/src/tasks/automations/automations.controller.ts @@ -0,0 +1,182 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { + ApiHeader, + ApiOperation, + ApiParam, + ApiResponse, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger'; +import { OrganizationId } from '../../auth/auth-context.decorator'; +import { HybridAuthGuard } from '../../auth/hybrid-auth.guard'; +import { TasksService } from '../tasks.service'; +import { AutomationsService } from './automations.service'; +import { CreateAutomationDto } from './dto/create-automation.dto'; +import { UpdateAutomationDto } from './dto/update-automation.dto'; +import { AUTOMATION_OPERATIONS } from './schemas/automation-operations'; +import { CREATE_AUTOMATION_RESPONSES } from './schemas/create-automation.responses'; +import { UPDATE_AUTOMATION_RESPONSES } from './schemas/update-automation.responses'; + +@ApiTags('Task Automations') +@Controller({ path: 'tasks/:taskId/automations', version: '1' }) +@UseGuards(HybridAuthGuard) +@ApiSecurity('apikey') +@ApiHeader({ + name: 'X-Organization-Id', + description: + 'Organization ID (required for session auth, optional for API key auth)', + required: false, +}) +export class AutomationsController { + constructor( + private readonly automationsService: AutomationsService, + private readonly tasksService: TasksService, + ) {} + + @Get() + @ApiOperation({ + summary: 'Get all automations for a task', + description: 'Retrieve all automations for a specific task', + }) + @ApiParam({ + name: 'taskId', + description: 'Unique task identifier', + example: 'tsk_abc123def456', + }) + @ApiResponse({ + status: 200, + description: 'Automations retrieved successfully', + }) + async getTaskAutomations( + @OrganizationId() organizationId: string, + @Param('taskId') taskId: string, + ) { + // Verify task access first + await this.tasksService.verifyTaskAccess(organizationId, taskId); + + return this.automationsService.findByTaskId(organizationId, taskId); + } + + @Get(':automationId') + @ApiOperation({ + summary: 'Get automation details', + description: 'Retrieve details for a specific automation', + }) + @ApiParam({ + name: 'taskId', + description: 'Unique task identifier', + example: 'tsk_abc123def456', + }) + @ApiParam({ + name: 'automationId', + description: 'Unique automation identifier', + example: 'auto_abc123def456', + }) + @ApiResponse({ + status: 200, + description: 'Automation details retrieved successfully', + }) + async getAutomation( + @OrganizationId() organizationId: string, + @Param('taskId') taskId: string, + @Param('automationId') automationId: string, + ) { + // Verify task access first + await this.tasksService.verifyTaskAccess(organizationId, taskId); + + return this.automationsService.findById(organizationId, automationId); + } + + @Post() + @ApiOperation(AUTOMATION_OPERATIONS.createAutomation) + @ApiParam({ + name: 'taskId', + description: 'Unique task identifier', + example: 'tsk_abc123def456', + }) + @ApiResponse(CREATE_AUTOMATION_RESPONSES[201]) + @ApiResponse(CREATE_AUTOMATION_RESPONSES[400]) + @ApiResponse(CREATE_AUTOMATION_RESPONSES[401]) + @ApiResponse(CREATE_AUTOMATION_RESPONSES[404]) + async createAutomation( + @OrganizationId() organizationId: string, + @Param('taskId') taskId: string, + @Body() createAutomationDto: CreateAutomationDto, + ) { + // Verify task access first + await this.tasksService.verifyTaskAccess(organizationId, taskId); + + return this.automationsService.create(organizationId, { taskId }); + } + + @Patch(':automationId') + @ApiOperation(AUTOMATION_OPERATIONS.updateAutomation) + @ApiParam({ + name: 'taskId', + description: 'Unique task identifier', + example: 'tsk_abc123def456', + }) + @ApiParam({ + name: 'automationId', + description: 'Unique automation identifier', + example: 'auto_abc123def456', + }) + @ApiResponse(UPDATE_AUTOMATION_RESPONSES[200]) + @ApiResponse(UPDATE_AUTOMATION_RESPONSES[400]) + @ApiResponse(UPDATE_AUTOMATION_RESPONSES[401]) + @ApiResponse(UPDATE_AUTOMATION_RESPONSES[404]) + async updateAutomation( + @OrganizationId() organizationId: string, + @Param('taskId') taskId: string, + @Param('automationId') automationId: string, + @Body() updateAutomationDto: UpdateAutomationDto, + ) { + // Verify task access first + await this.tasksService.verifyTaskAccess(organizationId, taskId); + + return this.automationsService.update( + organizationId, + automationId, + updateAutomationDto, + ); + } + + @Delete(':automationId') + @ApiOperation({ + summary: 'Delete an automation', + description: 'Delete a specific automation and all its associated data', + }) + @ApiParam({ + name: 'taskId', + description: 'Unique task identifier', + example: 'tsk_abc123def456', + }) + @ApiParam({ + name: 'automationId', + description: 'Unique automation identifier', + example: 'auto_abc123def456', + }) + @ApiResponse({ + status: 200, + description: 'Automation deleted successfully', + }) + async deleteAutomation( + @OrganizationId() organizationId: string, + @Param('taskId') taskId: string, + @Param('automationId') automationId: string, + ) { + // Verify task access first + await this.tasksService.verifyTaskAccess(organizationId, taskId); + + return this.automationsService.delete(organizationId, automationId); + } +} diff --git a/apps/api/src/tasks/automations/automations.module.ts b/apps/api/src/tasks/automations/automations.module.ts new file mode 100644 index 000000000..27127d42f --- /dev/null +++ b/apps/api/src/tasks/automations/automations.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { AuthModule } from '../../auth/auth.module'; +import { TasksService } from '../tasks.service'; +import { AutomationsController } from './automations.controller'; +import { AutomationsService } from './automations.service'; + +@Module({ + imports: [AuthModule], + controllers: [AutomationsController], + providers: [AutomationsService, TasksService], + exports: [AutomationsService], +}) +export class AutomationsModule {} diff --git a/apps/api/src/tasks/automations/automations.service.ts b/apps/api/src/tasks/automations/automations.service.ts new file mode 100644 index 000000000..0bfd8281f --- /dev/null +++ b/apps/api/src/tasks/automations/automations.service.ts @@ -0,0 +1,139 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { db } from '@trycompai/db'; +import { CreateAutomationDto } from './dto/create-automation.dto'; +import { UpdateAutomationDto } from './dto/update-automation.dto'; + +@Injectable() +export class AutomationsService { + async findByTaskId(organizationId: string, taskId: string) { + const automations = await db.evidenceAutomation.findMany({ + where: { + taskId: taskId, + organizationId: organizationId, + }, + orderBy: { + createdAt: 'asc', + }, + }); + + return { + success: true, + automations, + }; + } + + async findById(organizationId: string, automationId: string) { + const automation = await db.evidenceAutomation.findFirst({ + where: { + id: automationId, + organizationId: organizationId, + }, + }); + + if (!automation) { + throw new NotFoundException('Automation not found'); + } + + return { + success: true, + automation, + }; + } + + async create( + organizationId: string, + createAutomationDto: CreateAutomationDto, + ) { + const { taskId } = createAutomationDto; + + // Verify task exists and belongs to organization + const task = await db.task.findFirst({ + where: { + id: taskId, + organizationId: organizationId, + }, + }); + + if (!task) { + throw new NotFoundException('Task not found'); + } + + // Create the automation + const automation = await db.evidenceAutomation.create({ + data: { + name: `${task.title} - Evidence Collection`, + taskId: taskId, + organizationId: organizationId, + }, + }); + + return { + success: true, + automation: { + id: automation.id, + name: automation.name, + }, + }; + } + + async update( + organizationId: string, + automationId: string, + updateAutomationDto: UpdateAutomationDto, + ) { + // Verify automation exists and belongs to organization + const existingAutomation = await db.evidenceAutomation.findFirst({ + where: { + id: automationId, + organizationId: organizationId, + }, + }); + + if (!existingAutomation) { + throw new NotFoundException('Automation not found'); + } + + // Update the automation + const automation = await db.evidenceAutomation.update({ + where: { + id: automationId, + }, + data: updateAutomationDto, + }); + + return { + success: true, + automation: { + id: automation.id, + name: automation.name, + description: automation.description, + }, + }; + } + + async delete(organizationId: string, automationId: string) { + // Verify automation exists and belongs to organization + const existingAutomation = await db.evidenceAutomation.findFirst({ + where: { + id: automationId, + organizationId: organizationId, + }, + }); + + if (!existingAutomation) { + throw new NotFoundException('Automation not found'); + } + + // Delete the automation + await db.evidenceAutomation.delete({ + where: { + id: automationId, + }, + }); + + return { + success: true, + message: 'Automation deleted successfully', + }; + } +} diff --git a/apps/api/src/tasks/automations/dto/automation-error-responses.dto.ts b/apps/api/src/tasks/automations/dto/automation-error-responses.dto.ts new file mode 100644 index 000000000..57c394af9 --- /dev/null +++ b/apps/api/src/tasks/automations/dto/automation-error-responses.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class BadRequestResponseDto { + @ApiProperty({ + description: 'Error message', + example: 'Invalid task ID or organization ID', + }) + message: string; +} + +export class UnauthorizedResponseDto { + @ApiProperty({ + description: 'Error message', + example: 'Unauthorized', + }) + message: string; +} + +export class TaskNotFoundResponseDto { + @ApiProperty({ + description: 'Error message', + example: 'Task not found', + }) + message: string; +} diff --git a/apps/api/src/tasks/automations/dto/automation-responses.dto.ts b/apps/api/src/tasks/automations/dto/automation-responses.dto.ts new file mode 100644 index 000000000..8c5106ea1 --- /dev/null +++ b/apps/api/src/tasks/automations/dto/automation-responses.dto.ts @@ -0,0 +1,63 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AutomationResponseDto { + @ApiProperty({ + description: 'Automation ID', + example: 'auto_abc123def456', + }) + id: string; + + @ApiProperty({ + description: 'Automation name', + example: 'Task Name - Evidence Collection', + }) + name: string; + + @ApiProperty({ + description: 'Task ID this automation belongs to', + example: 'tsk_abc123def456', + }) + taskId: string; + + @ApiProperty({ + description: 'Organization ID', + example: 'org_abc123def456', + }) + organizationId: string; + + @ApiProperty({ + description: 'Automation status', + example: 'active', + enum: ['active', 'inactive', 'draft'], + }) + status: string; + + @ApiProperty({ + description: 'Creation timestamp', + example: '2024-01-15T10:30:00Z', + }) + createdAt: Date; + + @ApiProperty({ + description: 'Last update timestamp', + example: '2024-01-15T10:30:00Z', + }) + updatedAt: Date; +} + +export class CreateAutomationResponseDto { + @ApiProperty({ + description: 'Success status', + example: true, + }) + success: boolean; + + @ApiProperty({ + description: 'Created automation details', + type: () => AutomationResponseDto, + }) + automation: { + id: string; + name: string; + }; +} diff --git a/apps/api/src/tasks/automations/dto/create-automation.dto.ts b/apps/api/src/tasks/automations/dto/create-automation.dto.ts new file mode 100644 index 000000000..3a75723f3 --- /dev/null +++ b/apps/api/src/tasks/automations/dto/create-automation.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; + +export class CreateAutomationDto { + @ApiProperty({ + description: 'Task ID', + example: 'tsk_abc123def456', + }) + @IsString() + @IsNotEmpty() + taskId: string; +} diff --git a/apps/api/src/tasks/automations/dto/update-automation.dto.ts b/apps/api/src/tasks/automations/dto/update-automation.dto.ts new file mode 100644 index 000000000..9890f98d3 --- /dev/null +++ b/apps/api/src/tasks/automations/dto/update-automation.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsOptional } from 'class-validator'; + +export class UpdateAutomationDto { + @ApiProperty({ + description: 'Automation name', + example: 'GitHub Security Check - Evidence Collection', + required: false, + }) + @IsString() + @IsOptional() + name?: string; + + @ApiProperty({ + description: 'Automation description', + example: 'Collects evidence about GitHub repository security settings', + required: false, + }) + @IsString() + @IsOptional() + description?: string; +} diff --git a/apps/api/src/tasks/automations/schemas/automation-operations.ts b/apps/api/src/tasks/automations/schemas/automation-operations.ts new file mode 100644 index 000000000..b5d9056fb --- /dev/null +++ b/apps/api/src/tasks/automations/schemas/automation-operations.ts @@ -0,0 +1,11 @@ +export const AUTOMATION_OPERATIONS = { + createAutomation: { + summary: 'Create a new evidence automation', + description: + 'Create an automation for collecting evidence for a specific task', + }, + updateAutomation: { + summary: 'Update an existing automation', + description: 'Update the name or description of an existing automation', + }, +}; diff --git a/apps/api/src/tasks/automations/schemas/create-automation.responses.ts b/apps/api/src/tasks/automations/schemas/create-automation.responses.ts new file mode 100644 index 000000000..5b60ccd75 --- /dev/null +++ b/apps/api/src/tasks/automations/schemas/create-automation.responses.ts @@ -0,0 +1,71 @@ +export const CREATE_AUTOMATION_RESPONSES = { + 201: { + status: 201, + description: 'Automation created successfully', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + success: { type: 'boolean', example: true }, + automation: { + type: 'object', + properties: { + id: { type: 'string', example: 'auto_abc123def456' }, + name: { + type: 'string', + example: 'Task Name - Evidence Collection', + }, + }, + }, + }, + }, + }, + }, + }, + 400: { + status: 400, + description: 'Bad request - Invalid task ID or organization ID', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { + type: 'string', + example: 'Invalid task ID or organization ID', + }, + }, + }, + }, + }, + }, + 401: { + status: 401, + description: 'Unauthorized - Invalid authentication', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string', example: 'Unauthorized' }, + }, + }, + }, + }, + }, + 404: { + status: 404, + description: 'Task not found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string', example: 'Task not found' }, + }, + }, + }, + }, + }, +}; diff --git a/apps/api/src/tasks/automations/schemas/update-automation.responses.ts b/apps/api/src/tasks/automations/schemas/update-automation.responses.ts new file mode 100644 index 000000000..8a8a5ec71 --- /dev/null +++ b/apps/api/src/tasks/automations/schemas/update-automation.responses.ts @@ -0,0 +1,69 @@ +export const UPDATE_AUTOMATION_RESPONSES = { + 200: { + status: 200, + description: 'Automation updated successfully', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + success: { type: 'boolean', example: true }, + automation: { + type: 'object', + properties: { + id: { type: 'string', example: 'auto_abc123def456' }, + name: { type: 'string', example: 'Updated Automation Name' }, + description: { type: 'string', example: 'Updated description' }, + }, + }, + }, + }, + }, + }, + }, + 400: { + status: 400, + description: 'Bad request - Invalid automation ID or data', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { + type: 'string', + example: 'Invalid automation data', + }, + }, + }, + }, + }, + }, + 401: { + status: 401, + description: 'Unauthorized - Invalid authentication', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string', example: 'Unauthorized' }, + }, + }, + }, + }, + }, + 404: { + status: 404, + description: 'Automation not found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string', example: 'Automation not found' }, + }, + }, + }, + }, + }, +}; diff --git a/apps/api/src/tasks/tasks.controller.ts b/apps/api/src/tasks/tasks.controller.ts index d6faee26a..8aab5919e 100644 --- a/apps/api/src/tasks/tasks.controller.ts +++ b/apps/api/src/tasks/tasks.controller.ts @@ -8,6 +8,7 @@ import { HttpCode, HttpStatus, Param, + Patch, Post, UseGuards, } from '@nestjs/common'; diff --git a/apps/api/src/tasks/tasks.module.ts b/apps/api/src/tasks/tasks.module.ts index 34a6604a8..914cb2612 100644 --- a/apps/api/src/tasks/tasks.module.ts +++ b/apps/api/src/tasks/tasks.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { AttachmentsModule } from '../attachments/attachments.module'; import { AuthModule } from '../auth/auth.module'; +import { AutomationsModule } from './automations/automations.module'; import { TasksController } from './tasks.controller'; import { TasksService } from './tasks.service'; @Module({ - imports: [AuthModule, AttachmentsModule], + imports: [AuthModule, AttachmentsModule, AutomationsModule], controllers: [TasksController], providers: [TasksService], exports: [TasksService], diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx index fb3e55f31..a8a1b4e2d 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/chat.tsx @@ -10,7 +10,13 @@ import { BreadcrumbSeparator, } from '@comp/ui/breadcrumb'; import { Card, CardDescription, CardHeader } from '@comp/ui/card'; -import { ChevronRight } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@comp/ui/dropdown-menu'; +import { ChevronRight, CogIcon, Edit, Settings, Trash2 } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -19,10 +25,16 @@ import { ConversationContent, ConversationScrollButton, } from './components/ai-elements/conversation'; +import { + DeleteAutomationDialog, + EditDescriptionDialog, + EditNameDialog, +} from './components/AutomationSettingsDialogs'; import { Message } from './components/chat/message'; import type { ChatUIMessage } from './components/chat/types'; import { PanelHeader } from './components/panels/panels'; import { Input } from './components/ui/input'; +import { useTaskAutomation } from './hooks/use-task-automation'; import { useSharedChatContext } from './lib/chat-context'; import { useTaskAutomationStore } from './lib/task-automation-store'; @@ -75,6 +87,12 @@ export function Chat({ className, orgId, taskId, taskName, automationId }: Props const { messages, sendMessage, status } = useChat({ chat }); const { setChatStatus, scriptUrl } = useTaskAutomationStore(); const inputRef = useRef(null); + const { automation } = useTaskAutomation(); + + // Dialog states + const [editNameOpen, setEditNameOpen] = useState(false); + const [editDescriptionOpen, setEditDescriptionOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const handleExampleClick = useCallback( (prompt: string) => { @@ -190,13 +208,36 @@ export function Chat({ className, orgId, taskId, taskName, automationId }: Props - Integration Builder + {automation?.name || 'Automation'}
+ + + + + + + setEditNameOpen(true)}> + + Edit Name + + setEditDescriptionOpen(true)}> + + Edit Description + + setDeleteDialogOpen(true)} + > + + Delete Automation + + +
@@ -294,6 +335,11 @@ export function Chat({ className, orgId, taskId, taskName, automationId }: Props
)} + + {/* Settings Dialogs */} + + +
); } diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationSettingsDialogs.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationSettingsDialogs.tsx new file mode 100644 index 000000000..f516f4c34 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/AutomationSettingsDialogs.tsx @@ -0,0 +1,240 @@ +'use client'; + +import { api } from '@/lib/api-client'; +import { Button } from '@comp/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@comp/ui/dialog'; +import { Input } from '@comp/ui/input'; +import { Label } from '@comp/ui/label'; +import { Textarea } from '@comp/ui/textarea'; +import { useParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; +import { useTaskAutomation } from '../hooks/use-task-automation'; + +interface EditNameDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +interface EditDescriptionDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +interface DeleteDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function EditNameDialog({ open, onOpenChange }: EditNameDialogProps) { + const { automation, mutate } = useTaskAutomation(); + const { orgId, taskId, automationId } = useParams<{ + orgId: string; + taskId: string; + automationId: string; + }>(); + const [name, setName] = useState(automation?.name || ''); + const [isSaving, setIsSaving] = useState(false); + + // Update local state when automation data changes + useEffect(() => { + setName(automation?.name || ''); + }, [automation?.name]); + + const handleSave = async () => { + if (!name.trim()) { + toast.error('Name cannot be empty'); + return; + } + + setIsSaving(true); + try { + const response = await api.patch( + `/v1/tasks/${taskId}/automations/${automationId}`, + { name: name.trim() }, + orgId, + ); + + if (response.error) { + throw new Error(response.error); + } + + await mutate(); // Refresh automation data + onOpenChange(false); + toast.success('Automation name updated'); + } catch (error) { + toast.error('Failed to update name'); + } finally { + setIsSaving(false); + } + }; + + return ( + + + + Edit Automation Name + + Update the name for this automation. This will help you identify it later. + + + +
+
+ + setName(e.target.value)} + placeholder="Enter automation name" + /> +
+
+ + + + + +
+
+ ); +} + +export function EditDescriptionDialog({ open, onOpenChange }: EditDescriptionDialogProps) { + const { automation, mutate } = useTaskAutomation(); + const { orgId, taskId, automationId } = useParams<{ + orgId: string; + taskId: string; + automationId: string; + }>(); + const [description, setDescription] = useState(automation?.description || ''); + const [isSaving, setIsSaving] = useState(false); + + // Update local state when automation data changes + useEffect(() => { + setDescription(automation?.description || ''); + }, [automation?.description]); + + const handleSave = async () => { + setIsSaving(true); + try { + const response = await api.patch( + `/v1/tasks/${taskId}/automations/${automationId}`, + { description: description.trim() }, + orgId, + ); + + if (response.error) { + throw new Error(response.error); + } + + await mutate(); // Refresh automation data + onOpenChange(false); + toast.success('Automation description updated'); + } catch (error) { + toast.error('Failed to update description'); + } finally { + setIsSaving(false); + } + }; + + return ( + + + + Edit Automation Description + + Add or update the description for this automation to help others understand its purpose. + + + +
+
+ +