mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-03 19:25:19 +02:00
Progressbar (#231)
* added basic progress bar * step 1 turns green when any agent instructio is changed * step 2 is done after playground chat * step 3 turns green on publish * step 4 turns green on use assistant * step 1 turns green on copilot changes too * reduced font size of the live workflow warning * better hover texts for steps * change progress bar style * better tool tips * reverted styling of use assistant button * chat with assistant option collapses the left pane * remove hide left panel button * made progress bar hover text more prominent * add labels to progress bar * added tour for build * added tour for test * added tour for publish * added tour for use step * added tool tip for each step to click for tour * refined wording in product tours * added jobs and conversations to the product tour
This commit is contained in:
parent
c793f0a344
commit
d899966107
7 changed files with 449 additions and 38 deletions
|
|
@ -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<number>(0);
|
||||
const [showDebugMessages, setShowDebugMessages] = useState<boolean>(true);
|
||||
|
|
@ -142,6 +144,7 @@ export function App({
|
|||
showDebugMessages={showDebugMessages}
|
||||
triggerCopilotChat={triggerCopilotChat}
|
||||
isLiveWorkflow={isLiveWorkflow}
|
||||
onMessageSent={onMessageSent}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export function Chat({
|
|||
showJsonMode = false,
|
||||
triggerCopilotChat,
|
||||
isLiveWorkflow,
|
||||
onMessageSent,
|
||||
}: {
|
||||
projectId: string;
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
|
|
@ -31,6 +32,7 @@ export function Chat({
|
|||
showJsonMode?: boolean;
|
||||
triggerCopilotChat?: (message: string) => void;
|
||||
isLiveWorkflow: boolean;
|
||||
onMessageSent?: () => void;
|
||||
}) {
|
||||
const conversationId = useRef<string | null>(null);
|
||||
const [messages, setMessages] = useState<z.infer<typeof Message>[]>([]);
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="rounded-xl bg-white/70 dark:bg-zinc-800/70 shadow-sm backdrop-blur-sm border border-zinc-200 dark:border-zinc-800 px-5 py-2">
|
||||
<div className="flex justify-between items-center">
|
||||
|
|
@ -92,16 +129,33 @@ export function TopBar({
|
|||
</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>}
|
||||
|
||||
{/* Progress Bar - Center */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
<ProgressBar
|
||||
steps={progressSteps}
|
||||
onStepClick={(step) => {
|
||||
if (step.id === 1 && onStartBuildTour) onStartBuildTour();
|
||||
if (step.id === 2 && onStartTestTour) onStartTestTour();
|
||||
if (step.id === 3 && onStartPublishTour) onStartPublishTour();
|
||||
if (step.id === 4 && onStartUseTour) onStartUseTour();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{showCopySuccess && <div className="flex items-center gap-2 mr-4">
|
||||
<div className="text-green-500">Copied to clipboard</div>
|
||||
</div>}
|
||||
|
||||
{showBuildModeBanner && <div className="flex items-center gap-2 mr-4">
|
||||
<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>}
|
||||
|
||||
|
||||
{!isLive && <>
|
||||
<CustomButton
|
||||
|
|
@ -139,24 +193,41 @@ export function TopBar({
|
|||
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} />}
|
||||
onPress={onUseAssistantClick}
|
||||
>
|
||||
Use Assistant
|
||||
<ChevronDownIcon size={14} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Assistant access options">
|
||||
<DropdownItem
|
||||
key="chat"
|
||||
startContent={<MessageCircleIcon size={16} />}
|
||||
onPress={() => {
|
||||
onUseAssistantClick();
|
||||
onStartNewChatAndFocus();
|
||||
}}
|
||||
>
|
||||
Chat with Assistant
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="api-sdk"
|
||||
startContent={<SettingsIcon size={16} />}
|
||||
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/config`); } }}
|
||||
onPress={() => {
|
||||
onUseAssistantClick();
|
||||
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`); } }}
|
||||
>
|
||||
onPress={() => {
|
||||
onUseAssistantClick();
|
||||
if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); }
|
||||
}}
|
||||
>
|
||||
Manage Triggers
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -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<Action | null>(null);
|
||||
const [configKey, setConfigKey] = useState(0);
|
||||
const [lastWorkflowId, setLastWorkflowId] = useState<string | null>(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<string | null>(null);
|
||||
const [isEditingProjectName, setIsEditingProjectName] = useState<boolean>(false);
|
||||
const [pendingProjectName, setPendingProjectName] = useState<string | null>(null);
|
||||
|
||||
// Build progress tracking - persists once set to true
|
||||
const [hasAgentInstructionChanges, setHasAgentInstructionChanges] = useState<boolean>(() => {
|
||||
return localStorage.getItem(`agent_instructions_changed_${projectId}`) === 'true';
|
||||
});
|
||||
|
||||
// Test progress tracking - persists once set to true
|
||||
const [hasPlaygroundTested, setHasPlaygroundTested] = useState<boolean>(() => {
|
||||
return localStorage.getItem(`playground_tested_${projectId}`) === 'true';
|
||||
});
|
||||
|
||||
// Publish progress tracking - persists once set to true
|
||||
const [hasPublished, setHasPublished] = useState<boolean>(() => {
|
||||
return localStorage.getItem(`has_published_${projectId}`) === 'true';
|
||||
});
|
||||
|
||||
// Use progress tracking - persists once set to true
|
||||
const [hasClickedUse, setHasClickedUse] = useState<boolean>(() => {
|
||||
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<z.infer<typeof WorkflowAgent>>) {
|
||||
// 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' : ''}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<EntityList
|
||||
|
|
@ -1612,7 +1718,7 @@ export function WorkflowEditor({
|
|||
/>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${!state.present.selection ? 'hidden' : ''}`} />
|
||||
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${(isLeftPanelCollapsed || !state.present.selection) ? 'hidden' : ''}`} />
|
||||
|
||||
{/* Config Panel - always rendered, visibility controlled */}
|
||||
<ResizablePanel
|
||||
|
|
@ -1705,8 +1811,8 @@ export function WorkflowEditor({
|
|||
</Panel>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
{/* Second handle - always show (between config and chat panels) */}
|
||||
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
|
||||
{/* Second handle - between config and chat panels */}
|
||||
<ResizableHandle withHandle className={`w-[3px] bg-transparent ${(isLeftPanelCollapsed && !state.present.selection) ? 'hidden' : ''}`} />
|
||||
|
||||
{/* ChatApp/Copilot Panel - always visible */}
|
||||
<ResizablePanel
|
||||
|
|
@ -1726,6 +1832,7 @@ export function WorkflowEditor({
|
|||
isLiveWorkflow={isLive}
|
||||
activePanel={activePanel}
|
||||
onTogglePanel={handleTogglePanel}
|
||||
onMessageSent={markPlaygroundTested}
|
||||
/>
|
||||
</div>
|
||||
<div className={(activePanel === 'copilot') ? 'block h-full' : 'hidden h-full'}>
|
||||
|
|
@ -1761,6 +1868,65 @@ export function WorkflowEditor({
|
|||
onComplete={() => setShowTour(false)}
|
||||
/>
|
||||
)}
|
||||
{showBuildTour && (
|
||||
<ProductTour
|
||||
projectId={projectId}
|
||||
forceStart
|
||||
stepsOverride={[
|
||||
{ target: 'copilot', title: 'Step 1/5', content: 'Use Copilot to create and refine agents. Describe what you need, then iterate with its suggestions.' },
|
||||
{ target: 'entity-agents', title: 'Step 2/5', content: 'All your agents appear here. Adjust instructions, switch models, and fine-tune their behavior.' },
|
||||
{ target: 'entity-tools', title: 'Step 3/5', content: 'Pick from thousands of ready-made tools or connect your own MCP servers.' },
|
||||
{ target: 'entity-data', title: 'Step 4/5', content: 'Upload files, scrape websites, or add free-text knowledge to guide your agents.' },
|
||||
{ target: 'entity-prompts', title: 'Step 5/5', content: 'Define reusable context variables automatically shared across all agents.' },
|
||||
]}
|
||||
onStepChange={(_, step) => {
|
||||
if (step.target === 'copilot') setActivePanel('copilot');
|
||||
}}
|
||||
onComplete={() => setShowBuildTour(false)}
|
||||
/>
|
||||
)}
|
||||
{showTestTour && (
|
||||
<ProductTour
|
||||
projectId={projectId}
|
||||
forceStart
|
||||
stepsOverride={[
|
||||
{ target: 'playground', title: 'Step 1/2', content: 'Chat with your assistant to test it. Send messages, watch tool calls in action, and debug agent flows.' },
|
||||
{ target: 'copilot', title: 'Step 2/2', content: 'Ask Copilot to improve your agents based on test results. Use “Fix” and “Explain” to iterate quickly.' },
|
||||
]}
|
||||
onStepChange={(index) => {
|
||||
if (index === 0) setActivePanel('playground');
|
||||
if (index === 1) setActivePanel('copilot');
|
||||
}}
|
||||
onComplete={() => setShowTestTour(false)}
|
||||
/>
|
||||
)}
|
||||
{showUseTour && (
|
||||
<ProductTour
|
||||
projectId={projectId}
|
||||
forceStart
|
||||
stepsOverride={[
|
||||
{ target: 'playground', title: 'Step 1/5', content: 'Chat: you can chat with your assistant here.' },
|
||||
{ target: 'triggers', title: 'Step 2/5', content: 'Triggers: set up external (webhook/integration) or time-based schedules.' },
|
||||
{ target: 'jobs', title: 'Step 3/5', content: 'Jobs: monitor your trigger runs and scheduled tasks here.' },
|
||||
{ target: 'settings', title: 'Step 4/5', content: 'Settings: find API keys to connect with the API and SDK.' },
|
||||
{ target: 'conversations', title: 'Step 5/5', content: 'Conversations: see all past interactions in one place, including manual chats, trigger activity, and API calls.' },
|
||||
]}
|
||||
onStepChange={(index) => {
|
||||
if (index === 0) setActivePanel('playground');
|
||||
}}
|
||||
onComplete={() => setShowUseTour(false)}
|
||||
/>
|
||||
)}
|
||||
{showPublishTour && (
|
||||
<ProductTour
|
||||
projectId={projectId}
|
||||
forceStart
|
||||
stepsOverride={[
|
||||
{ target: 'deploy', title: 'Publish', content: 'Click Publish to make your workflow live, enabling triggers and API/SDK access. You can revert to a draft at any time.' },
|
||||
]}
|
||||
onComplete={() => setShowPublishTour(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Revert to Live Confirmation Modal */}
|
||||
<Modal isOpen={isRevertModalOpen} onClose={onRevertModalClose}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
size={COLLAPSED_ICON_SIZE}
|
||||
|
|
@ -218,7 +230,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
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
size={EXPANDED_ICON_SIZE}
|
||||
|
|
@ -375,4 +399,4 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
|
|||
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DOMRect | null>(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<Element | null>(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({
|
|||
<XIcon size={16} />
|
||||
</button>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
{TOUR_STEPS[currentStep].title}
|
||||
{steps[currentStep].title}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3 whitespace-pre-line [&>a]:underline"
|
||||
dangerouslySetInnerHTML={{ __html: TOUR_STEPS[currentStep].content }}
|
||||
dangerouslySetInnerHTML={{ __html: steps[currentStep].content }}
|
||||
/>
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
|
|
@ -263,4 +289,4 @@ export function ProductTour({
|
|||
</div>
|
||||
</FloatingPortal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
114
apps/rowboat/components/ui/progress-bar.tsx
Normal file
114
apps/rowboat/components/ui/progress-bar.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
"use client";
|
||||
import React from 'react';
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Tooltip } from "@heroui/react";
|
||||
|
||||
export interface ProgressStep {
|
||||
id: number;
|
||||
label: string;
|
||||
completed: boolean;
|
||||
icon?: string; // The icon/symbol to show instead of number
|
||||
isCurrent?: boolean; // Whether this is the current step
|
||||
shortLabel?: string; // Optional short label to show inline on larger screens
|
||||
}
|
||||
|
||||
interface ProgressBarProps {
|
||||
steps: ProgressStep[];
|
||||
className?: string;
|
||||
onStepClick?: (step: ProgressStep, index: number) => void;
|
||||
}
|
||||
|
||||
export function ProgressBar({ steps, className, onStepClick }: ProgressBarProps) {
|
||||
const getShortLabel = (label: string) => {
|
||||
if (!label) return "";
|
||||
const beforeColon = label.split(":")[0]?.trim();
|
||||
if (beforeColon) return beforeColon;
|
||||
const firstWord = label.split(" ")[0]?.trim();
|
||||
return firstWord || label;
|
||||
};
|
||||
|
||||
return (
|
||||
<nav aria-label="Workflow progress" className={cn("flex items-center gap-4", className)}>
|
||||
{/* Progress Label */}
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 mr-2">
|
||||
Progress:
|
||||
</span>
|
||||
|
||||
{/* Steps */}
|
||||
<ol role="list" className="flex items-center gap-2">
|
||||
{steps.map((step, index) => {
|
||||
const isLast = index === steps.length - 1;
|
||||
const tooltipText = (() => {
|
||||
switch (step.id) {
|
||||
case 1:
|
||||
return 'Build your assistant - click for tour';
|
||||
case 2:
|
||||
return 'Test your assistant - click for tour';
|
||||
case 3:
|
||||
return 'Make assistant live - click for tour';
|
||||
case 4:
|
||||
return 'Interact with your assistant - click for tour';
|
||||
default:
|
||||
return 'Click for tour';
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<li key={step.id} className="flex items-center">
|
||||
{/* Step Circle with Tooltip */}
|
||||
<div className="flex flex-col items-center">
|
||||
<Tooltip
|
||||
content={tooltipText}
|
||||
size="lg"
|
||||
delay={100}
|
||||
placement="bottom"
|
||||
classNames={{ content: "text-base" }}
|
||||
>
|
||||
<div
|
||||
tabIndex={0}
|
||||
aria-label={`${step.completed ? "Completed" : step.isCurrent ? "Current" : "Pending"} step ${step.id}: ${step.label}`}
|
||||
aria-current={step.isCurrent ? "step" : undefined}
|
||||
role={onStepClick ? 'button' as const : undefined}
|
||||
onClick={onStepClick ? () => onStepClick(step, index) : undefined}
|
||||
onKeyDown={onStepClick ? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onStepClick(step, index);
|
||||
}
|
||||
} : undefined}
|
||||
className={cn(
|
||||
"w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-semibold transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-400",
|
||||
step.completed
|
||||
? "bg-green-500 border-green-500 text-white"
|
||||
: step.isCurrent
|
||||
? "bg-yellow-500 border-yellow-500 text-white ring-2 ring-yellow-300/60 shadow-sm"
|
||||
: "bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-400"
|
||||
, onStepClick ? "cursor-pointer hover:scale-105" : "cursor-default")}
|
||||
>
|
||||
{step.completed ? "✓" : step.isCurrent ? "⚡" : "○"}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<span className="hidden md:block mt-1 text-[11px] leading-none text-gray-700 dark:text-gray-300 font-medium">
|
||||
{step.shortLabel ?? getShortLabel(step.label)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Connecting Line */}
|
||||
{!isLast && (
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"h-0.5 w-8 mx-2 transition-all duration-300 motion-reduce:transition-none",
|
||||
step.completed
|
||||
? "bg-green-500"
|
||||
: "border-t-2 border-dashed border-gray-300 dark:border-gray-600"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue