@locusai/web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/next.config.js +7 -0
  3. package/package.json +37 -0
  4. package/postcss.config.mjs +5 -0
  5. package/src/app/backlog/page.tsx +19 -0
  6. package/src/app/docs/page.tsx +7 -0
  7. package/src/app/globals.css +603 -0
  8. package/src/app/layout.tsx +43 -0
  9. package/src/app/page.tsx +16 -0
  10. package/src/app/providers.tsx +16 -0
  11. package/src/app/settings/page.tsx +194 -0
  12. package/src/components/BoardFilter.tsx +98 -0
  13. package/src/components/Header.tsx +21 -0
  14. package/src/components/PropertyItem.tsx +98 -0
  15. package/src/components/Sidebar.tsx +109 -0
  16. package/src/components/TaskCard.tsx +138 -0
  17. package/src/components/TaskCreateModal.tsx +243 -0
  18. package/src/components/TaskPanel.tsx +765 -0
  19. package/src/components/index.ts +7 -0
  20. package/src/components/ui/Badge.tsx +77 -0
  21. package/src/components/ui/Button.tsx +47 -0
  22. package/src/components/ui/Checkbox.tsx +52 -0
  23. package/src/components/ui/Dropdown.tsx +107 -0
  24. package/src/components/ui/Input.tsx +36 -0
  25. package/src/components/ui/Modal.tsx +79 -0
  26. package/src/components/ui/Textarea.tsx +21 -0
  27. package/src/components/ui/index.ts +7 -0
  28. package/src/hooks/useTasks.ts +119 -0
  29. package/src/lib/api-client.ts +24 -0
  30. package/src/lib/utils.ts +6 -0
  31. package/src/services/doc.service.ts +27 -0
  32. package/src/services/index.ts +3 -0
  33. package/src/services/sprint.service.ts +26 -0
  34. package/src/services/task.service.ts +75 -0
  35. package/src/views/Backlog.tsx +691 -0
  36. package/src/views/Board.tsx +306 -0
  37. package/src/views/Docs.tsx +625 -0
  38. package/tsconfig.json +21 -0
