deep-research / src /components /Research /WorkflowProgress.tsx
Leon4gr45's picture
Deploy app
c16e487 verified
"use client";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { CheckCircle2, Circle, LoaderCircle } from "lucide-react";
import { useTaskStore } from "@/store/task";
import { cn } from "@/utils/style";
type StepState = "completed" | "active" | "pending";
interface WorkflowStep {
id: string;
label: string;
completed: boolean;
state: StepState;
}
function StepIcon({ state }: { state: StepState }) {
if (state === "completed") {
return <CheckCircle2 className="h-4 w-4 text-emerald-600" />;
} else if (state === "active") {
return <LoaderCircle className="h-4 w-4 text-blue-600 animate-spin" />;
} else {
return <Circle className="h-4 w-4 text-slate-400" />;
}
}
function WorkflowProgress() {
const { t } = useTranslation();
const { question, questions, reportPlan, tasks, finalReport } =
useTaskStore();
const completedTaskCount = useMemo(() => {
return tasks.filter((task) => task.state === "completed").length;
}, [tasks]);
const collectionDone =
tasks.length > 0 && completedTaskCount === tasks.length;
const steps = useMemo(() => {
const rawSteps: Omit<WorkflowStep, "state">[] = [
{
id: "topic",
label: t("research.workflow.topic"),
completed: question.trim().length > 0,
},
{
id: "questions",
label: t("research.workflow.questions"),
completed: questions.trim().length > 0,
},
{
id: "plan",
label: t("research.workflow.plan"),
completed: reportPlan.trim().length > 0,
},
{
id: "collection",
label: t("research.workflow.collection"),
completed: collectionDone,
},
{
id: "report",
label: t("research.workflow.report"),
completed: finalReport.trim().length > 0,
},
];
const activeIndex = rawSteps.findIndex((step) => !step.completed);
return rawSteps.map((step, idx) => ({
...step,
state:
step.completed || activeIndex === -1
? "completed"
: idx === activeIndex
? "active"
: "pending",
})) as WorkflowStep[];
}, [collectionDone, finalReport, question, questions, reportPlan, t]);
const completedStages = useMemo(() => {
return steps.filter((step) => step.completed).length;
}, [steps]);
const progress = Math.round((completedStages / steps.length) * 100);
function getStepDescription(step: WorkflowStep): string {
if (step.id === "collection" && tasks.length > 0) {
return t("research.workflow.collectionDetail", {
completed: completedTaskCount,
total: tasks.length,
});
}
if (step.state === "completed") {
return t("research.workflow.done");
} else if (step.state === "active") {
return t("research.workflow.active");
} else {
return t("research.workflow.pending");
}
}
return (
<section className="p-4 border rounded-md mt-4 print:hidden bg-slate-50 dark:bg-slate-900/30">
<div className="flex gap-2 items-center justify-between max-sm:flex-col max-sm:items-start">
<h3 className="font-semibold text-lg leading-6">
{t("research.workflow.title")}
</h3>
<span className="text-sm text-muted-foreground">
{t("research.workflow.summary", {
progress,
completed: completedStages,
total: steps.length,
})}
</span>
</div>
<div className="mt-3 h-2 rounded-full bg-slate-200 dark:bg-slate-700 overflow-hidden">
<div
className="h-full rounded-full bg-blue-600 transition-all"
style={{ width: `${progress}%` }}
></div>
</div>
<div className="mt-3 grid gap-2 max-sm:hidden sm:grid-cols-2 lg:grid-cols-5">
{steps.map((step) => (
<div
key={step.id}
className={cn(
"rounded-md border px-2 py-2",
step.state === "completed"
? "border-emerald-200 bg-emerald-50 dark:border-emerald-900 dark:bg-emerald-950/20"
: "",
step.state === "active"
? "border-blue-200 bg-blue-50 dark:border-blue-900 dark:bg-blue-950/20"
: "",
step.state === "pending"
? "border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900/40"
: "",
)}
>
<div className="flex items-center gap-2">
<StepIcon state={step.state} />
<p className="text-sm font-medium truncate">{step.label}</p>
</div>
<p className="text-xs mt-1 text-muted-foreground">
{getStepDescription(step)}
</p>
</div>
))}
</div>
</section>
);
}
export default WorkflowProgress;