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

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

View 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>
);
}