Task Rows

AI & Agents

Live agent task status — running, failed, completed.

Interactive Live Preview
components/ui/task-rows.tsxTypeScript · React 19
1"use client";
2
3import { useEffect, useState } from "react";
4
5/* ─────────────────────────────────────────────────────────
6 * TASK ROWS
7 *
8 * 0ms rows enter staggered (80ms apart)
9 * 600ms row 1 ring sweeps 0 → 66%
10 * 1500ms row 1 expands — detail steps drop down
11 * 3900ms row 1 collapses; row 2 flips to Failed + retry
12 * 5300ms row 2 resolves to Completed
13 * The status run completes once; task details stay clickable.
14 * ───────────────────────────────────────────────────────── */
15
16const TICKS = [600, 900, 2400, 1400, 2400, 600];
17
18function useTick(intervals: number[]) {
19 const [tick, setTick] = useState(0);
20 useEffect(() => {
21 if (tick >= intervals.length - 1) return;
22 const t = setTimeout(() => setTick((x) => x + 1), intervals[tick]);
23 return () => clearTimeout(t);
24 }, [tick, intervals]);
25 return tick;
26}
27
28function SpinnerRing({ active, children }: { active?: boolean; children?: React.ReactNode }) {
29 const size = 24, stroke = 2;
30 const r = (size - stroke) / 2;
31 const c = 2 * Math.PI * r;
32 return (
33 <span className="relative inline-flex shrink-0 items-center justify-center" style={{ width: size, height: size }}>
34 <svg
35 width={size} height={size} className="absolute inset-0"
36 style={active ? { animation: "spin 1.1s linear infinite" } : undefined}
37 >
38 <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="var(--line)" strokeWidth={stroke} />
39 {active && (
40 <circle
41 cx={size / 2} cy={size / 2} r={r} fill="none"
42 stroke="var(--ink-3)" strokeWidth={stroke} strokeLinecap="round"
43 strokeDasharray={`${c * 0.28} ${c * 0.72}`}
44 />
45 )}
46 </svg>
47 <span className="relative text-[10.5px] font-semibold tabular-nums text-ink">{children}</span>
48 </span>
49 );
50}
51
52function Badge({ tone, children }: { tone: "red" | "green"; children: React.ReactNode }) {
53 return (
54 <span
55 className={`flex size-5.5 shrink-0 items-center justify-center rounded-full text-white
56 ${tone === "red" ? "bg-red" : "bg-green"}`}
57 style={{ animation: "pop-in 300ms cubic-bezier(0.23,1,0.32,1) both" }}
58 >
59 {children}
60 </span>
61 );
62}
63
64const XIcon = (
65 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round"><path d="M18 6L6 18M6 6l12 12" /></svg>
66);
67const CheckIcon = (
68 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5" /></svg>
69);
70const RetryIcon = (
71 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6" /></svg>
72);
73
74export default function TaskRows({ variant = "Capsules" }: { variant?: string }) {
75 const tick = useTick(TICKS);
76 const [manualOpen, setManualOpen] = useState<Record<string, boolean>>({});
77 const row2: "pending" | "failed" | "done" = tick < 3 ? "pending" : tick === 3 ? "failed" : "done";
78
79 const rows = [
80 {
81 key: "verify",
82 badge: <Badge tone="green">{CheckIcon}</Badge>,
83 label: "Verified vendor records",
84 amount: "12 suppliers",
85 pill: (
86 <span className="inline-flex h-5.5 items-center rounded-full bg-green-tint px-2 text-[11.5px] font-medium text-green">
87 Completed
88 </span>
89 ),
90 details: [
91 { label: "Matched tax and contact IDs", meta: "12/12" },
92 { label: "Flagged stale records", meta: "0" },
93 ],
94 },
95 {
96 key: "index",
97 badge: <SpinnerRing active>2</SpinnerRing>,
98 label: "Build reorder task list",
99 amount: "7 SKUs",
100 pill: null,
101 details: [
102 { label: "Reading POS export", meta: "3 files" },
103 { label: "Scoring stockout risk", meta: "68%" },
104 ],
105 },
106 {
107 key: "draft",
108 badge:
109 row2 === "pending" ? (
110 <SpinnerRing>3</SpinnerRing>
111 ) : row2 === "failed" ? (
112 <Badge tone="red">{XIcon}</Badge>
113 ) : (
114 <Badge tone="green">{CheckIcon}</Badge>
115 ),
116 label: "Draft supplier emails",
117 amount: "2 messages",
118 pill:
119 row2 === "failed" ? (
120 <span className="inline-flex h-5.5 items-center gap-1.5 rounded-full bg-red-tint px-2 text-[11.5px] font-medium text-red" style={{ animation: "fade-in 200ms ease-out both" }}>
121 Failed <span style={{ animation: "spin 1.2s linear infinite" }} className="flex">{RetryIcon}</span>
122 </span>
123 ) : row2 === "done" ? (
124 <span className="inline-flex h-5.5 items-center gap-1.5 rounded-full bg-green-tint px-2 text-[11.5px] font-medium text-green" style={{ animation: "fade-in 200ms ease-out both" }}>
125 Completed
126 </span>
127 ) : null,
128 details: [
129 { label: "Cone supplier follow-up", meta: "draft" },
130 { label: "Pistachio reorder note", meta: "draft" },
131 ],
132 },
133 ];
134
135 const list = variant === "List";
136 return (
137 <div
138 className={`flex w-full max-w-110 flex-col ${
139 list ? "gap-0 self-start overflow-hidden rounded-card bg-surface shadow-card" : "min-h-[196px] gap-2"
140 }`}
141 >
142 {rows.map((row, i) => {
143 const open = manualOpen[row.key] ?? (row.key === "index" && tick === 2);
144 return (
145 <div
146 key={row.key}
147 className={`self-stretch overflow-hidden transition-[border-radius,background-color] duration-300 hover:bg-inset ${
148 list ? "border-b border-line last:border-0" : "bg-surface shadow-card"
149 }`}
150 style={{
151 borderRadius: list ? 0 : open ? 14 : 22,
152 animation: `fade-up 450ms cubic-bezier(0.23,1,0.32,1) ${i * 80}ms both`,
153 }}
154 >
155 <button
156 type="button"
157 aria-expanded={open}
158 onClick={() => setManualOpen((current) => ({ ...current, [row.key]: !open }))}
159 className="flex h-11 w-full items-center gap-2.5 px-2.5 text-left"
160 >
161 <span className="flex size-6 shrink-0 items-center justify-center">
162 {row.badge}
163 </span>
164 <span className="min-w-0 flex-1 truncate text-[13px] font-medium text-ink">
165 {row.label}
166 </span>
167 <span className="text-[12.5px] text-ink-2 tabular-nums">{row.amount}</span>
168 {row.pill}
169 <span
170 aria-hidden="true"
171 className="-ml-2 flex size-7 shrink-0 items-center justify-center rounded-full text-ink-3"
172 >
173 <svg
174 width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"
175 className="transition-transform duration-300"
176 style={{ transform: open ? "rotate(180deg)" : "rotate(0)" }}
177 >
178 <path d="M6 9l6 6 6-6" />
179 </svg>
180 </span>
181 </button>
182
183 {/* dropdown detail — same expandable grammar as Chain of Thought */}
184 <div
185 className="grid transition-[grid-template-rows,opacity] duration-300"
186 style={{
187 gridTemplateRows: open ? "1fr" : "0fr",
188 opacity: open ? 1 : 0,
189 transitionTimingFunction: "cubic-bezier(0.23, 1, 0.32, 1)",
190 }}
191 >
192 <div className="overflow-hidden">
193 <div className="mb-2.5 grid grid-cols-[24px_1fr] gap-2.5 px-2.5">
194 <span aria-hidden className="mx-auto h-full w-px bg-line" />
195 <div className="flex flex-col gap-1.5">
196 {row.details.map((d, j) => (
197 <div
198 key={d.label}
199 className="flex items-center justify-between"
200 style={
201 open
202 ? { animation: `fade-up 300ms cubic-bezier(0.23,1,0.32,1) ${120 + j * 100}ms both` }
203 : undefined
204 }
205 >
206 <span className="text-[12px] text-ink-2">{d.label}</span>
207 <span className="font-mono text-[11.5px] text-ink-3 tabular-nums">
208 {d.meta}
209 </span>
210 </div>
211 ))}
212 </div>
213 </div>
214 </div>
215 </div>
216 </div>
217 );
218 })}
219 </div>
220 );
221}
222

Props & Specification

PropTypeDefaultDescription
variantCapsules | ListCapsulesVisual state and layout presentation mode.
classNamestring""Optional Tailwind CSS wrapper class overrides.

Related Components in AI & Agents