2025-06-09 15:43:34 -07:00
|
|
|
|
"use client";
|
2025-06-10 12:59:35 -07:00
|
|
|
|
|
2025-06-09 15:43:34 -07:00
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
TooltipContent,
|
|
|
|
|
|
TooltipProvider,
|
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
|
} from "@/components/ui/tooltip";
|
2025-06-10 12:59:35 -07:00
|
|
|
|
import { List, HelpCircle } from "lucide-react";
|
2025-06-09 15:43:34 -07:00
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
import { useState } from "react";
|
|
|
|
|
|
import { PlanItem, TaskPlan } from "@open-swe/shared/open-swe/types";
|
2025-06-10 12:59:35 -07:00
|
|
|
|
import {
|
|
|
|
|
|
HoverCard,
|
|
|
|
|
|
HoverCardContent,
|
|
|
|
|
|
HoverCardTrigger,
|
|
|
|
|
|
} from "@/components/ui/hover-card";
|
2025-06-09 15:43:34 -07:00
|
|
|
|
|
|
|
|
|
|
interface ProgressBarProps {
|
|
|
|
|
|
taskPlan?: TaskPlan;
|
|
|
|
|
|
className?: string;
|
|
|
|
|
|
onOpenSidebar?: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function ProgressBar({
|
|
|
|
|
|
taskPlan,
|
|
|
|
|
|
className,
|
|
|
|
|
|
onOpenSidebar,
|
|
|
|
|
|
}: ProgressBarProps) {
|
|
|
|
|
|
const [showLegend, setShowLegend] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
if (!taskPlan || !taskPlan.tasks.length) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"mt-2 w-full rounded-md border border-gray-200 bg-gray-50 px-3 py-2 sm:mt-4",
|
|
|
|
|
|
className,
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="text-xs text-gray-500">No active plan</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentTask = taskPlan.tasks[taskPlan.activeTaskIndex];
|
|
|
|
|
|
if (!currentTask || !currentTask.planRevisions.length) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"mt-2 w-full rounded-md border border-gray-200 bg-gray-50 px-3 py-2 sm:mt-4",
|
|
|
|
|
|
className,
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="text-xs text-gray-500">No active plan</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const activeRevision =
|
|
|
|
|
|
currentTask.planRevisions[currentTask.activeRevisionIndex];
|
|
|
|
|
|
const planItems = [...activeRevision.plans].sort((a, b) => a.index - b.index);
|
|
|
|
|
|
|
|
|
|
|
|
// Find the current task (lowest index among uncompleted tasks)
|
|
|
|
|
|
const currentTaskIndex = planItems
|
|
|
|
|
|
.filter((item) => !item.completed)
|
|
|
|
|
|
.reduce(
|
|
|
|
|
|
(min, item) => (item.index < min ? item.index : min),
|
|
|
|
|
|
Number.POSITIVE_INFINITY,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const getItemState = (
|
|
|
|
|
|
item: PlanItem,
|
|
|
|
|
|
): "completed" | "current" | "remaining" => {
|
|
|
|
|
|
if (item.completed) return "completed";
|
|
|
|
|
|
if (item.index === currentTaskIndex) return "current";
|
|
|
|
|
|
return "remaining";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const completedCount = planItems.filter((item) => item.completed).length;
|
|
|
|
|
|
const progressPercentage =
|
|
|
|
|
|
planItems.length > 0 ? (completedCount / planItems.length) * 100 : 0;
|
|
|
|
|
|
|
|
|
|
|
|
const getSegmentColor = (state: string) => {
|
|
|
|
|
|
switch (state) {
|
|
|
|
|
|
case "completed":
|
|
|
|
|
|
return "bg-green-400";
|
|
|
|
|
|
case "current":
|
|
|
|
|
|
return "bg-blue-400";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "bg-gray-200";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"w-full rounded-md border border-gray-200 bg-white shadow-sm",
|
|
|
|
|
|
className,
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Compact header */}
|
2025-06-10 12:59:35 -07:00
|
|
|
|
<div className="overflow-hidden px-1 py-1.5 sm:px-2">
|
|
|
|
|
|
<div className="mb-1 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
2025-06-09 15:43:34 -07:00
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<span className="text-xs font-medium text-gray-700 sm:text-sm">
|
|
|
|
|
|
Plan Progress
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<TooltipProvider>
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="h-4 w-4 p-0 sm:h-5 sm:w-5"
|
|
|
|
|
|
onClick={() => setShowLegend(!showLegend)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<HelpCircle className="h-3 w-3 text-gray-500" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
|
<TooltipContent
|
|
|
|
|
|
side="top"
|
|
|
|
|
|
className="max-w-xs p-2 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<p className="font-medium">Plan Progress</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
Shows the current progress of the AI agent's plan
|
|
|
|
|
|
execution.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</TooltipProvider>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<span className="text-xs text-gray-500 sm:text-sm">
|
|
|
|
|
|
{Math.round(progressPercentage)}%
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={onOpenSidebar}
|
2025-06-10 12:59:35 -07:00
|
|
|
|
className="h-6 border-blue-200 text-xs hover:bg-blue-50"
|
2025-06-09 15:43:34 -07:00
|
|
|
|
>
|
2025-06-10 12:59:35 -07:00
|
|
|
|
<List className="size-3" />
|
2025-06-09 15:43:34 -07:00
|
|
|
|
<span className="hidden sm:inline">Tasks</span>
|
|
|
|
|
|
<span className="sm:hidden">View</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Legend - conditionally shown */}
|
|
|
|
|
|
{showLegend && (
|
|
|
|
|
|
<div className="mb-2 flex flex-wrap items-center gap-2 rounded border border-gray-200 bg-gray-50 px-2 py-1 text-xs sm:gap-3">
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
|
<div className="h-2 w-2 rounded-full bg-green-400"></div>
|
|
|
|
|
|
<span>Completed</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
|
<div className="h-2 w-2 animate-pulse rounded-full bg-blue-400"></div>
|
|
|
|
|
|
<span>Current</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
|
<div className="h-2 w-2 rounded-full bg-gray-200"></div>
|
|
|
|
|
|
<span>Pending</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="ml-auto h-4 touch-manipulation p-0 text-xs"
|
|
|
|
|
|
onClick={() => setShowLegend(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Progress Stats */}
|
|
|
|
|
|
<div className="mb-2 flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
|
<span className="text-xs text-gray-600 sm:text-sm">
|
|
|
|
|
|
{completedCount} of {planItems.length} tasks completed
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-xs text-gray-500 sm:text-sm">
|
|
|
|
|
|
Task #{currentTask.taskIndex + 1}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Progress Bar */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<div
|
2025-06-10 12:59:35 -07:00
|
|
|
|
className="flex h-3 cursor-pointer touch-manipulation gap-[1px] overflow-hidden rounded-sm bg-gray-100 transition-all sm:h-2"
|
2025-06-09 15:43:34 -07:00
|
|
|
|
onClick={onOpenSidebar}
|
|
|
|
|
|
aria-label="Click to view all tasks"
|
|
|
|
|
|
title="Click to view all tasks"
|
|
|
|
|
|
>
|
|
|
|
|
|
{planItems.map((item) => {
|
|
|
|
|
|
const state = getItemState(item);
|
|
|
|
|
|
const segmentWidth = `${100 / planItems.length}%`;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-06-10 12:59:35 -07:00
|
|
|
|
<HoverCard key={item.index}>
|
|
|
|
|
|
<HoverCardTrigger asChild>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"transition-all hover:opacity-80",
|
|
|
|
|
|
getSegmentColor(state),
|
|
|
|
|
|
state === "current" && "animate-pulse",
|
|
|
|
|
|
)}
|
|
|
|
|
|
style={{ width: segmentWidth }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</HoverCardTrigger>
|
|
|
|
|
|
<HoverCardContent
|
|
|
|
|
|
side="bottom"
|
|
|
|
|
|
className="min-w-xs p-2 text-xs sm:max-w-sm md:max-w-lg"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<div className="flex flex-col gap-0.5 sm:flex-row sm:items-center sm:gap-1">
|
|
|
|
|
|
<span className="font-medium">
|
|
|
|
|
|
Task #{item.index + 1}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-xs text-gray-500">
|
|
|
|
|
|
{state === "completed"
|
|
|
|
|
|
? "Completed"
|
|
|
|
|
|
: state === "current"
|
|
|
|
|
|
? "Current"
|
|
|
|
|
|
: "Pending"}
|
|
|
|
|
|
</span>
|
2025-06-09 15:43:34 -07:00
|
|
|
|
</div>
|
2025-06-10 12:59:35 -07:00
|
|
|
|
<p className="text-xs break-words">{item.plan}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</HoverCardContent>
|
|
|
|
|
|
</HoverCard>
|
2025-06-09 15:43:34 -07:00
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|