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 2e6f350c..7684bfec 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx @@ -98,7 +98,7 @@ export function TopBar({ {showBuildModeBanner &&
- Switched to draft mode to enable Build panel. You can now make changes to your workflow. + Switched to draft mode. You can now make changes to your workflow.
}
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index ea790de0..ff2c380d 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -316,23 +316,9 @@ function reducer(state: State, action: Action): State { break; } default: { - // Check if this is a workflow modification action in live mode - const isWorkflowModification = [ - "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" - ].includes(action.type); - const [nextState, patches, inversePatches] = produceWithPatches( state.present, (draft) => { - // If this is a workflow modification in live mode, switch to draft - if (isWorkflowModification && isLive) { - draft.isLive = false; - - } - switch (action.type) { case "select_agent": draft.selection = { @@ -958,7 +944,39 @@ export function WorkflowEditor({ 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); @@ -1191,7 +1209,6 @@ export function WorkflowEditor({ } function handleReorderAgents(agents: z.infer[]) { - handleWorkflowChange(); // Save order to localStorage const orderMap = agents.reduce((acc, agent, index) => { acc[agent.name] = index; @@ -1204,7 +1221,6 @@ export function WorkflowEditor({ } function handleReorderPipelines(pipelines: z.infer[]) { - handleWorkflowChange(); // Save order to localStorage const orderMap = pipelines.reduce((acc, pipeline, index) => { acc[pipeline.name] = index; @@ -1220,6 +1236,8 @@ export function WorkflowEditor({ 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'); @@ -1342,19 +1360,76 @@ export function WorkflowEditor({ ])).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) { - onChangeMode('draft'); - setShowBuildModeBanner(true); - setTimeout(() => setShowBuildModeBanner(false), 5000); + setPendingAction(action); + setShowEditModal(true); + return; // Block the action - it never reaches the reducer } - dispatch(action); - }, [WORKFLOW_MOD_ACTIONS, isLive, state.present.publishing, onChangeMode, dispatch]); + 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 - onChangeMode('draft'); - setActivePanel('copilot'); // Switch to Build mode as intended + handleModeTransition('draft', 'switch_draft'); setShowBuildModeBanner(true); // Auto-hide banner after 5 seconds setTimeout(() => setShowBuildModeBanner(false), 5000); @@ -1363,16 +1438,6 @@ export function WorkflowEditor({ } } - function handleWorkflowChange() { - if (isLive) { - // User is making changes in live mode - switch to draft - onChangeMode('draft'); - setShowBuildModeBanner(true); - // Auto-hide banner after 5 seconds - setTimeout(() => setShowBuildModeBanner(false), 5000); - } - } - const validateProjectName = (value: string) => { if (value.length === 0) { setProjectNameError("Project name cannot be empty"); @@ -1433,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 */} {state.present.selection?.type === "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)!} @@ -1545,7 +1643,7 @@ export function WorkflowEditor({ (tool) => tool.name === state.present.selection!.name ); return tool.name !== state.present.selection!.name).map((tool) => tool.name), @@ -1555,7 +1653,7 @@ export function WorkflowEditor({ />; })()} {state.present.selection?.type === "prompt" && prompt.name === state.present.selection!.name)!} agents={state.present.workflow.agents} tools={state.present.workflow.tools} @@ -1565,13 +1663,13 @@ export function WorkflowEditor({ handleClose={handleUnselectPrompt} />} {state.present.selection?.type === "datasource" && dispatch({ type: "unselect_datasource" })} onDataSourceUpdate={onDataSourcesUpdated} />} {state.present.selection?.type === "pipeline" && pipeline.name === state.present.selection!.name)!}