mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-30 10:56:29 +02:00
* feat: Add Copilot trigger creation support
- Add support for One-Time and Recurring triggers in Copilot
- Extend CopilotAssistantMessageActionPart schema with trigger config types
- Update Copilot instructions with trigger creation examples and guidelines
- Implement trigger action handling in messages.tsx component
- Add trigger icons (⏰ for one-time, 🔄 for recurring) in action cards
- Update workflow reducer to handle trigger creation via existing APIs
- Fix action parser to recognize trigger config types in comment format
- Add async trigger processing using createScheduledJobRule and createRecurringJobRule APIs
Users can now ask Copilot to create triggers with natural language requests like:
'Create a daily report trigger at 9 AM' or 'Set up a one-time reminder for next Friday'
* 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.
* refactor: route trigger actions via copilot helper
Keep workflow reducer synchronous by removing trigger jobs from the switch and moving job rule API calls into a dedicated helper in messages.tsx. Cache dynamic imports and guard types so Copilot Apply/Apply All handle trigger creation without touching reducer state.
* feat: Add current time to the copilot context
* added conext of triggers to the copilot along with being able to edit and delete triggers
* bug fix for deleting composio triggers
* Add the edit function that allows editing triggers and lets copilot edit triggers too without losing previous jobs
feat: Add update functionality for recurring and scheduled job rules
- Implemented update actions for recurring job rules and scheduled job rules, allowing users to modify existing rules with new input and scheduling configurations.
- Enhanced the UI components to support editing of job rules, including forms for both creating and updating rules.
- Updated the repository interfaces and MongoDB implementations to handle the new update operations for job rules.
This update improves the flexibility of managing job rules within the application.
* Add trigger context to copilot
feat: Enhance trigger management in Copilot
- Added functionality to search for relevant external triggers using the new `search_relevant_triggers` tool, allowing users to discover available triggers based on toolkit slugs and optional query keywords.
- Updated the Copilot context to include detailed descriptions of various external trigger toolkits, enhancing user guidance for trigger creation.
- Improved the overall trigger handling process, ensuring that users can effectively integrate external triggers into their workflows.
This update significantly enhances the Copilot's capabilities in managing and utilizing external triggers.
* 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.
* External trigger cant be edited so we delete and recreate for this
feat: Improve external trigger handling in Copilot
- Added validation for editing external triggers, ensuring users are informed that existing triggers must be deleted and recreated for changes.
- Updated documentation to clarify the limitations of external trigger modifications.
This update enhances user experience by providing clear guidance on managing external triggers within the Copilot interface.
* preventing message.tsx from ballooning up in size
feat: Refactor Copilot message handling and trigger actions
- Removed deprecated logic for loading scheduled and recurring job actions, streamlining the trigger action process.
- Integrated `useCopilotTriggerActions` hook to manage trigger setup and actions more efficiently.
- Enhanced parsing of action parts to improve handling of triggers and their configurations.
- Updated the UI to reflect changes in action handling, ensuring a smoother user experience.
This update optimizes the Copilot's ability to manage triggers and enhances the overall message processing flow.
* refactor: Simplify trigger filtering in Copilot
- Removed unnecessary filtering logic for triggers based on user queries, streamlining the search process.
- Updated response messages to clarify the context of displayed triggers, enhancing user understanding.
This update improves the efficiency of the trigger search functionality within the Copilot interface.
* Revert "refactor: Simplify trigger filtering in Copilot"
This reverts commit b3d041677c.
* simplify the filtering logic for triggers in copilot
feat: Enhance external trigger creation and search functionality in Copilot
- Introduced a critical flow for adding external triggers, emphasizing minimal user input and UI configuration.
- Updated documentation to clarify the external trigger creation process and provided examples for better guidance.
- Simplified the trigger search logic, ensuring users receive relevant results while maintaining clarity in response messages.
This update improves the user experience by streamlining external trigger management and enhancing the search capabilities within the Copilot interface.
---------
Co-authored-by: tusharmagar <tushmag@gmail.com>
438 lines
18 KiB
TypeScript
438 lines
18 KiB
TypeScript
'use client';
|
||
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, 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";
|
||
import { z } from "zod";
|
||
import { Action as WorkflowDispatch } from "@/app/projects/[projectId]/workflow/workflow_editor";
|
||
import { Panel } from "@/components/common/panel-common";
|
||
import { ComposeBoxCopilot } from "@/components/common/compose-box-copilot";
|
||
import { Messages } from "./components/messages";
|
||
import { CopyIcon, CheckIcon, PlusIcon, XIcon, InfoIcon, Sparkles } from "lucide-react";
|
||
import { useCopilot } from "./use-copilot";
|
||
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
||
import { SHOW_COPILOT_MARQUEE } from "@/app/lib/feature_flags";
|
||
import Image from "next/image";
|
||
import mascot from "@/public/mascot.png";
|
||
|
||
const CopilotContext = createContext<{
|
||
workflow: z.infer<typeof Workflow> | null;
|
||
dispatch: (action: any) => void;
|
||
}>({ workflow: null, dispatch: () => { } });
|
||
|
||
export function getAppliedChangeKey(messageIndex: number, actionIndex: number, field: string) {
|
||
return `${messageIndex}-${actionIndex}-${field}`;
|
||
}
|
||
|
||
interface AppProps {
|
||
projectId: string;
|
||
workflow: z.infer<typeof Workflow>;
|
||
dispatch: (action: any) => void;
|
||
chatContext?: any;
|
||
onCopyJson?: (data: { messages: any[] }) => void;
|
||
onMessagesChange?: (messages: z.infer<typeof CopilotMessage>[]) => void;
|
||
isInitialState?: boolean;
|
||
dataSources?: z.infer<typeof DataSource>[];
|
||
triggers?: z.infer<typeof TriggerSchemaForCopilot>[];
|
||
onTriggersUpdated?: () => Promise<void> | void;
|
||
}
|
||
|
||
const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message: string) => void }, AppProps>(function App({
|
||
projectId,
|
||
workflow,
|
||
dispatch,
|
||
chatContext = undefined,
|
||
onCopyJson,
|
||
onMessagesChange,
|
||
isInitialState = false,
|
||
dataSources,
|
||
triggers,
|
||
onTriggersUpdated,
|
||
}, ref) {
|
||
|
||
|
||
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
|
||
const [discardContext, setDiscardContext] = useState(false);
|
||
const [isLastInteracted, setIsLastInteracted] = useState(isInitialState);
|
||
const workflowRef = useRef(workflow);
|
||
const startRef = useRef<any>(null);
|
||
const cancelRef = useRef<any>(null);
|
||
const [statusBar, setStatusBar] = useState<any>(null);
|
||
|
||
// Always use effectiveContext for the user's current selection
|
||
const effectiveContext = discardContext ? null : chatContext;
|
||
|
||
// Context locking state
|
||
const [lockedContext, setLockedContext] = useState<any>(effectiveContext);
|
||
const [pendingContext, setPendingContext] = useState<any>(effectiveContext);
|
||
const [isStreaming, setIsStreaming] = useState(false);
|
||
|
||
// Keep workflow ref up to date
|
||
workflowRef.current = workflow;
|
||
|
||
// Copilot streaming state
|
||
const {
|
||
streamingResponse,
|
||
loading: loadingResponse,
|
||
toolCalling,
|
||
toolQuery,
|
||
error: responseError,
|
||
clearError: clearResponseError,
|
||
billingError,
|
||
clearBillingError,
|
||
start,
|
||
cancel
|
||
} = useCopilot({
|
||
projectId,
|
||
workflow: workflowRef.current,
|
||
context: effectiveContext,
|
||
dataSources: dataSources,
|
||
triggers: triggers
|
||
});
|
||
|
||
// Store latest start/cancel functions in refs
|
||
startRef.current = start;
|
||
cancelRef.current = cancel;
|
||
|
||
// Notify parent of message changes
|
||
useEffect(() => {
|
||
onMessagesChange?.(messages);
|
||
}, [messages, onMessagesChange]);
|
||
|
||
// Removed localStorage auto-start. Initial prompts are sent by parent via ref.
|
||
|
||
// Reset discardContext when chatContext changes
|
||
useEffect(() => {
|
||
setDiscardContext(false);
|
||
}, [chatContext]);
|
||
|
||
// Memoized handleUserMessage for useImperativeHandle and hooks
|
||
const handleUserMessage = useCallback((prompt: string) => {
|
||
// Before starting streaming, lock the context to the current pendingContext
|
||
setLockedContext(pendingContext);
|
||
setMessages(currentMessages => [...currentMessages, {
|
||
role: 'user',
|
||
content: prompt
|
||
}]);
|
||
setIsLastInteracted(true);
|
||
}, [setMessages, setIsLastInteracted, pendingContext, setLockedContext]);
|
||
|
||
// Effect for getting copilot response
|
||
useEffect(() => {
|
||
if (!messages.length || messages.at(-1)?.role !== 'user') return;
|
||
|
||
if (responseError) {
|
||
return;
|
||
}
|
||
|
||
const currentStart = startRef.current;
|
||
const currentCancel = cancelRef.current;
|
||
|
||
if (currentStart) {
|
||
currentStart(messages, (finalResponse: string) => {
|
||
setMessages(prev => [
|
||
...prev,
|
||
{
|
||
role: 'assistant',
|
||
content: finalResponse
|
||
}
|
||
]);
|
||
});
|
||
} else {
|
||
// startRef not yet ready; no-op
|
||
}
|
||
|
||
return () => currentCancel();
|
||
}, [messages, responseError]);
|
||
|
||
// --- CONTEXT LOCKING LOGIC ---
|
||
// Always update pendingContext to the latest effectiveContext
|
||
useEffect(() => {
|
||
setPendingContext(effectiveContext);
|
||
}, [effectiveContext]);
|
||
|
||
// Lock/unlock context based on streaming state
|
||
useEffect(() => {
|
||
if (loadingResponse) {
|
||
// Streaming started: lock context to the value at the start
|
||
setIsStreaming(true);
|
||
setLockedContext((prev: any) => prev ?? pendingContext); // lock to previous if already set, else to pending
|
||
} else {
|
||
// Streaming ended: update lockedContext to the last pendingContext
|
||
setIsStreaming(false);
|
||
setLockedContext(pendingContext);
|
||
}
|
||
}, [loadingResponse, pendingContext]);
|
||
|
||
// After streaming ends, update lockedContext live as effectiveContext changes
|
||
useEffect(() => {
|
||
if (!isStreaming) {
|
||
setLockedContext(effectiveContext);
|
||
}
|
||
// If streaming, do not update lockedContext
|
||
}, [effectiveContext, isStreaming]);
|
||
// --- END CONTEXT LOCKING LOGIC ---
|
||
|
||
const handleCopyChat = useCallback(() => {
|
||
if (onCopyJson) {
|
||
onCopyJson({
|
||
messages,
|
||
});
|
||
}
|
||
}, [messages, onCopyJson]);
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
handleCopyChat,
|
||
handleUserMessage
|
||
}), [handleCopyChat, handleUserMessage]);
|
||
|
||
// Memoized status bar change handler to prevent infinite update loop
|
||
const handleStatusBarChange = useCallback((status: any) => {
|
||
setStatusBar((prev: any) => {
|
||
// Shallow compare previous and next status
|
||
const next = { ...status, context: lockedContext };
|
||
const keys = Object.keys(next);
|
||
if (
|
||
prev &&
|
||
keys.every(key => prev[key] === next[key])
|
||
) {
|
||
return prev;
|
||
}
|
||
return next;
|
||
});
|
||
}, [lockedContext]);
|
||
|
||
return (
|
||
<CopilotContext.Provider value={{ workflow: workflowRef.current, dispatch }}>
|
||
<div className="h-full flex flex-col">
|
||
<div className="flex-1 overflow-auto">
|
||
{messages.length === 0 && (
|
||
<div className="flex flex-col items-center justify-center py-4 pointer-events-none">
|
||
{/* Replace Sparkles icon with mascot image */}
|
||
<Image src={mascot} alt="Rowboat Mascot" width={160} height={160} className="object-contain mb-3 animate-float" />
|
||
|
||
{/* Welcome/Intro Section */}
|
||
<div className="text-center max-w-md px-6 mb-3">
|
||
<h3 className="text-xl font-semibold text-zinc-700 dark:text-zinc-300 mb-2 text-center">
|
||
👋 Hi there!
|
||
</h3>
|
||
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-4 text-center">
|
||
I’m Skipper, your copilot for building agents and adding tools to them.
|
||
</p>
|
||
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-3 text-center">
|
||
Here's what you can do in Rowboat:
|
||
</p>
|
||
<div className="space-y-2 max-w-2xl mx-auto text-left">
|
||
<div className="flex items-start gap-3">
|
||
<span className="text-lg">⚡</span>
|
||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Build AI agents instantly with natural language.</span>
|
||
</div>
|
||
<div className="flex items-start gap-3">
|
||
<span className="text-lg">🔌</span>
|
||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Connect tools with one-click integrations.</span>
|
||
</div>
|
||
<div className="flex items-start gap-3">
|
||
<span className="text-lg">📂</span>
|
||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Power with knowledge by adding documents for RAG.</span>
|
||
</div>
|
||
<div className="flex items-start gap-3">
|
||
<span className="text-lg">🔄</span>
|
||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Automate workflows by setting up triggers and actions.</span>
|
||
</div>
|
||
<div className="flex items-start gap-3">
|
||
<span className="text-lg">🚀</span>
|
||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Deploy anywhere via API or SDK.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{SHOW_COPILOT_MARQUEE && (
|
||
<div className="relative mt-2 max-w-full px-8">
|
||
<div className="font-mono text-sm whitespace-nowrap text-blue-400/60 dark:text-blue-500/40 font-small inline-flex">
|
||
<div className="overflow-hidden w-0 animate-typing">What can I help you build?</div>
|
||
<div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor"> </div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
<Messages
|
||
projectId={projectId}
|
||
messages={messages}
|
||
streamingResponse={streamingResponse}
|
||
loadingResponse={loadingResponse}
|
||
workflow={workflowRef.current}
|
||
dispatch={dispatch}
|
||
onStatusBarChange={handleStatusBarChange}
|
||
toolCalling={toolCalling}
|
||
toolQuery={toolQuery}
|
||
triggers={triggers}
|
||
onTriggersUpdated={onTriggersUpdated}
|
||
/>
|
||
</div>
|
||
<div className="shrink-0 px-0 pb-10">
|
||
{responseError && (
|
||
<div className="mb-4 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex gap-2 justify-between items-center text-sm">
|
||
<p className="text-red-600 dark:text-red-400">{responseError}</p>
|
||
<Button
|
||
size="sm"
|
||
color="danger"
|
||
onClick={() => {
|
||
// remove the last assistant message, if any
|
||
setMessages(prev => {
|
||
const lastMessage = prev[prev.length - 1];
|
||
if (lastMessage?.role === 'assistant') {
|
||
return prev.slice(0, -1);
|
||
}
|
||
return prev;
|
||
});
|
||
clearResponseError();
|
||
}}
|
||
>
|
||
Retry
|
||
</Button>
|
||
</div>
|
||
)}
|
||
<ComposeBoxCopilot
|
||
handleUserMessage={handleUserMessage}
|
||
messages={messages}
|
||
loading={loadingResponse}
|
||
initialFocus={isInitialState}
|
||
shouldAutoFocus={isLastInteracted}
|
||
onFocus={() => setIsLastInteracted(true)}
|
||
onCancel={cancel}
|
||
statusBar={statusBar || { context: lockedContext }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<BillingUpgradeModal
|
||
isOpen={!!billingError}
|
||
onClose={clearBillingError}
|
||
errorMessage={billingError || ''}
|
||
/>
|
||
</CopilotContext.Provider>
|
||
);
|
||
});
|
||
|
||
App.displayName = 'App';
|
||
|
||
export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void }, {
|
||
projectId: string;
|
||
workflow: z.infer<typeof Workflow>;
|
||
chatContext?: z.infer<typeof CopilotChatContext>;
|
||
dispatch: (action: WorkflowDispatch) => void;
|
||
isInitialState?: boolean;
|
||
dataSources?: z.infer<typeof DataSource>[];
|
||
triggers?: z.infer<typeof TriggerSchemaForCopilot>[];
|
||
activePanel?: 'playground' | 'copilot';
|
||
onTogglePanel?: () => void;
|
||
onTriggersUpdated?: () => Promise<void> | void;
|
||
}>(({
|
||
projectId,
|
||
workflow,
|
||
chatContext = undefined,
|
||
dispatch,
|
||
isInitialState = false,
|
||
dataSources,
|
||
triggers,
|
||
activePanel,
|
||
onTogglePanel,
|
||
onTriggersUpdated,
|
||
}, ref) => {
|
||
console.log('🎪 Copilot wrapper component mounted:', {
|
||
projectId,
|
||
isInitialState,
|
||
activePanel,
|
||
chatContextType: chatContext?.type
|
||
});
|
||
|
||
const [copilotKey, setCopilotKey] = useState(0);
|
||
const [showCopySuccess, setShowCopySuccess] = useState(false);
|
||
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
|
||
const [billingError, setBillingError] = useState<string | null>(null);
|
||
const appRef = useRef<{ handleCopyChat: () => void; handleUserMessage: (message: string) => void }>(null);
|
||
|
||
function handleNewChat() {
|
||
setCopilotKey(prev => prev + 1);
|
||
setMessages([]);
|
||
}
|
||
|
||
function handleCopyJson(data: { messages: any[] }) {
|
||
const jsonString = JSON.stringify(data, null, 2);
|
||
navigator.clipboard.writeText(jsonString);
|
||
setShowCopySuccess(true);
|
||
setTimeout(() => {
|
||
setShowCopySuccess(false);
|
||
}, 2000);
|
||
}
|
||
|
||
// Expose handleUserMessage through ref
|
||
useImperativeHandle(ref, () => ({
|
||
handleUserMessage: (message: string) => {
|
||
const app = appRef.current as any;
|
||
if (app?.handleUserMessage) {
|
||
app.handleUserMessage(message);
|
||
}
|
||
}
|
||
}), []);
|
||
|
||
return (
|
||
<>
|
||
<Panel
|
||
variant="copilot"
|
||
tourTarget="copilot"
|
||
title={<div className="flex items-center gap-2 text-zinc-800 dark:text-zinc-200 font-semibold"><Sparkles className="w-4 h-4" /> Skipper</div>}
|
||
subtitle="Build your assistant"
|
||
rightActions={
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="primary"
|
||
size="sm"
|
||
onClick={handleNewChat}
|
||
className="bg-blue-50 text-blue-700 hover:bg-blue-100"
|
||
showHoverContent={true}
|
||
hoverContent="New chat"
|
||
>
|
||
<PlusIcon className="w-4 h-4" />
|
||
</Button>
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
onClick={() => appRef.current?.handleCopyChat()}
|
||
showHoverContent={true}
|
||
hoverContent={showCopySuccess ? "Copied" : "Copy JSON"}
|
||
>
|
||
{showCopySuccess ? (
|
||
<CheckIcon className="w-4 h-4" />
|
||
) : (
|
||
<CopyIcon className="w-4 h-4" />
|
||
)}
|
||
</Button>
|
||
</div>
|
||
}
|
||
>
|
||
<div className="h-full overflow-auto px-3 pt-4">
|
||
<App
|
||
key={copilotKey}
|
||
ref={appRef}
|
||
projectId={projectId}
|
||
workflow={workflow}
|
||
dispatch={dispatch}
|
||
chatContext={chatContext}
|
||
onCopyJson={handleCopyJson}
|
||
onMessagesChange={setMessages}
|
||
isInitialState={isInitialState}
|
||
dataSources={dataSources}
|
||
triggers={triggers}
|
||
onTriggersUpdated={onTriggersUpdated}
|
||
/>
|
||
</div>
|
||
</Panel>
|
||
</>
|
||
);
|
||
});
|
||
|
||
Copilot.displayName = 'Copilot';
|