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]/workflow/components/TopBar.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx index 412b8a59..b17704c9 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx @@ -1,6 +1,6 @@ "use client"; import React from "react"; -import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input } from "@heroui/react"; +import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input, ButtonGroup } from "@heroui/react"; import { Button as CustomButton } from "@/components/ui/button"; import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug, MessageCircleIcon, ShareIcon } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; @@ -18,6 +18,7 @@ interface TopBarProps { 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,6 +30,8 @@ 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; @@ -52,6 +55,7 @@ export function TopBar({ canUndo, canRedo, activePanel, + viewMode, hasAgentInstructionChanges, hasPlaygroundTested, hasPublished, @@ -63,6 +67,8 @@ export function TopBar({ onChangeMode, onRevertToLive, onTogglePanel, + onSetViewMode, + hasAgents = true, onUseAssistantClick, onStartNewChatAndFocus, onStartBuildTour, @@ -121,19 +127,24 @@ export function TopBar({ />
- {/* Show divider and CTA only in live view */} + {/* Show divider and mode indicator */} {isLive &&
} {isLive ? ( - ) : null} + ) : ( +
+ + Draft +
+ )}
{/* Progress Bar - Center */} @@ -163,31 +174,106 @@ export function TopBar({
} - {!isLive && <> + {!isLive &&
- + - + - } +
} + {/* 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 - always visible */}
{isLive ? ( @@ -196,13 +282,13 @@ export function TopBar({ @@ -239,7 +325,6 @@ export function TopBar({ - {/* Live workflow label moved here */}
{publishing && }
@@ -270,7 +355,7 @@ export function TopBar({ - - - - - - } - onPress={() => onChangeMode('live')} - > - View live version - - } - onPress={onRevertToLive} - className="text-red-600 dark:text-red-400" - > - Reset to live version - - - + {(!hasAgents) ? ( + + + + + + ) : ( + + )} + {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 &&
@@ -357,7 +474,7 @@ export function TopBar({