From 3d36884c73e36bb7a3fde4f60f78fc56020268df Mon Sep 17 00:00:00 2001 From: arkml Date: Tue, 2 Sep 2025 22:09:09 +0530 Subject: [PATCH 01/10] Small improvements (#228) * make the start tag for all agents the same * remove scroll from the my assistants page --- .../app/projects/[projectId]/workflow/entity_list.tsx | 4 ++-- .../app/projects/components/build-assistant-section.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx index f939e029..173bec94 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx @@ -207,8 +207,8 @@ const ListItemWithMenu = ({ }; const StartLabel = () => ( -
- Start +
+ START
); diff --git a/apps/rowboat/app/projects/components/build-assistant-section.tsx b/apps/rowboat/app/projects/components/build-assistant-section.tsx index 94200964..5eddfb0e 100644 --- a/apps/rowboat/app/projects/components/build-assistant-section.tsx +++ b/apps/rowboat/app/projects/components/build-assistant-section.tsx @@ -21,7 +21,7 @@ const SHOW_PREBUILT_CARDS = process.env.NEXT_PUBLIC_SHOW_PREBUILT_CARDS === 'tru -const ITEMS_PER_PAGE = 6; +const ITEMS_PER_PAGE = 10; const copilotPrompts = { "Blog assistant": { @@ -363,7 +363,7 @@ export function BuildAssistantSection() {
-
+
{projectsLoading ? (
Loading assistants... @@ -374,7 +374,7 @@ export function BuildAssistantSection() {
) : ( <> -
+
{currentProjects.map((project) => ( Date: Thu, 4 Sep 2025 11:05:43 +0400 Subject: [PATCH 02/10] 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 --- .../app/projects/[projectId]/copilot/app.tsx | 36 +- .../projects/[projectId]/playground/app.tsx | 34 +- .../playground/components/chat.tsx | 2 +- .../app/projects/[projectId]/workflow/app.tsx | 2 + .../workflow/components/TopBar.tsx | 224 +++++----- .../[projectId]/workflow/entity_list.tsx | 6 +- .../[projectId]/workflow/workflow_editor.tsx | 399 ++++++++++++------ .../components/common/compose-box-copilot.tsx | 6 +- .../components/common/panel-common.tsx | 29 +- .../components/common/tool-param-card.tsx | 2 +- 10 files changed, 487 insertions(+), 253 deletions(-) diff --git a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx index b3704a15..cfd74b5f 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx @@ -269,7 +269,7 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message toolQuery={toolQuery} />
-
+
{responseError && (

{responseError}

@@ -322,6 +322,8 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void dispatch: (action: WorkflowDispatch) => void; isInitialState?: boolean; dataSources?: z.infer[]; + 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 } - title="Skipper" + title={ +
+
+ + +
+
+ } subtitle="Build your assistant" rightActions={
diff --git a/apps/rowboat/app/projects/[projectId]/playground/app.tsx b/apps/rowboat/app/projects/[projectId]/playground/app.tsx index 05c67dc7..f02ee8e8 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/app.tsx @@ -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(0); const [showDebugMessages, setShowDebugMessages] = useState(true); @@ -56,8 +60,34 @@ export function App({ className={`${hidden ? 'hidden' : 'block'}`} variant="playground" tourTarget="playground" - icon={} - title="Chat" + title={ +
+
+ + +
+
+ } subtitle="Chat with your assistant" rightActions={
diff --git a/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx b/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx index 5f634bdc..c7edde70 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx @@ -425,7 +425,7 @@ export function Chat({ )} -
+
{showSuccessMessage && (
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx index 1044810f..ea948813 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx @@ -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() { diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx index 98707fd2..7684bfec 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx @@ -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" }} />
- -
- -
- {publishing && } - {isLive &&
- - Live workflow -
} - {!isLive &&
- - Draft workflow -
} - {/* Download JSON icon button, with tooltip, to the left of the menu */} - - - -
+ {/* Show divider and CTA only in live view */} + {isLive &&
} + {isLive ? ( + + ) : null}
{showCopySuccess &&
Copied to clipboard
} + {showBuildModeBanner &&
+ +
+ Switched to draft mode. You can now make changes to your workflow. +
+
}
- {isLive &&
-
- - This version is locked. Changes applied will not be reflected. -
-
} {!isLive && <> - - + + } {/* Deploy CTA - always visible */} -
+
{isLive ? ( - - - - - - } - onPress={() => { if (projectId) { router.push(`/projects/${projectId}/config`); } }} - > - API & SDK Settings - - } - onPress={() => { if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } }} + <> + + + + + + } + onPress={() => { if (projectId) { router.push(`/projects/${projectId}/config`); } }} + > + API & SDK Settings + + } + onPress={() => { if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } }} + > + Manage Triggers + + + + + {/* Live workflow label moved here */} +
+ {publishing && } +
+ + Live workflow +
+ + + +
+ ) : ( <> +
@@ -203,31 +222,34 @@ export function TopBar({ +
+ + {/* Moved draft/live labels and download button here */} +
+ {publishing && } + {isLive &&
+ + Live workflow +
} + {!isLive &&
+ + Draft workflow +
} + + + +
)}
- - {isLive &&
- -
} - - {!isLive && } +
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx index 173bec94..9c7b8205 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx @@ -1175,7 +1175,7 @@ export const EntityList = forwardRef< tourTarget="entity-prompts" className={clsx( "h-full", - !expandedPanels.prompts && "h-[53px]!" + !expandedPanels.prompts && "h-[61px]!" )} title={
@@ -1208,7 +1208,7 @@ export const EntityList = forwardRef< } > {expandedPanels.prompts && ( -
+
{prompts.length > 0 ? (
@@ -2116,4 +2116,4 @@ function AddVariableModal({ isOpen, onClose, onConfirm, initialName, initialValu ); -} \ No newline at end of file +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index a1ab84fd..ff2c380d 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -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[]>([]); const saving = useRef(false); const [showCopySuccess, setShowCopySuccess] = useState(false); - const [showCopilot, setShowCopilot] = useState(true); - const [copilotWidth, setCopilotWidth] = useState(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(null); + const [configKey, setConfigKey] = useState(0); + const [lastWorkflowId, setLastWorkflowId] = useState(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> = {}) { - dispatch({ type: "add_tool", tool }); + dispatchGuarded({ type: "add_tool", tool }); } function handleAddPrompt(prompt: Partial> = {}) { - 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> = {}) { - 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, }}>
+ {/* Live Workflow Edit Modal */} + + + +
+ + Edit Live Workflow +
+
+ +

+ Seems like you're trying to edit the live workflow. Only the draft version can be modified. Changes will not be saved. +

+
+ + + + +
+
+ {/* Top Bar - Isolated like sidebar */} 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 */} - +
- + + + {/* Config Panel - always rendered, visibility controlled */} - - {showCopilot && ( - <> - - setCopilotWidth(size)} - > - 0 - ? { type: 'chat', messages: chatMessages } - : undefined - } - isInitialState={isInitialState} - dataSources={dataSources} - /> - - - )} + {/* Second handle - always show (between config and chat panels) */} + + + {/* ChatApp/Copilot Panel - always visible */} + +
+ +
+
+ 0 + ? { type: 'chat', messages: chatMessages } + : undefined + } + isInitialState={isInitialState} + dataSources={dataSources} + activePanel={activePanel} + onTogglePanel={handleTogglePanel} + /> +
+
{USE_PRODUCT_TOUR && showTour && ( Press ⌘ + Enter to send
- {/* Outer container with padding */} -
{/* Textarea */} -
+