@@ -0,0 +1,765 @@
1
+ "use client";
2
+
3
+ import {
4
+ type AcceptanceItem,
5
+ AssigneeRole,
6
+ EventType,
7
+ type Task,
8
+ type Event as TaskEvent,
9
+ TaskPriority,
10
+ TaskStatus,
11
+ } from "@locusai/shared";
12
+ import { format, formatDistanceToNow } from "date-fns";
13
+ import { motion } from "framer-motion";
14
+ import {
15
+ CheckCircle,
16
+ ChevronRight,
17
+ Edit,
18
+ FileText,
19
+ Lock,
20
+ MessageSquare,
21
+ PlusSquare,
22
+ Tag,
23
+ Terminal,
24
+ Trash2,
25
+ Unlock,
26
+ X,
27
+ } from "lucide-react";
28
+ import { useCallback, useEffect, useState } from "react";
29
+ import { PropertyItem } from "@/components";
30
+ import {
31
+ Button,
32
+ Checkbox,
33
+ Input,
34
+ PriorityBadge,
35
+ StatusBadge,
36
+ Textarea,
37
+ } from "@/components/ui";
38
+ import { taskService } from "@/services";
39
+
40
+ interface TaskPanelProps {
41
+ taskId: number;
42
+ onClose: () => void;
43
+ onDeleted: () => void;
44
+ onUpdated: () => void;
45
+ }
46
+
47
+ export function TaskPanel({
48
+ taskId,
49
+ onClose,
50
+ onDeleted,
51
+ onUpdated,
52
+ }: TaskPanelProps) {
53
+ const [task, setTask] = useState<Task | null>(null);
54
+ const [isEditingTitle, setIsEditingTitle] = useState(false);
55
+ const [editTitle, setEditTitle] = useState("");
56
+ const [editDesc, setEditDesc] = useState("");
57
+ const [newComment, setNewComment] = useState("");
58
+ const [newChecklistItem, setNewChecklistItem] = useState("");
59
+ const [descMode, setDescMode] = useState<"edit" | "preview">("preview");
60
+ const [showRejectModal, setShowRejectModal] = useState(false);
61
+ const [rejectReason, setRejectReason] = useState("");
62
+
63
+ const formatDate = (date: string | number | Date) => {
64
+ return format(new Date(date), "MMM d, yyyy");
65
+ };
66
+
67
+ const fetchTask = useCallback(async () => {
68
+ try {
69
+ const taskData = await taskService.getById(taskId);
70
+ const initializedTask: Task = {
71
+ ...taskData,
72
+ acceptanceChecklist: taskData.acceptanceChecklist || [],
73
+ artifacts: taskData.artifacts || [],
74
+ activityLog: taskData.activityLog || [],
75
+ comments: taskData.comments || [],
76
+ };
77
+ setTask(initializedTask);
78
+ setEditTitle(taskData.title);
79
+ setEditDesc(taskData.description || "");
80
+ } catch (err) {
81
+ console.error("Failed to fetch task:", err);
82
+ }
83
+ }, [taskId]);
84
+
85
+ useEffect(() => {
86
+ fetchTask();
87
+ }, [fetchTask]);
88
+
89
+ const handleUpdateTask = async (updates: Partial<Task>) => {
90
+ try {
91
+ await taskService.update(taskId, updates);
92
+ fetchTask();
93
+ onUpdated();
94
+ } catch (err) {
95
+ console.error("Failed to update task:", err);
96
+ }
97
+ };
98
+
99
+ const handleDelete = async () => {
100
+ if (
101
+ !confirm(
102
+ "Are you sure you want to delete this task? This action cannot be undone."
103
+ )
104
+ ) {
105
+ return;
106
+ }
107
+ try {
108
+ await taskService.delete(taskId);
109
+ onDeleted();
110
+ onClose();
111
+ } catch (err) {
112
+ console.error("Failed to delete task:", err);
113
+ }
114
+ };
115
+
116
+ const handleTitleSave = () => {
117
+ if (editTitle.trim() && editTitle !== task?.title) {
118
+ handleUpdateTask({ title: editTitle.trim() });
119
+ }
120
+ setIsEditingTitle(false);
121
+ };
122
+
123
+ const handleDescSave = () => {
124
+ if (editDesc !== task?.description) {
125
+ handleUpdateTask({ description: editDesc });
126
+ }
127
+ };
128
+
129
+ const handleAddChecklistItem = () => {
130
+ if (!newChecklistItem.trim() || !task) return;
131
+ const newItem: AcceptanceItem = {
132
+ id: crypto.randomUUID(),
133
+ text: newChecklistItem.trim(),
134
+ done: false,
135
+ };
136
+ handleUpdateTask({
137
+ acceptanceChecklist: [...task.acceptanceChecklist, newItem],
138
+ });
139
+ setNewChecklistItem("");
140
+ };
141
+
142
+ const handleToggleChecklistItem = (itemId: string) => {
143
+ if (!task) return;
144
+ const updated = task.acceptanceChecklist.map((item) =>
145
+ item.id === itemId ? { ...item, done: !item.done } : item
146
+ );
147
+ handleUpdateTask({ acceptanceChecklist: updated });
148
+ };
149
+
150
+ const handleRemoveChecklistItem = (itemId: string) => {
151
+ if (!task) return;
152
+ const updated = task.acceptanceChecklist.filter(
153
+ (item) => item.id !== itemId
154
+ );
155
+ handleUpdateTask({ acceptanceChecklist: updated });
156
+ };
157
+
158
+ const handleAddComment = async () => {
159
+ if (!newComment.trim()) return;
160
+ try {
161
+ await taskService.addComment(taskId, {
162
+ author: "Human",
163
+ text: newComment,
164
+ });
165
+ setNewComment("");
166
+ await fetchTask();
167
+ } catch (err) {
168
+ console.error("Failed to add comment:", err);
169
+ }
170
+ };
171
+
172
+ const handleRunCi = async (preset: string) => {
173
+ try {
174
+ const data = await taskService.runCi(taskId, preset);
175
+ alert(data.summary);
176
+ fetchTask();
177
+ } catch (err) {
178
+ console.error("Failed to run CI:", err);
179
+ }
180
+ };
181
+
182
+ const handleLock = async () => {
183
+ try {
184
+ await taskService.lock(taskId, "human", 3600);
185
+ fetchTask();
186
+ onUpdated();
187
+ } catch (err) {
188
+ console.error("Failed to lock task:", err);
189
+ }
190
+ };
191
+
192
+ const handleUnlock = async () => {
193
+ try {
194
+ await taskService.unlock(taskId, "human");
195
+ fetchTask();
196
+ onUpdated();
197
+ } catch (err) {
198
+ console.error("Failed to unlock task:", err);
199
+ }
200
+ };
201
+
202
+ const handleReject = async () => {
203
+ if (!rejectReason.trim()) return;
204
+ try {
205
+ // Move back to IN_PROGRESS
206
+ await taskService.update(taskId, { status: TaskStatus.IN_PROGRESS });
207
+ // Add rejection comment
208
+ await taskService.addComment(taskId, {
209
+ author: "Manager",
210
+ text: `❌ **Rejected**: ${rejectReason}`,
211
+ });
212
+ setShowRejectModal(false);
213
+ setRejectReason("");
214
+ fetchTask();
215
+ onUpdated();
216
+ } catch (err) {
217
+ console.error("Failed to reject task:", err);
218
+ }
219
+ };
220
+
221
+ const handleApprove = async () => {
222
+ try {
223
+ await taskService.update(taskId, { status: TaskStatus.DONE });
224
+ fetchTask();
225
+ onUpdated();
226
+ } catch (err) {
227
+ console.error("Failed to approve task:", err);
228
+ }
229
+ };
230
+
231
+ if (!task) {
232
+ return (
233
+ <div className="fixed top-0 right-0 bottom-0 w-[1152px] max-w-[95vw] bg-background border-l border-border z-950 flex items-center justify-center">
234
+ <div className="text-muted-foreground animate-pulse font-medium">
235
+ Loading task specs...
236
+ </div>
237
+ </div>
238
+ );
239
+ }
240
+
241
+ const isLocked =
242
+ task.lockedBy && (!task.lockExpiresAt || task.lockExpiresAt > Date.now());
243
+ const checklistProgress = task.acceptanceChecklist.length
244
+ ? Math.round(
245
+ (task.acceptanceChecklist.filter((i) => i.done).length /
246
+ task.acceptanceChecklist.length) *
247
+ 100
248
+ )
249
+ : 0;
250
+
251
+ return (
252
+ <>
253
+ <motion.div
254
+ initial={{ opacity: 0 }}
255
+ animate={{ opacity: 1 }}
256
+ exit={{ opacity: 0 }}
257
+ className="fixed inset-0 bg-black/60 backdrop-blur-sm z-940"
258
+ onClick={onClose}
259
+ />
260
+ <motion.div
261
+ initial={{ x: "100%" }}
262
+ animate={{ x: 0 }}
263
+ exit={{ x: "100%" }}
264
+ transition={{ type: "spring", damping: 25, stiffness: 300 }}
265
+ className="fixed top-0 right-0 bottom-0 w-[1000px] max-w-[95vw] bg-background border-l border-border z-950 flex flex-col shadow-[-20px_0_80px_rgba(0,0,0,0.6)]"
266
+ >
267
+ <header className="flex items-center gap-6 px-10 border-b border-border bg-card/50 backdrop-blur-md h-[84px] shrink-0">
268
+ <button
269
+ className="p-2.5 rounded-xl text-muted-foreground hover:bg-secondary hover:text-foreground hover:scale-105 transition-all duration-200 border border-transparent hover:border-border"
270
+ onClick={onClose}
271
+ >
272
+ <ChevronRight size={20} />
273
+ </button>
274
+
275
+ <div className="flex-1 min-w-0">
276
+ <span className="text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground/60 mb-1 block">
277
+ Reference: #{task.id}
278
+ </span>
279
+ <div className="flex gap-3">
280
+ <StatusBadge status={task.status} />
281
+ <PriorityBadge priority={task.priority || TaskPriority.MEDIUM} />
282
+ </div>
283
+ </div>
284
+
285
+ <div className="flex items-center gap-2">
286
+ <Button
287
+ size="icon"
288
+ variant="ghost"
289
+ onClick={() => handleRunCi("quick")}
290
+ title="Run Quality Checks"
291
+ className="h-10 w-10 hover:bg-primary/10 hover:text-primary transition-all rounded-xl"
292
+ >
293
+ <Terminal size={18} />
294
+ </Button>
295
+ {isLocked ? (
296
+ <Button
297
+ size="icon"
298
+ variant="ghost"
299
+ onClick={handleUnlock}
300
+ title="Unlock"
301
+ className="h-10 w-10 text-amber-500 bg-amber-500/10 hover:bg-amber-500/20 rounded-xl"
302
+ >
303
+ <Unlock size={18} />
304
+ </Button>
305
+ ) : (
306
+ <Button
307
+ size="icon"
308
+ variant="ghost"
309
+ onClick={handleLock}
310
+ title="Lock"
311
+ className="h-10 w-10 hover:bg-primary/10 hover:text-primary rounded-xl"
312
+ >
313
+ <Lock size={18} />
314
+ </Button>
315
+ )}
316
+ <div className="w-px h-6 bg-border mx-1" />
317
+ {task.status === TaskStatus.VERIFICATION && (
318
+ <>
319
+ <Button
320
+ variant="danger"
321
+ size="sm"
322
+ onClick={() => setShowRejectModal(true)}
323
+ className="h-10 px-4 rounded-xl"
324
+ >
325
+ Reject
326
+ </Button>
327
+ <Button
328
+ size="sm"
329
+ variant="success"
330
+ onClick={handleApprove}
331
+ className="h-10 px-4 rounded-xl"
332
+ >
333
+ <CheckCircle size={16} className="mr-2" />
334
+ Approve
335
+ </Button>
336
+ <div className="w-px h-6 bg-border mx-1" />
337
+ </>
338
+ )}
339
+ <Button
340
+ size="icon"
341
+ variant="danger"
342
+ onClick={handleDelete}
343
+ title="Delete"
344
+ className="h-10 w-10 hover:scale-105 active:scale-95 transition-transform rounded-xl"
345
+ >
346
+ <Trash2 size={18} />
347
+ </Button>
348
+ </div>
349
+ </header>
350
+
351
+ <div className="flex-1 grid grid-cols-[1fr_360px] overflow-hidden min-h-0">
352
+ <div className="p-6 overflow-y-auto scrollbar-thin">
353
+ {/* Title Section */}
354
+ <div className="mb-6">
355
+ {isEditingTitle ? (
356
+ <Input
357
+ value={editTitle}
358
+ onChange={(e) => setEditTitle(e.target.value)}
359
+ onBlur={handleTitleSave}
360
+ onKeyDown={(e) => {
361
+ if (e.key === "Enter") handleTitleSave();
362
+ if (e.key === "Escape") {
363
+ setEditTitle(task.title);
364
+ setIsEditingTitle(false);
365
+ }
366
+ }}
367
+ className="text-2xl h-14 font-bold tracking-tight"
368
+ autoFocus
369
+ />
370
+ ) : (
371
+ <h2
372
+ className="text-2xl font-bold tracking-tight hover:text-primary transition-all cursor-pointer leading-tight group"
373
+ onClick={() => setIsEditingTitle(true)}
374
+ >
375
+ {task.title}
376
+ <Edit
377
+ size={18}
378
+ className="inline-block ml-3 opacity-0 group-hover:opacity-40 transition-opacity"
379
+ />
380
+ </h2>
381
+ )}
382
+ </div>
383
+
384
+ {/* Description Section */}
385
+ <div className="mb-6">
386
+ <div className="flex items-center justify-between mb-3">
387
+ <div className="flex items-center gap-3">
388
+ <div className="h-8 w-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
389
+ <FileText size={16} />
390
+ </div>
391
+ <h4 className="text-sm font-bold uppercase tracking-widest text-foreground/80">
392
+ Documentation
393
+ </h4>
394
+ </div>
395
+ <div className="flex bg-secondary/30 p-1 rounded-xl">
396
+ <button
397
+ className={`px-4 py-1.5 rounded-lg text-xs font-bold transition-all ${descMode === "preview" ? "bg-background shadow-md text-foreground scale-105" : "text-muted-foreground hover:text-foreground"}`}
398
+ onClick={() => setDescMode("preview")}
399
+ >
400
+ Visual
401
+ </button>
402
+ <button
403
+ className={`px-4 py-1.5 rounded-lg text-xs font-bold transition-all ${descMode === "edit" ? "bg-background shadow-md text-foreground scale-105" : "text-muted-foreground hover:text-foreground"}`}
404
+ onClick={() => setDescMode("edit")}
405
+ >
406
+ Markdown
407
+ </button>
408
+ </div>
409
+ </div>
410
+
411
+ {descMode === "edit" ? (
412
+ <div className="group border border-border rounded-2xl overflow-hidden focus-within:ring-2 focus-within:ring-primary/20 transition-all bg-secondary/5">
413
+ <Textarea
414
+ value={editDesc}
415
+ onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
416
+ setEditDesc(e.target.value)
417
+ }
418
+ placeholder="Describe the implementation details, edge cases, and technical requirements..."
419
+ rows={10}
420
+ className="border-none focus:ring-0 text-base leading-relaxed p-6 bg-transparent"
421
+ onBlur={handleDescSave}
422
+ />
423
+ </div>
424
+ ) : (
425
+ <div className="prose prose-invert max-w-none bg-secondary/10 p-8 rounded-2xl border border-border/50 shadow-inner">
426
+ {task.description ? (
427
+ <p className="text-foreground/90 leading-relaxed whitespace-pre-wrap">
428
+ {task.description}
429
+ </p>
430
+ ) : (
431
+ <span className="text-muted-foreground/30 italic text-sm select-none">
432
+ Waiting for technical documentation...
433
+ </span>
434
+ )}
435
+ </div>
436
+ )}
437
+ </div>
438
+
439
+ {/* Acceptance Checklist */}
440
+ <div className="mb-4">
441
+ <div className="flex items-center justify-between mb-4">
442
+ <div className="flex items-center gap-3">
443
+ <div className="h-8 w-8 rounded-lg bg-sky-500/10 flex items-center justify-center text-sky-500">
444
+ <CheckCircle size={16} />
445
+ </div>
446
+ <h4 className="text-sm font-bold uppercase tracking-widest text-foreground/80">
447
+ Definition of Done
448
+ </h4>
449
+ </div>
450
+ <div className="flex items-center gap-4">
451
+ <div className="flex flex-col items-end">
452
+ <span className="text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground mb-1">
453
+ Status
454
+ </span>
455
+ <span className="text-sm font-mono font-black text-sky-500">
456
+ {checklistProgress}%
457
+ </span>
458
+ </div>
459
+ <div className="w-48 h-2 bg-secondary/30 rounded-full overflow-hidden border border-border/50">
460
+ <div
461
+ className="h-full bg-sky-500 transition-all duration-1000 ease-out shadow-[0_0_20px_rgba(14,165,233,0.5)]"
462
+ style={{ width: `${checklistProgress}%` }}
463
+ />
464
+ </div>
465
+ </div>
466
+ </div>
467
+
468
+ <div className="grid gap-2 mb-4">
469
+ {task.acceptanceChecklist.length === 0 && (
470
+ <div className="text-center py-6 border-2 border-dashed border-border/40 rounded-xl group hover:border-accent/40 transition-colors">
471
+ <p className="text-[10px] font-bold uppercase tracking-[0.3em] text-muted-foreground/40 mb-2 italic">
472
+ Standard quality checks required
473
+ </p>
474
+ <Button
475
+ variant="ghost"
476
+ size="sm"
477
+ onClick={() => setNewChecklistItem("Add unit tests")}
478
+ className="text-xs hover:text-sky-500"
479
+ >
480
+ Suggest Criteria
481
+ </Button>
482
+ </div>
483
+ )}
484
+ {task.acceptanceChecklist.map((item) => (
485
+ <div
486
+ key={item.id}
487
+ className="group flex items-center gap-3 p-3 bg-secondary/10 border border-border/40 rounded-xl hover:border-sky-500/50 hover:bg-secondary/20 transition-all duration-300 shadow-sm"
488
+ >
489
+ <Checkbox
490
+ checked={item.done}
491
+ onChange={() => handleToggleChecklistItem(item.id)}
492
+ />
493
+ <span
494
+ className={`flex-1 text-sm font-semibold transition-all duration-300 ${item.done ? "line-through text-muted-foreground/40 scale-[0.98] translate-x-1" : "text-foreground"}`}
495
+ >
496
+ {item.text}
497
+ </span>
498
+ <Button
499
+ size="icon"
500
+ variant="ghost"
501
+ className="opacity-0 group-hover:opacity-100 h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-all rounded-lg"
502
+ onClick={() => handleRemoveChecklistItem(item.id)}
503
+ >
504
+ <X size={14} />
505
+ </Button>
506
+ </div>
507
+ ))}
508
+ </div>
509
+
510
+ <div className="flex gap-3 p-2 bg-secondary/5 rounded-xl border border-border/40">
511
+ <Input
512
+ value={newChecklistItem}
513
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
514
+ setNewChecklistItem(e.target.value)
515
+ }
516
+ placeholder="Add quality criteria..."
517
+ className="h-10 bg-transparent border-none focus:ring-0 text-sm font-medium"
518
+ onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
519
+ if (e.key === "Enter") handleAddChecklistItem();
520
+ }}
521
+ />
522
+ <Button
523
+ onClick={handleAddChecklistItem}
524
+ className="px-5 h-10 bg-foreground text-background font-bold text-xs tracking-wide hover:scale-105 active:scale-95 transition-all rounded-lg"
525
+ >
526
+ Append
527
+ </Button>
528
+ </div>
529
+ </div>
530
+ </div>
531
+
532
+ <div className="p-4 overflow-y-auto border-l border-border bg-secondary/10 backdrop-blur-3xl shadow-[inset_1px_0_0_rgba(255,255,255,0.02)] scrollbar-thin">
533
+ {/* Properties Section */}
534
+ <div className="mb-6">
535
+ <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground/60 mb-4 pb-2 border-b border-border/40">
536
+ Specifications
537
+ </h4>
538
+ <div className="space-y-2">
539
+ <PropertyItem
540
+ label="Status"
541
+ value={task.status}
542
+ onEdit={(newValue: string) =>
543
+ handleUpdateTask({ status: newValue as TaskStatus })
544
+ }
545
+ options={Object.values(TaskStatus)}
546
+ type="dropdown"
547
+ />
548
+ <PropertyItem
549
+ label="Assignee Group"
550
+ value={task.assigneeRole || "Unassigned"}
551
+ onEdit={(newValue: string) =>
552
+ handleUpdateTask({ assigneeRole: newValue as AssigneeRole })
553
+ }
554
+ options={Object.values(AssigneeRole)}
555
+ type="dropdown"
556
+ />
557
+ <PropertyItem
558
+ label="Priority"
559
+ value={task.priority || TaskPriority.MEDIUM}
560
+ onEdit={(newValue: string) =>
561
+ handleUpdateTask({ priority: newValue as TaskPriority })
562
+ }
563
+ options={Object.values(TaskPriority)}
564
+ type="dropdown"
565
+ />
566
+ <PropertyItem
567
+ label="Deadline"
568
+ value={
569
+ task.dueDate ? formatDate(task.dueDate) : "Not Defined"
570
+ }
571
+ onEdit={(newValue: string) =>
572
+ handleUpdateTask({ dueDate: newValue || null })
573
+ }
574
+ type="date"
575
+ />
576
+ <PropertyItem
577
+ label="Owner"
578
+ value={task.assignedTo || "Open Seat"}
579
+ onEdit={(newValue: string) =>
580
+ handleUpdateTask({ assignedTo: newValue || null })
581
+ }
582
+ type="text"
583
+ />
584
+ </div>
585
+ </div>
586
+
587
+ {/* Artifacts Section */}
588
+ <div className="mb-6">
589
+ <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground/60 mb-4 pb-2 border-b border-border/40">
590
+ Generated Assets
591
+ </h4>
592
+ <div className="grid gap-2">
593
+ {task.artifacts.length > 0 ? (
594
+ task.artifacts.map((artifact) => (
595
+ <a
596
+ key={artifact.id}
597
+ href={artifact.url}
598
+ target="_blank"
599
+ rel="noopener noreferrer"
600
+ className="group flex items-center gap-3 p-3 bg-background/50 border border-border/40 rounded-xl hover:border-primary/50 hover:bg-background transition-all duration-300 shadow-sm"
601
+ >
602
+ <div className="h-8 w-8 bg-primary/10 rounded-lg flex items-center justify-center text-primary group-hover:scale-110 group-hover:bg-primary group-hover:text-primary-foreground transition-all duration-500">
603
+ <FileText size={14} />
604
+ </div>
605
+ <div className="flex-1 min-w-0">
606
+ <span className="block text-xs font-black truncate text-foreground/90">
607
+ {artifact.title}
608
+ </span>
609
+ <span className="text-[9px] text-muted-foreground font-black uppercase tracking-widest mt-1 block">
610
+ {artifact.type} • {artifact.size}
611
+ </span>
612
+ </div>
613
+ </a>
614
+ ))
615
+ ) : (
616
+ <div className="py-6 text-center bg-background/20 rounded-xl border border-dashed border-border/40">
617
+ <p className="text-[9px] font-black uppercase tracking-[0.2em] text-muted-foreground/30 italic">
618
+ No output generated
619
+ </p>
620
+ </div>
621
+ )}
622
+ </div>
623
+ </div>
624
+
625
+ {/* Activity Feed */}
626
+ <div>
627
+ <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground/60 mb-4 pb-2 border-b border-border/40">
628
+ Activity Stream
629
+ </h4>
630
+
631
+ <div className="flex gap-2 mb-4">
632
+ <Input
633
+ value={newComment}
634
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
635
+ setNewComment(e.target.value)
636
+ }
637
+ placeholder="Post an update..."
638
+ className="h-11 text-xs bg-background/40 border-border/40 rounded-xl focus:bg-background"
639
+ onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
640
+ if (e.key === "Enter") handleAddComment();
641
+ }}
642
+ />
643
+ <Button
644
+ onClick={handleAddComment}
645
+ variant="secondary"
646
+ className="h-9 px-4 rounded-lg group"
647
+ >
648
+ <MessageSquare
649
+ size={14}
650
+ className="group-hover:rotate-12 transition-transform"
651
+ />
652
+ </Button>
653
+ </div>
654
+
655
+ <div className="space-y-8 max-h-[500px] overflow-y-auto pr-3 scrollbar-none hover:scrollbar-thin transition-all">
656
+ {task.activityLog.map((event: TaskEvent) => (
657
+ <div key={event.id} className="relative flex gap-5 group">
658
+ <div className="absolute left-[17px] top-8 bottom-[-24px] w-px bg-border/40 group-last:hidden" />
659
+ <div className="h-9 w-9 rounded-xl bg-card border border-border/60 flex items-center justify-center shrink-0 z-10 shadow-sm group-hover:border-primary/40 transition-colors">
660
+ {event.type === EventType.COMMENT_ADDED && (
661
+ <MessageSquare size={14} className="text-blue-500" />
662
+ )}
663
+ {event.type === EventType.STATUS_CHANGED && (
664
+ <Tag size={14} className="text-amber-500" />
665
+ )}
666
+ {event.type === EventType.TASK_CREATED && (
667
+ <PlusSquare size={14} className="text-emerald-400" />
668
+ )}
669
+ {event.type === EventType.TASK_UPDATED && (
670
+ <Edit size={14} className="text-primary" />
671
+ )}
672
+ {event.type === EventType.ARTIFACT_ADDED && (
673
+ <FileText size={14} className="text-purple-400" />
674
+ )}
675
+ {(event.type === EventType.LOCKED ||
676
+ event.type === EventType.UNLOCKED) && (
677
+ <Lock size={14} className="text-rose-400" />
678
+ )}
679
+ {event.type === EventType.CI_RAN && (
680
+ <CheckCircle size={14} className="text-accent" />
681
+ )}
682
+ </div>
683
+ <div className="pt-1.5 min-w-0">
684
+ <p className="text-xs font-bold text-foreground/90 leading-snug mb-1.5">
685
+ {formatActivityEvent(event)}
686
+ </p>
687
+ <span className="text-[10px] font-black uppercase tracking-[0.15em] text-muted-foreground/50">
688
+ {formatDistanceToNow(new Date(event.createdAt), {
689
+ addSuffix: true,
690
+ })}
691
+ </span>
692
+ </div>
693
+ </div>
694
+ ))}
695
+ </div>
696
+ </div>
697
+ </div>
698
+ </div>
699
+ </motion.div>
700
+
701
+ {/* Rejection Modal */}
702
+ {showRejectModal && (
703
+ <div className="fixed inset-0 bg-black/70 backdrop-blur-sm z-960 flex items-center justify-center">
704
+ <div className="bg-background border border-border rounded-2xl p-6 w-[480px] shadow-2xl">
705
+ <h3 className="text-lg font-bold mb-4">Reject Task</h3>
706
+ <p className="text-sm text-muted-foreground mb-4">
707
+ This task will be moved back to IN_PROGRESS. The same agent will
708
+ receive your feedback and can fix the issues.
709
+ </p>
710
+ <Textarea
711
+ value={rejectReason}
712
+ onChange={(e) => setRejectReason(e.target.value)}
713
+ placeholder="Explain what needs to be fixed..."
714
+ className="min-h-[120px] mb-4"
715
+ autoFocus
716
+ />
717
+ <div className="flex gap-3 justify-end">
718
+ <Button
719
+ variant="ghost"
720
+ onClick={() => {
721
+ setShowRejectModal(false);
722
+ setRejectReason("");
723
+ }}
724
+ >
725
+ Cancel
726
+ </Button>
727
+ <Button
728
+ variant="danger"
729
+ onClick={handleReject}
730
+ disabled={!rejectReason.trim()}
731
+ >
732
+ Reject Task
733
+ </Button>
734
+ </div>
735
+ </div>
736
+ </div>
737
+ )}
738
+ </>
739
+ );
740
+ }
741
+
742
+ function formatActivityEvent(event: TaskEvent): string {
743
+ const { type, payload } = event;
744
+ const p = payload as Record<string, string | number | undefined>;
745
+ switch (type) {
746
+ case EventType.STATUS_CHANGED:
747
+ return `Status moved ${p.oldStatus} ➟ ${p.newStatus}`;
748
+ case EventType.COMMENT_ADDED:
749
+ return `${p.author}: "${p.text}"`;
750
+ case EventType.TASK_CREATED:
751
+ return "Task initialized";
752
+ case EventType.TASK_UPDATED:
753
+ return "Parameters calibrated";
754
+ case EventType.ARTIFACT_ADDED:
755
+ return `Output: ${p.title}`;
756
+ case EventType.LOCKED:
757
+ return `Protected by ${p.agentId}`;
758
+ case EventType.UNLOCKED:
759
+ return "Protection released";
760
+ case EventType.CI_RAN:
761
+ return `Valuation complete: ${p.summary}`;
762
+ default:
763
+ return (type as string).replace(/_/g, " ").toLowerCase();
764
+ }
765
+ }