mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
feat: Enhance Copilot message handling and trigger actions
- Pass projectId to Messages and AssistantMessage components for better context - Refactor applyAction to handle one-time and recurring triggers with improved error handling - Update handleApplyAll and handleSingleApply to support async action processing - Remove deprecated pending trigger logic from workflow editor This update improves the Copilot's ability to manage triggers and enhances the overall message processing flow.
This commit is contained in:
parent
24f930fa86
commit
651a998fbb
4 changed files with 106 additions and 151 deletions
|
|
@ -255,6 +255,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Messages
|
<Messages
|
||||||
|
projectId={projectId}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
streamingResponse={streamingResponse}
|
streamingResponse={streamingResponse}
|
||||||
loadingResponse={loadingResponse}
|
loadingResponse={loadingResponse}
|
||||||
|
|
|
||||||
|
|
@ -171,14 +171,16 @@ function AssistantMessage({
|
||||||
dispatch,
|
dispatch,
|
||||||
messageIndex,
|
messageIndex,
|
||||||
loading,
|
loading,
|
||||||
onStatusBarChange
|
onStatusBarChange,
|
||||||
|
projectId,
|
||||||
}: {
|
}: {
|
||||||
content: z.infer<typeof CopilotAssistantMessage>['content'],
|
content: z.infer<typeof CopilotAssistantMessage>['content'],
|
||||||
workflow: z.infer<typeof Workflow>,
|
workflow: z.infer<typeof Workflow>,
|
||||||
dispatch: (action: any) => void,
|
dispatch: (action: any) => void,
|
||||||
messageIndex: number,
|
messageIndex: number,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
onStatusBarChange?: (status: any) => void
|
onStatusBarChange?: (status: any) => void;
|
||||||
|
projectId: string;
|
||||||
}) {
|
}) {
|
||||||
const blocks = useParsedBlocks(content);
|
const blocks = useParsedBlocks(content);
|
||||||
const [appliedActions, setAppliedActions] = useState<Set<number>>(new Set());
|
const [appliedActions, setAppliedActions] = useState<Set<number>>(new Set());
|
||||||
|
|
@ -208,14 +210,13 @@ function AssistantMessage({
|
||||||
const allApplied = pendingCount === 0 && totalActions > 0;
|
const allApplied = pendingCount === 0 && totalActions > 0;
|
||||||
|
|
||||||
// Memoized applyAction for useCallback dependencies
|
// Memoized applyAction for useCallback dependencies
|
||||||
const applyAction = useCallback((action: any, actionIndex: number) => {
|
const applyAction = useCallback(async (action: any, actionIndex: number): Promise<boolean> => {
|
||||||
// Only apply, do not update appliedActions here
|
|
||||||
if (action.action === 'create_new') {
|
if (action.action === 'create_new') {
|
||||||
switch (action.config_type) {
|
switch (action.config_type) {
|
||||||
case 'agent': {
|
case 'agent': {
|
||||||
// Prevent duplicate agent names
|
// Prevent duplicate agent names
|
||||||
if (workflow.agents.some((agent: any) => agent.name === action.name)) {
|
if (workflow.agents.some((agent: any) => agent.name === action.name)) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'add_agent',
|
type: 'add_agent',
|
||||||
|
|
@ -225,12 +226,12 @@ function AssistantMessage({
|
||||||
},
|
},
|
||||||
fromCopilot: true
|
fromCopilot: true
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
}
|
}
|
||||||
case 'tool': {
|
case 'tool': {
|
||||||
// Prevent duplicate tool names
|
// Prevent duplicate tool names
|
||||||
if (workflow.tools.some((tool: any) => tool.name === action.name)) {
|
if (workflow.tools.some((tool: any) => tool.name === action.name)) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'add_tool',
|
type: 'add_tool',
|
||||||
|
|
@ -240,7 +241,7 @@ function AssistantMessage({
|
||||||
},
|
},
|
||||||
fromCopilot: true
|
fromCopilot: true
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
}
|
}
|
||||||
case 'prompt':
|
case 'prompt':
|
||||||
dispatch({
|
dispatch({
|
||||||
|
|
@ -251,7 +252,7 @@ function AssistantMessage({
|
||||||
},
|
},
|
||||||
fromCopilot: true
|
fromCopilot: true
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
case 'pipeline':
|
case 'pipeline':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'add_pipeline',
|
type: 'add_pipeline',
|
||||||
|
|
@ -261,27 +262,45 @@ function AssistantMessage({
|
||||||
},
|
},
|
||||||
fromCopilot: true
|
fromCopilot: true
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
case 'one_time_trigger':
|
case 'one_time_trigger': {
|
||||||
dispatch({
|
const { scheduledTime, input } = action.config_changes || {};
|
||||||
type: 'add_one_time_trigger',
|
if (!scheduledTime || !input) {
|
||||||
trigger: {
|
console.error('Missing scheduledTime or input for one-time trigger', action);
|
||||||
name: action.name,
|
return false;
|
||||||
...action.config_changes
|
}
|
||||||
},
|
try {
|
||||||
fromCopilot: true
|
const { createScheduledJobRule } = await import('@/app/actions/scheduled-job-rules.actions');
|
||||||
});
|
await createScheduledJobRule({
|
||||||
break;
|
projectId,
|
||||||
case 'recurring_trigger':
|
scheduledTime,
|
||||||
dispatch({
|
input,
|
||||||
type: 'add_recurring_trigger',
|
});
|
||||||
trigger: {
|
return true;
|
||||||
name: action.name,
|
} catch (error) {
|
||||||
...action.config_changes
|
console.error('Failed to create one-time trigger', error);
|
||||||
},
|
return false;
|
||||||
fromCopilot: true
|
}
|
||||||
});
|
}
|
||||||
break;
|
case 'recurring_trigger': {
|
||||||
|
const { cron, input } = action.config_changes || {};
|
||||||
|
if (!cron || !input) {
|
||||||
|
console.error('Missing cron or input for recurring trigger', action);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { createRecurringJobRule } = await import('@/app/actions/recurring-job-rules.actions');
|
||||||
|
await createRecurringJobRule({
|
||||||
|
projectId,
|
||||||
|
cron,
|
||||||
|
input,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create recurring trigger', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (action.action === 'edit') {
|
} else if (action.action === 'edit') {
|
||||||
switch (action.config_type) {
|
switch (action.config_type) {
|
||||||
|
|
@ -291,34 +310,34 @@ function AssistantMessage({
|
||||||
name: action.name,
|
name: action.name,
|
||||||
agent: action.config_changes
|
agent: action.config_changes
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
case 'tool':
|
case 'tool':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'update_tool_no_select',
|
type: 'update_tool_no_select',
|
||||||
name: action.name,
|
name: action.name,
|
||||||
tool: action.config_changes
|
tool: action.config_changes
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
case 'prompt':
|
case 'prompt':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'update_prompt',
|
type: 'update_prompt',
|
||||||
name: action.name,
|
name: action.name,
|
||||||
prompt: action.config_changes
|
prompt: action.config_changes
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
case 'pipeline':
|
case 'pipeline':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'update_pipeline',
|
type: 'update_pipeline',
|
||||||
name: action.name,
|
name: action.name,
|
||||||
pipeline: action.config_changes
|
pipeline: action.config_changes
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
case 'start_agent':
|
case 'start_agent':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'set_main_agent',
|
type: 'set_main_agent',
|
||||||
name: action.name,
|
name: action.name,
|
||||||
})
|
});
|
||||||
break;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (action.action === 'delete') {
|
} else if (action.action === 'delete') {
|
||||||
switch (action.config_type) {
|
switch (action.config_type) {
|
||||||
|
|
@ -327,61 +346,80 @@ function AssistantMessage({
|
||||||
type: 'delete_agent',
|
type: 'delete_agent',
|
||||||
name: action.name
|
name: action.name
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
case 'tool':
|
case 'tool':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'delete_tool',
|
type: 'delete_tool',
|
||||||
name: action.name
|
name: action.name
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
case 'prompt':
|
case 'prompt':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'delete_prompt',
|
type: 'delete_prompt',
|
||||||
name: action.name
|
name: action.name
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
case 'pipeline':
|
case 'pipeline':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'delete_pipeline',
|
type: 'delete_pipeline',
|
||||||
name: action.name
|
name: action.name
|
||||||
});
|
});
|
||||||
break;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [dispatch, workflow.agents, workflow.tools]);
|
|
||||||
|
console.warn('Unhandled action from Copilot applyAction', action, actionIndex);
|
||||||
|
return false;
|
||||||
|
}, [dispatch, projectId, workflow.agents, workflow.tools]);
|
||||||
|
|
||||||
// Memoized handleApplyAll for useEffect dependencies
|
// Memoized handleApplyAll for useEffect dependencies
|
||||||
const handleApplyAll = useCallback(() => {
|
const handleApplyAll = useCallback(async () => {
|
||||||
// Find all unapplied action indices
|
|
||||||
const unapplied = parsed
|
const unapplied = parsed
|
||||||
.map((part, idx) => ({ part, actionIndex: idx }))
|
.map((part, idx) => ({ part, actionIndex: idx }))
|
||||||
.filter(({ part, actionIndex }) => part.type === 'action' && !appliedActions.has(actionIndex))
|
.filter(({ part, actionIndex }) => part.type === 'action' && !appliedActions.has(actionIndex))
|
||||||
.map(({ part, actionIndex }) => ({
|
.map(({ part, actionIndex }) => ({
|
||||||
action: part.type === 'action' ? part.action : null,
|
action: part.type === 'action' ? part.action : null,
|
||||||
actionIndex
|
actionIndex,
|
||||||
}))
|
}))
|
||||||
.filter(({ action }) => action !== null);
|
.filter(({ action }) => action !== null);
|
||||||
|
|
||||||
// Synchronously apply all unapplied actions
|
const newlyApplied: number[] = [];
|
||||||
unapplied.forEach(({ action, actionIndex }) => {
|
|
||||||
applyAction(action, actionIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
// After all are applied, update the state in one go
|
for (const { action, actionIndex } of unapplied) {
|
||||||
setAppliedActions(prev => {
|
try {
|
||||||
const next = new Set(prev);
|
const success = await applyAction(action, actionIndex);
|
||||||
unapplied.forEach(({ actionIndex }) => next.add(actionIndex));
|
if (success) {
|
||||||
return next;
|
newlyApplied.push(actionIndex);
|
||||||
});
|
}
|
||||||
}, [parsed, appliedActions, setAppliedActions, applyAction]);
|
} catch (error) {
|
||||||
|
console.error('Failed to apply Copilot action', action, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newlyApplied.length > 0) {
|
||||||
|
setAppliedActions(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
newlyApplied.forEach(index => next.add(index));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [parsed, appliedActions, applyAction]);
|
||||||
|
|
||||||
// Manual single apply (from card)
|
// Manual single apply (from card)
|
||||||
const handleSingleApply = (action: any, actionIndex: number) => {
|
const handleSingleApply = useCallback(async (action: any, actionIndex: number) => {
|
||||||
if (!appliedActions.has(actionIndex)) {
|
if (appliedActions.has(actionIndex)) {
|
||||||
applyAction(action, actionIndex);
|
return;
|
||||||
setAppliedActions(prev => new Set([...prev, actionIndex]));
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
try {
|
||||||
|
const success = await applyAction(action, actionIndex);
|
||||||
|
if (success) {
|
||||||
|
setAppliedActions(prev => new Set([...prev, actionIndex]));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to apply Copilot action', action, error);
|
||||||
|
}
|
||||||
|
}, [appliedActions, applyAction]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|
@ -481,7 +519,7 @@ function AssistantMessage({
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
stale={false}
|
stale={false}
|
||||||
onApplied={() => handleSingleApply(part.action, idx)}
|
onApplied={() => { void handleSingleApply(part.action, idx); }}
|
||||||
externallyApplied={appliedActions.has(idx)}
|
externallyApplied={appliedActions.has(idx)}
|
||||||
defaultExpanded={true}
|
defaultExpanded={true}
|
||||||
/>
|
/>
|
||||||
|
|
@ -526,6 +564,7 @@ function AssistantMessageLoading({ currentStatus }: { currentStatus: 'thinking'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Messages({
|
export function Messages({
|
||||||
|
projectId,
|
||||||
messages,
|
messages,
|
||||||
streamingResponse,
|
streamingResponse,
|
||||||
loadingResponse,
|
loadingResponse,
|
||||||
|
|
@ -535,6 +574,7 @@ export function Messages({
|
||||||
toolCalling,
|
toolCalling,
|
||||||
toolQuery
|
toolQuery
|
||||||
}: {
|
}: {
|
||||||
|
projectId: string;
|
||||||
messages: z.infer<typeof CopilotMessage>[];
|
messages: z.infer<typeof CopilotMessage>[];
|
||||||
streamingResponse: string;
|
streamingResponse: string;
|
||||||
loadingResponse: boolean;
|
loadingResponse: boolean;
|
||||||
|
|
@ -584,6 +624,7 @@ export function Messages({
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
messageIndex={messageIndex}
|
messageIndex={messageIndex}
|
||||||
loading={loadingResponse}
|
loading={loadingResponse}
|
||||||
|
projectId={projectId}
|
||||||
onStatusBarChange={status => {
|
onStatusBarChange={status => {
|
||||||
// Only update for the last assistant message
|
// Only update for the last assistant message
|
||||||
if (messageIndex === displayMessages.length - 1) {
|
if (messageIndex === displayMessages.length - 1) {
|
||||||
|
|
|
||||||
|
|
@ -103,15 +103,6 @@ interface StateItem {
|
||||||
lastUpdatedAt: string;
|
lastUpdatedAt: string;
|
||||||
isLive: boolean;
|
isLive: boolean;
|
||||||
agentInstructionsChanged: boolean;
|
agentInstructionsChanged: boolean;
|
||||||
pendingTriggers?: Array<{
|
|
||||||
type: 'one_time' | 'recurring';
|
|
||||||
name: string;
|
|
||||||
scheduledTime?: string;
|
|
||||||
cron?: string;
|
|
||||||
input: {
|
|
||||||
messages: z.infer<typeof Message>[];
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
|
@ -153,28 +144,6 @@ export type Action = {
|
||||||
pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
|
pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
|
||||||
defaultModel?: string;
|
defaultModel?: string;
|
||||||
fromCopilot?: boolean;
|
fromCopilot?: boolean;
|
||||||
} | {
|
|
||||||
type: "add_one_time_trigger";
|
|
||||||
trigger: {
|
|
||||||
name: string;
|
|
||||||
scheduledTime: string;
|
|
||||||
input: {
|
|
||||||
messages: z.infer<typeof Message>[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
fromCopilot?: boolean;
|
|
||||||
} | {
|
|
||||||
type: "add_recurring_trigger";
|
|
||||||
trigger: {
|
|
||||||
name: string;
|
|
||||||
cron: string;
|
|
||||||
input: {
|
|
||||||
messages: z.infer<typeof Message>[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
fromCopilot?: boolean;
|
|
||||||
} | {
|
|
||||||
type: "clear_pending_triggers";
|
|
||||||
} | {
|
} | {
|
||||||
type: "select_agent";
|
type: "select_agent";
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -618,30 +587,6 @@ function reducer(state: State, action: Action): State {
|
||||||
draft.chatKey++;
|
draft.chatKey++;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "add_one_time_trigger": {
|
|
||||||
// Mark that we need to create a trigger - the actual API call will be handled outside the reducer
|
|
||||||
draft.pendingTriggers = draft.pendingTriggers || [];
|
|
||||||
draft.pendingTriggers.push({
|
|
||||||
type: 'one_time',
|
|
||||||
...action.trigger
|
|
||||||
});
|
|
||||||
draft.pendingChanges = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "add_recurring_trigger": {
|
|
||||||
// Mark that we need to create a trigger - the actual API call will be handled outside the reducer
|
|
||||||
draft.pendingTriggers = draft.pendingTriggers || [];
|
|
||||||
draft.pendingTriggers.push({
|
|
||||||
type: 'recurring',
|
|
||||||
...action.trigger
|
|
||||||
});
|
|
||||||
draft.pendingChanges = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "clear_pending_triggers": {
|
|
||||||
draft.pendingTriggers = [];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "delete_agent":
|
case "delete_agent":
|
||||||
// Remove the agent
|
// Remove the agent
|
||||||
draft.workflow.agents = draft.workflow.agents.filter(
|
draft.workflow.agents = draft.workflow.agents.filter(
|
||||||
|
|
@ -1370,38 +1315,6 @@ export function WorkflowEditor({
|
||||||
}
|
}
|
||||||
}, [state.present.agentInstructionsChanged, markAgentInstructionsChanged]);
|
}, [state.present.agentInstructionsChanged, markAgentInstructionsChanged]);
|
||||||
|
|
||||||
// Handle pending trigger creation
|
|
||||||
useEffect(() => {
|
|
||||||
if (state.present.pendingTriggers && state.present.pendingTriggers.length > 0) {
|
|
||||||
const processTriggers = async () => {
|
|
||||||
for (const trigger of state.present.pendingTriggers!) {
|
|
||||||
try {
|
|
||||||
if (trigger.type === 'one_time') {
|
|
||||||
const { createScheduledJobRule } = await import('@/app/actions/scheduled-job-rules.actions');
|
|
||||||
await createScheduledJobRule({
|
|
||||||
projectId,
|
|
||||||
input: trigger.input,
|
|
||||||
scheduledTime: trigger.scheduledTime!
|
|
||||||
});
|
|
||||||
} else if (trigger.type === 'recurring') {
|
|
||||||
const { createRecurringJobRule } = await import('@/app/actions/recurring-job-rules.actions');
|
|
||||||
await createRecurringJobRule({
|
|
||||||
projectId,
|
|
||||||
input: trigger.input,
|
|
||||||
cron: trigger.cron!
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to create ${trigger.type} trigger:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Clear pending triggers after processing
|
|
||||||
dispatch({ type: "clear_pending_triggers" });
|
|
||||||
};
|
|
||||||
processTriggers();
|
|
||||||
}
|
|
||||||
}, [state.present.pendingTriggers, projectId]);
|
|
||||||
|
|
||||||
function handleSelectAgent(name: string) {
|
function handleSelectAgent(name: string) {
|
||||||
dispatch({ type: "select_agent", name });
|
dispatch({ type: "select_agent", name });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ function CopilotStatusBar({
|
||||||
// Show real button when ready
|
// Show real button when ready
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleApplyAll}
|
onClick={() => { void handleApplyAll?.(); }}
|
||||||
disabled={allApplied}
|
disabled={allApplied}
|
||||||
className={`flex items-center gap-2 px-3 py-1 rounded-full font-medium text-xs transition-colors duration-200
|
className={`flex items-center gap-2 px-3 py-1 rounded-full font-medium text-xs transition-colors duration-200
|
||||||
${
|
${
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue