@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.
- package/LICENSE +21 -0
- package/next.config.js +7 -0
- package/package.json +37 -0
- package/postcss.config.mjs +5 -0
- package/src/app/backlog/page.tsx +19 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/globals.css +603 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/page.tsx +16 -0
- package/src/app/providers.tsx +16 -0
- package/src/app/settings/page.tsx +194 -0
- package/src/components/BoardFilter.tsx +98 -0
- package/src/components/Header.tsx +21 -0
- package/src/components/PropertyItem.tsx +98 -0
- package/src/components/Sidebar.tsx +109 -0
- package/src/components/TaskCard.tsx +138 -0
- package/src/components/TaskCreateModal.tsx +243 -0
- package/src/components/TaskPanel.tsx +765 -0
- package/src/components/index.ts +7 -0
- package/src/components/ui/Badge.tsx +77 -0
- package/src/components/ui/Button.tsx +47 -0
- package/src/components/ui/Checkbox.tsx +52 -0
- package/src/components/ui/Dropdown.tsx +107 -0
- package/src/components/ui/Input.tsx +36 -0
- package/src/components/ui/Modal.tsx +79 -0
- package/src/components/ui/Textarea.tsx +21 -0
- package/src/components/ui/index.ts +7 -0
- package/src/hooks/useTasks.ts +119 -0
- package/src/lib/api-client.ts +24 -0
- package/src/lib/utils.ts +6 -0
- package/src/services/doc.service.ts +27 -0
- package/src/services/index.ts +3 -0
- package/src/services/sprint.service.ts +26 -0
- package/src/services/task.service.ts +75 -0
- package/src/views/Backlog.tsx +691 -0
- package/src/views/Board.tsx +306 -0
- package/src/views/Docs.tsx +625 -0
- 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
|
+
}
|