Spaces:
Running
Running
| "use client"; | |
| import type { WorkPackage } from "@/lib/work-package-types"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { | |
| DropdownMenu, | |
| DropdownMenuContent, | |
| DropdownMenuItem, | |
| DropdownMenuTrigger, | |
| } from "@/components/ui/dropdown-menu"; | |
| import { | |
| CheckCircle2, | |
| Circle, | |
| LoaderCircle, | |
| MoreHorizontal, | |
| Play, | |
| } from "lucide-react"; | |
| import { | |
| getWorkPackageVersion, | |
| getWorkPackageVisualState, | |
| } from "@/lib/work-package-ui"; | |
| function statusLabel(status: WorkPackage["status"]) { | |
| if (status === "todo") return "To do"; | |
| if (status === "in_progress") return "In progress"; | |
| return "Done"; | |
| } | |
| function priorityLabel(priority: WorkPackage["priority"]) { | |
| if (priority === "high") return "High"; | |
| if (priority === "medium") return "Medium"; | |
| return "Low"; | |
| } | |
| function displayTitle(title: string) { | |
| return title | |
| .replace("Customer Requirements Specification", "Customer Requirements") | |
| .replace("System Requirements Specification", "System Requirements") | |
| .replace("Final Engineering Concept", "Engineering Concept") | |
| .replace("Final Concept", "Product Concept") | |
| .replace("Product Certification", "Certification") | |
| .replace("Safety of Products", "Product Safety") | |
| .replace("Service Ability Review", "Service Review") | |
| .replace("Decompose System", "System Breakdown") | |
| .replace("Industrial Design", "Industrial Design") | |
| .replace("Patent Check", "Patent Review") | |
| .replace("Test Management", "Test Planning"); | |
| } | |
| export function WorkPackageCard(props: { | |
| wp: WorkPackage; | |
| activeWorkPackageId?: string; | |
| selected: boolean; | |
| onSelect: () => void; | |
| onPrefill: (text: string) => void; | |
| }) { | |
| const { wp, activeWorkPackageId, selected, onSelect, onPrefill } = props; | |
| const refName = wp.shortName || wp.title; | |
| const visualState = getWorkPackageVisualState({ | |
| workPackage: wp, | |
| activeWorkPackageId, | |
| }); | |
| const version = getWorkPackageVersion(wp); | |
| async function copyReference() { | |
| const text = `@${refName} `; | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| } catch { | |
| // ignore | |
| } | |
| } | |
| function prefill(mode: "ask" | "plan" | "change" | "execute") { | |
| onPrefill(`@${refName} ${mode} `); | |
| } | |
| return ( | |
| <Card | |
| className={[ | |
| "min-h-[112px] rounded-2xl border-0 bg-background/96 transition-colors shadow-sm ring-1 ring-black/5 hover:bg-background", | |
| selected ? "bg-background ring-2 ring-primary/20" : "", | |
| visualState === "running" ? "ring-2 ring-amber-300/80 shadow-[0_0_0_1px_rgba(251,191,36,0.15)]" : "", | |
| visualState === "done" ? "ring-1 ring-emerald-300/70" : "", | |
| ].join(" ")} | |
| onClick={onSelect} | |
| role="button" | |
| tabIndex={0} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter" || e.key === " ") { | |
| e.preventDefault(); | |
| onSelect(); | |
| } | |
| }} | |
| > | |
| <CardHeader className="space-y-0 p-2.5"> | |
| <div className="flex items-start justify-between gap-2"> | |
| <div className="min-w-0 flex-1 pr-1"> | |
| <CardTitle className="line-clamp-2 text-[12.5px] font-semibold leading-[1.15rem]"> | |
| {displayTitle(wp.title)} | |
| </CardTitle> | |
| <div className="mt-1 flex flex-wrap items-center gap-1"> | |
| <Badge variant="secondary" className="h-4.5 px-1.5 text-[10px]"> | |
| {wp.shortName} | |
| </Badge> | |
| <Badge variant="outline" className="h-4.5 px-1.5 text-[10px]"> | |
| {statusLabel(wp.status)} | |
| </Badge> | |
| <Badge variant="outline" className="h-4.5 px-1.5 text-[10px]"> | |
| {priorityLabel(wp.priority)} | |
| </Badge> | |
| {visualState === "running" ? ( | |
| <Badge | |
| variant="outline" | |
| className="h-4.5 gap-1 border-amber-300 bg-amber-50 px-1.5 text-[10px] text-amber-700" | |
| > | |
| <LoaderCircle className="h-2.5 w-2.5 animate-spin" /> | |
| Running | |
| </Badge> | |
| ) : null} | |
| {visualState === "done" && version ? ( | |
| <Badge | |
| variant="outline" | |
| className="h-4.5 gap-1 border-emerald-300 bg-emerald-50 px-1.5 text-[10px] text-emerald-700" | |
| > | |
| <CheckCircle2 className="h-2.5 w-2.5" /> | |
| {version} | |
| </Badge> | |
| ) : null} | |
| </div> | |
| </div> | |
| <div className="flex shrink-0 items-center gap-0.5"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-6 w-6" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| prefill("execute"); | |
| }} | |
| > | |
| <Play className="h-3.5 w-3.5" /> | |
| </Button> | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-6 w-6 rounded-lg" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| <MoreHorizontal className="h-3.5 w-3.5" /> | |
| </Button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent align="end" className="rounded-xl"> | |
| <DropdownMenuItem | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| prefill("ask"); | |
| }} | |
| > | |
| Ask | |
| </DropdownMenuItem> | |
| <DropdownMenuItem | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| prefill("plan"); | |
| }} | |
| > | |
| Plan | |
| </DropdownMenuItem> | |
| <DropdownMenuItem | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| prefill("change"); | |
| }} | |
| > | |
| Change | |
| </DropdownMenuItem> | |
| <DropdownMenuItem | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| prefill("execute"); | |
| }} | |
| > | |
| Execute | |
| </DropdownMenuItem> | |
| <DropdownMenuItem | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| copyReference(); | |
| onPrefill(`@${refName} `); | |
| }} | |
| > | |
| Copy reference | |
| </DropdownMenuItem> | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="space-y-1.5 p-2.5 pt-0"> | |
| <div className="line-clamp-2 text-[10.5px] leading-4 text-muted-foreground"> | |
| {wp.objective} | |
| </div> | |
| <div className="flex flex-wrap items-center gap-1.5 text-[10px] text-muted-foreground"> | |
| <div className="inline-flex items-center gap-1"> | |
| <Circle className="h-3 w-3" /> | |
| {wp.tasks.length} tasks | |
| </div> | |
| <div>{wp.outputFiles.length} outputs</div> | |
| <div>{wp.outputs.length} generated</div> | |
| </div> | |
| {wp.outputs.length ? ( | |
| <div className="line-clamp-1 text-[10px] text-muted-foreground"> | |
| Latest output: {wp.outputs[wp.outputs.length - 1]?.title} | |
| </div> | |
| ) : null} | |
| </CardContent> | |
| </Card> | |
| ); | |
| } | |