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..cf398040 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx
@@ -1,8 +1,10 @@
"use client";
import React from "react";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input } from "@heroui/react";
-import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug } from "lucide-react";
+import { Button as CustomButton } from "@/components/ui/button";
+import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug, MessageCircleIcon } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
+import { ProgressBar, ProgressStep } from "@/components/ui/progress-bar";
interface TopBarProps {
localProjectName: string;
@@ -12,16 +14,27 @@ interface TopBarProps {
publishing: boolean;
isLive: boolean;
showCopySuccess: boolean;
+ showBuildModeBanner: boolean;
canUndo: boolean;
canRedo: boolean;
- showCopilot: boolean;
+ activePanel: 'playground' | 'copilot';
+ hasAgentInstructionChanges: boolean;
+ hasPlaygroundTested: boolean;
+ hasPublished: boolean;
+ hasClickedUse: boolean;
onUndo: () => void;
onRedo: () => void;
onDownloadJSON: () => void;
onPublishWorkflow: () => void;
onChangeMode: (mode: 'draft' | 'live') => void;
onRevertToLive: () => void;
- onToggleCopilot: () => void;
+ onTogglePanel: () => void;
+ onUseAssistantClick: () => void;
+ onStartNewChatAndFocus: () => void;
+ onStartBuildTour?: () => void;
+ onStartTestTour?: () => void;
+ onStartPublishTour?: () => void;
+ onStartUseTour?: () => void;
}
export function TopBar({
@@ -32,20 +45,47 @@ export function TopBar({
publishing,
isLive,
showCopySuccess,
+ showBuildModeBanner,
canUndo,
canRedo,
- showCopilot,
+ activePanel,
+ hasAgentInstructionChanges,
+ hasPlaygroundTested,
+ hasPublished,
+ hasClickedUse,
onUndo,
onRedo,
onDownloadJSON,
onPublishWorkflow,
onChangeMode,
onRevertToLive,
- onToggleCopilot,
+ onTogglePanel,
+ onUseAssistantClick,
+ onStartNewChatAndFocus,
+ onStartBuildTour,
+ onStartTestTour,
+ onStartPublishTour,
+ onStartUseTour,
}: TopBarProps) {
const router = useRouter();
const params = useParams();
const projectId = typeof (params as any).projectId === 'string' ? (params as any).projectId : (params as any).projectId?.[0];
+ // Progress bar steps with completion logic and current step detection
+ const step1Complete = hasAgentInstructionChanges;
+ const step2Complete = hasPlaygroundTested && hasAgentInstructionChanges;
+ const step3Complete = hasPublished && hasPlaygroundTested && hasAgentInstructionChanges;
+ const step4Complete = hasClickedUse && hasPublished && hasPlaygroundTested && hasAgentInstructionChanges;
+
+ // Determine current step (first incomplete step)
+ const currentStep = !step1Complete ? 1 : !step2Complete ? 2 : !step3Complete ? 3 : !step4Complete ? 4 : null;
+
+ const progressSteps: ProgressStep[] = [
+ { id: 1, label: "Build: Ask the copilot to create your assistant. Add tools and connect data sources.", completed: step1Complete, isCurrent: currentStep === 1 },
+ { id: 2, label: "Test: Test out your assistant by chatting with it. Use 'Fix' and 'Explain' to improve it.", completed: step2Complete, isCurrent: currentStep === 2 },
+ { id: 3, label: "Publish: Make it live with the Publish button. You can always switch back to draft.", completed: step3Complete, isCurrent: currentStep === 3 },
+ { id: 4, label: "Use: Click the 'Use Assistant' button to chat, set triggers (like emails), or connect via API.", completed: step4Complete, isCurrent: currentStep === 4 },
+ ];
+
return (
@@ -70,106 +110,156 @@ 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 &&
}
- {/* Download JSON icon button, with tooltip, to the left of the menu */}
-
-
-
-
+ {/* Show divider and CTA only in live view */}
+ {isLive &&
}
+ {isLive ? (
+
+ ) : null}
- {showCopySuccess &&
}
+
+ {/* Progress Bar - Center */}
+
+
{
+ if (step.id === 1 && onStartBuildTour) onStartBuildTour();
+ if (step.id === 2 && onStartTestTour) onStartTestTour();
+ if (step.id === 3 && onStartPublishTour) onStartPublishTour();
+ if (step.id === 4 && onStartUseTour) onStartUseTour();
+ }}
+ />
+
+
+ {/* Right side buttons */}
- {isLive &&
-
-
- This version is locked. Changes applied will not be reflected.
+ {showCopySuccess &&
}
+
+ {showBuildModeBanner &&
+
+
+ Switched to draft mode. You can now make changes to your workflow.
}
+
{!isLive && <>
-
-
+
+
-
-
+
+
>}
{/* Deploy CTA - always visible */}
-
+
{isLive ? (
-
-
- }
- >
- Use Assistant
-
-
-
-
- }
- onPress={() => { if (projectId) { router.push(`/projects/${projectId}/config`); } }}
- >
- API & SDK Settings
-
- }
- onPress={() => { if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } }}
+ <>
+
+
+ }
+ onPress={onUseAssistantClick}
>
- Manage Triggers
-
-
-
+ Use Assistant
+
+
+
+
+ }
+ onPress={() => {
+ onUseAssistantClick();
+ onStartNewChatAndFocus();
+ }}
+ >
+ Chat with Assistant
+
+ }
+ onPress={() => {
+ onUseAssistantClick();
+ if (projectId) { router.push(`/projects/${projectId}/config`); }
+ }}
+ >
+ API & SDK Settings
+
+ }
+ onPress={() => {
+ onUseAssistantClick();
+ if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); }
+ }}
+ >
+ Manage Triggers
+
+
+
+
+ {/* Live workflow label moved here */}
+
+ {publishing &&
}
+
+
+ Live workflow
+
+
+
+
+
+ >
) : (
<>
+
}
data-tour-target="deploy"
>
@@ -180,7 +270,7 @@ export function TopBar({
@@ -203,31 +293,34 @@ export function TopBar({
+
+
+ {/* Moved draft/live labels and download button here */}
+
+ {publishing &&
}
+ {isLive &&
+
+ Live workflow
+
}
+ {!isLive &&
}
+
+
+
+
>
)}
-
- {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 f939e029..27e7f02e 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
);
@@ -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 ? (
@@ -1631,17 +1631,6 @@ const ComposioCard = ({
))}
- {/* More tools option */}
-
)}
@@ -2116,4 +2105,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..dcd4ce64 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;
+ agentInstructionsChanged: 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,8 +656,9 @@ function reducer(state: State, action: Action): State {
break;
}
case "update_agent": {
- if (isLive) {
- break;
+ // Check if instructions are being changed
+ if (action.agent.instructions !== undefined) {
+ draft.agentInstructionsChanged = true;
}
// update agent data
@@ -724,9 +718,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 +760,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 +802,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 +840,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 +934,7 @@ export function WorkflowEditor({
chatKey: 0,
lastUpdatedAt: workflow.lastUpdatedAt,
isLive,
+ agentInstructionsChanged: false,
}
});
@@ -965,10 +945,47 @@ 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 [isLeftPanelCollapsed, setIsLeftPanelCollapsed] = 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);
+ const [showBuildTour, setShowBuildTour] = useState(false);
+ const [showTestTour, setShowTestTour] = useState(false);
+ const [showPublishTour, setShowPublishTour] = useState(false);
+ const [showUseTour, setShowUseTour] = useState(false);
+
+ // 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);
@@ -986,6 +1003,75 @@ export function WorkflowEditor({
const [projectNameError, setProjectNameError] = useState(null);
const [isEditingProjectName, setIsEditingProjectName] = useState(false);
const [pendingProjectName, setPendingProjectName] = useState(null);
+
+ // Build progress tracking - persists once set to true (guard SSR)
+ const [hasAgentInstructionChanges, setHasAgentInstructionChanges] = useState(() => {
+ if (typeof window === 'undefined') return false;
+ return localStorage.getItem(`agent_instructions_changed_${projectId}`) === 'true';
+ });
+
+ // Test progress tracking - persists once set to true (guard SSR)
+ const [hasPlaygroundTested, setHasPlaygroundTested] = useState(() => {
+ if (typeof window === 'undefined') return false;
+ return localStorage.getItem(`playground_tested_${projectId}`) === 'true';
+ });
+
+ // Publish progress tracking - persists once set to true (guard SSR)
+ const [hasPublished, setHasPublished] = useState(() => {
+ if (typeof window === 'undefined') return false;
+ return localStorage.getItem(`has_published_${projectId}`) === 'true';
+ });
+
+ // Use progress tracking - persists once set to true (guard SSR)
+ const [hasClickedUse, setHasClickedUse] = useState(() => {
+ if (typeof window === 'undefined') return false;
+ return localStorage.getItem(`has_clicked_use_${projectId}`) === 'true';
+ });
+
+ // Function to mark agent instructions as changed (persists in localStorage)
+ const markAgentInstructionsChanged = useCallback(() => {
+ if (!hasAgentInstructionChanges) {
+ setHasAgentInstructionChanges(true);
+ localStorage.setItem(`agent_instructions_changed_${projectId}`, 'true');
+ }
+ }, [hasAgentInstructionChanges, projectId]);
+
+ // Function to mark playground as tested (persists in localStorage)
+ const markPlaygroundTested = useCallback(() => {
+ if (!hasPlaygroundTested && hasAgentInstructionChanges) { // Only mark if step 1 is complete
+ setHasPlaygroundTested(true);
+ localStorage.setItem(`playground_tested_${projectId}`, 'true');
+ }
+ }, [hasPlaygroundTested, hasAgentInstructionChanges, projectId]);
+
+ // Function to mark as published (persists in localStorage)
+ const markAsPublished = useCallback(() => {
+ if (!hasPublished) {
+ setHasPublished(true);
+ localStorage.setItem(`has_published_${projectId}`, 'true');
+ }
+ }, [hasPublished, projectId]);
+
+ // Function to mark Use Assistant button as clicked (persists in localStorage)
+ const markUseAssistantClicked = useCallback(() => {
+ if (!hasClickedUse) {
+ setHasClickedUse(true);
+ localStorage.setItem(`has_clicked_use_${projectId}`, 'true');
+ }
+ }, [hasClickedUse, projectId]);
+
+ // Reference to start new chat function from playground
+ const startNewChatRef = useRef<(() => void) | null>(null);
+
+ // Function to start new chat and focus
+ const handleStartNewChatAndFocus = useCallback(() => {
+ if (startNewChatRef.current) {
+ startNewChatRef.current();
+ }
+ // Switch to playground (chat) mode and collapse left panel
+ setActivePanel('playground');
+ setIsLeftPanelCollapsed(true);
+ }, []);
// Load agent order from localStorage on mount
// useEffect(() => {
@@ -1010,7 +1096,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 +1114,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]);
@@ -1053,6 +1139,13 @@ export function WorkflowEditor({
}
}, [state.present.workflow, state.present.pendingChanges]);
+ // Track agent instruction changes from copilot
+ useEffect(() => {
+ if (state.present.agentInstructionsChanged) {
+ markAgentInstructionsChanged();
+ }
+ }, [state.present.agentInstructionsChanged, markAgentInstructionsChanged]);
+
function handleSelectAgent(name: string) {
dispatch({ type: "select_agent", name });
}
@@ -1093,15 +1186,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 +1202,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 +1223,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: {
@@ -1150,6 +1243,10 @@ export function WorkflowEditor({
}
function handleUpdateAgent(name: string, agent: Partial>) {
+ // Check if instructions are being changed
+ if (agent.instructions !== undefined) {
+ markAgentInstructionsChanged();
+ }
dispatch({ type: "update_agent", name, agent });
}
@@ -1225,8 +1322,18 @@ 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);
+ markAsPublished(); // Mark step 3 as completed when user publishes
+ // 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 +1433,105 @@ 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');
+ }
+ }
+
+ function handleToggleLeftPanel() {
+ setIsLeftPanelCollapsed(!isLeftPanelCollapsed);
+ }
+
const validateProjectName = (value: string) => {
if (value.length === 0) {
setProjectNameError("Project name cannot be empty");
@@ -1386,6 +1592,39 @@ export function WorkflowEditor({
onSelectPrompt: handleSelectPrompt,
}}>
+ {/* Live Workflow Edit Modal */}
+
+
+
+
+
+
+
+ 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}
+ hasAgentInstructionChanges={hasAgentInstructionChanges}
+ hasPlaygroundTested={hasPlaygroundTested}
+ hasPublished={hasPublished}
+ hasClickedUse={hasClickedUse}
+ onUndo={() => dispatchGuarded({ type: "undo" })}
+ onRedo={() => dispatchGuarded({ type: "redo" })}
onDownloadJSON={handleDownloadJSON}
onPublishWorkflow={handlePublishWorkflow}
onChangeMode={onChangeMode}
onRevertToLive={handleRevertToLive}
- onToggleCopilot={() => setShowCopilot(!showCopilot)}
+ onTogglePanel={handleTogglePanel}
+ onUseAssistantClick={markUseAssistantClicked}
+ onStartNewChatAndFocus={handleStartNewChatAndFocus}
+ onStartBuildTour={() => setShowBuildTour(true)}
+ onStartTestTour={() => setShowTestTour(true)}
+ onStartPublishTour={() => {
+ if (isLive) {
+ handleModeTransition('draft', 'switch_draft');
+ }
+ setShowPublishTour(true);
+ }}
+ onStartUseTour={() => setShowUseTour(true)}
/>
{/* Content Area */}
-
+
-
+
+
+ {/* Config Panel - always rendered, visibility controlled */}
-
{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)!}
@@ -1489,7 +1741,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 +1753,33 @@ export function WorkflowEditor({
(tool) => tool.name === state.present.selection!.name
);
return 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" && 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" && dispatch({ type: "unselect_datasource" })}
onDataSourceUpdate={onDataSourcesUpdated}
/>}
{state.present.selection?.type === "pipeline" && pipeline.name === state.present.selection!.name)!}
@@ -1563,38 +1815,56 @@ export function WorkflowEditor({
)}
- {showCopilot && (
- <>
-
- setCopilotWidth(size)}
- >
- 0
- ? { type: 'chat', messages: chatMessages }
- : undefined
- }
- isInitialState={isInitialState}
- dataSources={dataSources}
- />
-
- >
- )}
+ {/* Second handle - 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 && (
setShowTour(false)}
/>
)}
+ {showBuildTour && (
+ {
+ if (step.target === 'copilot') setActivePanel('copilot');
+ }}
+ onComplete={() => setShowBuildTour(false)}
+ />
+ )}
+ {showTestTour && (
+ {
+ if (index === 0) setActivePanel('playground');
+ if (index === 1) setActivePanel('copilot');
+ }}
+ onComplete={() => setShowTestTour(false)}
+ />
+ )}
+ {showUseTour && (
+ {
+ if (index === 0) setActivePanel('playground');
+ }}
+ onComplete={() => setShowUseTour(false)}
+ />
+ )}
+ {showPublishTour && (
+ setShowPublishTour(false)}
+ />
+ )}
{/* Revert to Live Confirmation Modal */}
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) => (
);
-}
\ No newline at end of file
+}
diff --git a/apps/rowboat/components/common/compose-box-copilot.tsx b/apps/rowboat/components/common/compose-box-copilot.tsx
index 31e49563..643fd0e6 100644
--- a/apps/rowboat/components/common/compose-box-copilot.tsx
+++ b/apps/rowboat/components/common/compose-box-copilot.tsx
@@ -85,11 +85,11 @@ export function ComposeBoxCopilot({
group-hover:opacity-100 transition-opacity">
Press ⌘ + Enter to send
- {/* Outer container with padding */}
-