Spaces:
Running
Running
| "use client"; | |
| import type { ReactNode } from "react"; | |
| import type { WorkPackage } from "@/lib/work-package-types"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Button } from "@/components/ui/button"; | |
| import { WorkPackageOutputCard } from "@/components/WorkPackageOutput"; | |
| import { MarkdownContent } from "@/components/MarkdownContent"; | |
| import { | |
| ArrowLeft, | |
| CheckCircle2, | |
| ClipboardCopy, | |
| HelpCircle, | |
| ListTodo, | |
| LoaderCircle, | |
| Play, | |
| SlidersHorizontal, | |
| } from "lucide-react"; | |
| import { | |
| getWorkPackageVersion, | |
| getWorkPackageVisualState, | |
| } from "@/lib/work-package-ui"; | |
| import { buildPackageQuestionPrompt } from "@/lib/package-chat-context"; | |
| 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 Section(props: { | |
| title: string; | |
| children: ReactNode; | |
| action?: ReactNode; | |
| }) { | |
| return ( | |
| <section className="rounded-2xl bg-background/72 px-3 py-3 shadow-sm ring-1 ring-black/5"> | |
| <div className="mb-2 flex items-center justify-between gap-2"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.04em] text-muted-foreground"> | |
| {props.title} | |
| </div> | |
| {props.action} | |
| </div> | |
| {props.children} | |
| </section> | |
| ); | |
| } | |
| function createExcerpt(value: string, maxLength = 220) { | |
| const normalized = value.trim().replace(/\s+/g, " "); | |
| if (normalized.length <= maxLength) return normalized; | |
| return `${normalized.slice(0, maxLength - 1).trimEnd()}...`; | |
| } | |
| function AskAboutButton(props: { | |
| label: string; | |
| onClick: () => void; | |
| }) { | |
| return ( | |
| <Button | |
| variant="ghost" | |
| className="h-7 rounded-lg px-2 text-[11px] text-muted-foreground" | |
| aria-label={props.label} | |
| onClick={props.onClick} | |
| > | |
| <HelpCircle className="h-3.5 w-3.5" /> | |
| <span className="ml-1">{props.label}</span> | |
| </Button> | |
| ); | |
| } | |
| export function WorkPackageDetail(props: { | |
| workPackage?: WorkPackage; | |
| activeWorkPackageId?: string; | |
| onPrefill: (text: string) => void; | |
| onBack: () => void; | |
| }) { | |
| const { workPackage: wp, activeWorkPackageId, onPrefill, onBack } = props; | |
| if (!wp) { | |
| return ( | |
| <div className="px-3 pb-3 md:px-4"> | |
| <div className="rounded-2xl bg-background/70 px-4 py-4 text-sm text-muted-foreground shadow-sm ring-1 ring-black/5"> | |
| Select a work package to inspect its details. | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const refName = wp.shortName || wp.title; | |
| const latestOutput = wp.outputs.at(-1); | |
| const visualState = getWorkPackageVisualState({ | |
| workPackage: wp, | |
| activeWorkPackageId, | |
| }); | |
| const version = getWorkPackageVersion(wp); | |
| const latestOutputExcerpt = latestOutput ? createExcerpt(latestOutput.content) : ""; | |
| async function copyReference() { | |
| try { | |
| await navigator.clipboard.writeText(`@${refName} `); | |
| } catch { | |
| // ignore | |
| } | |
| } | |
| return ( | |
| <div className="flex h-full min-h-0 flex-col"> | |
| <div className="px-3 py-2 md:px-4"> | |
| <div className="rounded-2xl bg-background/72 px-3 py-2.5 shadow-sm ring-1 ring-black/5"> | |
| <div className="flex items-center justify-between gap-3"> | |
| <div className="flex min-w-0 items-center gap-2"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-8 w-8 rounded-xl" | |
| onClick={onBack} | |
| > | |
| <ArrowLeft className="h-4 w-4" /> | |
| </Button> | |
| <div className="min-w-0"> | |
| <div className="truncate text-sm font-semibold">{wp.title}</div> | |
| <div className="truncate text-xs text-muted-foreground"> | |
| Work package detail view | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex shrink-0 items-center gap-1"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-7 w-7 rounded-lg" | |
| onClick={() => onPrefill(`@${refName} ask `)} | |
| > | |
| <HelpCircle className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-7 w-7 rounded-lg" | |
| onClick={() => onPrefill(`@${refName} plan `)} | |
| > | |
| <ListTodo className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-7 w-7 rounded-lg" | |
| onClick={() => onPrefill(`@${refName} change `)} | |
| > | |
| <SlidersHorizontal className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-7 w-7 rounded-lg" | |
| onClick={() => onPrefill(`@${refName} execute `)} | |
| > | |
| <Play className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-7 w-7 rounded-lg" | |
| onClick={copyReference} | |
| > | |
| <ClipboardCopy className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 pb-3 md:px-4"> | |
| <div className="space-y-3"> | |
| <section className="rounded-2xl bg-background/72 px-4 py-4 shadow-sm ring-1 ring-black/5"> | |
| <div className="flex flex-wrap items-start justify-between gap-3"> | |
| <div className="min-w-0"> | |
| <h2 className="text-base font-semibold">{wp.title}</h2> | |
| <div className="mt-1 flex flex-wrap items-center gap-1"> | |
| <Badge variant="secondary" className="text-[11px]"> | |
| {wp.shortName} | |
| </Badge> | |
| <Badge variant="outline" className="text-[11px]"> | |
| {statusLabel(wp.status)} | |
| </Badge> | |
| <Badge variant="outline" className="text-[11px]"> | |
| {priorityLabel(wp.priority)} | |
| </Badge> | |
| <Badge variant="outline" className="text-[11px]"> | |
| {wp.phase} | |
| </Badge> | |
| {visualState === "running" ? ( | |
| <Badge | |
| variant="outline" | |
| className="gap-1 border-amber-300 bg-amber-50 text-[11px] text-amber-700" | |
| > | |
| <LoaderCircle className="h-3 w-3 animate-spin" /> | |
| Running | |
| </Badge> | |
| ) : null} | |
| {visualState === "done" && version ? ( | |
| <Badge | |
| variant="outline" | |
| className="gap-1 border-emerald-300 bg-emerald-50 text-[11px] text-emerald-700" | |
| > | |
| <CheckCircle2 className="h-3 w-3" /> | |
| {version} | |
| </Badge> | |
| ) : null} | |
| </div> | |
| </div> | |
| </div> | |
| <p className="mt-3 text-sm leading-6 text-muted-foreground"> | |
| {wp.objective} | |
| </p> | |
| <div className="mt-3"> | |
| <AskAboutButton | |
| label="Ask about objective" | |
| onClick={() => | |
| onPrefill( | |
| buildPackageQuestionPrompt({ | |
| packageRef: refName, | |
| label: "objective", | |
| excerpt: wp.objective, | |
| }), | |
| ) | |
| } | |
| /> | |
| </div> | |
| </section> | |
| <Section title="Primary output"> | |
| {latestOutput ? ( | |
| <div className="space-y-3"> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| <Badge variant="secondary" className="text-[11px]"> | |
| {latestOutput.title} | |
| </Badge> | |
| <Badge variant="outline" className="text-[11px]"> | |
| {latestOutput.executionMode === "real" | |
| ? "LLM Automation" | |
| : "Simulated Execution"} | |
| </Badge> | |
| <Badge variant="outline" className="text-[11px]"> | |
| {latestOutput.type} | |
| </Badge> | |
| </div> | |
| <div className="rounded-xl bg-muted/20 px-4 py-4"> | |
| <MarkdownContent | |
| content={latestOutput.content} | |
| onAskExcerpt={(excerpt) => | |
| onPrefill( | |
| buildPackageQuestionPrompt({ | |
| packageRef: refName, | |
| label: "output excerpt", | |
| excerpt, | |
| }), | |
| ) | |
| } | |
| /> | |
| </div> | |
| <div> | |
| <AskAboutButton | |
| label="Ask about latest output" | |
| onClick={() => | |
| onPrefill( | |
| buildPackageQuestionPrompt({ | |
| packageRef: refName, | |
| label: "latest output", | |
| excerpt: latestOutputExcerpt, | |
| }), | |
| ) | |
| } | |
| /> | |
| </div> | |
| <div className="rounded-xl bg-muted/32 p-2.5 text-[11px] leading-5 text-muted-foreground"> | |
| {latestOutput.disclaimer} | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="text-sm leading-6 text-muted-foreground"> | |
| No output yet. Run the package to generate its primary artifact. | |
| </div> | |
| )} | |
| </Section> | |
| {wp.outputs.length > 1 ? ( | |
| <details className="rounded-2xl bg-background/72 px-3 py-3 shadow-sm ring-1 ring-black/5"> | |
| <summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-[0.04em] text-muted-foreground"> | |
| Output history | |
| </summary> | |
| <div className="mt-3 space-y-2"> | |
| {wp.outputs | |
| .slice(0, -1) | |
| .reverse() | |
| .map((output) => ( | |
| <WorkPackageOutputCard | |
| key={output.id} | |
| output={output} | |
| /> | |
| ))} | |
| </div> | |
| </details> | |
| ) : null} | |
| <details className="rounded-2xl bg-background/72 px-3 py-3 shadow-sm ring-1 ring-black/5"> | |
| <summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-[0.04em] text-muted-foreground"> | |
| Package context | |
| </summary> | |
| <div className="mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]"> | |
| <div className="space-y-3"> | |
| <Section title="Expected outputs"> | |
| <div className="flex flex-wrap gap-1.5"> | |
| {wp.outputFiles.map((output) => ( | |
| <Badge key={output} variant="outline" className="text-[11px]"> | |
| {output} | |
| </Badge> | |
| ))} | |
| </div> | |
| </Section> | |
| {wp.inputFiles.length ? ( | |
| <Section title="Expected inputs"> | |
| <div className="text-sm leading-6 text-muted-foreground"> | |
| {wp.inputFiles.join(", ")} | |
| </div> | |
| </Section> | |
| ) : null} | |
| {wp.coreSections.length ? ( | |
| <Section title="Core sections"> | |
| <div className="text-sm leading-6 text-muted-foreground"> | |
| {wp.coreSections.join(", ")} | |
| </div> | |
| </Section> | |
| ) : null} | |
| </div> | |
| <Section title="Tasks"> | |
| <div className="space-y-2"> | |
| {wp.tasks.map((task) => ( | |
| <div | |
| key={task.id} | |
| className="rounded-xl bg-muted/28 px-3 py-2.5 text-sm shadow-sm ring-1 ring-black/5" | |
| > | |
| <div className="flex items-start justify-between gap-3"> | |
| <div className="font-medium">{task.title}</div> | |
| <AskAboutButton | |
| label="Ask about this task" | |
| onClick={() => | |
| onPrefill( | |
| buildPackageQuestionPrompt({ | |
| packageRef: refName, | |
| label: "task", | |
| excerpt: `${task.title}: ${task.description}`, | |
| }), | |
| ) | |
| } | |
| /> | |
| </div> | |
| <div className="mt-1 leading-5 text-muted-foreground"> | |
| {task.description} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </Section> | |
| </div> | |
| </details> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |