mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-03 19:25:19 +02:00
Add options to hide panes (#244)
* Add multiple pane views * Fix rendering issues * Fix copilot apply selection pane error and remove old debug logs * Make top bar more compact * Fix copilot generation from starting prompt * Fix panel resizing issues * Fix content preservation upon hiding panes * Added changes on top of pane_layouts branch to remove example agent from the workflow editor * Grey out options for changing pane layout during zero agents state * Add zero-agent state for playground and publish buttons * Fix pane sizes and bugs --------- Co-authored-by: tusharmagar <tushmag@gmail.com>
This commit is contained in:
parent
431f835ba1
commit
ee02d61996
16 changed files with 900 additions and 400 deletions
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input } from "@heroui/react";
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input, ButtonGroup } from "@heroui/react";
|
||||
import { Button as CustomButton } from "@/components/ui/button";
|
||||
import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug, MessageCircleIcon, ShareIcon } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
|
@ -18,6 +18,7 @@ interface TopBarProps {
|
|||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
activePanel: 'playground' | 'copilot';
|
||||
viewMode: "two_agents_chat" | "two_agents_skipper" | "two_chat_skipper" | "three_all";
|
||||
hasAgentInstructionChanges: boolean;
|
||||
hasPlaygroundTested: boolean;
|
||||
hasPublished: boolean;
|
||||
|
|
@ -29,6 +30,8 @@ interface TopBarProps {
|
|||
onChangeMode: (mode: 'draft' | 'live') => void;
|
||||
onRevertToLive: () => void;
|
||||
onTogglePanel: () => void;
|
||||
onSetViewMode: (mode: "two_agents_chat" | "two_agents_skipper" | "two_chat_skipper" | "three_all") => void;
|
||||
hasAgents?: boolean;
|
||||
onUseAssistantClick: () => void;
|
||||
onStartNewChatAndFocus: () => void;
|
||||
onStartBuildTour?: () => void;
|
||||
|
|
@ -52,6 +55,7 @@ export function TopBar({
|
|||
canUndo,
|
||||
canRedo,
|
||||
activePanel,
|
||||
viewMode,
|
||||
hasAgentInstructionChanges,
|
||||
hasPlaygroundTested,
|
||||
hasPublished,
|
||||
|
|
@ -63,6 +67,8 @@ export function TopBar({
|
|||
onChangeMode,
|
||||
onRevertToLive,
|
||||
onTogglePanel,
|
||||
onSetViewMode,
|
||||
hasAgents = true,
|
||||
onUseAssistantClick,
|
||||
onStartNewChatAndFocus,
|
||||
onStartBuildTour,
|
||||
|
|
@ -121,19 +127,24 @@ export function TopBar({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Show divider and CTA only in live view */}
|
||||
{/* Show divider and mode indicator */}
|
||||
{isLive && <div className="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>}
|
||||
{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}
|
||||
) : (
|
||||
<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">
|
||||
<PenLine size={12} />
|
||||
<span>Draft</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Bar - Center */}
|
||||
|
|
@ -163,31 +174,106 @@ 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>}
|
||||
|
||||
{/* 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">
|
||||
{/* Unified icon: 3-pane visual */}
|
||||
<svg width="20" height="14" viewBox="0 0 22 16" aria-hidden="true">
|
||||
<rect x="1" y="1" width="20" height="14" rx="2" fill="none" stroke="currentColor" opacity=".55" />
|
||||
<rect x="2.3" y="2.5" width="5.5" height="11" rx="1.2" fill="currentColor" opacity=".8" />
|
||||
<rect x="8.5" y="2.5" width="6" height="11" rx="1.2" fill="currentColor" opacity=".5" />
|
||||
<rect x="15.5" y="2.5" width="5.5" height="11" rx="1.2" fill="currentColor" opacity=".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 - always visible */}
|
||||
<div className="flex items-center gap-3">
|
||||
{isLive ? (
|
||||
|
|
@ -196,13 +282,13 @@ export function TopBar({
|
|||
<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,7 +325,6 @@ 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">
|
||||
|
|
@ -270,7 +355,7 @@ export function TopBar({
|
|||
<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"
|
||||
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors cursor-pointer"
|
||||
aria-label="Download JSON"
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -282,47 +367,79 @@ export function TopBar({
|
|||
) : (
|
||||
<>
|
||||
<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>
|
||||
<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"
|
||||
>
|
||||
<ChevronDownIcon size={14} />
|
||||
</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>
|
||||
{(!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`}
|
||||
startContent={<RocketIcon size={14} />}
|
||||
data-tour-target="deploy"
|
||||
>
|
||||
Publish
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
variant="solid"
|
||||
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`}
|
||||
startContent={<RocketIcon size={14} />}
|
||||
data-tour-target="deploy"
|
||||
>
|
||||
Publish
|
||||
</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>
|
||||
)}
|
||||
</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">
|
||||
|
|
@ -357,7 +474,7 @@ export function TopBar({
|
|||
<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"
|
||||
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors cursor-pointer"
|
||||
aria-label="Download JSON"
|
||||
type="button"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -938,6 +1005,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);
|
||||
|
|
@ -1068,10 +1185,15 @@ 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);
|
||||
updateViewMode(
|
||||
viewMode === 'three_all' ? 'three_all' :
|
||||
(viewMode === 'two_agents_skipper' ? 'two_agents_chat' : 'two_chat_skipper')
|
||||
);
|
||||
setIsLeftPanelCollapsed(true);
|
||||
}, []);
|
||||
}, [updateViewMode, viewMode]);
|
||||
|
||||
// Load agent order from localStorage on mount
|
||||
// useEffect(() => {
|
||||
|
|
@ -1097,33 +1219,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 +1449,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>>) {
|
||||
|
|
@ -1521,7 +1663,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 +1737,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 +1812,9 @@ export function WorkflowEditor({
|
|||
}
|
||||
};
|
||||
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
useEffect(() => { setIsHydrated(true); }, []);
|
||||
|
||||
return (
|
||||
<EntitySelectionContext.Provider value={{
|
||||
onSelectAgent: handleSelectAgent,
|
||||
|
|
@ -1716,6 +1868,8 @@ export function WorkflowEditor({
|
|||
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 +1884,138 @@ export function WorkflowEditor({
|
|||
onChangeMode={onChangeMode}
|
||||
onRevertToLive={handleRevertToLive}
|
||||
onTogglePanel={handleTogglePanel}
|
||||
onSetViewMode={updateViewMode}
|
||||
onUseAssistantClick={markUseAssistantClicked}
|
||||
onStartNewChatAndFocus={handleStartNewChatAndFocus}
|
||||
onStartBuildTour={() => setShowBuildTour(true)}
|
||||
onStartTestTour={() => setShowTestTour(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);
|
||||
});
|
||||
});
|
||||
}}
|
||||
onStartPublishTour={() => {
|
||||
// Switch to 3-pane first to ensure elements are visible
|
||||
updateViewMode('three_all');
|
||||
if (isLive) {
|
||||
handleModeTransition('draft', 'switch_draft');
|
||||
}
|
||||
setShowPublishTour(true);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setShowPublishTour(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-${viewMode}`} 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 +2069,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-10">
|
||||
<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-10">
|
||||
<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,7 +2301,7 @@ 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');
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -379,6 +379,20 @@ function mapConfig(workflow: z.infer<typeof Workflow>): {
|
|||
}
|
||||
|
||||
async function* emitGreetingTurn(logger: PrefixLogger, workflow: z.infer<typeof Workflow>): AsyncIterable<z.infer<typeof ZOutMessage>> {
|
||||
// Check if there are no agents or no start agent
|
||||
if (workflow.agents.length === 0 || !workflow.startAgent) {
|
||||
logger.log(`no agents available, emitting no-agent message`);
|
||||
|
||||
// emit no-agent message
|
||||
yield* emitEvent(logger, {
|
||||
role: 'assistant',
|
||||
content: 'Hi! To get started with chatting with your Assistant please create an agent first!',
|
||||
agentName: 'System',
|
||||
responseType: 'external',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// find the greeting prompt
|
||||
const prompt = workflow.prompts.find(p => p.type === 'greeting')?.prompt || 'How can I help you today?';
|
||||
logger.log(`greeting turn: ${prompt}`);
|
||||
|
|
@ -1274,6 +1288,20 @@ export async function* streamResponse(
|
|||
logger.log(`start agent: ${workflow.startAgent}`);
|
||||
logger.log(`=== END CONFIGURATION ===`);
|
||||
|
||||
// Check if there are no agents or no start agent
|
||||
if (Object.keys(agentConfig).length === 0 || !workflow.startAgent) {
|
||||
logger.log(`no agents available, emitting no-agent message`);
|
||||
|
||||
// emit no-agent message
|
||||
yield* emitEvent(logger, {
|
||||
role: 'assistant',
|
||||
content: 'Hi! To get started with chatting with your Assistant please create an agent first!',
|
||||
agentName: 'System',
|
||||
responseType: 'external',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const stack: string[] = [];
|
||||
logger.log(`initialized stack: ${JSON.stringify(stack)}`);
|
||||
|
||||
|
|
@ -1296,6 +1324,20 @@ export async function* streamResponse(
|
|||
const startOfTurnAgentName = getStartOfTurnAgentName(logger, messages, agentConfig, pipelineConfig, workflow);
|
||||
logger.log(`🎯 START AGENT DECISION: ${startOfTurnAgentName}`);
|
||||
|
||||
// Additional safety check - if startOfTurnAgentName is empty or invalid, return no-agent message
|
||||
if (!startOfTurnAgentName || !agentConfig[startOfTurnAgentName]) {
|
||||
logger.log(`invalid start agent name: ${startOfTurnAgentName}, emitting no-agent message`);
|
||||
|
||||
// emit no-agent message
|
||||
yield* emitEvent(logger, {
|
||||
role: 'assistant',
|
||||
content: 'Hi! To get started with chatting with your Assistant please create an agent first!',
|
||||
agentName: 'System',
|
||||
responseType: 'external',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let agentName: string | null = startOfTurnAgentName;
|
||||
|
||||
// start the turn loop
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ When the user asks you to create agents for a multi-agent system, you should fol
|
|||
|
||||
1. Understand the user’s intent — what they want the workflow to achieve. Plan accordingly to build an elegant and efficient system.
|
||||
2. Identify required tools - if the user mentions specific tasks (e.g. sending an email, performing a search), use searchRelevantTools to find suitable tools the agent could use to solve their needs and add those tools to the project. Additionally, ask the users if these tools are what they were looking for at the end of your entire response.
|
||||
3. Create a first draft of a new agent for each step in the plan. If there is an example agent, you must start off by editing this into the Hub agent. Attach all tools to the relevant agents.
|
||||
3. Create a first draft of a new agent for each step in the plan. You must always ensure to set a start agent when creating a multi-agent system. Attach all tools to the relevant agents.
|
||||
4. Describe your work — briefly summarise what you've done at the end of your turn.
|
||||
|
||||
It is good practice to add tools first and then agents
|
||||
|
|
@ -77,6 +77,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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
`;
|
||||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue