diff --git a/apps/rowboat/app/actions/composio.actions.ts b/apps/rowboat/app/actions/composio.actions.ts index daddc930..8af20aaa 100644 --- a/apps/rowboat/app/actions/composio.actions.ts +++ b/apps/rowboat/app/actions/composio.actions.ts @@ -157,6 +157,7 @@ export async function createComposioTriggerDeployment(request: { export async function listComposioTriggerDeployments(request: { projectId: string, cursor?: string, + limit?: number, }) { const user = await authCheck(); @@ -166,6 +167,7 @@ export async function listComposioTriggerDeployments(request: { userId: user.id, projectId: request.projectId, cursor: request.cursor, + limit: request.limit, }); } @@ -191,4 +193,4 @@ export async function fetchComposioTriggerDeployment(request: { deploymentId: st userId: user.id, deploymentId: request.deploymentId, }); -} \ No newline at end of file +} diff --git a/apps/rowboat/app/actions/copilot.actions.ts b/apps/rowboat/app/actions/copilot.actions.ts index de9b1bae..e4d86731 100644 --- a/apps/rowboat/app/actions/copilot.actions.ts +++ b/apps/rowboat/app/actions/copilot.actions.ts @@ -3,6 +3,7 @@ import { CopilotAPIRequest, CopilotChatContext, CopilotMessage, DataSourceSchemaForCopilot, + TriggerSchemaForCopilot, } from "../../src/entities/models/copilot"; import { Workflow} from "../lib/types/workflow_types"; @@ -26,7 +27,8 @@ export async function getCopilotResponseStream( messages: z.infer[], current_workflow_config: z.infer, context: z.infer | null, - dataSources?: z.infer[] + dataSources?: z.infer[], + triggers?: z.infer[] ): Promise<{ streamId: string; } | { billingError: string }> { @@ -42,6 +44,7 @@ export async function getCopilotResponseStream( workflow: current_workflow_config, context, dataSources, + triggers, } }); return { diff --git a/apps/rowboat/app/actions/recurring-job-rules.actions.ts b/apps/rowboat/app/actions/recurring-job-rules.actions.ts index d64264d8..b807aa3b 100644 --- a/apps/rowboat/app/actions/recurring-job-rules.actions.ts +++ b/apps/rowboat/app/actions/recurring-job-rules.actions.ts @@ -6,6 +6,7 @@ import { IListRecurringJobRulesController } from "@/src/interface-adapters/contr import { IFetchRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/fetch-recurring-job-rule.controller"; import { IToggleRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/toggle-recurring-job-rule.controller"; import { IDeleteRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/delete-recurring-job-rule.controller"; +import { IUpdateRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/update-recurring-job-rule.controller"; import { authCheck } from "./auth.actions"; import { z } from "zod"; import { Message } from "@/app/lib/types/types"; @@ -15,6 +16,7 @@ const listRecurringJobRulesController = container.resolve('fetchRecurringJobRuleController'); const toggleRecurringJobRuleController = container.resolve('toggleRecurringJobRuleController'); const deleteRecurringJobRuleController = container.resolve('deleteRecurringJobRuleController'); +const updateRecurringJobRuleController = container.resolve('updateRecurringJobRuleController'); export async function createRecurringJobRule(request: { projectId: string, @@ -89,3 +91,23 @@ export async function deleteRecurringJobRule(request: { ruleId: request.ruleId, }); } + +export async function updateRecurringJobRule(request: { + projectId: string, + ruleId: string, + input: { + messages: z.infer[], + }, + cron: string, +}) { + const user = await authCheck(); + + return await updateRecurringJobRuleController.execute({ + caller: 'user', + userId: user.id, + projectId: request.projectId, + ruleId: request.ruleId, + input: request.input, + cron: request.cron, + }); +} diff --git a/apps/rowboat/app/actions/scheduled-job-rules.actions.ts b/apps/rowboat/app/actions/scheduled-job-rules.actions.ts index 640c4e08..144475a0 100644 --- a/apps/rowboat/app/actions/scheduled-job-rules.actions.ts +++ b/apps/rowboat/app/actions/scheduled-job-rules.actions.ts @@ -5,6 +5,7 @@ import { ICreateScheduledJobRuleController } from "@/src/interface-adapters/cont import { IListScheduledJobRulesController } from "@/src/interface-adapters/controllers/scheduled-job-rules/list-scheduled-job-rules.controller"; import { IFetchScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/fetch-scheduled-job-rule.controller"; import { IDeleteScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/delete-scheduled-job-rule.controller"; +import { IUpdateScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/update-scheduled-job-rule.controller"; import { authCheck } from "./auth.actions"; import { z } from "zod"; import { Message } from "@/app/lib/types/types"; @@ -13,6 +14,7 @@ const createScheduledJobRuleController = container.resolve('listScheduledJobRulesController'); const fetchScheduledJobRuleController = container.resolve('fetchScheduledJobRuleController'); const deleteScheduledJobRuleController = container.resolve('deleteScheduledJobRuleController'); +const updateScheduledJobRuleController = container.resolve('updateScheduledJobRuleController'); export async function createScheduledJobRule(request: { projectId: string, @@ -72,4 +74,24 @@ export async function deleteScheduledJobRule(request: { projectId: request.projectId, ruleId: request.ruleId, }); -} \ No newline at end of file +} + +export async function updateScheduledJobRule(request: { + projectId: string, + ruleId: string, + input: { + messages: z.infer[], + }, + scheduledTime: string, +}) { + const user = await authCheck(); + + return await updateScheduledJobRuleController.execute({ + caller: 'user', + userId: user.id, + projectId: request.projectId, + ruleId: request.ruleId, + input: request.input, + scheduledTime: request.scheduledTime, + }); +} diff --git a/apps/rowboat/app/lib/client_utils.ts b/apps/rowboat/app/lib/client_utils.ts index d47545b2..2ea39915 100644 --- a/apps/rowboat/app/lib/client_utils.ts +++ b/apps/rowboat/app/lib/client_utils.ts @@ -1,4 +1,5 @@ import { WorkflowTool, WorkflowAgent, WorkflowPrompt, WorkflowPipeline } from "./types/workflow_types"; +import { Message } from "./types/types"; import { z } from "zod"; const ZFallbackSchema = z.object({}).passthrough(); @@ -62,6 +63,40 @@ export function validateConfigChanges(configType: string, configChanges: Record< testObject = {}; break; } + case 'one_time_trigger': { + testObject = { + scheduledTime: new Date(0).toISOString(), + input: { + messages: [], + }, + }; + schema = z.object({ + scheduledTime: z.string().min(1), + input: z.object({ + messages: z.array(Message), + }), + }).passthrough(); + break; + } + case 'recurring_trigger': { + testObject = { + cron: '* * * * *', + input: { + messages: [], + }, + }; + schema = z.object({ + cron: z.string().min(1), + input: z.object({ + messages: z.array(Message), + }), + }).passthrough(); + break; + } + case 'external_trigger': { + // External triggers have flexible schemas per provider; do not strip any config. + return { changes: configChanges }; + } default: return { error: `Unknown config type: ${configType}` }; } diff --git a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx index ca765952..f8d4eda3 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx @@ -2,7 +2,7 @@ import { Button } from "@/components/ui/button"; import { Dropdown, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger, Spinner, Tooltip } from "@heroui/react"; import { useRef, useState, createContext, useContext, useCallback, forwardRef, useImperativeHandle, useEffect, Ref } from "react"; -import { CopilotChatContext } from "../../../../src/entities/models/copilot"; +import { CopilotChatContext, TriggerSchemaForCopilot } from "../../../../src/entities/models/copilot"; import { CopilotMessage } from "../../../../src/entities/models/copilot"; import { Workflow } from "@/app/lib/types/workflow_types"; import { DataSource } from "@/src/entities/models/data-source"; @@ -36,6 +36,8 @@ interface AppProps { onMessagesChange?: (messages: z.infer[]) => void; isInitialState?: boolean; dataSources?: z.infer[]; + triggers?: z.infer[]; + onTriggersUpdated?: () => Promise | void; } const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message: string) => void }, AppProps>(function App({ @@ -47,6 +49,8 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message onMessagesChange, isInitialState = false, dataSources, + triggers, + onTriggersUpdated, }, ref) { @@ -85,7 +89,8 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message projectId, workflow: workflowRef.current, context: effectiveContext, - dataSources: dataSources + dataSources: dataSources, + triggers: triggers }); // Store latest start/cancel functions in refs @@ -255,6 +260,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message )} void; handleUserMessage: (message onStatusBarChange={handleStatusBarChange} toolCalling={toolCalling} toolQuery={toolQuery} + triggers={triggers} + onTriggersUpdated={onTriggersUpdated} />
@@ -318,8 +326,10 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void dispatch: (action: WorkflowDispatch) => void; isInitialState?: boolean; dataSources?: z.infer[]; + triggers?: z.infer[]; activePanel?: 'playground' | 'copilot'; onTogglePanel?: () => void; + onTriggersUpdated?: () => Promise | void; }>(({ projectId, workflow, @@ -327,8 +337,10 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void dispatch, isInitialState = false, dataSources, + triggers, activePanel, onTogglePanel, + onTriggersUpdated, }, ref) => { console.log('πŸŽͺ Copilot wrapper component mounted:', { projectId, @@ -414,6 +426,8 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void onMessagesChange={setMessages} isInitialState={isInitialState} dataSources={dataSources} + triggers={triggers} + onTriggersUpdated={onTriggersUpdated} />
diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/TriggerSetupModal.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/TriggerSetupModal.tsx new file mode 100644 index 00000000..db49d714 --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/TriggerSetupModal.tsx @@ -0,0 +1,237 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Modal, ModalBody, ModalContent, ModalHeader } from '@heroui/react'; +import { z } from 'zod'; +import { ZToolkit } from '@/src/application/lib/composio/types'; +import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type'; +import { Project } from '@/src/entities/models/project'; +import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit'; +import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel'; +import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm'; +import { ToolkitAuthModal } from '../../tools/components/ToolkitAuthModal'; +import { fetchProject } from '@/app/actions/project.actions'; +import { createComposioTriggerDeployment } from '@/app/actions/composio.actions'; +import { Button, Spinner } from '@heroui/react'; + +interface TriggerSetupModalProps { + isOpen: boolean; + onClose: () => void; + projectId: string; + initialToolkitSlug?: string | null; + initialTriggerTypeSlug?: string | null; + initialTriggerConfig?: Record | null; + onCreated?: () => void; +} + +type Toolkit = z.infer; +type TriggerType = z.infer; +type ProjectConfig = z.infer; + +export function TriggerSetupModal({ + isOpen, + onClose, + projectId, + initialToolkitSlug = null, + initialTriggerTypeSlug = null, + initialTriggerConfig = null, + onCreated, +}: TriggerSetupModalProps) { + const [selectedToolkit, setSelectedToolkit] = useState(null); + const [selectedTriggerType, setSelectedTriggerType] = useState(null); + const [projectConfig, setProjectConfig] = useState(null); + const [showAuthModal, setShowAuthModal] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [pendingTriggerTypeSlug, setPendingTriggerTypeSlug] = useState(null); + const [initialConfig, setInitialConfig] = useState | undefined>(); + + const loadProjectConfig = useCallback(async () => { + try { + const config = await fetchProject(projectId); + setProjectConfig(config); + } catch (err) { + console.error('Failed to fetch project configuration', err); + } + }, [projectId]); + + const resetState = useCallback(() => { + setSelectedToolkit(null); + setSelectedTriggerType(null); + setShowAuthModal(false); + setError(null); + setPendingTriggerTypeSlug(initialTriggerTypeSlug); + setInitialConfig(initialTriggerConfig ?? undefined); + }, [initialTriggerConfig, initialTriggerTypeSlug]); + + useEffect(() => { + if (!isOpen) { + return; + } + resetState(); + void loadProjectConfig(); + }, [isOpen, loadProjectConfig, resetState]); + + const requiresAuth = useMemo(() => { + if (!selectedToolkit) return false; + return !selectedToolkit.no_auth; + }, [selectedToolkit]); + + const hasActiveConnection = useMemo(() => { + if (!selectedToolkit) return false; + const status = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.status; + return status === 'ACTIVE'; + }, [projectConfig, selectedToolkit]); + + const handleSelectToolkit = useCallback((toolkit: Toolkit) => { + setSelectedToolkit(toolkit); + setSelectedTriggerType(null); + setError(null); + if (!initialToolkitSlug || toolkit.slug === initialToolkitSlug) { + setPendingTriggerTypeSlug(initialTriggerTypeSlug); + } else { + setPendingTriggerTypeSlug(null); + } + }, [initialToolkitSlug, initialTriggerTypeSlug]); + + const handleSelectTriggerType = useCallback((triggerType: TriggerType) => { + setSelectedTriggerType(triggerType); + setError(null); + setPendingTriggerTypeSlug(null); + if (requiresAuth && !hasActiveConnection) { + setShowAuthModal(true); + } + }, [requiresAuth, hasActiveConnection]); + + const handleAuthComplete = useCallback(async () => { + await loadProjectConfig(); + setShowAuthModal(false); + }, [loadProjectConfig]); + + const handleSubmit = useCallback(async (triggerConfig: Record) => { + if (!selectedToolkit || !selectedTriggerType) { + return; + } + + try { + setIsSubmitting(true); + setError(null); + + const connectedAccountId = projectConfig?.composioConnectedAccounts?.[selectedToolkit.slug]?.id; + if (!connectedAccountId) { + setShowAuthModal(true); + throw new Error('Connect this toolkit before creating a trigger.'); + } + + await createComposioTriggerDeployment({ + projectId, + triggerTypeSlug: selectedTriggerType.slug, + connectedAccountId, + triggerConfig, + }); + + onCreated?.(); + onClose(); + } catch (err: any) { + console.error('Failed to create trigger', err); + setError(err?.message || 'Failed to create trigger. Please try again.'); + } finally { + setIsSubmitting(false); + } + }, [onClose, onCreated, projectConfig, projectId, selectedToolkit, selectedTriggerType]); + + const handleClose = useCallback(() => { + if (isSubmitting) { + return; + } + onClose(); + }, [isSubmitting, onClose]); + + return ( + <> + + + +

Set up External Trigger

+

+ Follow the guided flow to authenticate and configure the trigger. +

+
+ + {!selectedToolkit && ( + + )} + + {selectedToolkit && !selectedTriggerType && ( + setSelectedToolkit(null)} + onSelectTriggerType={handleSelectTriggerType} + initialTriggerTypeSlug={pendingTriggerTypeSlug} + /> + )} + + {selectedToolkit && selectedTriggerType && (!requiresAuth || hasActiveConnection) && ( +
+
+ +
+ setSelectedTriggerType(null)} + onSubmit={handleSubmit} + isSubmitting={isSubmitting} + initialConfig={initialConfig} + /> +
+ )} + + {selectedToolkit && selectedTriggerType && requiresAuth && !hasActiveConnection && !showAuthModal && ( +
+ +

+ Waiting for authentication to complete... +

+
+ )} + + {error && ( +
+ {error} +
+ )} +
+
+
+ + {selectedToolkit && ( + setShowAuthModal(false)} + projectId={projectId} + toolkitSlug={selectedToolkit.slug} + onComplete={handleAuthComplete} + /> + )} + + ); +} diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx index 55f950ae..c9c25a8b 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx @@ -29,6 +29,7 @@ export function Action({ onApplied, externallyApplied = false, defaultExpanded = false, + onRequestTriggerSetup, }: { msgIndex: number; actionIndex: number; @@ -39,10 +40,12 @@ export function Action({ onApplied?: () => void; externallyApplied?: boolean; defaultExpanded?: boolean; + onRequestTriggerSetup?: (params: { action: z.infer['content']; msgIndex: number; actionIndex: number }) => void; }) { const { showPreview } = usePreviewModal(); const [expanded, setExpanded] = useState(defaultExpanded); const [appliedChanges, setAppliedChanges] = useState>({}); + const isExternalTriggerCreate = action.config_type === 'external_trigger' && action.action === 'create_new'; if (!action || typeof action !== 'object') { console.warn('Invalid action object:', action); @@ -108,6 +111,10 @@ export function Action({ // Handle applying all changes - delegate to parent const handleApplyAll = () => { + if (isExternalTriggerCreate) { + onRequestTriggerSetup?.({ action, msgIndex, actionIndex }); + return; + } // Mark all fields as applied locally for UI state const appliedKeys = Object.keys(action.config_changes).reduce((acc, key) => { acc[getAppliedChangeKey(msgIndex, actionIndex, key)] = true; @@ -211,7 +218,7 @@ export function Action({ {action.config_type === 'tool' && toolkitLogo ? ( ) : ( - action.config_type === 'agent' ? 'πŸ§‘β€πŸ’Ό' : action.config_type === 'tool' ? 'πŸ› οΈ' : action.config_type === 'pipeline' ? 'βš™οΈ' : action.config_type === 'start_agent' ? '🏁' : action.config_type === 'prompt' ? 'πŸ’¬' : 'πŸ’¬' + action.config_type === 'agent' ? 'πŸ§‘β€πŸ’Ό' : action.config_type === 'tool' ? 'πŸ› οΈ' : action.config_type === 'pipeline' ? 'βš™οΈ' : action.config_type === 'start_agent' ? '🏁' : action.config_type === 'prompt' ? 'πŸ’¬' : action.config_type === 'one_time_trigger' ? '⏰' : action.config_type === 'recurring_trigger' ? 'πŸ”„' : action.config_type === 'external_trigger' ? 'πŸ”—' : 'πŸ’¬' )} @@ -230,9 +237,9 @@ export function Action({ onClick={() => handleApplyAll()} > - {allApplied ? 'Applied' : 'Apply'} + {allApplied ? 'Applied' : isExternalTriggerCreate ? 'Open setup' : 'Apply'} - {action.action !== 'delete' && - ) : hasExistingTriggers ? ( - - - + ) : ( + + + + ) ) : null}
- CREATE RECURRING JOB RULE + {title}
-

- Note: Triggers run only on the published version of your workflow. Publish any changes to make them active. -

+ {description ? ( +

+ {description} +

+ ) : null}
} @@ -262,7 +323,7 @@ export function CreateRecurringJobRuleForm({ isLoading={loading} className="px-6 py-2 whitespace-nowrap" > - {loading ? "Creating..." : "Create Rule"} + {loading ? submittingLabel : submitLabel} @@ -271,3 +332,99 @@ export function CreateRecurringJobRuleForm({ ); } + +export function CreateRecurringJobRuleForm({ + projectId, + onBack, + hasExistingTriggers = true, +}: { + projectId: string; + onBack?: () => void; + hasExistingTriggers?: boolean; +}) { + const handleSubmit = async ({ cron, messages }: FormSubmitPayload) => { + const convertedMessages = convertFormMessagesToMessages(messages); + await createRecurringJobRule({ + projectId, + input: { messages: convertedMessages }, + cron, + }); + }; + + const handleSuccess = onBack ? () => onBack() : undefined; + const backButton: BackButtonConfig | undefined = hasExistingTriggers + ? onBack + ? { label: "Back", onClick: onBack } + : { label: "Back", href: `/projects/${projectId}/manage-triggers?tab=recurring` } + : undefined; + + return ( + + ); +} + +export function EditRecurringJobRuleForm({ + projectId, + rule, + onCancel, + onUpdated, +}: { + projectId: string; + rule: z.infer; + onCancel: () => void; + onUpdated?: (rule: z.infer) => void; +}) { + const initialMessages = useMemo(() => { + return rule.input.messages + .filter((message): message is Extract, { role: "system" | "user" | "assistant" }> => { + return message.role === "system" || message.role === "user" || message.role === "assistant"; + }) + .map((message) => ({ + role: message.role, + content: message.content ?? "", + })); + }, [rule.input.messages]); + + const handleSubmit = async ({ cron, messages }: FormSubmitPayload) => { + const convertedMessages = convertFormMessagesToMessages(messages); + const updatedRule = await updateRecurringJobRule({ + projectId, + ruleId: rule.id, + input: { messages: convertedMessages }, + cron, + }); + return updatedRule; + }; + + const handleSuccess = (result: unknown) => { + if (result && typeof result === 'object' && onUpdated) { + onUpdated(result as z.infer); + } + onCancel(); + }; + + return ( + + ); +} diff --git a/apps/rowboat/app/projects/[projectId]/manage-triggers/components/recurring-job-rule-view.tsx b/apps/rowboat/app/projects/[projectId]/manage-triggers/components/recurring-job-rule-view.tsx index 3b55cc31..a8413479 100644 --- a/apps/rowboat/app/projects/[projectId]/manage-triggers/components/recurring-job-rule-view.tsx +++ b/apps/rowboat/app/projects/[projectId]/manage-triggers/components/recurring-job-rule-view.tsx @@ -5,12 +5,13 @@ import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Panel } from "@/components/common/panel-common"; import { fetchRecurringJobRule, toggleRecurringJobRule, deleteRecurringJobRule } from "@/app/actions/recurring-job-rules.actions"; -import { ArrowLeftIcon, PlayIcon, PauseIcon, ClockIcon, AlertCircleIcon, Trash2Icon } from "lucide-react"; +import { ArrowLeftIcon, PlayIcon, PauseIcon, ClockIcon, AlertCircleIcon, Trash2Icon, PencilIcon } from "lucide-react"; import Link from "next/link"; import { RecurringJobRule } from "@/src/entities/models/recurring-job-rule"; import { Spinner } from "@heroui/react"; import { z } from "zod"; import { JobsList } from "@/app/projects/[projectId]/jobs/components/jobs-list"; +import { EditRecurringJobRuleForm } from "./create-recurring-job-rule-form"; export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string; ruleId: string }) { const router = useRouter(); @@ -19,6 +20,7 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string; const [updating, setUpdating] = useState(false); const [deleting, setDeleting] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [editing, setEditing] = useState(false); const jobsFilters = useMemo(() => ({ recurringJobRuleId: ruleId }), [ruleId]); @@ -145,128 +147,161 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string; } rightActions={
- - + {editing ? ( + + ) : ( + <> + + + + + )}
} >
- {/* Status */} -
-
-
- - Status: {rule.disabled ? 'Disabled' : 'Active'} - -
- {rule.lastError && ( -
- -
- Last Error: {rule.lastError} -
-
- )} -
- - {/* Schedule Information */} -
-

- Schedule Information -

-
-
- - Cron Expression: - - {rule.cron} - -
-
- Human Readable: {formatCronExpression(rule.cron)} -
-
- Next Run: {formatDate(rule.nextRunAt)} -
- {rule.lastProcessedAt && ( -
- Last Processed: {formatDate(rule.lastProcessedAt)} -
- )} -
-
- - {/* Messages */} -
-

- Messages -

-
- {rule.input.messages.map((message, index) => ( -
-
- - {message.role.charAt(0).toUpperCase() + message.role.slice(1)} - -
-
- {message.content} -
-
- ))} -
-
- - {/* Metadata */} -
-

- Metadata -

-
-
Created: {formatDate(rule.createdAt)}
- {rule.updatedAt && ( -
Last Updated: {formatDate(rule.updatedAt)}
- )} -
Rule ID: {rule.id}
-
-
- - {/* Jobs Created by This Rule */} -
-

- Jobs Created by This Rule -

- setEditing(false)} + onUpdated={(updatedRule) => setRule(updatedRule)} /> -
+ ) : ( + <> + {/* Status */} +
+
+
+ + Status: {rule.disabled ? 'Disabled' : 'Active'} + +
+ {rule.lastError && ( +
+ +
+ Last Error: {rule.lastError} +
+
+ )} +
+ + {/* Schedule Information */} +
+

+ Schedule Information +

+
+
+ + Cron Expression: + + {rule.cron} + +
+
+ Human Readable: {formatCronExpression(rule.cron)} +
+
+ Next Run: {formatDate(rule.nextRunAt)} +
+ {rule.lastProcessedAt && ( +
+ Last Processed: {formatDate(rule.lastProcessedAt)} +
+ )} +
+
+ + {/* Messages */} +
+

+ Messages +

+
+ {rule.input.messages.map((message, index) => ( +
+
+ + {message.role.charAt(0).toUpperCase() + message.role.slice(1)} + +
+
+ {message.content} +
+
+ ))} +
+
+ + {/* Metadata */} +
+

+ Metadata +

+
+
Created: {formatDate(rule.createdAt)}
+ {rule.updatedAt && ( +
Last Updated: {formatDate(rule.updatedAt)}
+ )} +
Rule ID: {rule.id}
+
+
+ + {/* Jobs Created by This Rule */} +
+

+ Jobs Created by This Rule +

+ +
+ + )}
diff --git a/apps/rowboat/app/projects/[projectId]/manage-triggers/scheduled/components/create-scheduled-job-rule-form.tsx b/apps/rowboat/app/projects/[projectId]/manage-triggers/scheduled/components/create-scheduled-job-rule-form.tsx index 193da0f6..ceb24d5b 100644 --- a/apps/rowboat/app/projects/[projectId]/manage-triggers/scheduled/components/create-scheduled-job-rule-form.tsx +++ b/apps/rowboat/app/projects/[projectId]/manage-triggers/scheduled/components/create-scheduled-job-rule-form.tsx @@ -1,132 +1,197 @@ 'use client'; -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Panel } from "@/components/common/panel-common"; -import { createScheduledJobRule } from "@/app/actions/scheduled-job-rules.actions"; +import { createScheduledJobRule, updateScheduledJobRule } from "@/app/actions/scheduled-job-rules.actions"; import { ArrowLeftIcon, PlusIcon, TrashIcon } from "lucide-react"; import Link from "next/link"; import { DatePicker } from "@heroui/react"; -import { ZonedDateTime, now, getLocalTimeZone } from "@internationalized/date"; +import { ZonedDateTime, now, getLocalTimeZone, parseAbsoluteToLocal } from "@internationalized/date"; +import { z } from "zod"; +import { Message } from "@/app/lib/types/types"; +import { ScheduledJobRule } from "@/src/entities/models/scheduled-job-rule"; -// Define a simpler message type for the form that only includes the fields we need type FormMessage = { role: "system" | "user" | "assistant"; content: string; }; -export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTriggers = true }: { projectId: string; onBack?: () => void; hasExistingTriggers?: boolean }) { - const router = useRouter(); - const [loading, setLoading] = useState(false); - const [messages, setMessages] = useState([ - { role: "user", content: "" } - ]); - // Set default to 30 minutes from now with timezone info - const getDefaultDateTime = () => { - const localTimeZone = getLocalTimeZone(); - const currentTime = now(localTimeZone); - const thirtyMinutesFromNow = currentTime.add({ minutes: 30 }); - return thirtyMinutesFromNow; - }; +type BackButtonConfig = + | { label: string; onClick: () => void } + | { label: string; href: string }; - const [scheduledDateTime, setScheduledDateTime] = useState(getDefaultDateTime()); +type FormSubmitPayload = { + messages: FormMessage[]; + scheduledDateTime: ZonedDateTime; +}; + +type ScheduledJobRuleFormBaseProps = { + title: string; + description?: string; + submitLabel: string; + submittingLabel: string; + errorMessage: string; + backButton?: BackButtonConfig; + initialMessages?: FormMessage[]; + initialDateTime?: ZonedDateTime | null; + placeholderDateTime: ZonedDateTime; + minDateTime: ZonedDateTime; + onSubmit: (payload: FormSubmitPayload) => Promise; + onSuccess?: (result: unknown) => void; + successHref?: string; +}; + +const createEmptyMessage = (): FormMessage => ({ role: "user", content: "" }); + +const normaliseMessages = (messages?: FormMessage[]): FormMessage[] => { + if (!messages || messages.length === 0) { + return [createEmptyMessage()]; + } + + return messages.map((message) => ({ ...message })); +}; + +const convertFormMessagesToMessages = (messages: FormMessage[]): z.infer[] => { + return messages.map((msg) => { + if (msg.role === "assistant") { + return { + role: msg.role, + content: msg.content, + agentName: null, + responseType: "internal" as const, + timestamp: undefined, + }; + } + + return { + role: msg.role, + content: msg.content, + timestamp: undefined, + }; + }); +}; + +function ScheduledJobRuleFormBase({ + title, + description, + submitLabel, + submittingLabel, + errorMessage, + backButton, + initialMessages, + initialDateTime, + placeholderDateTime, + minDateTime, + onSubmit, + onSuccess, + successHref, +}: ScheduledJobRuleFormBaseProps) { + const router = useRouter(); + const [messages, setMessages] = useState(normaliseMessages(initialMessages)); + const [scheduledDateTime, setScheduledDateTime] = useState(initialDateTime ?? placeholderDateTime); + const [loading, setLoading] = useState(false); + + useEffect(() => { + setMessages(normaliseMessages(initialMessages)); + }, [initialMessages]); + + useEffect(() => { + setScheduledDateTime(initialDateTime ?? placeholderDateTime); + }, [initialDateTime, placeholderDateTime]); const addMessage = () => { - setMessages([...messages, { role: "user", content: "" }]); + setMessages((prev) => [...prev, createEmptyMessage()]); }; const removeMessage = (index: number) => { - if (messages.length > 1) { - setMessages(messages.filter((_, i) => i !== index)); - } + setMessages((prev) => { + if (prev.length <= 1) { + return prev; + } + return prev.filter((_, i) => i !== index); + }); }; const updateMessage = (index: number, field: keyof FormMessage, value: string) => { - const newMessages = [...messages]; - newMessages[index] = { ...newMessages[index], [field]: value }; - setMessages(newMessages); + setMessages((prev) => { + const next = [...prev]; + next[index] = { ...next[index], [field]: value }; + return next; + }); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - - // Validate required fields + if (!scheduledDateTime) { alert("Please select date and time"); return; } - if (messages.some(msg => !msg.content?.trim())) { + if (messages.some((msg) => !msg.content?.trim())) { alert("Please fill in all message content"); return; } setLoading(true); try { - // Convert FormMessage to the expected Message type - const convertedMessages = messages.map(msg => { - if (msg.role === "assistant") { - return { - role: msg.role, - content: msg.content, - agentName: null, - responseType: "internal" as const, - timestamp: undefined - }; - } - return { - role: msg.role, - content: msg.content, - timestamp: undefined - }; + const result = await onSubmit({ + messages, + scheduledDateTime, }); - - // Convert ZonedDateTime to ISO string (already in UTC) - const scheduledTimeString = scheduledDateTime.toDate().toISOString(); - - await createScheduledJobRule({ - projectId, - input: { messages: convertedMessages }, - scheduledTime: scheduledTimeString, - }); - if (onBack) { - onBack(); - } else { - router.push(`/projects/${projectId}/manage-triggers?tab=scheduled`); + + if (onSuccess) { + onSuccess(result); + } else if (successHref) { + router.push(successHref); } } catch (error) { - console.error("Failed to create scheduled job rule:", error); - alert("Failed to create scheduled job rule"); + console.error(errorMessage, error); + alert(errorMessage); } finally { setLoading(false); } }; - - return ( - {hasExistingTriggers && onBack ? ( - - ) : hasExistingTriggers ? ( - - - + ) : ( + + + + ) ) : null}
- CREATE SCHEDULED JOB RULE + {title}
-

- Note: Triggers run only on the published version of your workflow. Publish any changes to make them active. -

+ {description ? ( +

+ {description} +

+ ) : null}
} @@ -142,8 +207,8 @@ export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTrigg - {loading ? "Creating..." : "Create Rule"} + {loading ? submittingLabel : submitLabel}
@@ -223,3 +288,111 @@ export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTrigg ); } + +export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTriggers = true }: { projectId: string; onBack?: () => void; hasExistingTriggers?: boolean }) { + const timeZone = useMemo(() => getLocalTimeZone(), []); + const minDateTime = useMemo(() => now(timeZone), [timeZone]); + const defaultDateTime = useMemo(() => now(timeZone).add({ minutes: 30 }), [timeZone]); + + const handleSubmit = async ({ messages, scheduledDateTime }: FormSubmitPayload) => { + const convertedMessages = convertFormMessagesToMessages(messages); + const scheduledTimeString = scheduledDateTime.toDate().toISOString(); + + await createScheduledJobRule({ + projectId, + input: { messages: convertedMessages }, + scheduledTime: scheduledTimeString, + }); + }; + + const handleSuccess = onBack ? () => onBack() : undefined; + const backButton: BackButtonConfig | undefined = hasExistingTriggers + ? onBack + ? { label: "Back", onClick: onBack } + : { label: "Back", href: `/projects/${projectId}/manage-triggers?tab=scheduled` } + : undefined; + + return ( + + ); +} + +export function EditScheduledJobRuleForm({ + projectId, + rule, + onCancel, + onUpdated, +}: { + projectId: string; + rule: z.infer; + onCancel: () => void; + onUpdated?: (rule: z.infer) => void; +}) { + const timeZone = useMemo(() => getLocalTimeZone(), []); + const initialDateTime = useMemo(() => parseAbsoluteToLocal(rule.nextRunAt), [rule.nextRunAt]); + const nowDateTime = useMemo(() => now(timeZone), [timeZone]); + const minDateTime = useMemo(() => { + return initialDateTime.compare(nowDateTime) < 0 ? initialDateTime : nowDateTime; + }, [initialDateTime, nowDateTime]); + + const initialMessages = useMemo(() => { + return rule.input.messages + .filter((message): message is Extract, { role: "system" | "user" | "assistant" }> => { + return message.role === "system" || message.role === "user" || message.role === "assistant"; + }) + .map((message) => ({ + role: message.role, + content: message.content ?? "", + })); + }, [rule.input.messages]); + + const handleSubmit = async ({ messages, scheduledDateTime }: FormSubmitPayload) => { + const convertedMessages = convertFormMessagesToMessages(messages); + const scheduledTimeString = scheduledDateTime.toDate().toISOString(); + + const updatedRule = await updateScheduledJobRule({ + projectId, + ruleId: rule.id, + input: { messages: convertedMessages }, + scheduledTime: scheduledTimeString, + }); + return updatedRule; + }; + + const handleSuccess = (result: unknown) => { + if (result && typeof result === 'object' && onUpdated) { + onUpdated(result as z.infer); + } + onCancel(); + }; + + return ( + + ); +} diff --git a/apps/rowboat/app/projects/[projectId]/manage-triggers/scheduled/components/scheduled-job-rule-view.tsx b/apps/rowboat/app/projects/[projectId]/manage-triggers/scheduled/components/scheduled-job-rule-view.tsx index 7b44fb46..2bdc8c2f 100644 --- a/apps/rowboat/app/projects/[projectId]/manage-triggers/scheduled/components/scheduled-job-rule-view.tsx +++ b/apps/rowboat/app/projects/[projectId]/manage-triggers/scheduled/components/scheduled-job-rule-view.tsx @@ -9,8 +9,9 @@ import { ScheduledJobRule } from "@/src/entities/models/scheduled-job-rule"; import { z } from "zod"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { ArrowLeftIcon, Trash2Icon } from "lucide-react"; +import { ArrowLeftIcon, Trash2Icon, PencilIcon } from "lucide-react"; import { MessageDisplay } from "@/app/lib/components/message-display"; +import { EditScheduledJobRuleForm } from "./create-scheduled-job-rule-form"; export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string; ruleId: string; }) { const router = useRouter(); @@ -18,6 +19,7 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string; const [loading, setLoading] = useState(true); const [deleting, setDeleting] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [editing, setEditing] = useState(false); useEffect(() => { let ignore = false; @@ -92,15 +94,37 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string; } rightActions={
- + {editing ? ( + + ) : ( + <> + + + + )}
} > @@ -114,74 +138,85 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string; )} {!loading && rule && (
- {/* Rule Metadata */} -
-
-
- Rule ID: - {rule.id} -
-
- Status: - - {getStatusText(rule.status, rule.processedAt || null)} - -
-
- Next Run: - - {formatDateTime(rule.nextRunAt)} - -
-
- Created: - - {formatDateTime(rule.createdAt)} - -
- {rule.processedAt && ( -
- Processed: - - {formatDateTime(rule.processedAt)} - + {editing ? ( + setEditing(false)} + onUpdated={(updatedRule) => setRule(updatedRule)} + /> + ) : ( + <> + {/* Rule Metadata */} +
+
+
+ Rule ID: + {rule.id}
- )} - {rule.output?.jobId && ( -
- Job ID: - - - {rule.output.jobId} - - +
+ Status: + + {getStatusText(rule.status, rule.processedAt || null)} + +
+
+ Next Run: + + {formatDateTime(rule.nextRunAt)} + +
+
+ Created: + + {formatDateTime(rule.createdAt)} + +
+ {rule.processedAt && ( +
+ Processed: + + {formatDateTime(rule.processedAt)} + +
+ )} + {rule.output?.jobId && ( +
+ Job ID: + + + {rule.output.jobId} + + +
+ )} + {rule.workerId && ( +
+ Worker ID: + {rule.workerId} +
+ )}
- )} - {rule.workerId && ( -
- Worker ID: - {rule.workerId} -
- )} -
-
+
- {/* Messages */} -
-

- Messages -

-
- {rule.input.messages.map((message, index) => ( -
- + {/* Messages */} +
+

+ Messages +

+
+ {rule.input.messages.map((message, index) => ( +
+ +
+ ))}
- ))} -
-
+
+ + )}
)}
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx index 9d7a4c8b..89308ba5 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx @@ -1,5 +1,6 @@ "use client"; import { DataSource } from "@/src/entities/models/data-source"; +import { TriggerSchemaForCopilot } from "@/src/entities/models/copilot"; import { Project } from "@/src/entities/models/project"; import { z } from "zod"; import { useCallback, useEffect, useState } from "react"; @@ -10,10 +11,15 @@ import { revertToLiveWorkflow } from "@/app/actions/project.actions"; import { fetchProject } from "@/app/actions/project.actions"; import { Workflow } from "@/app/lib/types/workflow_types"; import { ModelsResponse } from "@/app/lib/types/billing_types"; +import { listScheduledJobRules } from "@/app/actions/scheduled-job-rules.actions"; +import { listRecurringJobRules } from "@/app/actions/recurring-job-rules.actions"; +import { listComposioTriggerDeployments } from "@/app/actions/composio.actions"; +import { transformTriggersForCopilot, DEFAULT_TRIGGER_FETCH_LIMIT } from "./trigger-transform"; export function App({ initialProjectData, initialDataSources, + initialTriggers, eligibleModels, useRag, useRagUploads, @@ -24,6 +30,7 @@ export function App({ }: { initialProjectData: z.infer; initialDataSources: z.infer[]; + initialTriggers: z.infer[]; eligibleModels: z.infer | "*"; useRag: boolean; useRagUploads: boolean; @@ -44,6 +51,7 @@ export function App({ }); const [project, setProject] = useState>(initialProjectData); const [dataSources, setDataSources] = useState[]>(initialDataSources); + const [triggers, setTriggers] = useState[]>(initialTriggers); const [loading, setLoading] = useState(false); console.log('workflow app.tsx render'); @@ -65,21 +73,42 @@ export function App({ workflow = mode === 'live' ? project?.liveWorkflow : project?.draftWorkflow; } - const reloadData = useCallback(async () => { - setLoading(true); - const [ - projectData, - sourcesData, - ] = await Promise.all([ - fetchProject(initialProjectData.id), - listDataSources(initialProjectData.id), + const fetchTriggers = useCallback(async () => { + const [scheduled, recurring, composio] = await Promise.all([ + listScheduledJobRules({ projectId: initialProjectData.id, limit: DEFAULT_TRIGGER_FETCH_LIMIT }), + listRecurringJobRules({ projectId: initialProjectData.id, limit: DEFAULT_TRIGGER_FETCH_LIMIT }), + listComposioTriggerDeployments({ projectId: initialProjectData.id, limit: DEFAULT_TRIGGER_FETCH_LIMIT }), ]); - setProject(projectData); - setDataSources(sourcesData); - setLoading(false); + return transformTriggersForCopilot({ + scheduled: scheduled.items ?? [], + recurring: recurring.items ?? [], + composio: composio.items ?? [], + }); }, [initialProjectData.id]); + const refreshTriggers = useCallback(async () => { + const nextTriggers = await fetchTriggers(); + setTriggers(nextTriggers); + }, [fetchTriggers]); + + const reloadData = useCallback(async () => { + setLoading(true); + try { + const [projectData, sourcesData, triggerData] = await Promise.all([ + fetchProject(initialProjectData.id), + listDataSources(initialProjectData.id), + fetchTriggers(), + ]); + + setProject(projectData); + setDataSources(sourcesData); + setTriggers(triggerData); + } finally { + setLoading(false); + } + }, [fetchTriggers, initialProjectData.id]); + const handleProjectToolsUpdate = useCallback(async () => { // Lightweight refresh for tool-only updates const projectConfig = await fetchProject(initialProjectData.id); @@ -133,8 +162,12 @@ export function App({ async function handleRevertToLive() { setLoading(true); - await revertToLiveWorkflow(initialProjectData.id); - reloadData(); + try { + await revertToLiveWorkflow(initialProjectData.id); + await reloadData(); + } finally { + setLoading(false); + } } // if workflow is null, show the selector @@ -152,6 +185,7 @@ export function App({ onToggleAutoPublish={handleToggleAutoPublish} workflow={workflow} dataSources={dataSources} + triggers={triggers} projectConfig={project} useRag={useRag} useRagUploads={useRagUploads} @@ -164,6 +198,7 @@ export function App({ onProjectToolsUpdated={handleProjectToolsUpdate} onDataSourcesUpdated={handleDataSourcesUpdate} onProjectConfigUpdated={handleProjectConfigUpdate} + onTriggersUpdated={refreshTriggers} chatWidgetHost={chatWidgetHost} />} diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/ComposioTriggerTypesPanel.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/ComposioTriggerTypesPanel.tsx index 41426bf5..fec1a1f3 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/components/ComposioTriggerTypesPanel.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/ComposioTriggerTypesPanel.tsx @@ -13,6 +13,7 @@ interface ComposioTriggerTypesPanelProps { toolkit: z.infer; onBack: () => void; onSelectTriggerType: (triggerType: z.infer) => void; + initialTriggerTypeSlug?: string | null; } type TriggerType = z.infer; @@ -21,6 +22,7 @@ export function ComposioTriggerTypesPanel({ toolkit, onBack, onSelectTriggerType, + initialTriggerTypeSlug, }: ComposioTriggerTypesPanelProps) { const [triggerTypes, setTriggerTypes] = useState([]); const [loading, setLoading] = useState(true); @@ -28,6 +30,7 @@ export function ComposioTriggerTypesPanel({ const [cursor, setCursor] = useState(null); const [hasNextPage, setHasNextPage] = useState(false); const [loadingMore, setLoadingMore] = useState(false); + const [autoSelected, setAutoSelected] = useState(false); const loadTriggerTypes = useCallback(async (resetList = false, nextCursor?: string) => { try { @@ -70,8 +73,20 @@ export function ComposioTriggerTypesPanel({ useEffect(() => { loadTriggerTypes(true); + setAutoSelected(false); }, [loadTriggerTypes]); + useEffect(() => { + if (!initialTriggerTypeSlug || autoSelected || triggerTypes.length === 0) { + return; + } + const match = triggerTypes.find(triggerType => triggerType.slug === initialTriggerTypeSlug); + if (match) { + setAutoSelected(true); + onSelectTriggerType(match); + } + }, [initialTriggerTypeSlug, triggerTypes, onSelectTriggerType, autoSelected]); + if (loading) { return (
@@ -215,4 +230,4 @@ export function ComposioTriggerTypesPanel({ )}
); -} \ No newline at end of file +} diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/TriggerConfigForm.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/TriggerConfigForm.tsx index 29a32528..6e8267c8 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/components/TriggerConfigForm.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TriggerConfigForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { Button, Input, Card, CardBody, CardHeader } from '@heroui/react'; import { ArrowLeft, ZapIcon, CheckCircleIcon } from 'lucide-react'; import { z } from 'zod'; @@ -13,6 +13,7 @@ interface TriggerConfigFormProps { onBack: () => void; onSubmit: (config: Record) => void; isSubmitting?: boolean; + initialConfig?: Record; } interface JsonSchemaProperty { @@ -36,13 +37,36 @@ export function TriggerConfigForm({ onBack, onSubmit, isSubmitting = false, + initialConfig, }: TriggerConfigFormProps) { - const [formData, setFormData] = useState>({}); + const [formData, setFormData] = useState>(() => { + if (!initialConfig) { + return {}; + } + return Object.entries(initialConfig).reduce>((acc, [key, value]) => { + if (value !== undefined && value !== null) { + acc[key] = String(value); + } + return acc; + }, {}); + }); const [errors, setErrors] = useState>({}); // Parse the JSON schema from triggerType.config const schema = triggerType.config as JsonSchema; + useEffect(() => { + if (!initialConfig) { + return; + } + setFormData(Object.entries(initialConfig).reduce>((acc, [key, value]) => { + if (value !== undefined && value !== null) { + acc[key] = String(value); + } + return acc; + }, {})); + }, [initialConfig, triggerType.slug]); + const handleSubmit = useCallback(() => { // Validate required fields const newErrors: Record = {}; @@ -267,4 +291,4 @@ export function TriggerConfigForm({
); -} \ No newline at end of file +} diff --git a/apps/rowboat/app/projects/[projectId]/workflow/page.tsx b/apps/rowboat/app/projects/[projectId]/workflow/page.tsx index 864d31c4..d855c21b 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/page.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/page.tsx @@ -9,10 +9,17 @@ import { ModelsResponse } from "@/app/lib/types/billing_types"; import { requireAuth } from "@/app/lib/auth"; import { IFetchProjectController } from "@/src/interface-adapters/controllers/projects/fetch-project.controller"; import { IListDataSourcesController } from "@/src/interface-adapters/controllers/data-sources/list-data-sources.controller"; +import { IListScheduledJobRulesController } from "@/src/interface-adapters/controllers/scheduled-job-rules/list-scheduled-job-rules.controller"; +import { IListRecurringJobRulesController } from "@/src/interface-adapters/controllers/recurring-job-rules/list-recurring-job-rules.controller"; +import { IListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller"; import { z } from "zod"; +import { transformTriggersForCopilot, DEFAULT_TRIGGER_FETCH_LIMIT } from "./trigger-transform"; const fetchProjectController = container.resolve('fetchProjectController'); const listDataSourcesController = container.resolve('listDataSourcesController'); +const listScheduledJobRulesController = container.resolve('listScheduledJobRulesController'); +const listRecurringJobRulesController = container.resolve('listRecurringJobRulesController'); +const listComposioTriggerDeploymentsController = container.resolve('listComposioTriggerDeploymentsController'); const DEFAULT_MODEL = process.env.PROVIDER_DEFAULT_MODEL || "gpt-4.1"; @@ -39,23 +46,50 @@ export default async function Page( notFound(); } - const sources = await listDataSourcesController.execute({ - caller: "user", - userId: user.id, - projectId: params.projectId, - }); + const [sources, scheduledTriggers, recurringTriggers, composioTriggers] = await Promise.all([ + listDataSourcesController.execute({ + caller: "user", + userId: user.id, + projectId: params.projectId, + }), + listScheduledJobRulesController.execute({ + caller: "user", + userId: user.id, + projectId: params.projectId, + limit: DEFAULT_TRIGGER_FETCH_LIMIT, + }), + listRecurringJobRulesController.execute({ + caller: "user", + userId: user.id, + projectId: params.projectId, + limit: DEFAULT_TRIGGER_FETCH_LIMIT, + }), + listComposioTriggerDeploymentsController.execute({ + caller: "user", + userId: user.id, + projectId: params.projectId, + limit: DEFAULT_TRIGGER_FETCH_LIMIT, + }), + ]); let eligibleModels: z.infer | "*" = '*'; if (USE_BILLING) { eligibleModels = await getEligibleModels(customer.id); } + const triggers = transformTriggersForCopilot({ + scheduled: scheduledTriggers.items ?? [], + recurring: recurringTriggers.items ?? [], + composio: composioTriggers.items ?? [], + }); + console.log('/workflow page.tsx serve'); return ( ; + +interface TransformParams { + scheduled: Array<{ + id: string; + nextRunAt: string; + status: 'pending' | 'processing' | 'triggered'; + input?: { messages: Array> }; + }>; + recurring: Array<{ + id: string; + cron: string; + nextRunAt: string | null; + disabled: boolean; + input?: { messages: Array> }; + }>; + composio: Array<{ + id: string; + triggerTypeName: string; + toolkitSlug: string; + triggerTypeSlug: string; + triggerConfig: Record; + }>; +} + +export function transformTriggersForCopilot({ + scheduled, + recurring, + composio, +}: TransformParams): CopilotTrigger[] { + const placeholderInput = { + messages: [ + { + role: "user" as const, + content: "Trigger execution", + }, + ], + } satisfies { messages: Array> }; + + const oneTime = scheduled.map((trigger) => ({ + type: "one_time" as const, + id: trigger.id, + name: `One-time trigger (${new Date(trigger.nextRunAt).toLocaleDateString('en-US')})`, + nextRunAt: trigger.nextRunAt, + status: trigger.status, + input: trigger.input ?? placeholderInput, + })); + + const recurringTriggers = recurring.map((trigger) => ({ + type: "recurring" as const, + id: trigger.id, + name: `Recurring trigger (${trigger.cron})`, + cron: trigger.cron, + nextRunAt: trigger.nextRunAt ?? '', + disabled: trigger.disabled, + input: trigger.input ?? placeholderInput, + })); + + const external = composio.map((trigger) => ({ + type: "external" as const, + id: trigger.id, + name: trigger.triggerTypeName, + triggerTypeName: trigger.triggerTypeName, + toolkitSlug: trigger.toolkitSlug, + triggerTypeSlug: trigger.triggerTypeSlug, + triggerConfig: trigger.triggerConfig, + })); + + return [...oneTime, ...recurringTriggers, ...external] as CopilotTrigger[]; +} diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index 476d8366..6ef91e45 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -3,6 +3,7 @@ import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef, c import { MCPServer, Message, WithStringId } from "../../../lib/types/types"; import { Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent, WorkflowPipeline } from "../../../lib/types/workflow_types"; import { DataSource } from "@/src/entities/models/data-source"; +import { TriggerSchemaForCopilot } from "@/src/entities/models/copilot"; import { Project } from "@/src/entities/models/project"; import { produce, applyPatches, enablePatches, produceWithPatches, Patch } from 'immer'; import { AgentConfig } from "../entities/agent_config"; @@ -962,6 +963,7 @@ export function useEntitySelection() { export function WorkflowEditor({ projectId, dataSources, + triggers, workflow, useRag, useRagUploads, @@ -978,10 +980,12 @@ export function WorkflowEditor({ onProjectToolsUpdated, onDataSourcesUpdated, onProjectConfigUpdated, + onTriggersUpdated, chatWidgetHost, }: { projectId: string; dataSources: z.infer[]; + triggers: z.infer[]; workflow: z.infer; useRag: boolean; useRagUploads: boolean; @@ -998,6 +1002,7 @@ export function WorkflowEditor({ onProjectToolsUpdated?: () => void; onDataSourcesUpdated?: () => void; onProjectConfigUpdated?: () => void; + onTriggersUpdated?: () => Promise | void; chatWidgetHost: string; }) { @@ -2313,8 +2318,10 @@ export function WorkflowEditor({ } isInitialState={isInitialState} dataSources={dataSources} + triggers={triggers} activePanel={activePanel} onTogglePanel={handleTogglePanel} + onTriggersUpdated={onTriggersUpdated} /> {/* Config overlay above Copilot when agents + skipper layout is active */} {state.present.selection && viewMode === 'two_agents_skipper' && ( diff --git a/apps/rowboat/components/common/compose-box-copilot.tsx b/apps/rowboat/components/common/compose-box-copilot.tsx index d2ddd4d2..22c12503 100644 --- a/apps/rowboat/components/common/compose-box-copilot.tsx +++ b/apps/rowboat/components/common/compose-box-copilot.tsx @@ -268,7 +268,7 @@ function CopilotStatusBar({ // Show real button when ready return (