166 lines
6.4 KiB
TypeScript
166 lines
6.4 KiB
TypeScript
import { ReactNode } from "react";
|
|
import { Link } from "react-router";
|
|
import { CheckCircleIcon, TimeIcon, ArrowRightIcon } from "../../icons";
|
|
import { Tooltip } from "../ui/tooltip";
|
|
|
|
export interface WorkflowStep {
|
|
number: number;
|
|
title: string;
|
|
status: "completed" | "in_progress" | "pending";
|
|
count: number;
|
|
path: string;
|
|
description?: string;
|
|
details?: string; // Additional details for tooltip
|
|
}
|
|
|
|
interface WorkflowPipelineProps {
|
|
steps: WorkflowStep[];
|
|
onStepClick?: (step: WorkflowStep) => void;
|
|
showConnections?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export default function WorkflowPipeline({
|
|
steps,
|
|
onStepClick,
|
|
showConnections = true,
|
|
className = "",
|
|
}: WorkflowPipelineProps) {
|
|
const getStatusColor = (status: WorkflowStep["status"]) => {
|
|
switch (status) {
|
|
case "completed":
|
|
return "bg-success-500 border-success-500 text-white";
|
|
case "in_progress":
|
|
return "bg-warning-500 border-warning-500 text-white";
|
|
case "pending":
|
|
return "bg-gray-200 border-gray-300 text-gray-600 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-400";
|
|
}
|
|
};
|
|
|
|
const getStatusIcon = (status: WorkflowStep["status"], stepNumber: number) => {
|
|
switch (status) {
|
|
case "completed":
|
|
return <CheckCircleIcon className="size-5" />;
|
|
case "in_progress":
|
|
return <TimeIcon className="size-5" />;
|
|
case "pending":
|
|
return <span className="text-sm font-bold">{stepNumber}</span>;
|
|
}
|
|
};
|
|
|
|
const getConnectionColor = (currentStatus: WorkflowStep["status"], nextStatus?: WorkflowStep["status"]) => {
|
|
if (currentStatus === "completed" && nextStatus === "completed") {
|
|
return "bg-success-500";
|
|
}
|
|
if (currentStatus === "completed") {
|
|
return "bg-success-500";
|
|
}
|
|
return "bg-gray-300 dark:bg-gray-600";
|
|
};
|
|
|
|
return (
|
|
<div className={`relative ${className}`}>
|
|
<div className="flex items-center justify-between gap-4 overflow-x-auto pb-4 scrollbar-hide">
|
|
<style>{`
|
|
.scrollbar-hide::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
.scrollbar-hide {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
`}</style>
|
|
{steps.map((step, index) => {
|
|
const isLast = index === steps.length - 1;
|
|
const nextStep = !isLast ? steps[index + 1] : null;
|
|
|
|
return (
|
|
<div key={step.number} className="flex items-center flex-shrink-0">
|
|
{/* Step Node */}
|
|
<div className="flex flex-col items-center">
|
|
<Tooltip
|
|
text={
|
|
step.details ||
|
|
`${step.title}: ${step.count} ${step.title.toLowerCase().includes('keyword') ? 'keywords' :
|
|
step.title.toLowerCase().includes('cluster') ? 'clusters' :
|
|
step.title.toLowerCase().includes('idea') ? 'ideas' :
|
|
step.title.toLowerCase().includes('task') ? 'tasks' : 'items'}`
|
|
}
|
|
placement="top"
|
|
>
|
|
<Link
|
|
to={step.path}
|
|
onClick={(e) => {
|
|
if (onStepClick) {
|
|
e.preventDefault();
|
|
onStepClick(step);
|
|
}
|
|
}}
|
|
className={`
|
|
relative flex flex-col items-center justify-center
|
|
w-20 h-20 rounded-full border-2 transition-all duration-300
|
|
${getStatusColor(step.status)}
|
|
hover:scale-110 hover:shadow-lg
|
|
cursor-pointer group
|
|
`}
|
|
>
|
|
<div className="flex items-center justify-center">
|
|
{getStatusIcon(step.status, step.number)}
|
|
</div>
|
|
{step.status === "in_progress" && (
|
|
<div className="absolute inset-0 rounded-full border-2 border-warning-400 animate-ping opacity-75" />
|
|
)}
|
|
</Link>
|
|
</Tooltip>
|
|
|
|
{/* Step Label */}
|
|
<div className="mt-3 text-center max-w-[120px]">
|
|
<p className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
|
{step.title}
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
{step.count > 0 ? (
|
|
<>
|
|
{step.count} {step.title.toLowerCase().includes('keyword') ? 'keywords' :
|
|
step.title.toLowerCase().includes('cluster') ? 'clusters' :
|
|
step.title.toLowerCase().includes('idea') ? 'ideas' :
|
|
step.title.toLowerCase().includes('task') ? 'tasks' : 'items'}
|
|
</>
|
|
) : (
|
|
<span className="text-gray-400 dark:text-gray-500">No items</span>
|
|
)}
|
|
</p>
|
|
{step.status === "pending" && step.count === 0 && (
|
|
<Link
|
|
to={step.path}
|
|
className="inline-flex items-center gap-1 mt-2 text-xs font-medium text-brand-500 hover:text-brand-600 group-hover:translate-x-1 transition-transform"
|
|
onClick={(e) => {
|
|
if (onStepClick) {
|
|
e.preventDefault();
|
|
onStepClick(step);
|
|
}
|
|
}}
|
|
>
|
|
Start Now <ArrowRightIcon className="size-3" />
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Connection Line */}
|
|
{!isLast && showConnections && (
|
|
<div className="flex items-center mx-2 flex-shrink-0">
|
|
<div className={`h-0.5 w-16 ${getConnectionColor(step.status, nextStep?.status)} transition-colors duration-300`} />
|
|
<ArrowRightIcon className={`size-4 ${step.status === "completed" ? "text-success-500" : "text-gray-400"} transition-colors duration-300`} />
|
|
<div className={`h-0.5 w-16 ${getConnectionColor(step.status, nextStep?.status)} transition-colors duration-300`} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|