diff --git a/apps/rowboat/app/projects/[projectId]/playground/app.tsx b/apps/rowboat/app/projects/[projectId]/playground/app.tsx index f02ee8e8c..29e230c24 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/app.tsx @@ -19,6 +19,7 @@ export function App({ isLiveWorkflow, activePanel, onTogglePanel, + onMessageSent, }: { hidden?: boolean; projectId: string; @@ -29,6 +30,7 @@ export function App({ isLiveWorkflow: boolean; activePanel: 'playground' | 'copilot'; onTogglePanel: () => void; + onMessageSent?: () => void; }) { const [counter, setCounter] = useState(0); const [showDebugMessages, setShowDebugMessages] = useState(true); @@ -142,6 +144,7 @@ export function App({ showDebugMessages={showDebugMessages} triggerCopilotChat={triggerCopilotChat} isLiveWorkflow={isLiveWorkflow} + onMessageSent={onMessageSent} /> diff --git a/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx b/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx index c7edde700..8bd4cbf5f 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx @@ -22,6 +22,7 @@ export function Chat({ showJsonMode = false, triggerCopilotChat, isLiveWorkflow, + onMessageSent, }: { projectId: string; workflow: z.infer; @@ -31,6 +32,7 @@ export function Chat({ showJsonMode?: boolean; triggerCopilotChat?: (message: string) => void; isLiveWorkflow: boolean; + onMessageSent?: () => void; }) { const conversationId = useRef(null); const [messages, setMessages] = useState[]>([]); @@ -158,6 +160,11 @@ export function Chat({ setMessages(updatedMessages); setError(null); setIsLastInteracted(true); + + // Mark playground as tested when user sends a message + if (onMessageSent) { + onMessageSent(); + } } // clean up event source on component unmount diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx index 7684bfec8..cf398040a 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx @@ -2,8 +2,9 @@ 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 { 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; @@ -17,6 +18,10 @@ interface TopBarProps { canUndo: boolean; canRedo: boolean; activePanel: 'playground' | 'copilot'; + hasAgentInstructionChanges: boolean; + hasPlaygroundTested: boolean; + hasPublished: boolean; + hasClickedUse: boolean; onUndo: () => void; onRedo: () => void; onDownloadJSON: () => void; @@ -24,6 +29,12 @@ interface TopBarProps { onChangeMode: (mode: 'draft' | 'live') => void; onRevertToLive: () => void; onTogglePanel: () => void; + onUseAssistantClick: () => void; + onStartNewChatAndFocus: () => void; + onStartBuildTour?: () => void; + onStartTestTour?: () => void; + onStartPublishTour?: () => void; + onStartUseTour?: () => void; } export function TopBar({ @@ -38,6 +49,10 @@ export function TopBar({ canUndo, canRedo, activePanel, + hasAgentInstructionChanges, + hasPlaygroundTested, + hasPublished, + hasClickedUse, onUndo, onRedo, onDownloadJSON, @@ -45,10 +60,32 @@ export function TopBar({ onChangeMode, onRevertToLive, 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 (
@@ -92,16 +129,33 @@ export function TopBar({ ) : null}
- {showCopySuccess &&
-
Copied to clipboard
-
} - {showBuildModeBanner &&
- -
- Switched to draft mode. You can now make changes to your workflow. -
-
} + + {/* 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 */}
+ {showCopySuccess &&
+
Copied to clipboard
+
} + + {showBuildModeBanner &&
+ +
+ Switched to draft mode. You can now make changes to your workflow. +
+
} + {!isLive && <> } + onPress={onUseAssistantClick} > Use Assistant + } + onPress={() => { + onUseAssistantClick(); + onStartNewChatAndFocus(); + }} + > + Chat with Assistant + } - onPress={() => { if (projectId) { router.push(`/projects/${projectId}/config`); } }} + onPress={() => { + onUseAssistantClick(); + if (projectId) { router.push(`/projects/${projectId}/config`); } + }} > API & SDK Settings } - onPress={() => { if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } }} - > + onPress={() => { + onUseAssistantClick(); + if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } + }} + > Manage Triggers diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index ff2c380d4..034ffeea3 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -61,7 +61,7 @@ interface StateItem { chatKey: number; lastUpdatedAt: string; isLive: boolean; - + agentInstructionsChanged: boolean; } interface State { @@ -656,6 +656,10 @@ function reducer(state: State, action: Action): State { break; } case "update_agent": { + // Check if instructions are being changed + if (action.agent.instructions !== undefined) { + draft.agentInstructionsChanged = true; + } // update agent data draft.workflow.agents = draft.workflow.agents.map((agent) => @@ -930,7 +934,7 @@ export function WorkflowEditor({ chatKey: 0, lastUpdatedAt: workflow.lastUpdatedAt, isLive, - + agentInstructionsChanged: false, } }); @@ -944,11 +948,16 @@ export function WorkflowEditor({ 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') => { @@ -994,6 +1003,71 @@ 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 + const [hasAgentInstructionChanges, setHasAgentInstructionChanges] = useState(() => { + return localStorage.getItem(`agent_instructions_changed_${projectId}`) === 'true'; + }); + + // Test progress tracking - persists once set to true + const [hasPlaygroundTested, setHasPlaygroundTested] = useState(() => { + return localStorage.getItem(`playground_tested_${projectId}`) === 'true'; + }); + + // Publish progress tracking - persists once set to true + const [hasPublished, setHasPublished] = useState(() => { + return localStorage.getItem(`has_published_${projectId}`) === 'true'; + }); + + // Use progress tracking - persists once set to true + const [hasClickedUse, setHasClickedUse] = useState(() => { + 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(() => { @@ -1061,6 +1135,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 }); } @@ -1158,6 +1239,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 }); } @@ -1236,6 +1321,7 @@ export function WorkflowEditor({ 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 @@ -1438,6 +1524,10 @@ export function WorkflowEditor({ } } + function handleToggleLeftPanel() { + setIsLeftPanelCollapsed(!isLeftPanelCollapsed); + } + const validateProjectName = (value: string) => { if (value.length === 0) { setProjectNameError("Project name cannot be empty"); @@ -1544,6 +1634,10 @@ export function WorkflowEditor({ canUndo={state.currentIndex > 0} canRedo={state.currentIndex < state.patches.length} activePanel={activePanel} + hasAgentInstructionChanges={hasAgentInstructionChanges} + hasPlaygroundTested={hasPlaygroundTested} + hasPublished={hasPublished} + hasClickedUse={hasClickedUse} onUndo={() => dispatchGuarded({ type: "undo" })} onRedo={() => dispatchGuarded({ type: "redo" })} onDownloadJSON={handleDownloadJSON} @@ -1551,6 +1645,17 @@ export function WorkflowEditor({ onChangeMode={onChangeMode} onRevertToLive={handleRevertToLive} 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 */} @@ -1559,6 +1664,7 @@ export function WorkflowEditor({ key={`entity-list-${state.present.selection ? '3-pane' : '2-pane'}`} minSize={10} defaultSize={PANEL_RATIOS.entityList} + className={isLeftPanelCollapsed ? 'hidden' : ''} >
- + {/* Config Panel - always rendered, visibility controlled */} )} - {/* Second handle - always show (between config and chat panels) */} - + {/* Second handle - between config and chat panels */} + {/* ChatApp/Copilot Panel - always visible */}
@@ -1761,6 +1868,65 @@ export function WorkflowEditor({ onComplete={() => 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/layout/components/sidebar.tsx b/apps/rowboat/app/projects/layout/components/sidebar.tsx index 1713a74fd..c79b4630a 100644 --- a/apps/rowboat/app/projects/layout/components/sidebar.tsx +++ b/apps/rowboat/app/projects/layout/components/sidebar.tsx @@ -193,7 +193,19 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl : 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800/50 hover:text-zinc-900 dark:hover:text-zinc-300' } `} - data-tour-target={item.href === 'config' ? 'settings' : item.href === 'sources' ? 'entity-data-sources' : undefined} + data-tour-target={ + item.href === 'config' + ? 'settings' + : item.href === 'sources' + ? 'entity-data-sources' + : item.href === 'manage-triggers' + ? 'triggers' + : item.href === 'jobs' + ? 'jobs' + : item.href === 'conversations' + ? 'conversations' + : undefined + } > ); -} \ No newline at end of file +} diff --git a/apps/rowboat/components/common/product-tour.tsx b/apps/rowboat/components/common/product-tour.tsx index d4cc15ce4..4a6cb1db0 100644 --- a/apps/rowboat/components/common/product-tour.tsx +++ b/apps/rowboat/components/common/product-tour.tsx @@ -2,7 +2,7 @@ import { useFloating, offset, flip, shift, arrow, FloatingArrow, FloatingPortal, import { useCallback, useEffect, useRef, useState } from 'react'; import { XIcon } from 'lucide-react'; -interface TourStep { +export interface TourStep { target: string; content: string; title: string; @@ -59,7 +59,7 @@ const TOUR_STEPS: TourStep[] = [ function TourBackdrop({ targetElement }: { targetElement: Element | null }) { const [rect, setRect] = useState(null); const isPanelTarget = targetElement?.getAttribute('data-tour-target') && - ['entity-agents', 'entity-tools', 'entity-prompts', 'copilot', 'playground'].includes( + ['entity-agents', 'entity-tools', 'entity-prompts', 'copilot', 'playground', 'settings', 'triggers', 'jobs', 'conversations'].includes( targetElement.getAttribute('data-tour-target')! ); @@ -136,28 +136,36 @@ function TourBackdrop({ targetElement }: { targetElement: Element | null }) { export function ProductTour({ projectId, - onComplete + onComplete, + stepsOverride, + forceStart = false, + onStepChange, }: { projectId: string; onComplete: () => void; + stepsOverride?: TourStep[]; + forceStart?: boolean; + onStepChange?: (index: number, step: TourStep) => void; }) { + const steps = stepsOverride && stepsOverride.length > 0 ? stepsOverride : TOUR_STEPS; const [currentStep, setCurrentStep] = useState(0); const [shouldShow, setShouldShow] = useState(true); const arrowRef = useRef(null); - // Check if tour has been completed by the user + // Check if tour has been completed by the user, unless forced useEffect(() => { + if (forceStart) return; const tourCompleted = localStorage.getItem('user_product_tour_completed'); if (tourCompleted) { setShouldShow(false); } - }, []); + }, [forceStart]); - const currentTarget = TOUR_STEPS[currentStep].target; - const targetElement = document.querySelector(`[data-tour-target="${currentTarget}"]`); + const currentTarget = steps[currentStep].target; + const [targetElement, setTargetElement] = useState(null); // Determine if the target is a panel that should have the hint on the side - const isPanelTarget = ['entity-agents', 'entity-tools', 'entity-prompts', 'copilot', 'playground'].includes(currentTarget); + const isPanelTarget = ['entity-agents', 'entity-tools', 'entity-prompts', 'copilot', 'playground', 'entity-data', 'settings', 'triggers', 'jobs', 'conversations'].includes(currentTarget); const { x, y, strategy, refs, context, middlewareData } = useFloating({ placement: isPanelTarget ? 'right' : 'top', @@ -177,15 +185,33 @@ export function ProductTour({ whileElementsMounted: autoUpdate }); - // Update reference element when step changes + // Update reference element when step changes and notify parent first, then resolve target element useEffect(() => { - if (targetElement) { - refs.setReference(targetElement); + let raf1: number | undefined; + let raf2: number | undefined; + + if (onStepChange) { + onStepChange(currentStep, steps[currentStep]); } - }, [currentStep, targetElement, refs]); + + // Give the parent a frame to update DOM (e.g., switching panels), then query element + raf1 = requestAnimationFrame(() => { + raf2 = requestAnimationFrame(() => { + const el = document.querySelector(`[data-tour-target="${currentTarget}"]`); + setTargetElement(el); + if (el) refs.setReference(el as any); + }); + }); + + return () => { + if (raf1) cancelAnimationFrame(raf1); + if (raf2) cancelAnimationFrame(raf2); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentStep, currentTarget]); const handleNext = useCallback(() => { - if (currentStep < TOUR_STEPS.length - 1) { + if (currentStep < steps.length - 1) { setCurrentStep(prev => prev + 1); } else { // Mark tour as completed for the user @@ -195,7 +221,7 @@ export function ProductTour({ setShouldShow(false); onComplete(); } - }, [currentStep, projectId, onComplete]); + }, [currentStep, projectId, onComplete, steps.length]); const handleSkip = useCallback(() => { // Mark tour as completed for the user @@ -235,10 +261,10 @@ export function ProductTour({
- {TOUR_STEPS[currentStep].title} + {steps[currentStep].title}