mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-30 19:06:23 +02:00
Merge copilot and playground into single pane and prevent edits in live mode
* Display copilot and playground as toggles * Auto-switch live mode to draft mode when changes are made to workflow or build mode is toggled on * Show playground / copilot alongside entity config in 3-pane * Make tool params non-bold * Fix panel resizing issues * Fix logic around transitions back to draft mode from live mode * Change test to chat in the toggle * Fix workflow consistency issues while switching between live and draft modes
This commit is contained in:
parent
3d36884c73
commit
122d83d8f7
10 changed files with 487 additions and 253 deletions
|
|
@ -269,7 +269,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
|
|||
toolQuery={toolQuery}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 px-1">
|
||||
<div className="shrink-0 px-0 pb-0">
|
||||
{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,6 +322,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;
|
||||
}>(({
|
||||
projectId,
|
||||
workflow,
|
||||
|
|
@ -329,6 +331,8 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
|
|||
dispatch,
|
||||
isInitialState = false,
|
||||
dataSources,
|
||||
activePanel,
|
||||
onTogglePanel,
|
||||
}, ref) => {
|
||||
const [copilotKey, setCopilotKey] = useState(0);
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false);
|
||||
|
|
@ -365,8 +369,34 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
|
|||
<Panel
|
||||
variant="copilot"
|
||||
tourTarget="copilot"
|
||||
icon={<Sparkles className="w-5 h-5 text-indigo-600 dark:text-indigo-400" />}
|
||||
title="Skipper"
|
||||
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>
|
||||
}
|
||||
subtitle="Build your assistant"
|
||||
rightActions={
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export function App({
|
|||
onPanelClick,
|
||||
triggerCopilotChat,
|
||||
isLiveWorkflow,
|
||||
activePanel,
|
||||
onTogglePanel,
|
||||
}: {
|
||||
hidden?: boolean;
|
||||
projectId: string;
|
||||
|
|
@ -25,6 +27,8 @@ export function App({
|
|||
onPanelClick?: () => void;
|
||||
triggerCopilotChat?: (message: string) => void;
|
||||
isLiveWorkflow: boolean;
|
||||
activePanel: 'playground' | 'copilot';
|
||||
onTogglePanel: () => void;
|
||||
}) {
|
||||
const [counter, setCounter] = useState<number>(0);
|
||||
const [showDebugMessages, setShowDebugMessages] = useState<boolean>(true);
|
||||
|
|
@ -56,8 +60,34 @@ export function App({
|
|||
className={`${hidden ? 'hidden' : 'block'}`}
|
||||
variant="playground"
|
||||
tourTarget="playground"
|
||||
icon={<MessageCircle className="w-5 h-5 text-blue-600 dark:text-blue-400" />}
|
||||
title="Chat"
|
||||
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>
|
||||
}
|
||||
subtitle="Chat with your assistant"
|
||||
rightActions={
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ export function Chat({
|
|||
<ChevronDownIcon className="w-5 h-5" strokeWidth={2.2} />
|
||||
</button>
|
||||
)}
|
||||
<div className="bg-white dark:bg-zinc-900 pt-4 pb-2">
|
||||
<div className="bg-white dark:bg-zinc-900 pt-4 pb-6">
|
||||
{showSuccessMessage && (
|
||||
<div className="mb-4 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800
|
||||
rounded-lg flex gap-2 justify-between items-center">
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ export function App({
|
|||
|
||||
function handleSetMode(mode: 'draft' | 'live') {
|
||||
setMode(mode);
|
||||
// Reload data to ensure we have the latest workflow data for the current mode
|
||||
reloadData();
|
||||
}
|
||||
|
||||
async function handleRevertToLive() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input } from "@heroui/react";
|
||||
import { Button as CustomButton } from "@/components/ui/button";
|
||||
import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
||||
|
|
@ -12,16 +13,17 @@ interface TopBarProps {
|
|||
publishing: boolean;
|
||||
isLive: boolean;
|
||||
showCopySuccess: boolean;
|
||||
showBuildModeBanner: boolean;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
showCopilot: boolean;
|
||||
activePanel: 'playground' | 'copilot';
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onDownloadJSON: () => void;
|
||||
onPublishWorkflow: () => void;
|
||||
onChangeMode: (mode: 'draft' | 'live') => void;
|
||||
onRevertToLive: () => void;
|
||||
onToggleCopilot: () => void;
|
||||
onTogglePanel: () => void;
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
|
|
@ -32,16 +34,17 @@ export function TopBar({
|
|||
publishing,
|
||||
isLive,
|
||||
showCopySuccess,
|
||||
showBuildModeBanner,
|
||||
canUndo,
|
||||
canRedo,
|
||||
showCopilot,
|
||||
activePanel,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onDownloadJSON,
|
||||
onPublishWorkflow,
|
||||
onChangeMode,
|
||||
onRevertToLive,
|
||||
onToggleCopilot,
|
||||
onTogglePanel,
|
||||
}: TopBarProps) {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
|
@ -70,106 +73,122 @@ export function TopBar({
|
|||
classNames={{
|
||||
base: "max-w-xs",
|
||||
input: "text-base font-semibold px-2",
|
||||
inputWrapper: "min-h-[28px] h-[28px] border-gray-200 dark:border-gray-700 px-0"
|
||||
inputWrapper: "min-h-[36px] h-[36px] border-gray-200 dark:border-gray-700 px-0"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{publishing && <Spinner size="sm" />}
|
||||
{isLive && <div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
|
||||
<RadioIcon size={16} />
|
||||
Live workflow
|
||||
</div>}
|
||||
{!isLive && <div className="bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
|
||||
<PenLine size={16} />
|
||||
Draft workflow
|
||||
</div>}
|
||||
|
||||
{/* Download JSON icon button, with tooltip, to the left of the menu */}
|
||||
<Tooltip content="Download Assistant JSON">
|
||||
<button
|
||||
onClick={onDownloadJSON}
|
||||
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
|
||||
aria-label="Download JSON"
|
||||
type="button"
|
||||
>
|
||||
<DownloadIcon size={20} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* Show divider and CTA only in live view */}
|
||||
{isLive && <div className="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>}
|
||||
{isLive ? (
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
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} />}
|
||||
>
|
||||
Switch to draft
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{showCopySuccess && <div className="flex items-center gap-2">
|
||||
<div className="text-green-500">Copied to clipboard</div>
|
||||
</div>}
|
||||
{showBuildModeBanner && <div className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<div className="text-blue-700 dark:text-blue-300 text-sm">
|
||||
Switched to draft mode. You can now make changes to your workflow.
|
||||
</div>
|
||||
</div>}
|
||||
<div className="flex items-center gap-2">
|
||||
{isLive && <div className="flex items-center gap-2 absolute left-1/2 transform -translate-x-1/2">
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 px-3 py-1.5 rounded-md text-sm font-medium flex items-center gap-2">
|
||||
<AlertTriangle size={16} />
|
||||
This version is locked. Changes applied will not be reflected.
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{!isLive && <>
|
||||
<button
|
||||
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
||||
title="Undo"
|
||||
disabled={!canUndo}
|
||||
<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"
|
||||
showHoverContent={true}
|
||||
hoverContent="Undo"
|
||||
>
|
||||
<UndoIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
||||
title="Redo"
|
||||
disabled={!canRedo}
|
||||
<UndoIcon className="w-4 h-4" />
|
||||
</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"
|
||||
showHoverContent={true}
|
||||
hoverContent="Redo"
|
||||
>
|
||||
<RedoIcon size={16} />
|
||||
</button>
|
||||
<RedoIcon className="w-4 h-4" />
|
||||
</CustomButton>
|
||||
</>}
|
||||
|
||||
{/* Deploy CTA - always visible */}
|
||||
<div className="flex">
|
||||
<div className="flex items-center gap-3">
|
||||
{isLive ? (
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
className="gap-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm"
|
||||
startContent={<Plug size={16} />}
|
||||
>
|
||||
Use Assistant
|
||||
<ChevronDownIcon size={14} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Assistant access options">
|
||||
<DropdownItem
|
||||
key="api-sdk"
|
||||
startContent={<SettingsIcon size={16} />}
|
||||
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/config`); } }}
|
||||
>
|
||||
API & SDK Settings
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="manage-triggers"
|
||||
startContent={<ZapIcon size={16} />}
|
||||
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } }}
|
||||
<>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
className="gap-2 px-4 bg-blue-50 hover:bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 dark:text-blue-400 font-semibold text-sm border border-blue-200 dark:border-blue-700 shadow-sm"
|
||||
startContent={<Plug size={16} />}
|
||||
>
|
||||
Manage Triggers
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
Use Assistant
|
||||
<ChevronDownIcon size={14} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Assistant access options">
|
||||
<DropdownItem
|
||||
key="api-sdk"
|
||||
startContent={<SettingsIcon size={16} />}
|
||||
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/config`); } }}
|
||||
>
|
||||
API & SDK Settings
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="manage-triggers"
|
||||
startContent={<ZapIcon size={16} />}
|
||||
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } }}
|
||||
>
|
||||
Manage Triggers
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
|
||||
{/* Live workflow label moved here */}
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
{publishing && <Spinner size="sm" />}
|
||||
<div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
|
||||
<RadioIcon size={16} />
|
||||
Live workflow
|
||||
</div>
|
||||
<Tooltip content="Download Assistant JSON">
|
||||
<button
|
||||
onClick={onDownloadJSON}
|
||||
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
|
||||
aria-label="Download JSON"
|
||||
type="button"
|
||||
>
|
||||
<DownloadIcon size={20} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
onPress={onPublishWorkflow}
|
||||
className="gap-2 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold text-sm rounded-r-none"
|
||||
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"
|
||||
>
|
||||
|
|
@ -180,7 +199,7 @@ export function TopBar({
|
|||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
className="min-w-0 px-2 bg-green-600 hover:bg-green-700 border-l-1 border-green-500 text-white font-semibold text-sm rounded-l-none"
|
||||
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>
|
||||
|
|
@ -203,31 +222,34 @@ export function TopBar({
|
|||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{/* Moved draft/live labels and download button here */}
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
{publishing && <Spinner size="sm" />}
|
||||
{isLive && <div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
|
||||
<RadioIcon size={16} />
|
||||
Live workflow
|
||||
</div>}
|
||||
{!isLive && <div className="bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 px-2 py-1 rounded-md text-xs font-medium flex items-center gap-1.5">
|
||||
<PenLine size={16} />
|
||||
Draft workflow
|
||||
</div>}
|
||||
<Tooltip content="Download Assistant JSON">
|
||||
<button
|
||||
onClick={onDownloadJSON}
|
||||
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
|
||||
aria-label="Download JSON"
|
||||
type="button"
|
||||
>
|
||||
<DownloadIcon size={20} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLive && <div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
onPress={() => onChangeMode('draft')}
|
||||
className="gap-2 px-4 bg-gray-600 hover:bg-gray-700 text-white font-semibold text-sm"
|
||||
startContent={<PenLine size={16} />}
|
||||
>
|
||||
Switch to draft
|
||||
</Button>
|
||||
</div>}
|
||||
|
||||
{!isLive && <Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
onPress={onToggleCopilot}
|
||||
className="gap-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm"
|
||||
startContent={showCopilot ? null : <span className="text-indigo-300">✨</span>}
|
||||
>
|
||||
{showCopilot ? "Hide Skipper" : "Skipper"}
|
||||
</Button>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1175,7 +1175,7 @@ export const EntityList = forwardRef<
|
|||
tourTarget="entity-prompts"
|
||||
className={clsx(
|
||||
"h-full",
|
||||
!expandedPanels.prompts && "h-[53px]!"
|
||||
!expandedPanels.prompts && "h-[61px]!"
|
||||
)}
|
||||
title={
|
||||
<div className={`${headerClasses} rounded-md transition-colors h-full`}>
|
||||
|
|
@ -1208,7 +1208,7 @@ export const EntityList = forwardRef<
|
|||
}
|
||||
>
|
||||
{expandedPanels.prompts && (
|
||||
<div className="h-[calc(100%-53px)] overflow-y-auto">
|
||||
<div className="h-[calc(100%-61px)] overflow-y-auto">
|
||||
<div className="p-2">
|
||||
{prompts.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
|
|
@ -2116,4 +2116,4 @@ function AddVariableModal({ isOpen, onClose, onConfirm, initialName, initialValu
|
|||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,7 @@ interface StateItem {
|
|||
chatKey: number;
|
||||
lastUpdatedAt: string;
|
||||
isLive: boolean;
|
||||
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
|
@ -73,6 +74,15 @@ interface State {
|
|||
export type Action = {
|
||||
type: "update_workflow_name";
|
||||
name: string;
|
||||
} | {
|
||||
type: "switch_to_draft_due_to_changes";
|
||||
} | {
|
||||
type: "show_workflow_change_banner";
|
||||
} | {
|
||||
type: "clear_workflow_change_banner";
|
||||
} | {
|
||||
type: "set_is_live";
|
||||
isLive: boolean;
|
||||
} | {
|
||||
type: "set_publishing";
|
||||
publishing: boolean;
|
||||
|
|
@ -238,6 +248,19 @@ function reducer(state: State, action: Action): State {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case "switch_to_draft_due_to_changes": {
|
||||
newState = produce(state, draft => {
|
||||
draft.present.isLive = false;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "set_is_live": {
|
||||
newState = produce(state, draft => {
|
||||
draft.present.isLive = action.isLive;
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "set_saving": {
|
||||
newState = produce(state, draft => {
|
||||
draft.present.saving = action.saving;
|
||||
|
|
@ -335,9 +358,6 @@ function reducer(state: State, action: Action): State {
|
|||
draft.selection = null;
|
||||
break;
|
||||
case "add_agent": {
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
let newAgentName = "New agent";
|
||||
if (draft.workflow?.agents.some((agent) => agent.name === newAgentName)) {
|
||||
newAgentName = `New agent ${draft.workflow.agents.filter((agent) =>
|
||||
|
|
@ -368,9 +388,6 @@ function reducer(state: State, action: Action): State {
|
|||
break;
|
||||
}
|
||||
case "add_tool": {
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
let newToolName = "new_tool";
|
||||
if (draft.workflow?.tools.some((tool) => tool.name === newToolName)) {
|
||||
newToolName = `new_tool_${draft.workflow.tools.filter((tool) =>
|
||||
|
|
@ -396,9 +413,6 @@ function reducer(state: State, action: Action): State {
|
|||
break;
|
||||
}
|
||||
case "add_prompt": {
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
let newPromptName = "New Variable";
|
||||
if (draft.workflow?.prompts.some((prompt) => prompt.name === newPromptName)) {
|
||||
newPromptName = `New Variable ${draft.workflow?.prompts.filter((prompt) =>
|
||||
|
|
@ -419,9 +433,6 @@ function reducer(state: State, action: Action): State {
|
|||
break;
|
||||
}
|
||||
case "add_prompt_no_select": {
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
let newPromptName = "New Variable";
|
||||
if (draft.workflow?.prompts.some((prompt) => prompt.name === newPromptName)) {
|
||||
newPromptName = `New Variable ${draft.workflow?.prompts.filter((prompt) =>
|
||||
|
|
@ -440,9 +451,6 @@ function reducer(state: State, action: Action): State {
|
|||
}
|
||||
// TODO: parameterize this instead of writing if else based on pipeline length (pipelineAgents.length)
|
||||
case "add_pipeline": {
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!draft.workflow.pipelines) {
|
||||
draft.workflow.pipelines = [];
|
||||
|
|
@ -521,9 +529,6 @@ function reducer(state: State, action: Action): State {
|
|||
break;
|
||||
}
|
||||
case "delete_agent":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
// Remove the agent
|
||||
draft.workflow.agents = draft.workflow.agents.filter(
|
||||
(agent) => agent.name !== action.name
|
||||
|
|
@ -568,9 +573,6 @@ function reducer(state: State, action: Action): State {
|
|||
draft.chatKey++;
|
||||
break;
|
||||
case "delete_tool":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
draft.workflow.tools = draft.workflow.tools.filter(
|
||||
(tool) => tool.name !== action.name
|
||||
);
|
||||
|
|
@ -579,9 +581,6 @@ function reducer(state: State, action: Action): State {
|
|||
draft.chatKey++;
|
||||
break;
|
||||
case "delete_prompt":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
draft.workflow.prompts = draft.workflow.prompts.filter(
|
||||
(prompt) => prompt.name !== action.name
|
||||
);
|
||||
|
|
@ -590,9 +589,6 @@ function reducer(state: State, action: Action): State {
|
|||
draft.chatKey++;
|
||||
break;
|
||||
case "delete_pipeline":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
if (draft.workflow.pipelines) {
|
||||
// Find the pipeline to get its associated agents
|
||||
const pipelineToDelete = draft.workflow.pipelines.find(
|
||||
|
|
@ -649,9 +645,6 @@ function reducer(state: State, action: Action): State {
|
|||
draft.chatKey++;
|
||||
break;
|
||||
case "update_pipeline": {
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
if (draft.workflow.pipelines) {
|
||||
draft.workflow.pipelines = draft.workflow.pipelines.map(pipeline =>
|
||||
pipeline.name === action.name ? { ...pipeline, ...action.pipeline } : pipeline
|
||||
|
|
@ -663,9 +656,6 @@ function reducer(state: State, action: Action): State {
|
|||
break;
|
||||
}
|
||||
case "update_agent": {
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
|
||||
// update agent data
|
||||
draft.workflow.agents = draft.workflow.agents.map((agent) =>
|
||||
|
|
@ -724,9 +714,6 @@ function reducer(state: State, action: Action): State {
|
|||
break;
|
||||
}
|
||||
case "update_tool":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
|
||||
// update tool data
|
||||
draft.workflow.tools = draft.workflow.tools.map((tool) =>
|
||||
|
|
@ -769,9 +756,6 @@ function reducer(state: State, action: Action): State {
|
|||
draft.chatKey++;
|
||||
break;
|
||||
case "update_prompt":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
|
||||
// update prompt data
|
||||
draft.workflow.prompts = draft.workflow.prompts.map((prompt) =>
|
||||
|
|
@ -814,9 +798,6 @@ function reducer(state: State, action: Action): State {
|
|||
draft.chatKey++;
|
||||
break;
|
||||
case "update_prompt_no_select":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
|
||||
// update prompt data
|
||||
draft.workflow.prompts = draft.workflow.prompts.map((prompt) =>
|
||||
|
|
@ -855,18 +836,12 @@ function reducer(state: State, action: Action): State {
|
|||
draft.chatKey++;
|
||||
break;
|
||||
case "toggle_agent":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
draft.workflow.agents = draft.workflow.agents.map(agent =>
|
||||
agent.name === action.name ? { ...agent, disabled: !agent.disabled } : agent
|
||||
);
|
||||
draft.chatKey++;
|
||||
break;
|
||||
case "set_main_agent":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
draft.workflow.startAgent = action.name;
|
||||
draft.pendingChanges = true;
|
||||
draft.chatKey++;
|
||||
|
|
@ -955,6 +930,7 @@ export function WorkflowEditor({
|
|||
chatKey: 0,
|
||||
lastUpdatedAt: workflow.lastUpdatedAt,
|
||||
isLive,
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -965,10 +941,42 @@ export function WorkflowEditor({
|
|||
const saveQueue = useRef<z.infer<typeof Workflow>[]>([]);
|
||||
const saving = useRef(false);
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false);
|
||||
const [showCopilot, setShowCopilot] = useState(true);
|
||||
const [copilotWidth, setCopilotWidth] = useState<number>(PANEL_RATIOS.copilot);
|
||||
const [activePanel, setActivePanel] = useState<'playground' | 'copilot'>('copilot');
|
||||
const [isInitialState, setIsInitialState] = useState(true);
|
||||
const [showBuildModeBanner, setShowBuildModeBanner] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<Action | null>(null);
|
||||
const [configKey, setConfigKey] = useState(0);
|
||||
const [lastWorkflowId, setLastWorkflowId] = useState<string | null>(null);
|
||||
const [showTour, setShowTour] = useState(true);
|
||||
|
||||
// Centralized mode transition handler
|
||||
const handleModeTransition = useCallback((newMode: 'draft' | 'live', reason: 'publish' | 'view_live' | 'switch_draft' | 'modal_switch') => {
|
||||
// Clear any open entity configs
|
||||
dispatch({ type: "unselect_agent" });
|
||||
|
||||
// Set default panel based on mode
|
||||
setActivePanel(newMode === 'live' ? 'playground' : 'copilot');
|
||||
|
||||
// Force component re-render
|
||||
setConfigKey(prev => prev + 1);
|
||||
|
||||
// Handle mode-specific logic
|
||||
if (reason === 'publish') {
|
||||
// This will be handled by the publish function itself
|
||||
return;
|
||||
} else {
|
||||
// Direct mode switch
|
||||
onChangeMode(newMode);
|
||||
|
||||
// If switching to draft mode, we need to ensure we have the correct draft data
|
||||
// The parent component will update the workflow prop, but we need to wait for it
|
||||
if (newMode === 'draft') {
|
||||
// Force a workflow state reset when the workflow prop updates
|
||||
setLastWorkflowId(null);
|
||||
}
|
||||
}
|
||||
}, [onChangeMode]);
|
||||
const copilotRef = useRef<{ handleUserMessage: (message: string) => void }>(null);
|
||||
const entityListRef = useRef<{ openDataSourcesModal: () => void } | null>(null);
|
||||
|
||||
|
|
@ -1010,7 +1018,7 @@ export function WorkflowEditor({
|
|||
|
||||
// Function to trigger copilot chat
|
||||
const triggerCopilotChat = useCallback((message: string) => {
|
||||
setShowCopilot(true);
|
||||
setActivePanel('copilot');
|
||||
// Small delay to ensure copilot is mounted
|
||||
setTimeout(() => {
|
||||
copilotRef.current?.handleUserMessage(message);
|
||||
|
|
@ -1028,14 +1036,14 @@ export function WorkflowEditor({
|
|||
const prompt = localStorage.getItem(`project_prompt_${projectId}`);
|
||||
console.log('init project prompt', prompt);
|
||||
if (prompt) {
|
||||
setShowCopilot(true);
|
||||
setActivePanel('copilot');
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
// Hide copilot when switching to live mode
|
||||
// Switch to playground when switching to live mode
|
||||
useEffect(() => {
|
||||
if (isLive) {
|
||||
setShowCopilot(false);
|
||||
setActivePanel('playground');
|
||||
}
|
||||
}, [isLive]);
|
||||
|
||||
|
|
@ -1093,15 +1101,15 @@ export function WorkflowEditor({
|
|||
...agent,
|
||||
model: agent.model || defaultModel || "gpt-4.1"
|
||||
};
|
||||
dispatch({ type: "add_agent", agent: agentWithModel });
|
||||
dispatchGuarded({ type: "add_agent", agent: agentWithModel });
|
||||
}
|
||||
|
||||
function handleAddTool(tool: Partial<z.infer<typeof WorkflowTool>> = {}) {
|
||||
dispatch({ type: "add_tool", tool });
|
||||
dispatchGuarded({ type: "add_tool", tool });
|
||||
}
|
||||
|
||||
function handleAddPrompt(prompt: Partial<z.infer<typeof WorkflowPrompt>> = {}) {
|
||||
dispatch({ type: "add_prompt", prompt });
|
||||
dispatchGuarded({ type: "add_prompt", prompt });
|
||||
}
|
||||
|
||||
function handleSelectPipeline(name: string) {
|
||||
|
|
@ -1109,7 +1117,7 @@ export function WorkflowEditor({
|
|||
}
|
||||
|
||||
function handleAddPipeline(pipeline: Partial<z.infer<typeof WorkflowPipeline>> = {}) {
|
||||
dispatch({ type: "add_pipeline", pipeline, defaultModel });
|
||||
dispatchGuarded({ type: "add_pipeline", pipeline, defaultModel });
|
||||
}
|
||||
|
||||
function handleDeletePipeline(name: string) {
|
||||
|
|
@ -1130,12 +1138,12 @@ export function WorkflowEditor({
|
|||
};
|
||||
|
||||
// First add the agent
|
||||
dispatch({ type: "add_agent", agent: agentWithModel });
|
||||
dispatchGuarded({ type: "add_agent", agent: agentWithModel });
|
||||
|
||||
// Then add it to the pipeline
|
||||
const pipeline = state.present.workflow.pipelines?.find(p => p.name === pipelineName);
|
||||
if (pipeline) {
|
||||
dispatch({
|
||||
dispatchGuarded({
|
||||
type: "update_pipeline",
|
||||
name: pipelineName,
|
||||
pipeline: {
|
||||
|
|
@ -1225,8 +1233,17 @@ export function WorkflowEditor({
|
|||
}
|
||||
|
||||
async function handlePublishWorkflow() {
|
||||
await publishWorkflow(projectId, state.present.workflow);
|
||||
onChangeMode('live');
|
||||
dispatch({ type: 'set_publishing', publishing: true });
|
||||
try {
|
||||
await publishWorkflow(projectId, state.present.workflow);
|
||||
// Use centralized mode transition for publish
|
||||
handleModeTransition('live', 'publish');
|
||||
// reflect live mode both internally and externally in one go
|
||||
dispatch({ type: 'set_is_live', isLive: true });
|
||||
onChangeMode('live');
|
||||
} finally {
|
||||
dispatch({ type: 'set_publishing', publishing: false });
|
||||
}
|
||||
}
|
||||
|
||||
function handleRevertToLive() {
|
||||
|
|
@ -1326,6 +1343,101 @@ export function WorkflowEditor({
|
|||
setIsInitialState(false);
|
||||
}
|
||||
|
||||
// Centralized draft switch for any workflow modification while in live mode
|
||||
const ensureDraftForModify = useCallback(() => {
|
||||
if (isLive && !state.present.publishing) {
|
||||
onChangeMode('draft');
|
||||
setShowBuildModeBanner(true);
|
||||
setTimeout(() => setShowBuildModeBanner(false), 5000);
|
||||
}
|
||||
}, [isLive, state.present.publishing, onChangeMode]);
|
||||
|
||||
const WORKFLOW_MOD_ACTIONS = useRef(new Set([
|
||||
'add_agent','add_tool','add_prompt','add_prompt_no_select','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'
|
||||
])).current;
|
||||
|
||||
const dispatchGuarded = useCallback((action: Action) => {
|
||||
// Intercept workflow modifications in live mode before they reach the reducer
|
||||
if (WORKFLOW_MOD_ACTIONS.has((action as any).type) && isLive && !state.present.publishing) {
|
||||
setPendingAction(action);
|
||||
setShowEditModal(true);
|
||||
return; // Block the action - it never reaches the reducer
|
||||
}
|
||||
dispatch(action); // Allow the action to proceed
|
||||
}, [WORKFLOW_MOD_ACTIONS, isLive, state.present.publishing, dispatch]);
|
||||
|
||||
// Simplified modal handlers
|
||||
const handleSwitchToDraft = useCallback(() => {
|
||||
setShowEditModal(false);
|
||||
setPendingAction(null); // Don't apply the pending action
|
||||
handleModeTransition('draft', 'modal_switch');
|
||||
setShowBuildModeBanner(true);
|
||||
setTimeout(() => setShowBuildModeBanner(false), 5000);
|
||||
}, [handleModeTransition]);
|
||||
|
||||
const handleCancelEdit = useCallback(() => {
|
||||
setShowEditModal(false);
|
||||
setPendingAction(null);
|
||||
// Force re-render of config components to reset form values
|
||||
setConfigKey(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
// Single useEffect for data synchronization
|
||||
useEffect(() => {
|
||||
// Only sync when workflow data actually changes
|
||||
const currentWorkflowId = `${isLive ? 'live' : 'draft'}-${workflow.lastUpdatedAt}`;
|
||||
|
||||
// Special case: if we're switching to draft mode and the workflow data looks like live data
|
||||
// (same lastUpdatedAt as the previous live data), don't reset the state yet
|
||||
if (!isLive && lastWorkflowId && lastWorkflowId.startsWith('live-') &&
|
||||
currentWorkflowId === `draft-${workflow.lastUpdatedAt}`) {
|
||||
// This is likely stale draft data that matches live data
|
||||
// Don't reset the state, just update the ID
|
||||
setLastWorkflowId(currentWorkflowId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastWorkflowId !== currentWorkflowId) {
|
||||
dispatch({ type: "restore_state", state: { ...state.present, workflow } });
|
||||
setLastWorkflowId(currentWorkflowId);
|
||||
}
|
||||
}, [workflow, isLive, lastWorkflowId, state.present]);
|
||||
|
||||
// Handle the case where we switch to draft mode but get stale data
|
||||
useEffect(() => {
|
||||
// If we're in draft mode but the workflow data looks like live data (same lastUpdatedAt as live)
|
||||
// and we just switched from live mode, we need to wait for fresh draft data
|
||||
if (!isLive && lastWorkflowId && lastWorkflowId.startsWith('live-')) {
|
||||
// We just switched from live to draft, but we might have stale data
|
||||
// Clear the selection to prevent showing wrong data
|
||||
dispatch({ type: "unselect_agent" });
|
||||
}
|
||||
}, [isLive, lastWorkflowId]);
|
||||
|
||||
// Additional effect to handle mode changes that might not trigger workflow prop updates
|
||||
useEffect(() => {
|
||||
// If we're in draft mode but the workflow state contains live data, clear selection
|
||||
// This prevents showing wrong data while waiting for the correct workflow prop
|
||||
if (!isLive && state.present.isLive) {
|
||||
dispatch({ type: "unselect_agent" });
|
||||
}
|
||||
}, [isLive, state.present.isLive]);
|
||||
|
||||
function handleTogglePanel() {
|
||||
if (isLive && activePanel === 'playground') {
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
|
||||
const validateProjectName = (value: string) => {
|
||||
if (value.length === 0) {
|
||||
setProjectNameError("Project name cannot be empty");
|
||||
|
|
@ -1386,6 +1498,39 @@ export function WorkflowEditor({
|
|||
onSelectPrompt: handleSelectPrompt,
|
||||
}}>
|
||||
<div className="h-full flex flex-col gap-5">
|
||||
{/* Live Workflow Edit Modal */}
|
||||
<Modal isOpen={showEditModal} onClose={handleCancelEdit} size="md">
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-amber-500" />
|
||||
<span>Edit Live Workflow</span>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Seems like you're trying to edit the live workflow. Only the draft version can be modified. Changes will not be saved.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={handleCancelEdit}
|
||||
className="text-gray-600"
|
||||
>
|
||||
View the live version
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleSwitchToDraft}
|
||||
className="bg-blue-600 text-white"
|
||||
>
|
||||
Switch to draft
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* Top Bar - Isolated like sidebar */}
|
||||
<TopBar
|
||||
localProjectName={localProjectName}
|
||||
|
|
@ -1395,21 +1540,26 @@ export function WorkflowEditor({
|
|||
publishing={state.present.publishing}
|
||||
isLive={isLive}
|
||||
showCopySuccess={showCopySuccess}
|
||||
showBuildModeBanner={showBuildModeBanner}
|
||||
canUndo={state.currentIndex > 0}
|
||||
canRedo={state.currentIndex < state.patches.length}
|
||||
showCopilot={showCopilot}
|
||||
onUndo={() => dispatch({ type: "undo" })}
|
||||
onRedo={() => dispatch({ type: "redo" })}
|
||||
activePanel={activePanel}
|
||||
onUndo={() => dispatchGuarded({ type: "undo" })}
|
||||
onRedo={() => dispatchGuarded({ type: "redo" })}
|
||||
onDownloadJSON={handleDownloadJSON}
|
||||
onPublishWorkflow={handlePublishWorkflow}
|
||||
onChangeMode={onChangeMode}
|
||||
onRevertToLive={handleRevertToLive}
|
||||
onToggleCopilot={() => setShowCopilot(!showCopilot)}
|
||||
onTogglePanel={handleTogglePanel}
|
||||
/>
|
||||
|
||||
{/* Content Area */}
|
||||
<ResizablePanelGroup direction="horizontal" className="flex-1 flex overflow-auto gap-1 rounded-xl bg-zinc-50 dark:bg-zinc-900">
|
||||
<ResizablePanel minSize={10} defaultSize={PANEL_RATIOS.entityList}>
|
||||
<ResizablePanel
|
||||
key={`entity-list-${state.present.selection ? '3-pane' : '2-pane'}`}
|
||||
minSize={10}
|
||||
defaultSize={PANEL_RATIOS.entityList}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<EntityList
|
||||
ref={entityListRef}
|
||||
|
|
@ -1462,24 +1612,16 @@ export function WorkflowEditor({
|
|||
/>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
|
||||
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${!state.present.selection ? 'hidden' : ''}`} />
|
||||
|
||||
{/* Config Panel - always rendered, visibility controlled */}
|
||||
<ResizablePanel
|
||||
minSize={20}
|
||||
defaultSize={showCopilot ? PANEL_RATIOS.chatApp : PANEL_RATIOS.chatApp + PANEL_RATIOS.copilot}
|
||||
className="overflow-auto"
|
||||
defaultSize={45}
|
||||
className={`overflow-auto ${!state.present.selection ? 'hidden' : ''}`}
|
||||
>
|
||||
<ChatApp
|
||||
key={'' + state.present.chatKey}
|
||||
hidden={state.present.selection !== null}
|
||||
projectId={projectId}
|
||||
workflow={state.present.workflow}
|
||||
messageSubscriber={updateChatMessages}
|
||||
onPanelClick={handlePlaygroundClick}
|
||||
triggerCopilotChat={triggerCopilotChat}
|
||||
isLiveWorkflow={isLive}
|
||||
/>
|
||||
{state.present.selection?.type === "agent" && <AgentConfig
|
||||
key={`agent-${state.present.workflow.agents.findIndex(agent => agent.name === state.present.selection!.name)}`}
|
||||
key={`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)!}
|
||||
|
|
@ -1489,7 +1631,7 @@ export function WorkflowEditor({
|
|||
tools={state.present.workflow.tools}
|
||||
prompts={state.present.workflow.prompts}
|
||||
dataSources={dataSources}
|
||||
handleUpdate={handleUpdateAgent.bind(null, state.present.selection.name)}
|
||||
handleUpdate={(update) => { dispatchGuarded({ type: "update_agent", name: state.present.selection!.name, agent: update }); }}
|
||||
handleClose={handleUnselectAgent}
|
||||
useRag={useRag}
|
||||
triggerCopilotChat={triggerCopilotChat}
|
||||
|
|
@ -1501,33 +1643,33 @@ export function WorkflowEditor({
|
|||
(tool) => tool.name === state.present.selection!.name
|
||||
);
|
||||
return <ToolConfig
|
||||
key={state.present.selection.name}
|
||||
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={handleUpdateTool.bind(null, state.present.selection.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}
|
||||
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={handleUpdatePrompt.bind(null, state.present.selection.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}
|
||||
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}
|
||||
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)!}
|
||||
|
|
@ -1563,38 +1705,55 @@ export function WorkflowEditor({
|
|||
</Panel>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
{showCopilot && (
|
||||
<>
|
||||
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
|
||||
<ResizablePanel
|
||||
minSize={10}
|
||||
defaultSize={PANEL_RATIOS.copilot}
|
||||
onResize={(size) => setCopilotWidth(size)}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
{/* Second handle - always show (between config and chat panels) */}
|
||||
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
|
||||
|
||||
{/* 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}
|
||||
/>
|
||||
</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>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
{USE_PRODUCT_TOUR && showTour && (
|
||||
<ProductTour
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue