mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
Let copilot add external triggers
feat: Enhance external trigger handling in Copilot - Added support for flexible schemas in external triggers, allowing configuration changes without stripping any data. - Introduced a new `onRequestTriggerSetup` callback in the Action component to facilitate trigger setup requests. - Implemented a modal for trigger configuration, improving user experience when setting up external triggers. - Updated the ComposioTriggerTypesPanel to auto-select trigger types based on initial configuration. This update significantly improves the management and setup of external triggers within the Copilot interface.
This commit is contained in:
parent
885a7f3753
commit
69eec4e41b
6 changed files with 402 additions and 19 deletions
|
|
@ -93,6 +93,10 @@ export function validateConfigChanges(configType: string, configChanges: Record<
|
||||||
}).passthrough();
|
}).passthrough();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'external_trigger': {
|
||||||
|
// External triggers have flexible schemas per provider; do not strip any config.
|
||||||
|
return { changes: configChanges };
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return { error: `Unknown config type: ${configType}` };
|
return { error: `Unknown config type: ${configType}` };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<string, unknown> | null;
|
||||||
|
onCreated?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toolkit = z.infer<typeof ZToolkit>;
|
||||||
|
type TriggerType = z.infer<typeof ComposioTriggerType>;
|
||||||
|
type ProjectConfig = z.infer<typeof Project>;
|
||||||
|
|
||||||
|
export function TriggerSetupModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
projectId,
|
||||||
|
initialToolkitSlug = null,
|
||||||
|
initialTriggerTypeSlug = null,
|
||||||
|
initialTriggerConfig = null,
|
||||||
|
onCreated,
|
||||||
|
}: TriggerSetupModalProps) {
|
||||||
|
const [selectedToolkit, setSelectedToolkit] = useState<Toolkit | null>(null);
|
||||||
|
const [selectedTriggerType, setSelectedTriggerType] = useState<TriggerType | null>(null);
|
||||||
|
const [projectConfig, setProjectConfig] = useState<ProjectConfig | null>(null);
|
||||||
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [pendingTriggerTypeSlug, setPendingTriggerTypeSlug] = useState<string | null>(null);
|
||||||
|
const [initialConfig, setInitialConfig] = useState<Record<string, unknown> | 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<string, unknown>) => {
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
size="5xl"
|
||||||
|
scrollBehavior="inside"
|
||||||
|
classNames={{
|
||||||
|
base: 'max-h-[90vh]'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Set up External Trigger</h2>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Follow the guided flow to authenticate and configure the trigger.
|
||||||
|
</p>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody className="pb-6">
|
||||||
|
{!selectedToolkit && (
|
||||||
|
<SelectComposioToolkit
|
||||||
|
key={isOpen ? 'toolkit-selector' : 'toolkit-selector-hidden'}
|
||||||
|
projectId={projectId}
|
||||||
|
tools={[]}
|
||||||
|
onSelectToolkit={handleSelectToolkit}
|
||||||
|
initialToolkitSlug={initialToolkitSlug}
|
||||||
|
filterByTriggers={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedToolkit && !selectedTriggerType && (
|
||||||
|
<ComposioTriggerTypesPanel
|
||||||
|
key={selectedToolkit.slug}
|
||||||
|
toolkit={selectedToolkit}
|
||||||
|
onBack={() => setSelectedToolkit(null)}
|
||||||
|
onSelectTriggerType={handleSelectTriggerType}
|
||||||
|
initialTriggerTypeSlug={pendingTriggerTypeSlug}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedToolkit && selectedTriggerType && (!requiresAuth || hasActiveConnection) && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Button variant="light" size="sm" onPress={() => setSelectedTriggerType(null)}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<TriggerConfigForm
|
||||||
|
toolkit={selectedToolkit}
|
||||||
|
triggerType={selectedTriggerType}
|
||||||
|
onBack={() => setSelectedTriggerType(null)}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
initialConfig={initialConfig}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedToolkit && selectedTriggerType && requiresAuth && !hasActiveConnection && !showAuthModal && (
|
||||||
|
<div className="py-12 text-center space-y-4">
|
||||||
|
<Spinner className="mx-auto" />
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Waiting for authentication to complete...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-4 rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20 p-3 text-sm text-red-600 dark:text-red-300">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{selectedToolkit && (
|
||||||
|
<ToolkitAuthModal
|
||||||
|
isOpen={showAuthModal}
|
||||||
|
onClose={() => setShowAuthModal(false)}
|
||||||
|
projectId={projectId}
|
||||||
|
toolkitSlug={selectedToolkit.slug}
|
||||||
|
onComplete={handleAuthComplete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,7 @@ export function Action({
|
||||||
onApplied,
|
onApplied,
|
||||||
externallyApplied = false,
|
externallyApplied = false,
|
||||||
defaultExpanded = false,
|
defaultExpanded = false,
|
||||||
|
onRequestTriggerSetup,
|
||||||
}: {
|
}: {
|
||||||
msgIndex: number;
|
msgIndex: number;
|
||||||
actionIndex: number;
|
actionIndex: number;
|
||||||
|
|
@ -39,10 +40,12 @@ export function Action({
|
||||||
onApplied?: () => void;
|
onApplied?: () => void;
|
||||||
externallyApplied?: boolean;
|
externallyApplied?: boolean;
|
||||||
defaultExpanded?: boolean;
|
defaultExpanded?: boolean;
|
||||||
|
onRequestTriggerSetup?: (params: { action: z.infer<typeof CopilotAssistantMessageActionPart>['content']; msgIndex: number; actionIndex: number }) => void;
|
||||||
}) {
|
}) {
|
||||||
const { showPreview } = usePreviewModal();
|
const { showPreview } = usePreviewModal();
|
||||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||||
const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({});
|
const [appliedChanges, setAppliedChanges] = useState<Record<string, boolean>>({});
|
||||||
|
const isExternalTriggerCreate = action.config_type === 'external_trigger' && action.action === 'create_new';
|
||||||
|
|
||||||
if (!action || typeof action !== 'object') {
|
if (!action || typeof action !== 'object') {
|
||||||
console.warn('Invalid action object:', action);
|
console.warn('Invalid action object:', action);
|
||||||
|
|
@ -108,6 +111,10 @@ export function Action({
|
||||||
|
|
||||||
// Handle applying all changes - delegate to parent
|
// Handle applying all changes - delegate to parent
|
||||||
const handleApplyAll = () => {
|
const handleApplyAll = () => {
|
||||||
|
if (isExternalTriggerCreate) {
|
||||||
|
onRequestTriggerSetup?.({ action, msgIndex, actionIndex });
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Mark all fields as applied locally for UI state
|
// Mark all fields as applied locally for UI state
|
||||||
const appliedKeys = Object.keys(action.config_changes).reduce((acc, key) => {
|
const appliedKeys = Object.keys(action.config_changes).reduce((acc, key) => {
|
||||||
acc[getAppliedChangeKey(msgIndex, actionIndex, key)] = true;
|
acc[getAppliedChangeKey(msgIndex, actionIndex, key)] = true;
|
||||||
|
|
@ -230,9 +237,9 @@ export function Action({
|
||||||
onClick={() => handleApplyAll()}
|
onClick={() => handleApplyAll()}
|
||||||
>
|
>
|
||||||
<CheckIcon size={13} className={allApplied ? 'text-zinc-400' : 'text-green-600 group-hover:text-green-700'} />
|
<CheckIcon size={13} className={allApplied ? 'text-zinc-400' : 'text-green-600 group-hover:text-green-700'} />
|
||||||
<span>{allApplied ? 'Applied' : 'Apply'}</span>
|
<span>{allApplied ? 'Applied' : isExternalTriggerCreate ? 'Open setup' : 'Apply'}</span>
|
||||||
</button>
|
</button>
|
||||||
{action.action !== 'delete' && <button
|
{action.action !== 'delete' && !isExternalTriggerCreate && <button
|
||||||
className="flex items-center gap-1 rounded-full px-2 h-7 text-xs font-medium bg-transparent text-indigo-600 hover:text-indigo-700 transition-colors"
|
className="flex items-center gap-1 rounded-full px-2 h-7 text-xs font-medium bg-transparent text-indigo-600 hover:text-indigo-700 transition-colors"
|
||||||
onClick={handleViewDiff}
|
onClick={handleViewDiff}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import MarkdownContent from "@/app/lib/components/markdown-content";
|
||||||
import { MessageSquareIcon, EllipsisIcon, XIcon, CheckCheckIcon, ChevronDown, ChevronUp } from "lucide-react";
|
import { MessageSquareIcon, EllipsisIcon, XIcon, CheckCheckIcon, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { CopilotMessage, CopilotAssistantMessage, CopilotAssistantMessageActionPart, TriggerSchemaForCopilot } from "@/src/entities/models/copilot";
|
import { CopilotMessage, CopilotAssistantMessage, CopilotAssistantMessageActionPart, TriggerSchemaForCopilot } from "@/src/entities/models/copilot";
|
||||||
import { Action, StreamingAction } from './actions';
|
import { Action, StreamingAction } from './actions';
|
||||||
|
import { TriggerSetupModal } from './TriggerSetupModal';
|
||||||
import { useParsedBlocks } from "../use-parsed-blocks";
|
import { useParsedBlocks } from "../use-parsed-blocks";
|
||||||
import { validateConfigChanges } from "@/app/lib/client_utils";
|
import { validateConfigChanges } from "@/app/lib/client_utils";
|
||||||
import { PreviewModalProvider } from '../../workflow/preview-modal';
|
import { PreviewModalProvider } from '../../workflow/preview-modal';
|
||||||
|
|
@ -224,6 +225,14 @@ function AssistantMessage({
|
||||||
const triggersRef = useRef<CopilotTriggerType[] | undefined>(triggers);
|
const triggersRef = useRef<CopilotTriggerType[] | undefined>(triggers);
|
||||||
const pendingTriggerEditsRef = useRef<Map<string, CopilotTriggerType>>(new Map());
|
const pendingTriggerEditsRef = useRef<Map<string, CopilotTriggerType>>(new Map());
|
||||||
const triggerUpdateCallbackRef = useRef<typeof onTriggersUpdated>(onTriggersUpdated);
|
const triggerUpdateCallbackRef = useRef<typeof onTriggersUpdated>(onTriggersUpdated);
|
||||||
|
const [triggerSetupModal, setTriggerSetupModal] = useState<{
|
||||||
|
action: z.infer<typeof CopilotAssistantMessageActionPart>['content'];
|
||||||
|
actionIndex: number;
|
||||||
|
messageIndex: number;
|
||||||
|
initialToolkitSlug: string | null;
|
||||||
|
initialTriggerTypeSlug: string | null;
|
||||||
|
initialConfig?: Record<string, unknown>;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
triggersRef.current = triggers;
|
triggersRef.current = triggers;
|
||||||
|
|
@ -234,6 +243,82 @@ function AssistantMessage({
|
||||||
triggerUpdateCallbackRef.current = onTriggersUpdated;
|
triggerUpdateCallbackRef.current = onTriggersUpdated;
|
||||||
}, [onTriggersUpdated]);
|
}, [onTriggersUpdated]);
|
||||||
|
|
||||||
|
const refreshTriggers = useCallback(async () => {
|
||||||
|
const callback = triggerUpdateCallbackRef.current;
|
||||||
|
if (!callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await callback();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh triggers after Copilot action', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const requestTriggerSetup = useCallback((params: {
|
||||||
|
action: z.infer<typeof CopilotAssistantMessageActionPart>['content'];
|
||||||
|
actionIndex: number;
|
||||||
|
messageIndex: number;
|
||||||
|
}) => {
|
||||||
|
const { action, actionIndex, messageIndex: msgIndex } = params;
|
||||||
|
const changes = (action?.config_changes ?? {}) as Record<string, unknown>;
|
||||||
|
const toStringOrNull = (value: unknown): string | null => {
|
||||||
|
if (typeof value === 'string' && value.trim().length > 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const deriveSlug = (primary: unknown, secondary: unknown, tertiary: unknown): string | null => {
|
||||||
|
return toStringOrNull(primary) ?? toStringOrNull(secondary) ?? toStringOrNull(tertiary);
|
||||||
|
};
|
||||||
|
const toolkitSlug = deriveSlug(
|
||||||
|
changes.toolkitSlug,
|
||||||
|
changes.toolkit_slug,
|
||||||
|
typeof changes.toolkit === 'object' && changes.toolkit !== null ? (changes.toolkit as any).slug : changes.toolkit
|
||||||
|
);
|
||||||
|
const triggerTypeSlug = deriveSlug(
|
||||||
|
changes.triggerTypeSlug,
|
||||||
|
changes.trigger_type_slug,
|
||||||
|
typeof changes.triggerType === 'object' && changes.triggerType !== null ? (changes.triggerType as any).slug : changes.triggerType
|
||||||
|
);
|
||||||
|
const triggerConfigCandidate = (changes.triggerConfig ?? changes.trigger_config ?? changes.config) as unknown;
|
||||||
|
const triggerConfig = typeof triggerConfigCandidate === 'object' && triggerConfigCandidate !== null
|
||||||
|
? (triggerConfigCandidate as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
setTriggerSetupModal(prev => {
|
||||||
|
if (prev && prev.actionIndex === actionIndex && prev.messageIndex === msgIndex) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
action,
|
||||||
|
actionIndex,
|
||||||
|
messageIndex: msgIndex,
|
||||||
|
initialToolkitSlug: toolkitSlug,
|
||||||
|
initialTriggerTypeSlug: triggerTypeSlug,
|
||||||
|
initialConfig: triggerConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTriggerSetupCreated = useCallback(async () => {
|
||||||
|
if (!triggerSetupModal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = triggerSetupModal.actionIndex;
|
||||||
|
setAppliedActions(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(index);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
await refreshTriggers();
|
||||||
|
setTriggerSetupModal(null);
|
||||||
|
}, [refreshTriggers, triggerSetupModal]);
|
||||||
|
|
||||||
|
const handleTriggerSetupClosed = useCallback(() => {
|
||||||
|
setTriggerSetupModal(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// parse actions from parts
|
// parse actions from parts
|
||||||
const parsed = useMemo(() => {
|
const parsed = useMemo(() => {
|
||||||
const result: z.infer<typeof CopilotResponsePart>[] = [];
|
const result: z.infer<typeof CopilotResponsePart>[] = [];
|
||||||
|
|
@ -616,6 +701,15 @@ function AssistantMessage({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (configType === 'external_trigger') {
|
||||||
|
if (actionType === 'create_new') {
|
||||||
|
if (typeof actionIndex === 'number') {
|
||||||
|
requestTriggerSetup({ action, actionIndex, messageIndex });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((configType === 'external_trigger' || configType === 'external') && actionType === 'delete') {
|
if ((configType === 'external_trigger' || configType === 'external') && actionType === 'delete') {
|
||||||
const target = triggerList.find((trigger): trigger is Extract<CopilotTriggerType, { type: 'external' }> => {
|
const target = triggerList.find((trigger): trigger is Extract<CopilotTriggerType, { type: 'external' }> => {
|
||||||
if (trigger.type !== 'external') {
|
if (trigger.type !== 'external') {
|
||||||
|
|
@ -646,19 +740,7 @@ function AssistantMessage({
|
||||||
|
|
||||||
console.warn('Unhandled trigger action from Copilot applyAction', action);
|
console.warn('Unhandled trigger action from Copilot applyAction', action);
|
||||||
return false;
|
return false;
|
||||||
}, [projectId, parsed]);
|
}, [projectId, parsed, requestTriggerSetup, messageIndex]);
|
||||||
|
|
||||||
const refreshTriggers = useCallback(async () => {
|
|
||||||
const callback = triggerUpdateCallbackRef.current;
|
|
||||||
if (!callback) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await callback();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to refresh triggers after Copilot action', error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Memoized handleApplyAll for useEffect dependencies
|
// Memoized handleApplyAll for useEffect dependencies
|
||||||
const handleApplyAll = useCallback(async () => {
|
const handleApplyAll = useCallback(async () => {
|
||||||
|
|
@ -805,6 +887,7 @@ function AssistantMessage({
|
||||||
|
|
||||||
// Render all cards inline, not in a panel
|
// Render all cards inline, not in a panel
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="px-4 py-2.5 text-sm leading-relaxed text-gray-700 dark:text-gray-200">
|
<div className="px-4 py-2.5 text-sm leading-relaxed text-gray-700 dark:text-gray-200">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
|
@ -827,6 +910,9 @@ function AssistantMessage({
|
||||||
onApplied={() => { void handleSingleApply(part.action, idx); }}
|
onApplied={() => { void handleSingleApply(part.action, idx); }}
|
||||||
externallyApplied={appliedActions.has(idx)}
|
externallyApplied={appliedActions.has(idx)}
|
||||||
defaultExpanded={true}
|
defaultExpanded={true}
|
||||||
|
onRequestTriggerSetup={({ action, actionIndex }) =>
|
||||||
|
requestTriggerSetup({ action, actionIndex, messageIndex })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -845,6 +931,16 @@ function AssistantMessage({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<TriggerSetupModal
|
||||||
|
isOpen={Boolean(triggerSetupModal)}
|
||||||
|
onClose={handleTriggerSetupClosed}
|
||||||
|
projectId={projectId}
|
||||||
|
initialToolkitSlug={triggerSetupModal?.initialToolkitSlug ?? null}
|
||||||
|
initialTriggerTypeSlug={triggerSetupModal?.initialTriggerTypeSlug ?? null}
|
||||||
|
initialTriggerConfig={triggerSetupModal?.initialConfig}
|
||||||
|
onCreated={handleTriggerSetupCreated}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ interface ComposioTriggerTypesPanelProps {
|
||||||
toolkit: z.infer<typeof ZToolkit>;
|
toolkit: z.infer<typeof ZToolkit>;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onSelectTriggerType: (triggerType: z.infer<typeof ComposioTriggerType>) => void;
|
onSelectTriggerType: (triggerType: z.infer<typeof ComposioTriggerType>) => void;
|
||||||
|
initialTriggerTypeSlug?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TriggerType = z.infer<typeof ComposioTriggerType>;
|
type TriggerType = z.infer<typeof ComposioTriggerType>;
|
||||||
|
|
@ -21,6 +22,7 @@ export function ComposioTriggerTypesPanel({
|
||||||
toolkit,
|
toolkit,
|
||||||
onBack,
|
onBack,
|
||||||
onSelectTriggerType,
|
onSelectTriggerType,
|
||||||
|
initialTriggerTypeSlug,
|
||||||
}: ComposioTriggerTypesPanelProps) {
|
}: ComposioTriggerTypesPanelProps) {
|
||||||
const [triggerTypes, setTriggerTypes] = useState<TriggerType[]>([]);
|
const [triggerTypes, setTriggerTypes] = useState<TriggerType[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -28,6 +30,7 @@ export function ComposioTriggerTypesPanel({
|
||||||
const [cursor, setCursor] = useState<string | null>(null);
|
const [cursor, setCursor] = useState<string | null>(null);
|
||||||
const [hasNextPage, setHasNextPage] = useState(false);
|
const [hasNextPage, setHasNextPage] = useState(false);
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [autoSelected, setAutoSelected] = useState(false);
|
||||||
|
|
||||||
const loadTriggerTypes = useCallback(async (resetList = false, nextCursor?: string) => {
|
const loadTriggerTypes = useCallback(async (resetList = false, nextCursor?: string) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -70,8 +73,20 @@ export function ComposioTriggerTypesPanel({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTriggerTypes(true);
|
loadTriggerTypes(true);
|
||||||
|
setAutoSelected(false);
|
||||||
}, [loadTriggerTypes]);
|
}, [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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'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 { Button, Input, Card, CardBody, CardHeader } from '@heroui/react';
|
||||||
import { ArrowLeft, ZapIcon, CheckCircleIcon } from 'lucide-react';
|
import { ArrowLeft, ZapIcon, CheckCircleIcon } from 'lucide-react';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
@ -13,6 +13,7 @@ interface TriggerConfigFormProps {
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onSubmit: (config: Record<string, unknown>) => void;
|
onSubmit: (config: Record<string, unknown>) => void;
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
|
initialConfig?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JsonSchemaProperty {
|
interface JsonSchemaProperty {
|
||||||
|
|
@ -36,13 +37,36 @@ export function TriggerConfigForm({
|
||||||
onBack,
|
onBack,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isSubmitting = false,
|
isSubmitting = false,
|
||||||
|
initialConfig,
|
||||||
}: TriggerConfigFormProps) {
|
}: TriggerConfigFormProps) {
|
||||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
const [formData, setFormData] = useState<Record<string, string>>(() => {
|
||||||
|
if (!initialConfig) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return Object.entries(initialConfig).reduce<Record<string, string>>((acc, [key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
acc[key] = String(value);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// Parse the JSON schema from triggerType.config
|
// Parse the JSON schema from triggerType.config
|
||||||
const schema = triggerType.config as JsonSchema;
|
const schema = triggerType.config as JsonSchema;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFormData(Object.entries(initialConfig).reduce<Record<string, string>>((acc, [key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
acc[key] = String(value);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {}));
|
||||||
|
}, [initialConfig, triggerType.slug]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue