mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-30 10:56:29 +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
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue