Merge pull request #248 from rowboatlabs/dev

Dev
This commit is contained in:
arkml 2025-09-12 18:45:20 +05:30 committed by GitHub
commit df5a9b7bd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1341 additions and 526 deletions

View file

@ -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,

View file

@ -12,21 +12,8 @@ function buildTemplates(): { [key: string]: z.infer<typeof WorkflowTemplate> } {
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: [
{

View file

@ -48,6 +48,8 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
isInitialState = false,
dataSources,
}, ref) {
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
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}
/>
</div>
<div className="shrink-0 px-0 pb-0">
<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>
@ -322,8 +318,8 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
dispatch: (action: WorkflowDispatch) => void;
isInitialState?: boolean;
dataSources?: z.infer<typeof DataSource>[];
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<z.infer<typeof CopilotMessage>[]>([]);
@ -369,34 +372,7 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
<Panel
variant="copilot"
tourTarget="copilot"
title={
<div className="flex items-center">
<div className="flex items-center gap-2 rounded-lg p-1 bg-blue-50/70 dark:bg-blue-900/30">
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'copilot'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base"></span>
Build
</button>
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'playground'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base">💬</span>
Chat
</button>
</div>
</div>
}
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">

View file

@ -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
});

View file

@ -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
});

View file

@ -38,6 +38,7 @@ export function useCopilot({ projectId, workflow, context, dataSources }: UseCop
const [billingError, setBillingError] = useState<string | null>(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<typeof CopilotMessage>[],
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<void>((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 {

View file

@ -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<number>(0);
@ -56,6 +56,8 @@ export function App({
}
}, []);
const hasAgents = (workflow?.agents?.length || 0) > 0;
return (
<>
<Panel
@ -63,35 +65,13 @@ export function App({
variant="playground"
tourTarget="playground"
title={
<div className="flex items-center">
<div className="flex items-center gap-2 rounded-lg p-1 bg-blue-50/70 dark:bg-blue-900/30">
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'copilot'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base"></span>
Build
</button>
<button
onClick={onTogglePanel}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
activePanel === 'playground'
? 'bg-white dark:bg-zinc-700 text-indigo-700 dark:text-indigo-300 shadow-md border border-indigo-200 dark:border-indigo-700'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/60'
}`}
>
<span className="text-base">💬</span>
Chat
</button>
</div>
<div className="flex items-center gap-2 text-zinc-800 dark:text-zinc-200 font-semibold">
<MessageCircle className="w-4 h-4" />
Chat
</div>
}
subtitle="Chat with your assistant"
rightActions={
subtitle={hasAgents ? "Chat with your assistant" : "Create an agent to start chatting"}
rightActions={hasAgents ? (
<div className="flex items-center gap-2">
<Button
variant="primary"
@ -131,21 +111,46 @@ export function App({
)}
</Button>
</div>
}
) : (
// Preserve header height when there are zero agents
<div className="h-8" />
)}
onClick={onPanelClick}
>
<div className="h-full overflow-auto px-4 py-4">
<Chat
key={`chat-${counter}`}
projectId={projectId}
workflow={workflow}
messageSubscriber={messageSubscriber}
onCopyClick={(fn) => { getCopyContentRef.current = fn; }}
showDebugMessages={showDebugMessages}
triggerCopilotChat={triggerCopilotChat}
isLiveWorkflow={isLiveWorkflow}
onMessageSent={onMessageSent}
/>
{hasAgents ? (
<Chat
key={`chat-${counter}`}
projectId={projectId}
workflow={workflow}
messageSubscriber={messageSubscriber}
onCopyClick={(fn) => { getCopyContentRef.current = fn; }}
showDebugMessages={showDebugMessages}
triggerCopilotChat={triggerCopilotChat}
isLiveWorkflow={isLiveWorkflow}
onMessageSent={onMessageSent}
/>
) : (
<div className="h-full flex items-center justify-center">
<div className="text-center max-w-md">
<div className="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300">
<MessageCircle className="w-6 h-6" />
</div>
<div className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Create an agent to start chatting</div>
<div className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">Skipper can build agents for you!</div>
<div className="mt-4 flex items-center justify-center gap-3">
<Button
variant="primary"
size="sm"
className="!bg-blue-700 hover:!bg-blue-800 !text-white dark:!bg-blue-600 dark:hover:!bg-blue-700 !border !border-blue-700 dark:!border-blue-600"
onClick={() => triggerCopilotChat?.("Help me create my first agent.")}
>
Ask Skipper
</Button>
</div>
</div>
</div>
)}
</div>
</Panel>
</>

View file

@ -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({
/>
)}
</div>
<div className="text-sm leading-relaxed text-gray-800 dark:text-gray-100 animate-slideUpAndFade">
<div className="text-sm leading-relaxed text-gray-800 dark:text-gray-100 animate-slideUpAndFade pl-1">
<div className="flex flex-col gap-2">
<div className="text-left">
<MarkdownContent content={content} />
@ -200,10 +201,13 @@ function AssistantMessage({
>
<DownloadIcon size={16} className="text-gray-700 dark:text-gray-200" />
</a>
<img
<Image
src={src}
alt={`Image ${i+1}`}
className="max-h-80 max-w-full object-contain rounded"
width={800}
height={320}
style={{ objectFit: 'contain' }}
/>
{img.truncated && (
<div className="text-[11px] text-amber-600 dark:text-amber-400 mt-1">
@ -225,15 +229,14 @@ function AssistantMessage({
);
}
function AssistantMessageLoading() {
function TypingIndicator() {
return (
<div className="self-start flex flex-col gap-1 my-5">
<div className="max-w-[85%] inline-block">
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-2.5
rounded-lg border border-gray-200 dark:border-gray-700
shadow-sm animate-slideUpAndFade min-h-[2.5rem] flex items-center gap-2">
<Spinner size="sm" />
<span className="text-sm text-gray-600 dark:text-gray-400">Generating...</span>
<div className="flex justify-start items-center my-4 px-1">
<div className="flex items-center gap-1">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
<div className="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
<div className="w-2 h-2 bg-gray-400 dark:bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
</div>
</div>
</div>
@ -526,10 +529,13 @@ function ClientToolCall({
>
<DownloadIcon size={16} className="text-gray-700 dark:text-gray-200" />
</a>
<img
<Image
src={src}
alt={`Tool image ${i+1}`}
className="max-h-64 max-w-full object-contain rounded"
width={800}
height={256}
style={{ objectFit: 'contain' }}
/>
{img.truncated && (
<div className="text-[11px] text-amber-600 dark:text-amber-400 mt-1">
@ -618,10 +624,13 @@ function ClientToolCall({
<div className="flex flex-wrap gap-3">
{imagePreviews.map((img, i) => (
<div key={i} className="rounded-lg border border-gray-200 dark:border-gray-700 p-2 bg-white dark:bg-zinc-900">
<img
<Image
src={img.url ? img.url : `data:${img.mimeType};base64,${img.dataBase64}`}
alt={`Tool image ${i+1}`}
className="max-h-64 max-w-full object-contain rounded"
width={800}
height={256}
style={{ objectFit: 'contain' }}
/>
{img.truncated && (
<div className="text-[11px] text-amber-600 dark:text-amber-400 mt-1">
@ -948,7 +957,7 @@ export function Messages({
}
return null;
})}
{loadingAssistantResponse && <AssistantMessageLoading />}
{loadingAssistantResponse && <TypingIndicator />}
</div>
);
}

View file

@ -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<z.infer<typeof Project>>(initialProjectData);
const [dataSources, setDataSources] = useState<z.infer<typeof DataSource>[]>(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<typeof Workflow> | undefined = project?.draftWorkflow;
if (mode == 'live') {
workflow = project?.liveWorkflow;
let workflow: z.infer<typeof Workflow> | 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) && <WorkflowEditor
projectId={initialProjectData.id}
isLive={mode == 'live'}
autoPublishEnabled={autoPublishEnabled}
onToggleAutoPublish={handleToggleAutoPublish}
workflow={workflow}
dataSources={dataSources}
projectConfig={project}

View file

@ -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, Checkbox, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } 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";
@ -13,11 +13,14 @@ interface TopBarProps {
onProjectNameCommit: (value: string) => Promise<void>;
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 (
<>
<div className="rounded-xl bg-white/70 dark:bg-zinc-800/70 shadow-sm backdrop-blur-sm border border-zinc-200 dark:border-zinc-800 px-5 py-2">
<div className="flex justify-between items-center">
<div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100">
<div className="workflow-version-selector flex items-center gap-3 -ml-1 pr-2 text-gray-800 dark:text-gray-100">
{/* Project Name Editor */}
<div className="flex flex-col min-w-0 max-w-xs">
<Input
@ -115,25 +134,47 @@ export function TopBar({
size="sm"
classNames={{
base: "max-w-xs",
input: "text-base font-semibold px-2",
input: "text-sm font-semibold px-2",
inputWrapper: "min-h-[36px] h-[36px] border-gray-200 dark:border-gray-700 px-0"
}}
/>
</div>
{/* Show divider and CTA only in live view */}
{isLive && <div className="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>}
{isLive ? (
{/* Mode pill and auto-publish checkbox */}
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
{/* Mode pill */}
<div className="flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 font-medium text-xs rounded-full">
<RadioIcon size={12} />
<span>
{autoPublishEnabled ? 'Live ' : (isLive ? 'Live ' : 'Draft')}
</span>
</div>
{/* Auto-publish checkbox or Switch to draft button */}
{!autoPublishEnabled && isLive ? (
<Button
variant="solid"
size="md"
size="sm"
onPress={() => onChangeMode('draft')}
className="gap-2 px-4 bg-gray-100 hover:bg-gray-200 text-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-300 font-medium text-sm border border-gray-200 dark:border-gray-600 shadow-sm"
startContent={<PenLine size={16} />}
className="gap-2 px-3 h-8 bg-gray-100 hover:bg-gray-200 text-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-300 font-medium text-sm border border-gray-200 dark:border-gray-600 shadow-sm"
startContent={<PenLine size={14} />}
>
Switch to draft
</Button>
) : null}
) : (
!isLive && (
<div className="flex items-center">
<Checkbox
size="sm"
isSelected={autoPublishEnabled}
onValueChange={onToggleAutoPublish}
>
Auto-publish
</Checkbox>
</div>
)
)}
</div>
{/* 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({
</div>}
{!isLive && <>
{!isLive && <div className="flex items-center gap-0.5">
<CustomButton
variant="primary"
size="sm"
onClick={onUndo}
disabled={!canUndo}
className="bg-gray-50 text-gray-700 hover:bg-gray-100 disabled:bg-gray-25 disabled:text-gray-400"
className="min-w-8 h-8 px-2 bg-gray-50 text-gray-700 hover:bg-gray-100 disabled:bg-gray-25 disabled:text-gray-400"
showHoverContent={true}
hoverContent="Undo"
>
<UndoIcon className="w-4 h-4" />
<UndoIcon className="w-3.5 h-3.5" />
</CustomButton>
<CustomButton
variant="primary"
size="sm"
onClick={onRedo}
disabled={!canRedo}
className="bg-gray-50 text-gray-700 hover:bg-gray-100 disabled:bg-gray-25 disabled:text-gray-400"
className="min-w-8 h-8 px-2 bg-gray-50 text-gray-700 hover:bg-gray-100 disabled:bg-gray-25 disabled:text-gray-400"
showHoverContent={true}
hoverContent="Redo"
>
<RedoIcon className="w-4 h-4" />
<RedoIcon className="w-3.5 h-3.5" />
</CustomButton>
</>}
</div>}
{/* Deploy CTA - always visible */}
{/* View controls (hidden in live mode) */}
{!isLive && (<div className="flex items-center gap-2 mr-2">
{(() => {
// 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 (
<Dropdown>
<DropdownTrigger>
<Button variant="light" size="sm" aria-label="Layout options" className="h-8 min-w-0 bg-transparent text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100/60 dark:hover:bg-zinc-800/50 border border-transparent gap-1 px-2">
{/* 3-pane layout icon */}
<svg width="26" height="18" viewBox="0 0 18 12" aria-hidden="true">
<rect x="0.5" y="0.5" width="17" height="11" rx="1" fill="none" stroke="currentColor" strokeWidth="1" opacity="0.6" />
<rect x="2" y="2" width="4" height="8" rx="0.5" fill="currentColor" opacity="0.8" />
<rect x="7" y="2" width="4" height="8" rx="0.5" fill="currentColor" opacity="0.6" />
<rect x="12" y="2" width="4" height="8" rx="0.5" fill="currentColor" opacity="0.4" />
</svg>
<ChevronDownIcon size={14} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Choose layout" selectionMode="single" selectedKeys={[selectedKey]} closeOnSelect={true} onSelectionChange={(keys) => {
const key = Array.from(keys as Set<string>)[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);
}}>
<DropdownItem key="show-all" isDisabled={disableShowAll} className={selectedKey==='show-all' ? 'bg-zinc-100 dark:bg-zinc-800' : ''} startContent={<input type="radio" readOnly checked={selectedKey==='show-all'} className="accent-zinc-600 dark:accent-zinc-300" />}>Show All</DropdownItem>
<DropdownItem key="hide-agents" isDisabled={disableHideAgents} className={selectedKey==='hide-agents' ? 'bg-zinc-100 dark:bg-zinc-800' : ''} startContent={<input type="radio" readOnly checked={selectedKey==='hide-agents'} className="accent-zinc-600 dark:accent-zinc-300" />}>Hide Agents</DropdownItem>
<DropdownItem key="hide-chat" isDisabled={disableHideChat} className={selectedKey==='hide-chat' ? 'bg-zinc-100 dark:bg-zinc-800' : ''} startContent={<input type="radio" readOnly checked={selectedKey==='hide-chat'} className="accent-zinc-600 dark:accent-zinc-300" />}>Hide Chat</DropdownItem>
<DropdownItem key="hide-skipper" isDisabled={disableHideSkipper} className={selectedKey==='hide-skipper' ? 'bg-zinc-100 dark:bg-zinc-800' : ''} startContent={<input type="radio" readOnly checked={selectedKey==='hide-skipper'} className="accent-zinc-600 dark:accent-zinc-300" />}>Hide Skipper</DropdownItem>
</DropdownMenu>
</Dropdown>
);
})()}
</div>)}
{/* Deploy CTA - conditional based on auto-publish mode */}
<div className="flex items-center gap-3">
{isLive ? (
{autoPublishEnabled ? (
<>
{/* Auto-publish mode: Show Use Assistant button */}
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="md"
className="gap-2 px-4 bg-blue-50 hover:bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 dark:text-blue-400 font-semibold text-sm border border-blue-200 dark:border-blue-700 shadow-sm"
startContent={<Plug size={16} />}
size="sm"
className="gap-2 px-3 h-8 bg-blue-50 hover:bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 dark:text-blue-400 font-semibold text-sm border border-blue-200 dark:border-blue-700 shadow-sm"
startContent={<Plug size={14} />}
onPress={onUseAssistantClick}
>
Use Assistant
<ChevronDownIcon size={14} />
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Assistant access options">
@ -239,138 +355,291 @@ export function TopBar({
</DropdownMenu>
</Dropdown>
{/* Live workflow label moved here */}
<div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />}
<div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
<RadioIcon size={16} />
Live workflow
<div className="flex">
<Button
variant="solid"
size="sm"
onPress={handleShareClick}
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
startContent={<ShareIcon size={14} />}
>
Share
</Button>
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="sm"
className={`min-w-0 px-2 h-8 rounded-l-none border border-l-0 shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
>
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Share actions">
<DropdownItem
key="download-json"
startContent={<DownloadIcon size={16} />}
onPress={onDownloadJSON}
>
Download JSON
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
<Tooltip content="Share Assistant">
<button
onClick={onShareWorkflow}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
aria-label="Share Assistant"
type="button"
>
<ShareIcon size={20} />
</button>
</Tooltip>
{shareUrl && (
<Tooltip content="Copy share URL">
<button
onClick={onCopyShareUrl}
className="px-2 py-1 text-xs bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:hover:bg-indigo-900/50 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700 rounded-md transition-colors"
type="button"
>
Copy URL
</button>
</Tooltip>
)}
<Tooltip content="Download Assistant JSON">
<button
onClick={onDownloadJSON}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
aria-label="Download JSON"
type="button"
>
<DownloadIcon size={20} />
</button>
</Tooltip>
</div>
</>
) : (
<>
<div className="flex">
<Button
variant="solid"
size="md"
onPress={onPublishWorkflow}
className="gap-2 px-4 bg-green-100 hover:bg-green-200 text-green-800 font-semibold text-sm rounded-r-none border border-green-300 shadow-sm"
startContent={<RocketIcon size={16} />}
data-tour-target="deploy"
>
Publish
</Button>
<Dropdown>
<DropdownTrigger>
// Manual publish mode: Show current publish/live logic
isLive ? (
<>
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="sm"
className="gap-2 px-3 h-8 bg-blue-50 hover:bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 dark:text-blue-400 font-semibold text-sm border border-blue-200 dark:border-blue-700 shadow-sm"
startContent={<Plug size={14} />}
onPress={onUseAssistantClick}
>
Use Assistant
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Assistant access options">
<DropdownItem
key="chat"
startContent={<MessageCircleIcon size={16} />}
onPress={() => {
onUseAssistantClick();
onStartNewChatAndFocus();
}}
>
Chat with Assistant
</DropdownItem>
<DropdownItem
key="api-sdk"
startContent={<SettingsIcon size={16} />}
onPress={() => {
onUseAssistantClick();
if (projectId) { router.push(`/projects/${projectId}/config`); }
}}
>
API & SDK Settings
</DropdownItem>
<DropdownItem
key="manage-triggers"
startContent={<ZapIcon size={16} />}
onPress={() => {
onUseAssistantClick();
if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); }
}}
>
Manage Triggers
</DropdownItem>
</DropdownMenu>
</Dropdown>
<div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />}
<div className="flex">
<Button
variant="solid"
size="sm"
onPress={handleShareClick}
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
startContent={<ShareIcon size={14} />}
>
Share
</Button>
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="sm"
className={`min-w-0 px-2 h-8 rounded-l-none border border-l-0 shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
>
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Share actions">
<DropdownItem
key="download-json"
startContent={<DownloadIcon size={16} />}
onPress={onDownloadJSON}
>
Download JSON
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</div>
</>) : (
// Draft mode in manual publish: Show publish button
<>
<div className="flex">
{(!hasAgents) ? (
<Tooltip content="Create agents to publish your assistant">
<span className="inline-flex">
<Button
variant="solid"
size="sm"
onPress={onPublishWorkflow}
isDisabled
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed min-w-[120px]`}
startContent={<RocketIcon size={14} />}
data-tour-target="deploy"
>
Publish
</Button>
</span>
</Tooltip>
) : (
<Button
variant="solid"
size="md"
className="min-w-0 px-2 bg-green-100 hover:bg-green-200 text-green-800 rounded-l-none border border-l-0 border-green-300 shadow-sm"
size="sm"
onPress={onPublishWorkflow}
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-green-100 hover:bg-green-200 text-green-800 border-green-300 min-w-[132px]`}
startContent={<RocketIcon size={14} />}
data-tour-target="deploy"
>
<ChevronDownIcon size={14} />
Publish
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Deploy actions">
<DropdownItem
key="view-live"
startContent={<RadioIcon size={16} />}
onPress={() => onChangeMode('live')}
>
View live version
</DropdownItem>
<DropdownItem
key="reset-to-live"
startContent={<AlertTriangle size={16} />}
onPress={onRevertToLive}
className="text-red-600 dark:text-red-400"
>
Reset to live version
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
{/* Moved draft/live labels and download button here */}
<div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />}
{isLive && <div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
<RadioIcon size={16} />
Live workflow
</div>}
{!isLive && <div className="bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
<PenLine size={16} />
Draft workflow
</div>}
<Tooltip content="Share Assistant">
<button
onClick={onShareWorkflow}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
aria-label="Share Assistant"
type="button"
>
<ShareIcon size={20} />
</button>
</Tooltip>
{shareUrl && (
<Tooltip content="Copy share URL">
<button
onClick={onCopyShareUrl}
className="px-2 py-1 text-xs bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:hover:bg-indigo-900/50 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700 rounded-md transition-colors"
type="button"
>
Copy URL
</button>
)}
{hasAgents ? (
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="sm"
className={`min-w-0 px-2 h-8 rounded-l-none border border-l-0 shadow-sm bg-green-100 hover:bg-green-200 text-green-800 border-green-300`}
>
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Deploy actions">
<DropdownItem
key="view-live"
startContent={<RadioIcon size={16} />}
onPress={() => onChangeMode('live')}
>
View live version
</DropdownItem>
<DropdownItem
key="reset-to-live"
startContent={<AlertTriangle size={16} />}
onPress={onRevertToLive}
className="text-red-600 dark:text-red-400"
>
Reset to live version
</DropdownItem>
</DropdownMenu>
</Dropdown>
) : (
<Tooltip content="Create agents to publish your assistant">
<span className="inline-flex">
<Button
variant="solid"
size="sm"
isDisabled
className={`min-w-0 px-2 h-8 rounded-l-none border border-l-0 shadow-sm bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed`}
>
<ChevronDownIcon size={12} />
</Button>
</span>
</Tooltip>
)}
<Tooltip content="Download Assistant JSON">
<button
onClick={onDownloadJSON}
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
aria-label="Download JSON"
type="button"
>
<DownloadIcon size={20} />
</button>
</Tooltip>
</div>
</>
</div>
<div className="flex items-center gap-2 ml-2">
{publishing && <Spinner size="sm" />}
<div className="flex">
<Button
variant="solid"
size="sm"
onPress={handleShareClick}
className={`gap-2 px-3 h-8 font-semibold text-sm rounded-r-none border shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
startContent={<ShareIcon size={14} />}
>
Share
</Button>
<Dropdown>
<DropdownTrigger>
<Button
variant="solid"
size="sm"
className={`min-w-0 px-2 h-8 rounded-l-none border border-l-0 shadow-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-800 border-indigo-300`}
>
<ChevronDownIcon size={12} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Share actions">
<DropdownItem
key="download-json"
startContent={<DownloadIcon size={16} />}
onPress={onDownloadJSON}
>
Download JSON
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</div>
</>
)
)}
</div>
</div>
</div>
</div>
{/* Share Modal */}
<Modal isOpen={isShareModalOpen} onClose={onShareModalClose} size="lg">
<ModalContent>
<ModalHeader className="flex flex-col gap-1">
Share Assistant
</ModalHeader>
<ModalBody>
<div className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
Share this assistant with others using the URL below:
</p>
{shareUrl ? (
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<input
type="text"
value={shareUrl || ''}
readOnly
className="flex-1 bg-transparent text-sm text-gray-700 dark:text-gray-300 outline-none"
/>
<Button
size="sm"
variant="solid"
onPress={onCopyShareUrl}
className="bg-indigo-100 hover:bg-indigo-200 text-indigo-800"
>
Copy
</Button>
</div>
) : (
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<Spinner size="sm" />
<span className="text-sm text-gray-600 dark:text-gray-400">
Generating share URL...
</span>
</div>
)}
</div>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onShareModalClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
}

View file

@ -565,7 +565,7 @@ export const EntityList = forwardRef<
agents: true,
tools: true,
data: true,
prompts: false
prompts: true
});
// Default sizes when panels are expanded

View file

@ -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<typeof Workflow>;
publishing: boolean;
@ -89,19 +127,20 @@ export type Action = {
} | {
type: "add_agent";
agent: Partial<z.infer<typeof WorkflowAgent>>;
fromCopilot?: boolean;
} | {
type: "add_tool";
tool: Partial<z.infer<typeof WorkflowTool>>;
fromCopilot?: boolean;
} | {
type: "add_prompt";
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
} | {
type: "add_prompt_no_select";
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
fromCopilot?: boolean;
} | {
type: "add_pipeline";
pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
defaultModel?: string;
fromCopilot?: boolean;
} | {
type: "select_agent";
name: string;
@ -128,10 +167,18 @@ export type Action = {
type: "update_agent";
name: string;
agent: Partial<z.infer<typeof WorkflowAgent>>;
} | {
type: "update_agent_no_select";
name: string;
agent: Partial<z.infer<typeof WorkflowAgent>>;
} | {
type: "update_tool";
name: string;
tool: Partial<z.infer<typeof WorkflowTool>>;
} | {
type: "update_tool_no_select";
name: string;
tool: Partial<z.infer<typeof WorkflowTool>>;
} | {
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<typeof Project>;
eligibleModels: z.infer<typeof ModelsResponse> | "*";
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<ViewMode>(() => {
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<number>(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<z.infer<typeof Message>[]>([]);
const updateChatMessages = useCallback((messages: z.infer<typeof Message>[]) => {
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<boolean>(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<z.infer<typeof WorkflowPrompt>>) {
dispatch({ type: "add_prompt_no_select", prompt });
dispatch({ type: "add_prompt", prompt, fromCopilot: true });
}
function handleUpdatePromptFromModal(name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) {
@ -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 (
<EntitySelectionContext.Provider value={{
onSelectAgent: handleSelectAgent,
@ -1711,11 +1871,15 @@ export function WorkflowEditor({
onProjectNameCommit={handleProjectNameCommit}
publishing={state.present.publishing}
isLive={isLive}
autoPublishEnabled={autoPublishEnabled}
onToggleAutoPublish={onToggleAutoPublish}
showCopySuccess={showCopySuccess}
showBuildModeBanner={showBuildModeBanner}
canUndo={state.currentIndex > 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 */}
<ResizablePanelGroup direction="horizontal" className="flex-1 flex overflow-auto gap-1 rounded-xl bg-zinc-50 dark:bg-zinc-900">
{/* Content Area - hydration-safe layout */}
{!isHydrated ? (
<ResizablePanelGroup key={`hydration-${viewMode}`} direction="horizontal" className="flex-1 flex overflow-auto gap-1 rounded-xl bg-zinc-50 dark:bg-zinc-900">
{(viewMode !== 'two_chat_skipper') && (
<ResizablePanel
key={`entity-list-${state.present.selection ? '3-pane' : '2-pane'}`}
key={`entity-list-hydration`}
minSize={10}
defaultSize={PANEL_RATIOS.entityList}
className={isLeftPanelCollapsed ? 'hidden' : ''}
defaultSize={getPanelRatios(viewMode).entityList}
id="entities"
order={1}
className={`${isLeftPanelCollapsed ? 'hidden' : ''}`}
>
<div className="flex flex-col h-full">
<EntityList
ref={entityListRef}
agents={state.present.workflow.agents}
tools={state.present.workflow.tools}
prompts={state.present.workflow.prompts}
pipelines={state.present.workflow.pipelines || []}
dataSources={dataSources}
workflow={state.present.workflow}
selectedEntity={null}
startAgentName={state.present.workflow.startAgent}
onSelectAgent={handleSelectAgent}
onSelectTool={handleSelectTool}
onSelectPrompt={handleSelectPrompt}
onSelectPipeline={handleSelectPipeline}
onSelectDataSource={handleSelectDataSource}
onAddAgent={handleAddAgent}
onAddTool={handleAddTool}
onAddPrompt={handleAddPrompt}
onUpdatePrompt={handleUpdatePrompt}
onAddPromptFromModal={handleAddPromptFromModal}
onUpdatePromptFromModal={handleUpdatePromptFromModal}
onAddPipeline={handleAddPipeline}
onAddAgentToPipeline={handleAddAgentToPipeline}
onToggleAgent={handleToggleAgent}
onSetMainAgent={handleSetMainAgent}
onDeleteAgent={handleDeleteAgent}
onDeleteTool={handleDeleteTool}
onDeletePrompt={handleDeletePrompt}
onDeletePipeline={handleDeletePipeline}
onShowVisualise={handleShowVisualise}
projectId={projectId}
onProjectToolsUpdated={onProjectToolsUpdated}
onDataSourcesUpdated={onDataSourcesUpdated}
projectConfig={projectConfig}
onReorderAgents={handleReorderAgents}
onReorderPipelines={handleReorderPipelines}
useRagUploads={useRagUploads}
useRagS3Uploads={useRagS3Uploads}
useRagScraping={useRagScraping}
/>
</div>
</ResizablePanel>
)}
{(viewMode !== 'two_chat_skipper') && (
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${(isLeftPanelCollapsed) ? 'hidden' : ''}`} />
)}
{(viewMode === 'two_agents_chat' || viewMode === 'three_all') && (
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).chatApp} id="chat" order={2} className="overflow-hidden">
{/* Minimal mount of Chat during SSR hydration */}
<div className="h-full" />
</ResizablePanel>
)}
{(viewMode === 'three_all') && (<ResizableHandle withHandle className="w-[3px] bg-transparent" />)}
{(viewMode === 'two_agents_skipper' || viewMode === 'three_all') && (
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).copilot} id="copilot" order={3} className="overflow-hidden">
<div className="h-full" />
</ResizablePanel>
)}
{(viewMode === 'two_chat_skipper') && (
<>
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).chatApp} id="chat" order={1} className="overflow-hidden"><div className="h-full" /></ResizablePanel>
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).copilot} id="copilot" order={2} className="overflow-hidden"><div className="h-full" /></ResizablePanel>
</>
)}
</ResizablePanelGroup>
) : (
<ResizablePanelGroup key="main" direction="horizontal" className="flex-1 flex overflow-auto gap-1 rounded-xl bg-zinc-50 dark:bg-zinc-900">
{/* Agents (Entity List) column */}
{(viewMode !== 'two_chat_skipper') && (
<ResizablePanel
key={`entity-list-main`}
minSize={10}
defaultSize={getPanelRatios(viewMode).entityList}
id="entities"
order={1}
className={`${isLeftPanelCollapsed ? 'hidden' : ''}`}
>
<div className="flex flex-col h-full">
<EntityList
@ -1803,150 +2067,209 @@ export function WorkflowEditor({
/>
</div>
</ResizablePanel>
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${(isLeftPanelCollapsed || !state.present.selection) ? 'hidden' : ''}`} />
)}
{(viewMode !== 'two_chat_skipper') && (
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${(isLeftPanelCollapsed && !state.present.selection) ? 'hidden' : ''}`} />
)}
{/* Config Panel - always rendered, visibility controlled */}
<ResizablePanel
minSize={20}
defaultSize={45}
className={`overflow-auto ${!state.present.selection ? 'hidden' : ''}`}
>
{state.present.selection?.type === "agent" && <AgentConfig
key={`agent-${state.present.workflow.agents.findIndex(agent => agent.name === state.present.selection!.name)}-${configKey}`}
{/* Playground column - always mounted; hide via viewMode */}
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).chatApp} id="chat" order={2} className={`overflow-hidden relative ${viewMode === 'two_agents_skipper' ? 'hidden' : ''}`}>
<ChatApp
key={'' + state.present.chatKey}
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}
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 <ToolConfig
key={`${state.present.selection.name}-${configKey}`}
tool={selectedTool!}
usedToolNames={new Set([
...state.present.workflow.tools.filter((tool) => 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" && <PromptConfig
key={`${state.present.selection.name}-${configKey}`}
prompt={state.present.workflow.prompts.find((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" && <DataSourceConfig
key={`${state.present.selection.name}-${configKey}`}
dataSourceId={state.present.selection.name}
handleClose={() => dispatch({ type: "unselect_datasource" })}
onDataSourceUpdate={onDataSourcesUpdated}
/>}
{state.present.selection?.type === "pipeline" && <PipelineConfig
key={`${state.present.selection.name}-${configKey}`}
projectId={projectId}
workflow={state.present.workflow}
pipeline={state.present.workflow.pipelines?.find((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" && (
<Panel
title={
<div className="flex items-center justify-between w-full">
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">
Agent Graph Visualizer
</div>
<CustomButton
variant="secondary"
size="sm"
onClick={handleHideVisualise}
showHoverContent={true}
hoverContent="Close"
>
<XIcon className="w-4 h-4" />
</CustomButton>
</div>
}
>
<div className="h-full overflow-hidden">
<AgentGraphVisualizer workflow={state.present.workflow} />
isLiveWorkflow={isLive}
activePanel={activePanel}
onTogglePanel={handleTogglePanel}
onMessageSent={markPlaygroundTested}
/>
{/* Config overlay above Playground when selection open */}
{state.present.selection && viewMode !== 'two_agents_skipper' && (
<div className="absolute inset-0 z-20">
<div className="h-full overflow-auto">
{state.present.selection?.type === "agent" && <AgentConfig
key={`overlay-agent-${state.present.workflow.agents.findIndex(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 <ToolConfig
key={`overlay-${state.present.selection.name}-${configKey}`}
tool={selectedTool!}
usedToolNames={new Set([
...state.present.workflow.tools.filter((tool) => 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" && <PromptConfig
key={`overlay-${state.present.selection.name}-${configKey}`}
prompt={state.present.workflow.prompts.find((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" && <DataSourceConfig
key={`overlay-${state.present.selection.name}-${configKey}`}
dataSourceId={state.present.selection.name}
handleClose={() => dispatch({ type: "unselect_datasource" })}
onDataSourceUpdate={onDataSourcesUpdated}
/>}
{state.present.selection?.type === "pipeline" && <PipelineConfig
key={`overlay-${state.present.selection.name}-${configKey}`}
projectId={projectId}
workflow={state.present.workflow}
pipeline={state.present.workflow.pipelines?.find((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" && (
<Panel title={<div className="flex items-center justify-between w-full"><div className="text-base font-semibold text-gray-900 dark:text-gray-100">Agent Graph Visualizer</div><CustomButton variant="secondary" size="sm" onClick={handleHideVisualise} showHoverContent={true} hoverContent="Close"><XIcon className="w-4 h-4" /></CustomButton></div>}>
<div className="h-full overflow-hidden">
<AgentGraphVisualizer workflow={state.present.workflow} />
</div>
</Panel>
)}
</div>
</Panel>
</div>
)}
</ResizablePanel>
{/* Second handle - between config and chat panels */}
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${(isLeftPanelCollapsed && !state.present.selection) ? 'hidden' : ''}`} />
{/* ChatApp/Copilot Panel - always visible */}
<ResizablePanel
key={`chat-panel-${state.present.selection ? '3-pane' : '2-pane'}`}
minSize={20}
defaultSize={state.present.selection ? 30 : PANEL_RATIOS.chatApp + PANEL_RATIOS.copilot}
className="overflow-auto"
>
<div className={(activePanel === 'playground') ? 'block h-full' : 'hidden h-full'}>
<ChatApp
key={'' + state.present.chatKey}
projectId={projectId}
workflow={state.present.workflow}
messageSubscriber={updateChatMessages}
onPanelClick={handlePlaygroundClick}
triggerCopilotChat={triggerCopilotChat}
isLiveWorkflow={isLive}
activePanel={activePanel}
onTogglePanel={handleTogglePanel}
onMessageSent={markPlaygroundTested}
/>
</div>
<div className={(activePanel === 'copilot') ? 'block h-full' : 'hidden h-full'}>
<Copilot
ref={copilotRef}
projectId={projectId}
workflow={state.present.workflow}
dispatch={dispatch}
chatContext={
state.present.selection &&
(state.present.selection.type === "agent" ||
state.present.selection.type === "tool" ||
state.present.selection.type === "prompt")
? {
type: state.present.selection.type,
name: state.present.selection.name
}
: chatMessages.length > 0
? { type: 'chat', messages: chatMessages }
: undefined
}
isInitialState={isInitialState}
dataSources={dataSources}
activePanel={activePanel}
onTogglePanel={handleTogglePanel}
/>
</div>
{/* Divider between playground and copilot when both visible */}
{(viewMode === 'three_all' || viewMode === 'two_chat_skipper') && (
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
)}
{/* Copilot column - always mounted; hide via viewMode */}
<ResizablePanel minSize={20} defaultSize={getPanelRatios(viewMode).copilot} id="copilot" order={viewMode === 'three_all' ? 3 : 2} className={`overflow-hidden relative ${viewMode === 'two_agents_chat' ? 'hidden' : ''}`}>
<Copilot
ref={copilotRef}
projectId={projectId}
workflow={state.present.workflow}
dispatch={dispatch}
chatContext={
state.present.selection &&
(state.present.selection.type === "agent" ||
state.present.selection.type === "tool" ||
state.present.selection.type === "prompt")
? {
type: state.present.selection.type,
name: state.present.selection.name
}
: chatMessages.length > 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' && (
<div className="absolute inset-0 z-20">
<div className="h-full overflow-auto">
{state.present.selection?.type === "agent" && <AgentConfig
key={`overlay2-agent-${state.present.workflow.agents.findIndex(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 <ToolConfig
key={`overlay2-${state.present.selection.name}-${configKey}`}
tool={selectedTool!}
usedToolNames={new Set([
...state.present.workflow.tools.filter((tool) => 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" && <PromptConfig
key={`overlay2-${state.present.selection.name}-${configKey}`}
prompt={state.present.workflow.prompts.find((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" && <DataSourceConfig
key={`overlay2-${state.present.selection.name}-${configKey}`}
dataSourceId={state.present.selection.name}
handleClose={() => dispatch({ type: "unselect_datasource" })}
onDataSourceUpdate={onDataSourcesUpdated}
/>}
{state.present.selection?.type === "pipeline" && <PipelineConfig
key={`overlay2-${state.present.selection.name}-${configKey}`}
projectId={projectId}
workflow={state.present.workflow}
pipeline={state.present.workflow.pipelines?.find((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" && (
<Panel title={<div className="flex items-center justify-between w-full"><div className="text-base font-semibold text-gray-900 dark:text-gray-100">Agent Graph Visualizer</div><CustomButton variant="secondary" size="sm" onClick={handleHideVisualise} showHoverContent={true} hoverContent="Close"><XIcon className="w-4 h-4" /></CustomButton></div>}>
<div className="h-full overflow-hidden">
<AgentGraphVisualizer workflow={state.present.workflow} />
</div>
</Panel>
)}
</div>
</div>
)}
</ResizablePanel>
</ResizablePanelGroup>
)}
{USE_PRODUCT_TOUR && showTour && (
<ProductTour
projectId={projectId}
@ -1976,10 +2299,14 @@ export function WorkflowEditor({
forceStart
stepsOverride={[
{ target: 'playground', title: 'Step 1/2', content: 'Chat with your assistant to test it. Send messages, watch tool calls in action, and debug agent flows.' },
{ target: 'copilot', title: 'Step 2/2', content: 'Ask Copilot to improve your agents based on test results. Use “Fix” and “Explain” to iterate quickly.' },
{ target: 'copilot', title: 'Step 2/2', content: 'Ask Copilot to improve your agents based on test results. Use "Fix" and "Explain" to iterate quickly.' },
]}
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' });
}
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 && (
<ProductTour
projectId={projectId}
forceStart
stepsOverride={[
{ target: 'deploy', title: 'Publish', content: 'Click Publish to make your workflow live, enabling triggers and API/SDK access. You can revert to a draft at any time.' },
]}
onComplete={() => setShowPublishTour(false)}
/>
)}
{/* Revert to Live Confirmation Modal */}
<Modal isOpen={isRevertModalOpen} onClose={onRevertModalClose}>

View file

@ -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 ? (

View file

@ -83,14 +83,13 @@ export function Panel({
>
<div
className={clsx(
// For copilot and playground, mimic TopBar appearance
(variant === 'copilot' || variant === 'playground')
? "shrink-0 relative rounded-xl bg-white/70 dark:bg-zinc-800/70 shadow-sm backdrop-blur-sm border border-zinc-200 dark:border-zinc-800 px-0 pt-0 pb-2 mx-0 mt-0 mb-2 flex items-center justify-between"
: "shrink-0 border-b relative",
(variant !== 'copilot' && variant !== 'playground') && "border-zinc-100 dark:border-zinc-800",
"shrink-0 border-b relative",
// Use the same header treatment as entity list/default for playground/copilot
"border-zinc-100 dark:border-zinc-800",
{
"flex flex-col gap-3 px-4 py-3": variant === 'projects',
"flex items-center justify-between h-[53px] p-3": isEntityList,
"flex items-center justify-between px-3 py-2": variant === 'copilot' || variant === 'playground',
"flex items-center justify-between px-6 py-3": !isEntityList && variant !== 'projects' && variant !== 'copilot' && variant !== 'playground'
}
)}

View file

@ -22,18 +22,17 @@ export function SectionCard({ icon, title, children, labelWidth = 'md:w-32', cla
React.useEffect(() => {
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]);

View file

@ -28,28 +28,23 @@ export function ProgressBar({ steps, className, onStepClick }: ProgressBarProps)
};
return (
<nav aria-label="Workflow progress" className={cn("flex items-center gap-4", className)}>
{/* Progress Label */}
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 mr-2">
Progress:
</span>
<nav aria-label="Workflow progress" className={cn("flex items-center", className)}>
{/* Steps */}
<ol role="list" className="flex items-center gap-2">
<ol role="list" className="flex items-center gap-1.5">
{steps.map((step, index) => {
const isLast = index === steps.length - 1;
const tooltipText = (() => {
switch (step.id) {
case 1:
return 'Build your assistant - click for tour';
return 'Let skipper build your assistant for you';
case 2:
return 'Test your assistant - click for tour';
return 'Chat with your assistant to test it';
case 3:
return 'Make assistant live - click for tour';
return 'Make your assistant live';
case 4:
return 'Interact with your assistant - click for tour';
return 'Interact with your assistant';
default:
return 'Click for tour';
return '';
}
})();
@ -77,7 +72,7 @@ export function ProgressBar({ steps, className, onStepClick }: ProgressBarProps)
}
} : undefined}
className={cn(
"w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-semibold transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-400",
"w-5 h-5 rounded-full border-2 flex items-center justify-center text-xs font-semibold transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-400",
step.completed
? "bg-green-500 border-green-500 text-white"
: step.isCurrent
@ -88,7 +83,7 @@ export function ProgressBar({ steps, className, onStepClick }: ProgressBarProps)
{step.completed ? "✓" : step.isCurrent ? "⚡" : "○"}
</div>
</Tooltip>
<span className="hidden md:block mt-1 text-[11px] leading-none text-gray-700 dark:text-gray-300 font-medium">
<span className="hidden md:block mt-0.5 text-[10px] leading-none text-gray-700 dark:text-gray-300 font-medium">
{step.shortLabel ?? getShortLabel(step.label)}
</span>
</div>

View file

@ -39,7 +39,7 @@ When the user asks you to create agents for a multi-agent system, you should fol
1. Understand the users 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:

View file

@ -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.
---
`;

View file

@ -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. Heres 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 users 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:
- Ill 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": "<needs to be added>"
}
}
\`\`\`
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": "<needs to be added>"
}
}
\`\`\`
---
### 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?
---
`;