diff --git a/apps/rowboat/app/lib/prebuilt-cards/twitter-sentiment.json b/apps/rowboat/app/lib/prebuilt-cards/twitter-sentiment.json index 03f78daa..51d6fb76 100644 --- a/apps/rowboat/app/lib/prebuilt-cards/twitter-sentiment.json +++ b/apps/rowboat/app/lib/prebuilt-cards/twitter-sentiment.json @@ -5,7 +5,7 @@ "type": "pipeline", "description": "Searches Twitter for tweets about a specified keywords.", "disabled": false, - "instructions": "## πŸ§‘β€πŸ’Ό Role:\nSearch Twitter for tweets about a given keyword within a specified time window.\n\n---\n## βš™οΈ Steps to Follow:\n1. Receive the keywords, `start_time`, and `end_time` from the parent agent.\n2. Use the [@tool:Search full archive of tweets](#mention) tool with each keyword as the query, and the provided `start_time` and `end_time`.\n3. Return the text of the tweets to the next agent in the pipeline.\n\n---\n## 🎯 Scope:\nβœ… In Scope:\n- Searching Twitter for tweets within a given time period.\n\n❌ Out of Scope:\n- Analyzing sentiment.\n- Interacting with the user directly.\n\n---\n## πŸ“‹ Guidelines:\nβœ”οΈ Dos:\n- Return only the tweet text.\n- Ensure `start_time` and `end_time` are correctly passed to the tool.\n\n🚫 Don'ts:\n- Do not perform sentiment analysis.\n- Do not interact with the user directly.", + "instructions": "## πŸ§‘β€πŸ’Ό Role:\nSearch Twitter for tweets about a given keyword within a specified time window.\n\n---\n## βš™οΈ Steps to Follow:\n1. Receive the keywords. Use [@variable:ResultCount](#mention) for the Twitter search and [@variable:LookbackInHours](#mention) to search Twitter.\n2. Use the [@tool:Search full archive of tweets](#mention) tool with each keyword as the query, and the provided `start_time` and `end_time`.\n3. Return the text of the tweets to the next agent in the pipeline.\n\n---\n## 🎯 Scope:\nβœ… In Scope:\n- Searching Twitter for tweets within a given time period.\n\n❌ Out of Scope:\n- Analyzing sentiment.\n- Interacting with the user directly.\n\n---\n## πŸ“‹ Guidelines:\nβœ”οΈ Dos:\n- Return only the tweet text.\n- Ensure `start_time` and `end_time` are correctly passed to the tool.\n\n🚫 Don'ts:\n- Do not perform sentiment analysis.\n- Do not interact with the user directly.", "model": "google/gemini-2.5-flash", "locked": false, "toggleAble": true, diff --git a/apps/rowboat/app/lib/project_templates.ts b/apps/rowboat/app/lib/project_templates.ts index be28137a..3226d1c6 100644 --- a/apps/rowboat/app/lib/project_templates.ts +++ b/apps/rowboat/app/lib/project_templates.ts @@ -12,21 +12,8 @@ function buildTemplates(): { [key: string]: z.infer } { templates['default'] = { name: 'Blank Template', description: 'A blank canvas to build your agents.', - startAgent: "Example Agent", - agents: [ - { - name: "Example Agent", - type: "conversation", - description: "An example agent", - instructions: "## πŸ§‘β€ Role:\nYou are an helpful customer support assistant\n\n---\n## βš™οΈ Steps to Follow:\n1. Ask the user what they would like help with\n2. Ask the user for their email address and let them know someone will contact them soon.\n\n---\n## 🎯 Scope:\nβœ… In Scope:\n- Asking the user their issue\n- Getting their email\n\n❌ Out of Scope:\n- Questions unrelated to customer support\n- If a question is out of scope, politely inform the user and avoid providing an answer.\n\n---\n## πŸ“‹ Guidelines:\nβœ”οΈ Dos:\n- ask user their issue\n\n❌ Don'ts:\n- don't ask user any other detail than email", - model: DEFAULT_MODEL, - toggleAble: true, - ragReturnType: "chunks", - ragK: 3, - controlType: "retain", - outputVisibility: "user_facing", - }, - ], + startAgent: "", + agents: [], prompts: [], tools: [ { diff --git a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx index 2a1eddff..ca765952 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx @@ -48,6 +48,8 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message isInitialState = false, dataSources, }, ref) { + + const [messages, setMessages] = useState[]>([]); const [discardContext, setDiscardContext] = useState(false); const [isLastInteracted, setIsLastInteracted] = useState(isInitialState); @@ -95,17 +97,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message onMessagesChange?.(messages); }, [messages, onMessagesChange]); - // Check for initial prompt in local storage and send it - useEffect(() => { - const prompt = localStorage.getItem(`project_prompt_${projectId}`); - if (prompt && messages.length === 0) { - localStorage.removeItem(`project_prompt_${projectId}`); - setMessages([{ - role: 'user', - content: prompt - }]); - } - }, [projectId, messages.length]); + // Removed localStorage auto-start. Initial prompts are sent by parent via ref. // Reset discardContext when chatContext changes useEffect(() => { @@ -134,15 +126,19 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message const currentStart = startRef.current; const currentCancel = cancelRef.current; - currentStart(messages, (finalResponse: string) => { - setMessages(prev => [ - ...prev, - { - role: 'assistant', - content: finalResponse - } - ]); - }); + if (currentStart) { + currentStart(messages, (finalResponse: string) => { + setMessages(prev => [ + ...prev, + { + role: 'assistant', + content: finalResponse + } + ]); + }); + } else { + // startRef not yet ready; no-op + } return () => currentCancel(); }, [messages, responseError]); @@ -269,7 +265,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message toolQuery={toolQuery} /> -
+
{responseError && (

{responseError}

@@ -322,8 +318,8 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void dispatch: (action: WorkflowDispatch) => void; isInitialState?: boolean; dataSources?: z.infer[]; - activePanel: 'playground' | 'copilot'; - onTogglePanel: () => void; + activePanel?: 'playground' | 'copilot'; + onTogglePanel?: () => void; }>(({ projectId, workflow, @@ -334,6 +330,13 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void activePanel, onTogglePanel, }, 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[]>([]); @@ -369,34 +372,7 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void -
- - -
-
- } + title={
Skipper
} subtitle="Build your assistant" rightActions={
diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx index 62eb2237..55f950ae 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/actions.tsx @@ -67,14 +67,14 @@ export function Action({ switch (action.config_type) { case 'agent': dispatch({ - type: 'update_agent', + type: 'update_agent_no_select', name: action.name, agent: changes }); break; case 'tool': dispatch({ - type: 'update_tool', + type: 'update_tool_no_select', name: action.name, tool: changes }); diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx index 08db50cd..6ca63979 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx @@ -222,7 +222,8 @@ function AssistantMessage({ agent: { name: action.name, ...action.config_changes - } + }, + fromCopilot: true }); break; } @@ -236,7 +237,8 @@ function AssistantMessage({ tool: { name: action.name, ...action.config_changes - } + }, + fromCopilot: true }); break; } @@ -246,7 +248,8 @@ function AssistantMessage({ prompt: { name: action.name, ...action.config_changes - } + }, + fromCopilot: true }); break; case 'pipeline': @@ -255,7 +258,8 @@ function AssistantMessage({ pipeline: { name: action.name, ...action.config_changes - } + }, + fromCopilot: true }); break; } @@ -263,14 +267,14 @@ function AssistantMessage({ switch (action.config_type) { case 'agent': dispatch({ - type: 'update_agent', + type: 'update_agent_no_select', name: action.name, agent: action.config_changes }); break; case 'tool': dispatch({ - type: 'update_tool', + type: 'update_tool_no_select', name: action.name, tool: action.config_changes }); diff --git a/apps/rowboat/app/projects/[projectId]/copilot/use-copilot.tsx b/apps/rowboat/app/projects/[projectId]/copilot/use-copilot.tsx index fb29ce10..dd11bfbf 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/use-copilot.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/use-copilot.tsx @@ -38,6 +38,7 @@ export function useCopilot({ projectId, workflow, context, dataSources }: UseCop const [billingError, setBillingError] = useState(null); const cancelRef = useRef<() => void>(() => { }); const responseRef = useRef(''); + const inFlightRef = useRef(false); function clearError() { setError(null); @@ -51,7 +52,19 @@ export function useCopilot({ projectId, workflow, context, dataSources }: UseCop messages: z.infer[], onDone: (finalResponse: string) => void, ) => { - if (!messages.length || messages.at(-1)?.role !== 'user') return; + + + if (!messages.length || messages.at(-1)?.role !== 'user') { + + return; + } + + // Prevent duplicate/concurrent starts (e.g., StrictMode double effects or remounts) + if (inFlightRef.current) { + + return; + } + inFlightRef.current = true; setStreamingResponse(''); responseRef.current = ''; @@ -61,16 +74,23 @@ export function useCopilot({ projectId, workflow, context, dataSources }: UseCop setLoading(true); try { + // Wait 2 rAF frames to let layout stabilize (avoids StrictMode/remount race on initial load) + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))); + const res = await getCopilotResponseStream(projectId, messages, workflow, context || null, dataSources); + // Check for billing error if ('billingError' in res) { + setLoading(false); setError(res.billingError); setBillingError(res.billingError); + inFlightRef.current = false; return; } + const eventSource = new EventSource(`/api/copilot-stream-response/${res.streamId}`); eventSource.onmessage = (event) => { @@ -102,24 +122,29 @@ export function useCopilot({ projectId, workflow, context, dataSources }: UseCop eventSource.close(); setLoading(false); onDone(responseRef.current); + inFlightRef.current = false; }); eventSource.onerror = () => { eventSource.close(); setError('Streaming failed'); setLoading(false); + inFlightRef.current = false; }; cancelRef.current = () => eventSource.close(); } catch (err) { + console.error('❌ Error in useCopilot.start:', err); setError('Failed to initiate stream'); setLoading(false); + inFlightRef.current = false; } }, [projectId, workflow, context, dataSources]); const cancel = useCallback(() => { cancelRef.current?.(); setLoading(false); + inFlightRef.current = false; }, []); return { diff --git a/apps/rowboat/app/projects/[projectId]/playground/app.tsx b/apps/rowboat/app/projects/[projectId]/playground/app.tsx index 29e230c2..8429f205 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/app.tsx @@ -28,8 +28,8 @@ export function App({ onPanelClick?: () => void; triggerCopilotChat?: (message: string) => void; isLiveWorkflow: boolean; - activePanel: 'playground' | 'copilot'; - onTogglePanel: () => void; + activePanel?: 'playground' | 'copilot'; + onTogglePanel?: () => void; onMessageSent?: () => void; }) { const [counter, setCounter] = useState(0); @@ -56,6 +56,8 @@ export function App({ } }, []); + const hasAgents = (workflow?.agents?.length || 0) > 0; + return ( <> -
- - -
+
+ + Chat
} - subtitle="Chat with your assistant" - rightActions={ + subtitle={hasAgents ? "Chat with your assistant" : "Create an agent to start chatting"} + rightActions={hasAgents ? (
- } + ) : ( + // Preserve header height when there are zero agents +
+ )} onClick={onPanelClick} >
- { getCopyContentRef.current = fn; }} - showDebugMessages={showDebugMessages} - triggerCopilotChat={triggerCopilotChat} - isLiveWorkflow={isLiveWorkflow} - onMessageSent={onMessageSent} - /> + {hasAgents ? ( + { getCopyContentRef.current = fn; }} + showDebugMessages={showDebugMessages} + triggerCopilotChat={triggerCopilotChat} + isLiveWorkflow={isLiveWorkflow} + onMessageSent={onMessageSent} + /> + ) : ( +
+
+
+ +
+
Create an agent to start chatting
+
Skipper can build agents for you!
+
+ +
+
+
+ )}
diff --git a/apps/rowboat/app/projects/[projectId]/playground/components/messages.tsx b/apps/rowboat/app/projects/[projectId]/playground/components/messages.tsx index ed84db8f..dd837e96 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/components/messages.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/components/messages.tsx @@ -2,6 +2,7 @@ import { Spinner } from "@heroui/react"; import { useMemo, useState } from "react"; import z from "zod"; +import Image from "next/image"; import { Workflow } from "@/app/lib/types/workflow_types"; import { WorkflowTool } from "@/app/lib/types/workflow_types"; import MarkdownContent from "@/app/lib/components/markdown-content"; @@ -179,7 +180,7 @@ function AssistantMessage({ /> )}
-
+
@@ -200,10 +201,13 @@ function AssistantMessage({ > - {`Image {img.truncated && (
@@ -225,15 +229,14 @@ function AssistantMessage({ ); } -function AssistantMessageLoading() { +function TypingIndicator() { return ( -
-
-
- - Generating... +
+
+
+
+
+
@@ -526,10 +529,13 @@ function ClientToolCall({ > - {`Tool {img.truncated && (
@@ -618,10 +624,13 @@ function ClientToolCall({
{imagePreviews.map((img, i) => (
- {`Tool {img.truncated && (
@@ -948,7 +957,7 @@ export function Messages({ } return null; })} - {loadingAssistantResponse && } + {loadingAssistantResponse && }
); } diff --git a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx index ab719c3f..9d7a4c8b 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx @@ -37,16 +37,32 @@ export function App({ const stored = window.localStorage.getItem(`workflow_mode_${initialProjectData.id}`); return stored === 'live' || stored === 'draft' ? stored : 'draft'; }); + const [autoPublishEnabled, setAutoPublishEnabled] = useState(() => { + if (typeof window === 'undefined') return true; // Default to auto-publish + const stored = window.localStorage.getItem(`auto_publish_${initialProjectData.id}`); + return stored !== null ? stored === 'true' : true; + }); const [project, setProject] = useState>(initialProjectData); const [dataSources, setDataSources] = useState[]>(initialDataSources); const [loading, setLoading] = useState(false); console.log('workflow app.tsx render'); + const handleToggleAutoPublish = (enabled: boolean) => { + setAutoPublishEnabled(enabled); + if (typeof window !== 'undefined') { + window.localStorage.setItem(`auto_publish_${initialProjectData.id}`, enabled.toString()); + } + }; + // choose which workflow to display - let workflow: z.infer | undefined = project?.draftWorkflow; - if (mode == 'live') { - workflow = project?.liveWorkflow; + let workflow: z.infer | undefined; + if (autoPublishEnabled) { + // In auto-publish mode, always use draft (since they're synced) + workflow = project?.draftWorkflow; + } else { + // Manual mode: use current logic + workflow = mode === 'live' ? project?.liveWorkflow : project?.draftWorkflow; } const reloadData = useCallback(async () => { @@ -132,6 +148,8 @@ export function App({ {!loading && project && workflow && (dataSources !== null) && Promise; publishing: boolean; isLive: boolean; + autoPublishEnabled: boolean; + onToggleAutoPublish: (enabled: boolean) => void; showCopySuccess: boolean; showBuildModeBanner: boolean; canUndo: boolean; canRedo: boolean; activePanel: 'playground' | 'copilot'; + viewMode: "two_agents_chat" | "two_agents_skipper" | "two_chat_skipper" | "three_all"; hasAgentInstructionChanges: boolean; hasPlaygroundTested: boolean; hasPublished: boolean; @@ -29,11 +32,12 @@ interface TopBarProps { onChangeMode: (mode: 'draft' | 'live') => void; onRevertToLive: () => void; onTogglePanel: () => void; + onSetViewMode: (mode: "two_agents_chat" | "two_agents_skipper" | "two_chat_skipper" | "three_all") => void; + hasAgents?: boolean; onUseAssistantClick: () => void; onStartNewChatAndFocus: () => void; onStartBuildTour?: () => void; onStartTestTour?: () => void; - onStartPublishTour?: () => void; onStartUseTour?: () => void; onShareWorkflow: () => void; shareUrl: string | null; @@ -47,11 +51,14 @@ export function TopBar({ onProjectNameCommit, publishing, isLive, + autoPublishEnabled, + onToggleAutoPublish, showCopySuccess, showBuildModeBanner, canUndo, canRedo, activePanel, + viewMode, hasAgentInstructionChanges, hasPlaygroundTested, hasPublished, @@ -63,11 +70,12 @@ export function TopBar({ onChangeMode, onRevertToLive, onTogglePanel, + onSetViewMode, + hasAgents = true, onUseAssistantClick, onStartNewChatAndFocus, onStartBuildTour, onStartTestTour, - onStartPublishTour, onStartUseTour, onShareWorkflow, shareUrl, @@ -76,26 +84,37 @@ export function TopBar({ const router = useRouter(); const params = useParams(); const projectId = typeof (params as any).projectId === 'string' ? (params as any).projectId : (params as any).projectId?.[0]; + + // Share modal state + const { isOpen: isShareModalOpen, onOpen: onShareModalOpen, onClose: onShareModalClose } = useDisclosure(); + + const handleShareClick = () => { + onShareWorkflow(); // Call the original share function to generate URL + onShareModalOpen(); // Open the modal + }; + // Progress bar steps with completion logic and current step detection const step1Complete = hasAgentInstructionChanges; const step2Complete = hasPlaygroundTested && hasAgentInstructionChanges; - const step3Complete = hasPublished && hasPlaygroundTested && hasAgentInstructionChanges; - const step4Complete = hasClickedUse && hasPublished && hasPlaygroundTested && hasAgentInstructionChanges; + // Keep publish as a prerequisite for Use completion, but remove it from the visual steps + // Mark "Use" complete as soon as a Use Assistant option is clicked + const step4Complete = hasClickedUse; - // Determine current step (first incomplete step) - const currentStep = !step1Complete ? 1 : !step2Complete ? 2 : !step3Complete ? 3 : !step4Complete ? 4 : null; + // Determine current step (first incomplete visual step: 1 -> 2 -> 4) + const currentStep = !step1Complete ? 1 : !step2Complete ? 2 : !step4Complete ? 4 : null; const progressSteps: ProgressStep[] = [ { id: 1, label: "Build: Ask the copilot to create your assistant. Add tools and connect data sources.", completed: step1Complete, isCurrent: currentStep === 1 }, { id: 2, label: "Test: Test out your assistant by chatting with it. Use 'Fix' and 'Explain' to improve it.", completed: step2Complete, isCurrent: currentStep === 2 }, - { id: 3, label: "Publish: Make it live with the Publish button. You can always switch back to draft.", completed: step3Complete, isCurrent: currentStep === 3 }, + // Removed the 'Publish' step from the progress bar { id: 4, label: "Use: Click the 'Use Assistant' button to chat, set triggers (like emails), or connect via API.", completed: step4Complete, isCurrent: currentStep === 4 }, ]; return ( + <>
-
+
{/* Project Name Editor */}
- {/* Show divider and CTA only in live view */} - {isLive &&
} - {isLive ? ( + {/* Mode pill and auto-publish checkbox */} +
+ + {/* Mode pill */} +
+ + + {autoPublishEnabled ? 'Live ' : (isLive ? 'Live ' : 'Draft')} + +
+ + {/* Auto-publish checkbox or Switch to draft button */} + {!autoPublishEnabled && isLive ? ( - ) : null} + ) : ( + !isLive && ( +
+ + Auto-publish + +
+ ) + )}
{/* Progress Bar - Center */} @@ -143,7 +184,6 @@ export function TopBar({ onStepClick={(step) => { if (step.id === 1 && onStartBuildTour) onStartBuildTour(); if (step.id === 2 && onStartTestTour) onStartTestTour(); - if (step.id === 3 && onStartPublishTour) onStartPublishTour(); if (step.id === 4 && onStartUseTour) onStartUseTour(); }} /> @@ -163,46 +203,122 @@ export function TopBar({
} - {!isLive && <> + {!isLive &&
- + - + - } +
} - {/* Deploy CTA - always visible */} + {/* View controls (hidden in live mode) */} + {!isLive && (
+ {(() => { + // Current visibility booleans + const showAgents = viewMode !== "two_chat_skipper"; + const showChat = viewMode !== "two_agents_skipper"; + const showSkipper = viewMode !== "two_agents_chat"; + + // Determine selected radio option + type RadioKey = 'show-all' | 'hide-agents' | 'hide-chat' | 'hide-skipper'; + let selectedKey: RadioKey = 'show-all'; + if (!(showAgents && showChat && showSkipper)) { + if (!showAgents) selectedKey = 'hide-agents'; + else if (!showChat) selectedKey = 'hide-chat'; + else if (!showSkipper) selectedKey = 'hide-skipper'; + } + + // Map radio selection to viewMode + const setByKey = (key: RadioKey) => { + switch (key) { + case 'show-all': + onSetViewMode('three_all'); + break; + case 'hide-agents': + onSetViewMode('two_chat_skipper'); + break; + case 'hide-chat': + onSetViewMode('two_agents_skipper'); + break; + case 'hide-skipper': + onSetViewMode('two_agents_chat'); + break; + } + }; + + // Disable rules + // When there are zero agents, allow only Show All and Hide Chat + const zeroAgents = !hasAgents; + const disableShowAll = false; // always allow switching to 3-pane view + const disableHideAgents = zeroAgents; // cannot hide agents if none exist + const disableHideChat = false; // allow hide chat even with zero agents (default) + const disableHideSkipper = zeroAgents; // keep skipper visible when no agents + + return ( + + + + + { + const key = Array.from(keys as Set)[0] as RadioKey; + const zeroAgents = !hasAgents; + // Allow only permitted options when zero agents + if (zeroAgents && key !== 'show-all' && key !== 'hide-chat') return; + if (key === 'hide-chat' && disableHideChat) return; + setByKey(key); + }}> + }>Show All + }>Hide Agents + }>Hide Chat + }>Hide Skipper + + + ); + })()} +
)} + + {/* Deploy CTA - conditional based on auto-publish mode */}
- {isLive ? ( + {autoPublishEnabled ? ( <> + {/* Auto-publish mode: Show Use Assistant button */} @@ -239,138 +355,291 @@ export function TopBar({ - {/* Live workflow label moved here */}
{publishing && } -
- - Live workflow +
+ + + + + + + } + onPress={onDownloadJSON} + > + Download JSON + + +
- - - - {shareUrl && ( - - - - )} - - -
) : ( - <> -
- - - + // Manual publish mode: Show current publish/live logic + isLive ? ( + <> + + + + + + } + onPress={() => { + onUseAssistantClick(); + onStartNewChatAndFocus(); + }} + > + Chat with Assistant + + } + onPress={() => { + onUseAssistantClick(); + if (projectId) { router.push(`/projects/${projectId}/config`); } + }} + > + API & SDK Settings + + } + onPress={() => { + onUseAssistantClick(); + if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } + }} + > + Manage Triggers + + + + +
+ {publishing && } +
+ + + + + + + } + onPress={onDownloadJSON} + > + Download JSON + + + +
+
+ ) : ( + // Draft mode in manual publish: Show publish button + <> +
+ {(!hasAgents) ? ( + + + + + + ) : ( - - - } - onPress={() => onChangeMode('live')} - > - View live version - - } - onPress={onRevertToLive} - className="text-red-600 dark:text-red-400" - > - Reset to live version - - - -
- - {/* Moved draft/live labels and download button here */} -
- {publishing && } - {isLive &&
- - Live workflow -
} - {!isLive &&
- - Draft workflow -
} - - - - {shareUrl && ( - - + )} + {hasAgents ? ( + + + + + + } + onPress={() => onChangeMode('live')} + > + View live version + + } + onPress={onRevertToLive} + className="text-red-600 dark:text-red-400" + > + Reset to live version + + + + ) : ( + + + + )} - - - -
- +
+ +
+ {publishing && } +
+ + + + + + + } + onPress={onDownloadJSON} + > + Download JSON + + + +
+
+ + ) )}
+ + {/* Share Modal */} + + + + Share Assistant + + +
+

+ Share this assistant with others using the URL below: +

+ {shareUrl ? ( +
+ + +
+ ) : ( +
+ + + Generating share URL... + +
+ )} +
+
+ + + +
+
+ ); } diff --git a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx index 27e7f02e..c8538636 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx @@ -565,7 +565,7 @@ export const EntityList = forwardRef< agents: true, tools: true, data: true, - prompts: false + prompts: true }); // Default sizes when panels are expanded diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index c4b62e73..72f1e94d 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -41,12 +41,50 @@ import { TopBar } from "./components/TopBar"; enablePatches(); +// View mode specific panel ratios +// To maintain same absolute width for entityList across modes, we need to calculate +// the percentage relative to visible panels only +const VIEW_MODE_RATIOS = { + three_all: { + // Three panel layout with equal distribution between chat and copilot + entityList: 25, // Agents panel takes 25% of total width + chatApp: 37.5, // Chat panel takes 37.5% of total width + copilot: 37.5 // Copilot panel takes 37.5% of total width + }, + two_agents_chat: { + // Two panel layout showing agents and chat + // entityList maintains same absolute width as three panel layout (25/62.5 = 40%) + entityList: 40, // Agents panel takes 40% of visible width + chatApp: 60, // Chat panel takes remaining 60% width + copilot: 0 // Copilot panel is hidden + }, + two_agents_skipper: { + // Two panel layout showing agents and copilot + // entityList maintains same absolute width as three panel layout (25/62.5 = 40%) + entityList: 40, // Agents panel takes 40% of visible width + chatApp: 0, // Chat panel is hidden + copilot: 60 // Copilot panel takes remaining 60% width + }, + two_chat_skipper: { + // Two panel layout showing chat and copilot with equal split + entityList: 0, // Agents panel is hidden + chatApp: 50, // Chat panel takes 50% width + copilot: 50 // Copilot panel takes 50% width + } +} as const; + +// Legacy PANEL_RATIOS for backward compatibility const PANEL_RATIOS = { entityList: 25, // Left panel chatApp: 40, // Middle panel copilot: 35 // Right panel } as const; +// Helper function to get panel ratios for current view mode +const getPanelRatios = (viewMode: "two_agents_chat" | "two_agents_skipper" | "two_chat_skipper" | "three_all") => { + return VIEW_MODE_RATIOS[viewMode]; +}; + interface StateItem { workflow: z.infer; publishing: boolean; @@ -89,19 +127,20 @@ export type Action = { } | { type: "add_agent"; agent: Partial>; + fromCopilot?: boolean; } | { type: "add_tool"; tool: Partial>; + fromCopilot?: boolean; } | { type: "add_prompt"; prompt: Partial>; -} | { - type: "add_prompt_no_select"; - prompt: Partial>; + fromCopilot?: boolean; } | { type: "add_pipeline"; pipeline: Partial>; defaultModel?: string; + fromCopilot?: boolean; } | { type: "select_agent"; name: string; @@ -128,10 +167,18 @@ export type Action = { type: "update_agent"; name: string; agent: Partial>; +} | { + type: "update_agent_no_select"; + name: string; + agent: Partial>; } | { type: "update_tool"; name: string; tool: Partial>; +} | { + type: "update_tool_no_select"; + name: string; + tool: Partial>; } | { type: "set_saving"; saving: boolean; @@ -363,6 +410,9 @@ function reducer(state: State, action: Action): State { newAgentName = `New agent ${draft.workflow.agents.filter((agent) => agent.name.startsWith("New agent")).length + 1}`; } + + const finalAgentName = action.agent.name || newAgentName; + draft.workflow?.agents.push({ name: newAgentName, type: "conversation", @@ -379,10 +429,19 @@ function reducer(state: State, action: Action): State { maxCallsPerParentAgent: 3, ...action.agent }); - draft.selection = { - type: "agent", - name: action.agent.name || newAgentName - }; + + // If this is the first agent or there's no start agent, set it as start agent + if (!draft.workflow?.startAgent || draft.workflow.agents.length === 1) { + draft.workflow.startAgent = finalAgentName; + } + + // Only set selection if not from Copilot + if (!action.fromCopilot) { + draft.selection = { + type: "agent", + name: action.agent.name || newAgentName + }; + } draft.pendingChanges = true; draft.chatKey++; break; @@ -404,10 +463,13 @@ function reducer(state: State, action: Action): State { mockTool: false, ...action.tool }); - draft.selection = { - type: "tool", - name: action.tool.name || newToolName - }; + // Only set selection if not from Copilot + if (!action.fromCopilot) { + draft.selection = { + type: "tool", + name: action.tool.name || newToolName + }; + } draft.pendingChanges = true; draft.chatKey++; break; @@ -424,27 +486,13 @@ function reducer(state: State, action: Action): State { prompt: "", ...action.prompt }); - draft.selection = { - type: "prompt", - name: action.prompt.name || newPromptName - }; - draft.pendingChanges = true; - draft.chatKey++; - break; - } - case "add_prompt_no_select": { - let newPromptName = "New Variable"; - if (draft.workflow?.prompts.some((prompt) => prompt.name === newPromptName)) { - newPromptName = `New Variable ${draft.workflow?.prompts.filter((prompt) => - prompt.name.startsWith("New Variable")).length + 1}`; + // Only set selection if not from Copilot + if (!action.fromCopilot) { + draft.selection = { + type: "prompt", + name: action.prompt.name || newPromptName + }; } - draft.workflow?.prompts.push({ - name: newPromptName, - type: "base_prompt", - prompt: "", - ...action.prompt - }); - // Don't set selection - this is the key difference draft.pendingChanges = true; draft.chatKey++; break; @@ -516,8 +564,8 @@ function reducer(state: State, action: Action): State { ...action.pipeline }); - // 4. βœ… Select the first agent for configuration - if (pipelineAgents.length > 0) { + // 4. βœ… Select the first agent for configuration (only if not from Copilot) + if (pipelineAgents.length > 0 && !action.fromCopilot) { draft.selection = { type: "agent", name: pipelineAgents[0] @@ -717,6 +765,18 @@ function reducer(state: State, action: Action): State { draft.chatKey++; break; } + case "update_agent_no_select": { + // Same as update_agent but do not change selection + if (action.agent.instructions !== undefined) { + draft.agentInstructionsChanged = true; + } + draft.workflow.agents = draft.workflow.agents.map((agent) => + agent.name === action.name ? { ...agent, ...action.agent } : agent + ); + draft.pendingChanges = true; + draft.chatKey++; + break; + } case "update_tool": // update tool data @@ -759,6 +819,13 @@ function reducer(state: State, action: Action): State { draft.pendingChanges = true; draft.chatKey++; break; + case "update_tool_no_select": + draft.workflow.tools = draft.workflow.tools.map((tool) => + tool.name === action.name ? { ...tool, ...action.tool } : tool + ); + draft.pendingChanges = true; + draft.chatKey++; + break; case "update_prompt": // update prompt data @@ -893,6 +960,8 @@ export function WorkflowEditor({ projectConfig, eligibleModels, isLive, + autoPublishEnabled, + onToggleAutoPublish, onChangeMode, onRevertToLive, onProjectToolsUpdated, @@ -911,6 +980,8 @@ export function WorkflowEditor({ projectConfig: z.infer; eligibleModels: z.infer | "*"; isLive: boolean; + autoPublishEnabled: boolean; + onToggleAutoPublish: (enabled: boolean) => void; onChangeMode: (mode: 'draft' | 'live') => void; onRevertToLive: () => void; onProjectToolsUpdated?: () => void; @@ -938,6 +1009,56 @@ export function WorkflowEditor({ } }); + // View mode state controls top-level layout visibility (not unmounting panes) + type ViewMode = "two_agents_chat" | "two_agents_skipper" | "two_chat_skipper" | "three_all"; + const [viewMode, setViewMode] = useState(() => { + if (typeof window === 'undefined') return "three_all"; + const fromUrl = new URLSearchParams(window.location.search).get('view'); + const valid: ViewMode[] = ["two_agents_chat", "two_agents_skipper", "two_chat_skipper", "three_all"]; + if (fromUrl && (valid as string[]).includes(fromUrl)) { + localStorage.setItem('workflow_view_mode', fromUrl); + return fromUrl as ViewMode; + } + return (localStorage.getItem('workflow_view_mode') as ViewMode) || "three_all"; + }); + + const updateViewMode = useCallback((mode: ViewMode) => { + setViewMode(mode); + + // Clear selection when switching to hide agents mode to close configuration panels + if (mode === 'two_chat_skipper') { + // Clear any active selection to close configuration panels + // All unselect actions set selection to null, so we can use any of them + dispatch({ type: "unselect_agent" }); + } + + if (typeof window !== 'undefined') { + localStorage.setItem('workflow_view_mode', mode); + const url = new URL(window.location.href); + url.searchParams.set('view', mode); + window.history.replaceState({}, '', url.toString()); + } + }, []); + + // 1) Auto-layout: when no agents exist, prefer Agents + Skipper + const prevAgentCountRef = useRef(state.present.workflow.agents.length); + useEffect(() => { + const count = state.present.workflow.agents.length; + // If live mode, another effect will pin Agents + Chat; skip here + if (!isLive) { + if (count === 0) { + // Only auto-pin to Agents+Skipper if user hasn't explicitly chosen 3-pane + if (viewMode !== 'two_agents_skipper' && viewMode !== 'three_all') { + updateViewMode('two_agents_skipper'); + } + } else if (prevAgentCountRef.current === 0 && count > 0) { + // 2) As soon as first agent is created from zero, switch to default (three panes) + updateViewMode('three_all'); + } + } + prevAgentCountRef.current = count; + }, [state.present.workflow.agents.length, isLive, updateViewMode, viewMode]); + const [chatMessages, setChatMessages] = useState[]>([]); const updateChatMessages = useCallback((messages: z.infer[]) => { setChatMessages(messages); @@ -956,7 +1077,6 @@ export function WorkflowEditor({ const [showTour, setShowTour] = useState(true); const [showBuildTour, setShowBuildTour] = useState(false); const [showTestTour, setShowTestTour] = useState(false); - const [showPublishTour, setShowPublishTour] = useState(false); const [showUseTour, setShowUseTour] = useState(false); // Centralized mode transition handler @@ -1068,10 +1188,13 @@ export function WorkflowEditor({ if (startNewChatRef.current) { startNewChatRef.current(); } - // Switch to playground (chat) mode and collapse left panel + // Ensure chat is visible and collapse left panel setActivePanel('playground'); + setViewMode((prev: ViewMode) => prev); + // Expand Chat to full view: hide Copilot panel and collapse Agents panel + updateViewMode('two_agents_chat'); setIsLeftPanelCollapsed(true); - }, []); + }, [updateViewMode, viewMode]); // Load agent order from localStorage on mount // useEffect(() => { @@ -1097,33 +1220,53 @@ export function WorkflowEditor({ // Function to trigger copilot chat const triggerCopilotChat = useCallback((message: string) => { setActivePanel('copilot'); + updateViewMode( + viewMode === 'three_all' ? 'three_all' : + (viewMode === 'two_agents_chat' ? 'two_agents_skipper' : 'two_chat_skipper') + ); // Small delay to ensure copilot is mounted setTimeout(() => { copilotRef.current?.handleUserMessage(message); }, 100); - }, []); + }, [updateViewMode, viewMode]); const handleOpenDataSourcesModal = useCallback(() => { entityListRef.current?.openDataSourcesModal(); }, []); - console.log(`workflow editor chat key: ${state.present.chatKey}`); - // Auto-show copilot and increment key when prompt is present + // Auto-show copilot and send initial prompt exactly once when present + const hasSentInitPromptRef = useRef(false); useEffect(() => { + if (hasSentInitPromptRef.current) return; const prompt = localStorage.getItem(`project_prompt_${projectId}`); console.log('init project prompt', prompt); - if (prompt) { - setActivePanel('copilot'); - } - }, [projectId]); + if (!prompt) return; + + // Mark as handled and remove immediately to avoid any other readers + hasSentInitPromptRef.current = true; + localStorage.removeItem(`project_prompt_${projectId}`); + + // Switch UI to show Copilot + setActivePanel('copilot'); + updateViewMode(viewMode === 'three_all' ? 'three_all' : (viewMode.includes('agents') ? 'two_agents_skipper' : 'two_chat_skipper')); + + // Allow layout to render Copilot, then send the prompt via ref + requestAnimationFrame(() => { + requestAnimationFrame(() => { + copilotRef.current?.handleUserMessage(prompt); + }); + }); + }, [projectId, updateViewMode, viewMode]); // Switch to playground when switching to live mode useEffect(() => { if (isLive) { setActivePanel('playground'); + // 3) In live mode, pin view to Agents + Chat + updateViewMode('two_agents_chat'); } - }, [isLive]); + }, [isLive, updateViewMode, viewMode]); // Reset initial state when user interacts with copilot or opens other menus useEffect(() => { @@ -1307,7 +1450,7 @@ export function WorkflowEditor({ // Modal-specific handlers that don't auto-select function handleAddPromptFromModal(prompt: Partial>) { - dispatch({ type: "add_prompt_no_select", prompt }); + dispatch({ type: "add_prompt", prompt, fromCopilot: true }); } function handleUpdatePromptFromModal(name: string, prompt: Partial>) { @@ -1462,10 +1605,17 @@ export function WorkflowEditor({ saveQueue.current = []; try { - if (isLive) { - return; - } else { + if (autoPublishEnabled) { + // Auto-publish mode: save to both draft and live await saveWorkflow(projectId, workflowToSave); + await publishWorkflow(projectId, workflowToSave); + } else { + // Manual mode: current logic + if (isLive) { + return; + } else { + await saveWorkflow(projectId, workflowToSave); + } } } finally { saving.current = false; @@ -1475,7 +1625,7 @@ export function WorkflowEditor({ dispatch({ type: "set_saving", saving: false }); } } - }, [isLive, projectId]); + }, [autoPublishEnabled, isLive, projectId]); useEffect(() => { if (state.present.pendingChanges && state.present.workflow) { @@ -1521,7 +1671,7 @@ export function WorkflowEditor({ }, [isLive, state.present.publishing, onChangeMode]); const WORKFLOW_MOD_ACTIONS = useRef(new Set([ - 'add_agent','add_tool','add_prompt','add_prompt_no_select','add_pipeline', + 'add_agent','add_tool','add_prompt','add_pipeline', 'update_agent','update_tool','update_prompt','update_prompt_no_select','update_pipeline', 'delete_agent','delete_tool','delete_prompt','delete_pipeline', 'toggle_agent','set_main_agent','reorder_agents','reorder_pipelines' @@ -1595,14 +1745,21 @@ export function WorkflowEditor({ }, [isLive, state.present.isLive]); function handleTogglePanel() { - if (isLive && activePanel === 'playground') { + if (isLive && (viewMode === 'two_agents_chat' || viewMode === 'two_chat_skipper' || viewMode === 'three_all')) { // User is trying to switch to Build mode in live mode handleModeTransition('draft', 'switch_draft'); setShowBuildModeBanner(true); // Auto-hide banner after 5 seconds setTimeout(() => setShowBuildModeBanner(false), 5000); } else { - setActivePanel(activePanel === 'playground' ? 'copilot' : 'playground'); + // Toggle between showing chat vs skipper within current context + if (viewMode === 'three_all') { + setActivePanel(activePanel === 'playground' ? 'copilot' : 'playground'); + return; + } + if (viewMode === 'two_agents_chat') updateViewMode('two_agents_skipper'); + else if (viewMode === 'two_agents_skipper') updateViewMode('two_agents_chat'); + else if (viewMode === 'two_chat_skipper') updateViewMode('two_chat_skipper'); } } @@ -1663,6 +1820,9 @@ export function WorkflowEditor({ } }; + const [isHydrated, setIsHydrated] = useState(false); + useEffect(() => { setIsHydrated(true); }, []); + return ( 0} canRedo={state.currentIndex < state.patches.length} activePanel={activePanel} + viewMode={viewMode} + hasAgents={state.present.workflow.agents.length > 0} hasAgentInstructionChanges={hasAgentInstructionChanges} hasPlaygroundTested={hasPlaygroundTested} hasPublished={hasPublished} @@ -1730,26 +1894,126 @@ export function WorkflowEditor({ onChangeMode={onChangeMode} onRevertToLive={handleRevertToLive} onTogglePanel={handleTogglePanel} + onSetViewMode={updateViewMode} onUseAssistantClick={markUseAssistantClicked} onStartNewChatAndFocus={handleStartNewChatAndFocus} - onStartBuildTour={() => setShowBuildTour(true)} - onStartTestTour={() => setShowTestTour(true)} - onStartPublishTour={() => { - if (isLive) { - handleModeTransition('draft', 'switch_draft'); - } - setShowPublishTour(true); + onStartBuildTour={() => { + // Ensure 3-pane layout first, then start tour after layout renders + updateViewMode('three_all'); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setShowBuildTour(true); + }); + }); + }} + onStartTestTour={() => { + updateViewMode('three_all'); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setShowTestTour(true); + }); + }); + }} + onStartUseTour={() => { + updateViewMode('three_all'); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setShowUseTour(true); + }); + }); }} - onStartUseTour={() => setShowUseTour(true)} /> - {/* Content Area */} - + {/* Content Area - hydration-safe layout */} + {!isHydrated ? ( + + {(viewMode !== 'two_chat_skipper') && ( +
+ +
+
+ )} + {(viewMode !== 'two_chat_skipper') && ( + + )} + {(viewMode === 'two_agents_chat' || viewMode === 'three_all') && ( + + {/* Minimal mount of Chat during SSR hydration */} +
+ + )} + {(viewMode === 'three_all') && ()} + {(viewMode === 'two_agents_skipper' || viewMode === 'three_all') && ( + +
+ + )} + {(viewMode === 'two_chat_skipper') && ( + <> +
+ +
+ + )} + + ) : ( + + {/* Agents (Entity List) column */} + {(viewMode !== 'two_chat_skipper') && ( +
- + )} + {(viewMode !== 'two_chat_skipper') && ( + + )} - {/* Config Panel - always rendered, visibility controlled */} - - {state.present.selection?.type === "agent" && agent.name === state.present.selection!.name)}-${configKey}`} + {/* Playground column - always mounted; hide via viewMode */} + + agent.name === state.present.selection!.name)!} - usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))} - usedPipelineNames={new Set((state.present.workflow.pipelines || []).map((pipeline) => pipeline.name))} - agents={state.present.workflow.agents} - tools={state.present.workflow.tools} - prompts={state.present.workflow.prompts} - dataSources={dataSources} - handleUpdate={(update) => { dispatchGuarded({ type: "update_agent", name: state.present.selection!.name, agent: update }); }} - handleClose={handleUnselectAgent} - useRag={useRag} + messageSubscriber={updateChatMessages} + onPanelClick={handlePlaygroundClick} triggerCopilotChat={triggerCopilotChat} - eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels} - onOpenDataSourcesModal={handleOpenDataSourcesModal} - />} - {state.present.selection?.type === "tool" && (() => { - const selectedTool = state.present.workflow.tools.find( - (tool) => tool.name === state.present.selection!.name - ); - return tool.name !== state.present.selection!.name).map((tool) => tool.name), - ])} - handleUpdate={(update) => { dispatchGuarded({ type: "update_tool", name: state.present.selection!.name, tool: update }); }} - handleClose={handleUnselectTool} - />; - })()} - {state.present.selection?.type === "prompt" && prompt.name === state.present.selection!.name)!} - agents={state.present.workflow.agents} - tools={state.present.workflow.tools} - prompts={state.present.workflow.prompts} - usedPromptNames={new Set(state.present.workflow.prompts.filter((prompt) => prompt.name !== state.present.selection!.name).map((prompt) => prompt.name))} - handleUpdate={(update) => { dispatchGuarded({ type: "update_prompt", name: state.present.selection!.name, prompt: update }); }} - handleClose={handleUnselectPrompt} - />} - {state.present.selection?.type === "datasource" && dispatch({ type: "unselect_datasource" })} - onDataSourceUpdate={onDataSourcesUpdated} - />} - {state.present.selection?.type === "pipeline" && pipeline.name === state.present.selection!.name)!} - usedPipelineNames={new Set((state.present.workflow.pipelines || []).filter((pipeline) => pipeline.name !== state.present.selection!.name).map((pipeline) => pipeline.name))} - usedAgentNames={new Set(state.present.workflow.agents.map((agent) => agent.name))} - agents={state.present.workflow.agents} - pipelines={state.present.workflow.pipelines || []} - handleUpdate={handleUpdatePipeline.bind(null, state.present.selection.name)} - handleClose={() => dispatch({ type: "unselect_pipeline" })} - />} - {state.present.selection?.type === "visualise" && ( - -
- Agent Graph Visualizer -
- - - -
- } - > -
- + isLiveWorkflow={isLive} + activePanel={activePanel} + onTogglePanel={handleTogglePanel} + onMessageSent={markPlaygroundTested} + /> + {/* Config overlay above Playground when selection open */} + {state.present.selection && viewMode !== 'two_agents_skipper' && ( +
+
+ {state.present.selection?.type === "agent" && agent.name === state.present.selection!.name)}-${configKey}`} + projectId={projectId} + workflow={state.present.workflow} + agent={state.present.workflow.agents.find((agent) => agent.name === state.present.selection!.name)!} + usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))} + usedPipelineNames={new Set((state.present.workflow.pipelines || []).map((pipeline) => pipeline.name))} + agents={state.present.workflow.agents} + tools={state.present.workflow.tools} + prompts={state.present.workflow.prompts} + dataSources={dataSources} + handleUpdate={(update) => { dispatchGuarded({ type: "update_agent", name: state.present.selection!.name, agent: update }); }} + handleClose={handleUnselectAgent} + useRag={useRag} + triggerCopilotChat={triggerCopilotChat} + eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels} + onOpenDataSourcesModal={handleOpenDataSourcesModal} + />} + {state.present.selection?.type === "tool" && (() => { + const selectedTool = state.present.workflow.tools.find( + (tool) => tool.name === state.present.selection!.name + ); + return tool.name !== state.present.selection!.name).map((tool) => tool.name), + ])} + handleUpdate={(update) => { dispatchGuarded({ type: "update_tool", name: state.present.selection!.name, tool: update }); }} + handleClose={handleUnselectTool} + />; + })()} + {state.present.selection?.type === "prompt" && prompt.name === state.present.selection!.name)!} + agents={state.present.workflow.agents} + tools={state.present.workflow.tools} + prompts={state.present.workflow.prompts} + usedPromptNames={new Set(state.present.workflow.prompts.filter((prompt) => prompt.name !== state.present.selection!.name).map((prompt) => prompt.name))} + handleUpdate={(update) => { dispatchGuarded({ type: "update_prompt", name: state.present.selection!.name, prompt: update }); }} + handleClose={handleUnselectPrompt} + />} + {state.present.selection?.type === "datasource" && dispatch({ type: "unselect_datasource" })} + onDataSourceUpdate={onDataSourcesUpdated} + />} + {state.present.selection?.type === "pipeline" && pipeline.name === state.present.selection!.name)!} + usedPipelineNames={new Set((state.present.workflow.pipelines || []).filter((pipeline) => pipeline.name !== state.present.selection!.name).map((pipeline) => pipeline.name))} + usedAgentNames={new Set(state.present.workflow.agents.map((agent) => agent.name))} + agents={state.present.workflow.agents} + pipelines={state.present.workflow.pipelines || []} + handleUpdate={handleUpdatePipeline.bind(null, state.present.selection.name)} + handleClose={() => dispatch({ type: "unselect_pipeline" })} + />} + {state.present.selection?.type === "visualise" && ( +
Agent Graph Visualizer
}> +
+ +
+ + )}
- +
)}
- {/* Second handle - between config and chat panels */} - - - {/* ChatApp/Copilot Panel - always visible */} - -
- -
-
- 0 - ? { type: 'chat', messages: chatMessages } - : undefined - } - isInitialState={isInitialState} - dataSources={dataSources} - activePanel={activePanel} - onTogglePanel={handleTogglePanel} - /> -
+ + {/* Divider between playground and copilot when both visible */} + {(viewMode === 'three_all' || viewMode === 'two_chat_skipper') && ( + + )} + + {/* Copilot column - always mounted; hide via viewMode */} + + 0 + ? { type: 'chat', messages: chatMessages } + : undefined + } + isInitialState={isInitialState} + dataSources={dataSources} + activePanel={activePanel} + onTogglePanel={handleTogglePanel} + /> + {/* Config overlay above Copilot when agents + skipper layout is active */} + {state.present.selection && viewMode === 'two_agents_skipper' && ( +
+
+ {state.present.selection?.type === "agent" && agent.name === state.present.selection!.name)}-${configKey}`} + projectId={projectId} + workflow={state.present.workflow} + agent={state.present.workflow.agents.find((agent) => agent.name === state.present.selection!.name)!} + usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))} + usedPipelineNames={new Set((state.present.workflow.pipelines || []).map((pipeline) => pipeline.name))} + agents={state.present.workflow.agents} + tools={state.present.workflow.tools} + prompts={state.present.workflow.prompts} + dataSources={dataSources} + handleUpdate={(update) => { dispatchGuarded({ type: "update_agent", name: state.present.selection!.name, agent: update }); }} + handleClose={handleUnselectAgent} + useRag={useRag} + triggerCopilotChat={triggerCopilotChat} + eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels} + onOpenDataSourcesModal={handleOpenDataSourcesModal} + />} + {state.present.selection?.type === "tool" && (() => { + const selectedTool = state.present.workflow.tools.find( + (tool) => tool.name === state.present.selection!.name + ); + return tool.name !== state.present.selection!.name).map((tool) => tool.name), + ])} + handleUpdate={(update) => { dispatchGuarded({ type: "update_tool", name: state.present.selection!.name, tool: update }); }} + handleClose={handleUnselectTool} + />; + })()} + {state.present.selection?.type === "prompt" && prompt.name === state.present.selection!.name)!} + agents={state.present.workflow.agents} + tools={state.present.workflow.tools} + prompts={state.present.workflow.prompts} + usedPromptNames={new Set(state.present.workflow.prompts.filter((prompt) => prompt.name !== state.present.selection!.name).map((prompt) => prompt.name))} + handleUpdate={(update) => { dispatchGuarded({ type: "update_prompt", name: state.present.selection!.name, prompt: update }); }} + handleClose={handleUnselectPrompt} + />} + {state.present.selection?.type === "datasource" && dispatch({ type: "unselect_datasource" })} + onDataSourceUpdate={onDataSourcesUpdated} + />} + {state.present.selection?.type === "pipeline" && pipeline.name === state.present.selection!.name)!} + usedPipelineNames={new Set((state.present.workflow.pipelines || []).filter((pipeline) => pipeline.name !== state.present.selection!.name).map((pipeline) => pipeline.name))} + usedAgentNames={new Set(state.present.workflow.agents.map((agent) => agent.name))} + agents={state.present.workflow.agents} + pipelines={state.present.workflow.pipelines || []} + handleUpdate={handleUpdatePipeline.bind(null, state.present.selection.name)} + handleClose={() => dispatch({ type: "unselect_pipeline" })} + />} + {state.present.selection?.type === "visualise" && ( +
Agent Graph Visualizer
}> +
+ +
+ + )} +
+
+ )}
+ + )} {USE_PRODUCT_TOUR && showTour && ( { - if (index === 0) setActivePanel('playground'); + if (index === 0) { + // Ensure Chat is focused and any middle-pane detail overlay is dismissed + setActivePanel('playground'); + dispatch({ type: 'unselect_agent' }); + } if (index === 1) setActivePanel('copilot'); }} onComplete={() => setShowTestTour(false)} @@ -1997,21 +2324,16 @@ export function WorkflowEditor({ { target: 'conversations', title: 'Step 5/5', content: 'Conversations: see all past interactions in one place, including manual chats, trigger activity, and API calls.' }, ]} onStepChange={(index) => { - if (index === 0) setActivePanel('playground'); + if (index === 0) { + // Ensure Chat is focused and any middle-pane detail overlay is dismissed + setActivePanel('playground'); + dispatch({ type: 'unselect_agent' }); + } }} onComplete={() => setShowUseTour(false)} /> )} - {showPublishTour && ( - setShowPublishTour(false)} - /> - )} + {/* Revert to Live Confirmation Modal */} diff --git a/apps/rowboat/components/common/compose-box-copilot.tsx b/apps/rowboat/components/common/compose-box-copilot.tsx index d4abe3d9..d2ddd4d2 100644 --- a/apps/rowboat/components/common/compose-box-copilot.tsx +++ b/apps/rowboat/components/common/compose-box-copilot.tsx @@ -132,7 +132,7 @@ export function ComposeBoxCopilot({ scale-100 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:scale-95 hover:shadow-md dark:hover:shadow-indigo-950/10 - mb-0.5 + mb-1.5 mr-2 `} > {loading ? ( diff --git a/apps/rowboat/components/common/panel-common.tsx b/apps/rowboat/components/common/panel-common.tsx index 2a6e18c7..9281df07 100644 --- a/apps/rowboat/components/common/panel-common.tsx +++ b/apps/rowboat/components/common/panel-common.tsx @@ -83,14 +83,13 @@ export function Panel({ >
{ const btn = document.getElementById(`section-card-header-${title && typeof title === 'string' ? title : ''}`); if (btn) { - console.log('SectionCard header button:', btn, btn.getBoundingClientRect(), window.getComputedStyle(btn)); const chevron = btn.querySelector('svg'); if (chevron) { - console.log('Chevron:', chevron, chevron.getBoundingClientRect(), window.getComputedStyle(chevron)); + // Chevron positioning logic can go here if needed } const iconEl = btn.querySelector('.section-card-icon'); if (iconEl) { - console.log('Icon:', iconEl, iconEl.getBoundingClientRect(), window.getComputedStyle(iconEl)); + // Icon positioning logic can go here if needed } const label = btn.querySelector('span'); if (label) { - console.log('Label:', label, label.getBoundingClientRect(), window.getComputedStyle(label)); + // Label positioning logic can go here if needed } } }, [title]); diff --git a/apps/rowboat/components/ui/progress-bar.tsx b/apps/rowboat/components/ui/progress-bar.tsx index dad9e650..5bfd79b3 100644 --- a/apps/rowboat/components/ui/progress-bar.tsx +++ b/apps/rowboat/components/ui/progress-bar.tsx @@ -28,28 +28,23 @@ export function ProgressBar({ steps, className, onStepClick }: ProgressBarProps) }; return ( -
- + {step.shortLabel ?? getShortLabel(step.label)}
diff --git a/apps/rowboat/src/application/lib/copilot/copilot_multi_agent.ts b/apps/rowboat/src/application/lib/copilot/copilot_multi_agent.ts index 3b98903e..c3b9d196 100644 --- a/apps/rowboat/src/application/lib/copilot/copilot_multi_agent.ts +++ b/apps/rowboat/src/application/lib/copilot/copilot_multi_agent.ts @@ -39,7 +39,7 @@ When the user asks you to create agents for a multi-agent system, you should fol 1. Understand the user’s intent β€” what they want the workflow to achieve. Plan accordingly to build an elegant and efficient system. 2. Identify required tools - if the user mentions specific tasks (e.g. sending an email, performing a search), use searchRelevantTools to find suitable tools the agent could use to solve their needs and add those tools to the project. Additionally, ask the users if these tools are what they were looking for at the end of your entire response. -3. Create a first draft of a new agent for each step in the plan. If there is an example agent, you must start off by editing this into the Hub agent. Attach all tools to the relevant agents. +3. Create a first draft of a new agent for each step in the plan. You must always ensure to set a start agent when creating a multi-agent system. Attach all tools to the relevant agents. 4. Describe your work β€” briefly summarise what you've done at the end of your turn. It is good practice to add tools first and then agents @@ -54,6 +54,7 @@ Agents fall into two main types: 1. Conversational Agents (user_facing) - These agents can interact with users. - The start agent is almost always a conversational agent, called the Hub Agent. It orchestrates the overall workflow and directs task execution. +- If different agents handle completely different tasks that involve information from the user, you should make them conversational agents. - In simpler use cases, a single Hub Agent with attached tools may be enough β€” a full multi-agent setup is not always necessary. - Core responsibilities: - Break down the user's query into subtasks @@ -77,6 +78,8 @@ CRITICAL: Always include these required fields when creating agents: - For task agents: "outputVisibility": "internal", "controlType": "relinquish_to_parent" - For conversational agents: "outputVisibility": "user_facing", "controlType": "retain" +CRITICAL: When creating a multi-agent system, you MUST always set a start agent. Use the action "set_main_agent" or "edit" with "config_type": "start_agent" to set the start agent to the main conversational agent (usually the Hub agent). + However, there are some important things you need to instruct the individual agents when they call other agents (you need to customize the below to the specific agent and its): - SEQUENTIAL TRANSFERS AND RESPONSES: diff --git a/apps/rowboat/src/application/lib/copilot/current_workflow.ts b/apps/rowboat/src/application/lib/copilot/current_workflow.ts index d0fabab1..6dde4f9f 100644 --- a/apps/rowboat/src/application/lib/copilot/current_workflow.ts +++ b/apps/rowboat/src/application/lib/copilot/current_workflow.ts @@ -7,7 +7,7 @@ The design of the multi-agent system is represented by the following JSON schema {workflow_schema} \`\`\` -If the workflow has an 'Example Agent' as the main agent, it means the user is yet to create the main agent. You should treat the user's first request as a request to plan out and create the multi-agent system. +If the workflow has no agents or an empty startAgent, it means the user is yet to create their multi-agent system. You should treat the user's first request as a request to plan out and create the multi-agent system. When creating agents, you must always set a start agent. --- `; \ No newline at end of file diff --git a/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts b/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts index 946e48b7..32952654 100644 --- a/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts +++ b/apps/rowboat/src/application/lib/copilot/example_multi_agent_1.ts @@ -116,14 +116,14 @@ I'll add the suggested tools for Google Calendar, web search, and email: ### 2. Create Agents #### a. Hub Agent (Meeting Assistant Hub) -I'll edit the Example Agent to become the hub agent: +I'll create the hub agent: \`\`\`copilot_change -// action: edit +// action: create_new // config_type: agent -// name: Example Agent +// name: Meeting Assistant Hub { - "change_description": "Transformed Example Agent into the main hub agent orchestrating the meeting summary workflow.", + "change_description": "Created the main hub agent orchestrating the meeting summary workflow.", "config_changes": { "name": "Meeting Assistant Hub", "type": "conversation", @@ -223,6 +223,20 @@ I'll edit the Example Agent to become the hub agent: --- +### 6. Set the Start Agent + +\`\`\`copilot_change +// action: edit +// config_type: start_agent +// name: Meeting Assistant Hub +{ + "change_description": "Set the Meeting Assistant Hub as the start agent for the workflow.", + "config_changes": {} +} +\`\`\` + +--- + Once you review and apply the changes, you can try out a basic chat first. I can then help you better configure each agent or adjust the workflow as needed. Would you like to customize any step or add more details? --- @@ -277,14 +291,14 @@ I'm adding the "Get document by id" tool to fetch the content of a Google Doc by } \`\`\` -I'm replacing the Example Agent with a user-facing agent that fetches a Google Doc by ID and answers questions based on its content: +I'm creating a user-facing agent that fetches a Google Doc by ID and answers questions based on its content: \`\`\`copilot_change -// action: edit +// action: create_new // config_type: agent -// name: Example Agent +// name: Google Doc QnA Assistant { - "change_description": "Replaced Example Agent with a user-facing agent that fetches a Google Doc by ID and answers user questions based only on its content.", + "change_description": "Created a user-facing agent that fetches a Google Doc by ID and answers user questions based only on its content.", "config_changes": { "name": "Google Doc QnA Assistant", "type": "conversation", @@ -297,6 +311,16 @@ I'm replacing the Example Agent with a user-facing agent that fetches a Google D } \`\`\` +\`\`\`copilot_change +// action: edit +// config_type: start_agent +// name: Google Doc QnA Assistant +{ + "change_description": "Set the Google Doc QnA Assistant as the start agent for the workflow.", + "config_changes": {} +} +\`\`\` + Once you review and apply the changes, you can try out a basic chat by providing a Google Doc ID and a question. I can then help you further refine the assistant if needed. --- @@ -1078,4 +1102,184 @@ Yes, I can delete the Meeting Prep Pipeline. --- +### Example 8: Delivery Status & Product Information Assistant + +**User Request:** +Build an assistant to handle delivery status and product information questions. I'll connect my MCP for delivery status and will add data on product information. + +**Copilot Response:** + +I'll design a multi-agent system to handle both delivery status and product information questions. Here’s my plan: +Plan & Agent Decomposition: +1. Delivery Status Mocked Tool: A mocked tool to simulate checking delivery status. +2. Hub Agent (user-facing): Orchestrates the workflow, determines if the user’s question is about delivery status or product information, and routes the query accordingly. +3. Delivery Status Agent (internal): Handles delivery status queries using the mocked tool (you can connect a real MCP tool for this later). +4. Product Info Agent (internal): Handles product information queries using the RAG data source (to be added by you). + +Next Steps: +- I’ll create the agents and set the Hub Agent as the start agent. +- Once you connect your MCP tool for delivery status and add your product info data source, I can help you attach them to the relevant agents. + +--- + +### 1. Add Required Tools + +I'll add a mocked tool for getting delivery status. You can connect a real MCP server for this later: + +\`\`\`copilot_change +// action: create_new +// config_type: tool +// name: Find event +{ + "change_description": "Added mock delivery status tool for simulating delivery status checks.", + "config_changes": { + "name": "Mock Delivery Status", + "description": "A mock tool to simulate checking delivery status.", + "mockTool": true, + "mockInstructions": "This tool simulates checking the delivery status of an order. It will always return a predefined delivery status message.", + "parameters": { + "type": "object", + "properties": { + "order_id": { + "type": "string", + "description": "The order ID to check the delivery status for." + } + }, + "required": [ + "order_id" + ] + } + } +} +\`\`\` + +--- + +### 2. Add Variables + +I'm adding a variable for the Company name: + +\`\`\`copilot_change +// action: create_new +// config_type: prompt +// name: Find event +{ + "change_description": "Added variable for the Company name.", + "config_changes": { + "name": "Company name", + "type": "base_prompt", + "prompt": "" + } +} +\`\`\` + +I'm adding another variable for the assistant name: + +\`\`\`copilot_change +// action: create_new +// config_type: prompt +// name: Find event +{ + "change_description": "Added variable for the Assistant name.", + "config_changes": { + "name": "Assistant name", + "type": "base_prompt", + "prompt": "" + } + } +\`\`\` + +--- + +### 3. Create Agents + +#### a. Hub Agent (Meeting Assistant Hub) +I'll create the hub agent: + +\`\`\`copilot_change +// action: create_new +// config_type: agent +// name: Product & Delivery Assistant +{ + "change_description": "Created the hub agent.", + "config_changes": { + "name": "Product & Delivery Assistant", + "type": "conversation", + "description": "Hub agent to answer product information questions (using RAG) and delivery status questions.", + "instructions": "## πŸ§‘β€πŸ’Ό Role:\nYou are the hub agent responsible for orchestrating responses to product information and delivery status questions.\n\n---\n## βš™οΈ Steps to Follow:\n1. Greet the user and ask how you can help. Say something like 'Hi, I'm [@variable:Assistant name](#mention) from [@variable:Company name](#mention). How can I help you today?'\n2. Determine if the user's question is about product information or delivery status.\n3. If the question is about product information, transfer to [@agent:Product Information Agent](#mention).\n4. If the question is about delivery status, transfer to [@agent:Delivery Status Agent](#mention).\n5. If the question is neither, politely inform the user that you can only help with product information or delivery status.\n6. Return the final answer to the user.\n\n---\n## 🎯 Scope:\nβœ… In Scope:\n- Routing product information questions.\n- Routing delivery status questions.\n\n❌ Out of Scope:\n- Directly answering product or delivery questions.\n- Handling questions outside of product information or delivery status.\n\n---\n## πŸ“‹ Guidelines:\nβœ”οΈ Dos:\n- Clearly identify the type of user query.\n- Route to the correct agent.\n\n🚫 Don'ts:\n- Do not attempt to answer questions directly.\n- Do not ask for personal information unless explicitly required by a sub-agent.\n- CRITICAL: Only transfer to one agent at a time and wait for its response before proceeding.\n\n", + "model": "google/gemini-2.5-flash", + "toggleAble": true, + "ragReturnType": "chunks", + "ragK": 3, + "outputVisibility": "user_facing", + "controlType": "retain" + } +} +\`\`\` + +#### b. Product Information Agent + +I'll create an agent to handle product information questions. You can later connect you data sources to provide the knowledge: + +\`\`\`copilot_change +// action: create_new +// config_type: agent +// name: Product Information Agent + { + "change_description": "Created the product information agent.", + "config_changes": { + "name": "Product Information Agent", + "type": "conversation", + "description": "Answers product information questions using RAG data sources.", + "disabled": false, + "instructions": "## πŸ§‘β€πŸ’Ό Role:\nYou are an internal agent that answers product information questions using RAG data sources. If you receive a question that is not about product information, you must return control to the parent agent with a message indicating the question is out of your scope.\n\n---\n## βš™οΈ Steps to Follow:\n1. Receive the product information question from the parent agent.\n2. Determine if the question is about product information.\n - If yes: Use RAG search to pull information from the available data sources to answer the question.\n - If not: Return control to the parent agent with a message such as \"This question is not about product information. Returning to parent agent.\"\n3. Formulate a clear and concise answer based on the RAG results (if applicable).\n4. If question is out of scope call [@agent:Product & Delivery Assistant](#mention) \n\n---\n## 🎯 Scope:\nβœ… In Scope:\n- Answering product information questions using RAG.\n- Returning control to parent if the question is out of scope.\n\n❌ Out of Scope:\n- Handling delivery status questions.\n- Interacting directly with the user.\n\n---\n## πŸ“‹ Guidelines:\nβœ”οΈ Dos:\n- Use RAG search to find relevant information for product questions.\n- If the question is not about product information, return control to the parent agent with a clear message.\n\n🚫 Don'ts:\n- Do not answer questions outside of product information.\n- Do not interact with the user directly.\n- Do not ignore out-of-scope questions; always return to parent.\n", + "examples": "\n", + "model": "google/gemini-2.5-flash", + "locked": false, + "toggleAble": true, + "ragReturnType": "chunks", + "ragK": 3, + "outputVisibility": "user_facing", + "controlType": "retain", + "maxCallsPerParentAgent": 3 + } +} +\`\`\` + +#### c. Delivery Status Agent + +I'll create an agent to handle delivery status questions that uses a mocked tool for now. You can later connect your MCP tool to it. + +\`\`\`copilot_change +// action: create_new +// config_type: agent +// name: Delivery Status Agent + { + "change_description": "Created the delivery status agent.", + "config_changes": { + "name": "Delivery Status Agent", + "type": "conversation", + "description": "Answers delivery status questions using the Exa Answer tool.", + "disabled": false, + "instructions": "## πŸ§‘β€πŸ’Ό Role:\nYou are an internal agent that answers delivery status questions. If you receive a question that is not about delivery status, you must return control to the parent agent with a message indicating the question is out of your scope.\n\n---\n## βš™οΈ Steps to Follow:\n1. Receive the delivery status question from the parent agent.\n2. Determine if the question is about delivery status.\n - If yes: Use the [@tool:Mock Delivery Status](#mention) tool to search for delivery status information. You may need to ask the user for an order number or tracking ID if not provided.\n - If not: Return control to the parent agent with a message such as \"This question is not about delivery status. Returning to parent agent.\"\n3. Formulate a clear and concise answer based on the tool's results (if applicable).\n4. If question is out of scope call [@agent:Product & Delivery Assistant](#mention) \n---\n## 🎯 Scope:\nβœ… In Scope:\n- Answering delivery status questions using the Exa Answer tool.\n- Returning control to parent if the question is out of scope.\n\n❌ Out of Scope:\n- Handling product information questions.\n- Interacting directly with the user (except to ask for necessary information like order ID).\n\n---\n## πŸ“‹ Guidelines:\nβœ”οΈ Dos:\n- Use the Exa Answer tool to find delivery information for delivery status questions.\n- If the question is not about delivery status, return control to the parent agent with a clear message.\n- Ask for order details if needed.\n\n🚫 Don'ts:\n- Do not answer questions outside of delivery status.\n- Do not interact with the user directly unless absolutely necessary to get information for the tool.\n- Do not ignore out-of-scope questions; always return to parent.\n", + "examples": "\n", + "model": "gpt-4.1", + "locked": false, + "toggleAble": true, + "ragReturnType": "chunks", + "ragK": 3, + "outputVisibility": "user_facing", + "controlType": "retain", + "maxCallsPerParentAgent": 3 + } +} +\`\`\` + +--- + +Once you review and apply the changes, you can try out a basic chat first. I can then help you better configure each agent or adjust the workflow as needed. Would you like to customize any step or add more details? + +--- + + `; \ No newline at end of file