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:
arkml 2025-09-07 19:12:50 +05:30 committed by GitHub
parent c793f0a344
commit d899966107
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 449 additions and 38 deletions

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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}>

View file

@ -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
</>
);
}
}