@creezio/tasks 0.22.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/README.md +279 -0
- package/dist/.creezio-src-hash.json +4 -0
- package/dist/ai-task-agent.d.ts +47 -0
- package/dist/ai-task-agent.d.ts.map +1 -0
- package/dist/ai-task-agent.js +558 -0
- package/dist/ai-task-agent.js.map +1 -0
- package/dist/ai-task-runner.d.ts +78 -0
- package/dist/ai-task-runner.d.ts.map +1 -0
- package/dist/ai-task-runner.js +645 -0
- package/dist/ai-task-runner.js.map +1 -0
- package/dist/api-mount.d.ts +4 -0
- package/dist/api-mount.d.ts.map +1 -0
- package/dist/api-mount.js +91 -0
- package/dist/api-mount.js.map +1 -0
- package/dist/assistant-adapter.d.ts +7 -0
- package/dist/assistant-adapter.d.ts.map +1 -0
- package/dist/assistant-adapter.js +96 -0
- package/dist/assistant-adapter.js.map +1 -0
- package/dist/brand/config.d.ts +165 -0
- package/dist/brand/config.d.ts.map +1 -0
- package/dist/brand/config.js +39 -0
- package/dist/brand/config.js.map +1 -0
- package/dist/env-bridge.d.ts +12 -0
- package/dist/env-bridge.d.ts.map +1 -0
- package/dist/env-bridge.js +33 -0
- package/dist/env-bridge.js.map +1 -0
- package/dist/hono-routes.d.ts +8 -0
- package/dist/hono-routes.d.ts.map +1 -0
- package/dist/hono-routes.js +563 -0
- package/dist/hono-routes.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/kanban-service.d.ts +136 -0
- package/dist/kanban-service.d.ts.map +1 -0
- package/dist/kanban-service.js +580 -0
- package/dist/kanban-service.js.map +1 -0
- package/dist/mcp-host-tools.d.ts +59 -0
- package/dist/mcp-host-tools.d.ts.map +1 -0
- package/dist/mcp-host-tools.js +326 -0
- package/dist/mcp-host-tools.js.map +1 -0
- package/dist/mcp-workspace-tools.d.ts +15 -0
- package/dist/mcp-workspace-tools.d.ts.map +1 -0
- package/dist/mcp-workspace-tools.js +338 -0
- package/dist/mcp-workspace-tools.js.map +1 -0
- package/dist/memory-store.d.ts +3 -0
- package/dist/memory-store.d.ts.map +1 -0
- package/dist/memory-store.js +75 -0
- package/dist/memory-store.js.map +1 -0
- package/dist/sqlite-driver.d.ts +13 -0
- package/dist/sqlite-driver.d.ts.map +1 -0
- package/dist/sqlite-driver.js +13 -0
- package/dist/sqlite-driver.js.map +1 -0
- package/dist/sqlite-store.d.ts +15 -0
- package/dist/sqlite-store.d.ts.map +1 -0
- package/dist/sqlite-store.js +113 -0
- package/dist/sqlite-store.js.map +1 -0
- package/dist/task-runs.d.ts +108 -0
- package/dist/task-runs.d.ts.map +1 -0
- package/dist/task-runs.js +427 -0
- package/dist/task-runs.js.map +1 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist-cjs/ai-task-agent.js +569 -0
- package/dist-cjs/ai-task-agent.js.map +1 -0
- package/dist-cjs/ai-task-runner.js +660 -0
- package/dist-cjs/ai-task-runner.js.map +1 -0
- package/dist-cjs/api-mount.js +94 -0
- package/dist-cjs/api-mount.js.map +1 -0
- package/dist-cjs/assistant-adapter.js +99 -0
- package/dist-cjs/assistant-adapter.js.map +1 -0
- package/dist-cjs/brand/config.js +47 -0
- package/dist-cjs/brand/config.js.map +1 -0
- package/dist-cjs/env-bridge.js +36 -0
- package/dist-cjs/env-bridge.js.map +1 -0
- package/dist-cjs/hono-routes.js +566 -0
- package/dist-cjs/hono-routes.js.map +1 -0
- package/dist-cjs/index.js +117 -0
- package/dist-cjs/index.js.map +1 -0
- package/dist-cjs/kanban-service.js +601 -0
- package/dist-cjs/kanban-service.js.map +1 -0
- package/dist-cjs/mcp-host-tools.js +333 -0
- package/dist-cjs/mcp-host-tools.js.map +1 -0
- package/dist-cjs/mcp-workspace-tools.js +342 -0
- package/dist-cjs/mcp-workspace-tools.js.map +1 -0
- package/dist-cjs/memory-store.js +81 -0
- package/dist-cjs/memory-store.js.map +1 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/sqlite-driver.js +16 -0
- package/dist-cjs/sqlite-driver.js.map +1 -0
- package/dist-cjs/sqlite-store.js +119 -0
- package/dist-cjs/sqlite-store.js.map +1 -0
- package/dist-cjs/task-runs.js +457 -0
- package/dist-cjs/task-runs.js.map +1 -0
- package/dist-cjs/types.js +18 -0
- package/dist-cjs/types.js.map +1 -0
- package/package.json +79 -0
- package/scripts/port-from-tempoflow.mjs +405 -0
- package/ui/ai-activity-panel.tsx +559 -0
- package/ui/index.ts +7 -0
- package/ui/task-detail-sheet.tsx +668 -0
- package/ui/tasks-kanban-client.tsx +647 -0
- package/ui/tasks-types.ts +120 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Activity,
|
|
6
|
+
AlertCircle,
|
|
7
|
+
Bot,
|
|
8
|
+
CheckCircle2,
|
|
9
|
+
Clock3,
|
|
10
|
+
Eye,
|
|
11
|
+
FileText,
|
|
12
|
+
Loader2,
|
|
13
|
+
Play,
|
|
14
|
+
RefreshCw,
|
|
15
|
+
Sparkles,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
import { formatDistanceToNow } from "date-fns";
|
|
18
|
+
import { fr } from "date-fns/locale";
|
|
19
|
+
import { toast } from "sonner";
|
|
20
|
+
import { Badge } from "@creezio/shell-ui/ui/kit";
|
|
21
|
+
import { Button } from "@creezio/shell-ui/ui/kit";
|
|
22
|
+
import { ScrollArea } from "@creezio/shell-ui/ui/kit";
|
|
23
|
+
import {
|
|
24
|
+
Sheet,
|
|
25
|
+
SheetContent,
|
|
26
|
+
SheetDescription,
|
|
27
|
+
SheetHeader,
|
|
28
|
+
SheetTitle,
|
|
29
|
+
} from "@creezio/shell-ui/ui/kit";
|
|
30
|
+
import { openAiWorkspaceView } from "@creezio/shell-ui/ui/kit";
|
|
31
|
+
import { cn } from "@creezio/shell-ui";
|
|
32
|
+
import { AiActivityPanel } from "./ai-activity-panel";
|
|
33
|
+
import {
|
|
34
|
+
EXECUTOR_META,
|
|
35
|
+
STATUS_META,
|
|
36
|
+
sourceLabel,
|
|
37
|
+
type TaskCard,
|
|
38
|
+
} from "./tasks-types";
|
|
39
|
+
|
|
40
|
+
type RunRow = {
|
|
41
|
+
id: string;
|
|
42
|
+
status: string;
|
|
43
|
+
step_count: number;
|
|
44
|
+
last_error?: string | null;
|
|
45
|
+
hitl_prompt?: string | null;
|
|
46
|
+
started_at?: string | null;
|
|
47
|
+
finished_at?: string | null;
|
|
48
|
+
created_at: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type HermesDetail = {
|
|
52
|
+
task: {
|
|
53
|
+
id: string;
|
|
54
|
+
title: string;
|
|
55
|
+
body?: string | null;
|
|
56
|
+
status: string;
|
|
57
|
+
result?: string | null;
|
|
58
|
+
last_failure_error?: string | null;
|
|
59
|
+
age_seconds?: {
|
|
60
|
+
created_age_seconds?: number | null;
|
|
61
|
+
time_to_complete_seconds?: number | null;
|
|
62
|
+
} | null;
|
|
63
|
+
};
|
|
64
|
+
comments: { id?: number | string; body?: string; author?: string }[];
|
|
65
|
+
events: {
|
|
66
|
+
id?: number;
|
|
67
|
+
kind?: string;
|
|
68
|
+
payload?: Record<string, unknown>;
|
|
69
|
+
created_at?: number;
|
|
70
|
+
}[];
|
|
71
|
+
runs: {
|
|
72
|
+
id?: string;
|
|
73
|
+
status?: string;
|
|
74
|
+
summary?: string | null;
|
|
75
|
+
error?: string | null;
|
|
76
|
+
started_at?: number | null;
|
|
77
|
+
finished_at?: number | null;
|
|
78
|
+
}[];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type DetailPayload = {
|
|
82
|
+
task: TaskCard;
|
|
83
|
+
hermes: HermesDetail | null;
|
|
84
|
+
hermesError: string | null;
|
|
85
|
+
runs: RunRow[];
|
|
86
|
+
subtasks: TaskCard[];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type TabId = "brief" | "activite" | "rapport" | "timeline";
|
|
90
|
+
|
|
91
|
+
function formatTs(isoOrUnix: string | number | null | undefined): string {
|
|
92
|
+
if (isoOrUnix == null || isoOrUnix === "") return "—";
|
|
93
|
+
const d =
|
|
94
|
+
typeof isoOrUnix === "number"
|
|
95
|
+
? new Date(isoOrUnix * 1000)
|
|
96
|
+
: new Date(isoOrUnix);
|
|
97
|
+
if (Number.isNaN(d.getTime())) return "—";
|
|
98
|
+
return formatDistanceToNow(d, { addSuffix: true, locale: fr });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function eventLabel(kind?: string): string {
|
|
102
|
+
const map: Record<string, string> = {
|
|
103
|
+
created: "Créée",
|
|
104
|
+
status_changed: "Statut modifié",
|
|
105
|
+
claimed: "Prise en charge",
|
|
106
|
+
completed: "Terminée",
|
|
107
|
+
blocked: "Bloquée",
|
|
108
|
+
unblocked: "Débloquée",
|
|
109
|
+
comment: "Commentaire",
|
|
110
|
+
dispatched: "Dispatch",
|
|
111
|
+
};
|
|
112
|
+
return kind ? map[kind] || kind : "Événement";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function TaskDetailSheet({
|
|
116
|
+
taskId,
|
|
117
|
+
open,
|
|
118
|
+
onOpenChange,
|
|
119
|
+
onStatusChange,
|
|
120
|
+
onChanged,
|
|
121
|
+
}: {
|
|
122
|
+
taskId: string | null;
|
|
123
|
+
open: boolean;
|
|
124
|
+
onOpenChange: (open: boolean) => void;
|
|
125
|
+
onStatusChange: (id: string, status: string) => Promise<void>;
|
|
126
|
+
onChanged?: () => void;
|
|
127
|
+
}) {
|
|
128
|
+
const [data, setData] = useState<DetailPayload | null>(null);
|
|
129
|
+
const [loading, setLoading] = useState(false);
|
|
130
|
+
const [tab, setTab] = useState<TabId>("brief");
|
|
131
|
+
const [acting, setActing] = useState(false);
|
|
132
|
+
|
|
133
|
+
async function load() {
|
|
134
|
+
if (!taskId) return;
|
|
135
|
+
setLoading(true);
|
|
136
|
+
try {
|
|
137
|
+
const res = await fetch(`/api/v1/tasks/${taskId}`);
|
|
138
|
+
const json = (await res.json()) as DetailPayload & { error?: string };
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
setData(null);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
setData(json);
|
|
144
|
+
} finally {
|
|
145
|
+
setLoading(false);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (open && taskId) void load();
|
|
151
|
+
if (!open) {
|
|
152
|
+
setData(null);
|
|
153
|
+
setTab("brief");
|
|
154
|
+
}
|
|
155
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
156
|
+
}, [open, taskId]);
|
|
157
|
+
|
|
158
|
+
const task = data?.task;
|
|
159
|
+
const hermes = data?.hermes;
|
|
160
|
+
const runs = data?.runs || [];
|
|
161
|
+
const latestRun = runs[0] || null;
|
|
162
|
+
const status = task ? STATUS_META[task.status] || STATUS_META.backlog : null;
|
|
163
|
+
const exec = task ? EXECUTOR_META[task.executor_kind] : null;
|
|
164
|
+
|
|
165
|
+
// À l'ouverture : onglet le plus pertinent selon l'exécutant et l'état.
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!task) return;
|
|
168
|
+
if (task.result || hermes?.task?.result) setTab("rapport");
|
|
169
|
+
else if (task.executor_kind === "ai" && latestRun) setTab("activite");
|
|
170
|
+
else setTab("brief");
|
|
171
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
172
|
+
}, [data?.task?.id]);
|
|
173
|
+
|
|
174
|
+
const reportText = useMemo(() => {
|
|
175
|
+
if (!task) return "";
|
|
176
|
+
const parts: string[] = [];
|
|
177
|
+
if (task.result) parts.push(task.result);
|
|
178
|
+
else if (hermes?.task?.result) parts.push(hermes.task.result);
|
|
179
|
+
for (const run of hermes?.runs || []) {
|
|
180
|
+
if (run.summary) parts.push(run.summary);
|
|
181
|
+
if (run.error) parts.push(`Erreur run : ${run.error}`);
|
|
182
|
+
}
|
|
183
|
+
for (const c of hermes?.comments || []) {
|
|
184
|
+
if (c.body) parts.push(c.body);
|
|
185
|
+
}
|
|
186
|
+
if (hermes?.task?.last_failure_error) {
|
|
187
|
+
parts.push(`Dernière erreur : ${hermes.task.last_failure_error}`);
|
|
188
|
+
}
|
|
189
|
+
return parts.filter(Boolean).join("\n\n---\n\n");
|
|
190
|
+
}, [task, hermes]);
|
|
191
|
+
|
|
192
|
+
const tabs = useMemo(() => {
|
|
193
|
+
const list: Array<{ id: TabId; label: string; icon: typeof FileText }> = [
|
|
194
|
+
{ id: "brief", label: "Brief", icon: FileText },
|
|
195
|
+
];
|
|
196
|
+
if (task?.executor_kind === "ai") {
|
|
197
|
+
list.push({ id: "activite", label: "Activité IA", icon: Activity });
|
|
198
|
+
}
|
|
199
|
+
list.push({ id: "rapport", label: "Rapport", icon: Bot });
|
|
200
|
+
if (task?.executor_kind === "hermes") {
|
|
201
|
+
list.push({ id: "timeline", label: "Timeline", icon: Clock3 });
|
|
202
|
+
}
|
|
203
|
+
return list;
|
|
204
|
+
}, [task?.executor_kind]);
|
|
205
|
+
|
|
206
|
+
async function act(statusNext: string) {
|
|
207
|
+
if (!task) return;
|
|
208
|
+
setActing(true);
|
|
209
|
+
try {
|
|
210
|
+
await onStatusChange(task.id, statusNext);
|
|
211
|
+
await load();
|
|
212
|
+
} finally {
|
|
213
|
+
setActing(false);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function launchRun() {
|
|
218
|
+
if (!task) return;
|
|
219
|
+
setActing(true);
|
|
220
|
+
try {
|
|
221
|
+
const res = await fetch(`/api/v1/tasks/${task.id}/launch`, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
});
|
|
224
|
+
const json = (await res.json()) as { error?: string };
|
|
225
|
+
if (!res.ok) {
|
|
226
|
+
toast.error(json.error || "Lancement impossible");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
toast.success("Run IA en file");
|
|
230
|
+
setTab("activite");
|
|
231
|
+
await load();
|
|
232
|
+
onChanged?.();
|
|
233
|
+
} finally {
|
|
234
|
+
setActing(false);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function viewAsAi() {
|
|
239
|
+
if (!task?.assignee) return;
|
|
240
|
+
const result = await openAiWorkspaceView(
|
|
241
|
+
task.assignee.id,
|
|
242
|
+
task.assignee.username,
|
|
243
|
+
);
|
|
244
|
+
if (!result.ok) toast.error(result.error || "Workspace IA indisponible");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const activeRun = runs.find(
|
|
248
|
+
(r) => r.status === "queued" || r.status === "running",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
253
|
+
<SheetContent
|
|
254
|
+
side="right"
|
|
255
|
+
className="w-full max-w-xl sm:max-w-xl md:max-w-2xl"
|
|
256
|
+
>
|
|
257
|
+
{loading && !task ? (
|
|
258
|
+
<div className="flex flex-1 items-center justify-center gap-2 text-sm text-slate-500">
|
|
259
|
+
<Loader2 className="h-4 w-4 animate-spin" /> Chargement du détail…
|
|
260
|
+
</div>
|
|
261
|
+
) : !task ? (
|
|
262
|
+
<div className="flex flex-1 items-center justify-center text-sm text-slate-500">
|
|
263
|
+
Tâche introuvable
|
|
264
|
+
</div>
|
|
265
|
+
) : (
|
|
266
|
+
<>
|
|
267
|
+
<SheetHeader className="border-b border-slate-100 bg-[linear-gradient(180deg,#f8fafc_0%,#fff_100%)]">
|
|
268
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
269
|
+
{status ? (
|
|
270
|
+
<Badge className={cn("border-0 font-medium", status.badgeClass)}>
|
|
271
|
+
{status.label}
|
|
272
|
+
</Badge>
|
|
273
|
+
) : null}
|
|
274
|
+
{exec ? (
|
|
275
|
+
<Badge className={cn("border-0 gap-1 font-medium", exec.badgeClass)}>
|
|
276
|
+
<Bot className="h-3 w-3" />
|
|
277
|
+
{task.assignee?.username
|
|
278
|
+
? `${exec.short} · ${task.assignee.username}`
|
|
279
|
+
: exec.label}
|
|
280
|
+
</Badge>
|
|
281
|
+
) : null}
|
|
282
|
+
{task.recurring_schedule ? (
|
|
283
|
+
<Badge variant="outline" className="gap-1 text-[11px]">
|
|
284
|
+
<RefreshCw className="h-3 w-3" />
|
|
285
|
+
{task.recurring_schedule}
|
|
286
|
+
</Badge>
|
|
287
|
+
) : null}
|
|
288
|
+
<Badge variant="outline" className="text-[11px]">
|
|
289
|
+
{sourceLabel(task.source)}
|
|
290
|
+
</Badge>
|
|
291
|
+
</div>
|
|
292
|
+
<SheetTitle className="text-lg leading-snug tracking-tight">
|
|
293
|
+
{task.title}
|
|
294
|
+
</SheetTitle>
|
|
295
|
+
<SheetDescription className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
|
296
|
+
<span>Màj {formatTs(task.updated_at)}</span>
|
|
297
|
+
{task.executor_kind === "hermes" ? (
|
|
298
|
+
task.hermes_task_id ? (
|
|
299
|
+
<span className="font-mono text-slate-400">
|
|
300
|
+
{task.hermes_task_id}
|
|
301
|
+
</span>
|
|
302
|
+
) : (
|
|
303
|
+
<span className="text-amber-700">Non liée à Hermes</span>
|
|
304
|
+
)
|
|
305
|
+
) : null}
|
|
306
|
+
{activeRun ? (
|
|
307
|
+
<span className="inline-flex items-center gap-1 text-violet-700">
|
|
308
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
309
|
+
Run {activeRun.status}
|
|
310
|
+
</span>
|
|
311
|
+
) : null}
|
|
312
|
+
</SheetDescription>
|
|
313
|
+
</SheetHeader>
|
|
314
|
+
|
|
315
|
+
<div className="flex shrink-0 gap-1 border-b border-slate-100 px-3 py-2">
|
|
316
|
+
{tabs.map((t) => {
|
|
317
|
+
const Icon = t.icon;
|
|
318
|
+
const active = tab === t.id;
|
|
319
|
+
const dot =
|
|
320
|
+
(t.id === "rapport" && Boolean(reportText)) ||
|
|
321
|
+
(t.id === "activite" && Boolean(activeRun));
|
|
322
|
+
return (
|
|
323
|
+
<button
|
|
324
|
+
key={t.id}
|
|
325
|
+
type="button"
|
|
326
|
+
onClick={() => setTab(t.id)}
|
|
327
|
+
className={cn(
|
|
328
|
+
"inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
|
|
329
|
+
active
|
|
330
|
+
? "bg-slate-900 text-white"
|
|
331
|
+
: "text-slate-600 hover:bg-slate-100",
|
|
332
|
+
)}
|
|
333
|
+
>
|
|
334
|
+
<Icon className="h-3.5 w-3.5" />
|
|
335
|
+
{t.label}
|
|
336
|
+
{dot && !active ? (
|
|
337
|
+
<span className="ml-0.5 h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
|
338
|
+
) : null}
|
|
339
|
+
</button>
|
|
340
|
+
);
|
|
341
|
+
})}
|
|
342
|
+
<button
|
|
343
|
+
type="button"
|
|
344
|
+
className="ml-auto inline-flex items-center gap-1 rounded-lg px-2 py-1.5 text-xs text-slate-500 hover:bg-slate-100"
|
|
345
|
+
onClick={() => void load()}
|
|
346
|
+
disabled={loading}
|
|
347
|
+
>
|
|
348
|
+
<RefreshCw className={cn("h-3.5 w-3.5", loading && "animate-spin")} />
|
|
349
|
+
</button>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<ScrollArea className="min-h-0 flex-1 px-4 py-4">
|
|
353
|
+
{tab === "brief" ? (
|
|
354
|
+
<div className="space-y-4">
|
|
355
|
+
<section className="rounded-2xl border border-slate-200 bg-slate-50/70 p-4">
|
|
356
|
+
<div className="mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
357
|
+
<Sparkles className="h-3.5 w-3.5 text-sky-600" />
|
|
358
|
+
Brief
|
|
359
|
+
</div>
|
|
360
|
+
{(hermes?.task?.body || task.body || "").trim() ? (
|
|
361
|
+
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed text-slate-800">
|
|
362
|
+
{(hermes?.task?.body || task.body).trim()}
|
|
363
|
+
</pre>
|
|
364
|
+
) : (
|
|
365
|
+
<p className="text-sm text-slate-500">
|
|
366
|
+
Aucun brief détaillé — seul le titre est renseigné.
|
|
367
|
+
</p>
|
|
368
|
+
)}
|
|
369
|
+
</section>
|
|
370
|
+
|
|
371
|
+
<dl className="grid grid-cols-2 gap-3 text-xs">
|
|
372
|
+
<div className="rounded-xl border border-slate-100 p-3">
|
|
373
|
+
<dt className="text-slate-400">Exécutant</dt>
|
|
374
|
+
<dd className="mt-1 font-medium text-slate-800">
|
|
375
|
+
{exec?.label}
|
|
376
|
+
{task.assignee ? ` — ${task.assignee.username}` : ""}
|
|
377
|
+
</dd>
|
|
378
|
+
</div>
|
|
379
|
+
<div className="rounded-xl border border-slate-100 p-3">
|
|
380
|
+
<dt className="text-slate-400">Créée</dt>
|
|
381
|
+
<dd className="mt-1 font-medium text-slate-800">
|
|
382
|
+
{formatTs(task.created_at)}
|
|
383
|
+
</dd>
|
|
384
|
+
</div>
|
|
385
|
+
<div className="rounded-xl border border-slate-100 p-3">
|
|
386
|
+
<dt className="text-slate-400">Priorité</dt>
|
|
387
|
+
<dd className="mt-1 font-medium text-slate-800">
|
|
388
|
+
{task.priority}
|
|
389
|
+
</dd>
|
|
390
|
+
</div>
|
|
391
|
+
<div className="rounded-xl border border-slate-100 p-3">
|
|
392
|
+
<dt className="text-slate-400">Runs IA</dt>
|
|
393
|
+
<dd className="mt-1 font-medium text-slate-800">
|
|
394
|
+
{runs.length || "—"}
|
|
395
|
+
</dd>
|
|
396
|
+
</div>
|
|
397
|
+
</dl>
|
|
398
|
+
|
|
399
|
+
{(data?.subtasks?.length || 0) > 0 ? (
|
|
400
|
+
<section className="space-y-2">
|
|
401
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
402
|
+
Sous-tâches déléguées
|
|
403
|
+
</h3>
|
|
404
|
+
{data!.subtasks.map((st) => {
|
|
405
|
+
const stExec = EXECUTOR_META[st.executor_kind];
|
|
406
|
+
const stStatus = STATUS_META[st.status];
|
|
407
|
+
return (
|
|
408
|
+
<div
|
|
409
|
+
key={st.id}
|
|
410
|
+
className="flex items-center justify-between gap-2 rounded-xl border border-slate-100 px-3 py-2 text-xs"
|
|
411
|
+
>
|
|
412
|
+
<span className="min-w-0 truncate font-medium text-slate-800">
|
|
413
|
+
{st.title}
|
|
414
|
+
</span>
|
|
415
|
+
<span className="flex shrink-0 items-center gap-1.5">
|
|
416
|
+
<span
|
|
417
|
+
className={cn(
|
|
418
|
+
"rounded-md px-1.5 py-0.5 text-[10px] font-medium",
|
|
419
|
+
stExec.badgeClass,
|
|
420
|
+
)}
|
|
421
|
+
>
|
|
422
|
+
{stExec.short}
|
|
423
|
+
</span>
|
|
424
|
+
<span
|
|
425
|
+
className={cn(
|
|
426
|
+
"rounded-md px-1.5 py-0.5 text-[10px] font-medium",
|
|
427
|
+
stStatus?.badgeClass,
|
|
428
|
+
)}
|
|
429
|
+
>
|
|
430
|
+
{stStatus?.label || st.status}
|
|
431
|
+
</span>
|
|
432
|
+
</span>
|
|
433
|
+
</div>
|
|
434
|
+
);
|
|
435
|
+
})}
|
|
436
|
+
</section>
|
|
437
|
+
) : null}
|
|
438
|
+
|
|
439
|
+
{data?.hermesError ? (
|
|
440
|
+
<div className="flex gap-2 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
|
441
|
+
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
442
|
+
Détail Hermes indisponible : {data.hermesError}
|
|
443
|
+
</div>
|
|
444
|
+
) : null}
|
|
445
|
+
</div>
|
|
446
|
+
) : null}
|
|
447
|
+
|
|
448
|
+
{tab === "activite" ? (
|
|
449
|
+
<div className="space-y-4">
|
|
450
|
+
{latestRun ? (
|
|
451
|
+
<AiActivityPanel
|
|
452
|
+
runId={activeRun?.id || latestRun.id}
|
|
453
|
+
title={`Run ${(activeRun || latestRun).status}`}
|
|
454
|
+
/>
|
|
455
|
+
) : (
|
|
456
|
+
<section className="flex flex-col items-center justify-center gap-2 rounded-2xl border border-dashed border-slate-200 bg-slate-50/50 px-6 py-12 text-center">
|
|
457
|
+
<Bot className="h-8 w-8 text-slate-300" />
|
|
458
|
+
<p className="text-sm font-medium text-slate-700">
|
|
459
|
+
Aucun run IA pour l'instant
|
|
460
|
+
</p>
|
|
461
|
+
<p className="max-w-sm text-xs text-slate-500">
|
|
462
|
+
Lancez la tâche pour que le collaborateur IA travaille
|
|
463
|
+
dans son workspace.
|
|
464
|
+
</p>
|
|
465
|
+
</section>
|
|
466
|
+
)}
|
|
467
|
+
|
|
468
|
+
{runs.length > 1 ? (
|
|
469
|
+
<section className="space-y-2">
|
|
470
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
471
|
+
Historique des runs
|
|
472
|
+
</h3>
|
|
473
|
+
{runs.map((r) => (
|
|
474
|
+
<div
|
|
475
|
+
key={r.id}
|
|
476
|
+
className="flex items-center justify-between rounded-xl border border-slate-100 px-3 py-2 text-xs"
|
|
477
|
+
>
|
|
478
|
+
<span
|
|
479
|
+
className={cn(
|
|
480
|
+
"font-medium",
|
|
481
|
+
r.status === "succeeded" && "text-emerald-700",
|
|
482
|
+
r.status === "failed" && "text-rose-700",
|
|
483
|
+
(r.status === "running" || r.status === "queued") &&
|
|
484
|
+
"text-violet-700",
|
|
485
|
+
)}
|
|
486
|
+
>
|
|
487
|
+
{r.status} · {r.step_count} étapes
|
|
488
|
+
</span>
|
|
489
|
+
<span className="text-slate-400">
|
|
490
|
+
{formatTs(r.finished_at || r.started_at || r.created_at)}
|
|
491
|
+
</span>
|
|
492
|
+
</div>
|
|
493
|
+
))}
|
|
494
|
+
</section>
|
|
495
|
+
) : null}
|
|
496
|
+
</div>
|
|
497
|
+
) : null}
|
|
498
|
+
|
|
499
|
+
{tab === "rapport" ? (
|
|
500
|
+
<div className="space-y-4">
|
|
501
|
+
{reportText ? (
|
|
502
|
+
<section className="rounded-2xl border border-emerald-200 bg-gradient-to-b from-emerald-50/80 to-white p-4">
|
|
503
|
+
<div className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-emerald-800">
|
|
504
|
+
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
505
|
+
Rapport
|
|
506
|
+
</div>
|
|
507
|
+
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed text-slate-800">
|
|
508
|
+
{reportText}
|
|
509
|
+
</pre>
|
|
510
|
+
</section>
|
|
511
|
+
) : (
|
|
512
|
+
<section className="flex flex-col items-center justify-center gap-2 rounded-2xl border border-dashed border-slate-200 bg-slate-50/50 px-6 py-12 text-center">
|
|
513
|
+
<Bot className="h-8 w-8 text-slate-300" />
|
|
514
|
+
<p className="text-sm font-medium text-slate-700">
|
|
515
|
+
Pas encore de rapport
|
|
516
|
+
</p>
|
|
517
|
+
<p className="max-w-sm text-xs text-slate-500">
|
|
518
|
+
{task.executor_kind === "hermes"
|
|
519
|
+
? "Dès que Hermes termine la mission, le résultat apparaît ici."
|
|
520
|
+
: task.executor_kind === "ai"
|
|
521
|
+
? "Le rapport du collaborateur IA apparaît ici quand il clôture la tâche."
|
|
522
|
+
: "Renseignez un résultat en terminant la tâche."}
|
|
523
|
+
</p>
|
|
524
|
+
</section>
|
|
525
|
+
)}
|
|
526
|
+
|
|
527
|
+
{(hermes?.runs?.length || 0) > 0 ? (
|
|
528
|
+
<section className="space-y-2">
|
|
529
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
530
|
+
Runs Hermes
|
|
531
|
+
</h3>
|
|
532
|
+
{hermes!.runs.map((run, i) => (
|
|
533
|
+
<div
|
|
534
|
+
key={run.id || i}
|
|
535
|
+
className="rounded-xl border border-slate-100 px-3 py-2 text-xs"
|
|
536
|
+
>
|
|
537
|
+
<div className="flex items-center justify-between gap-2">
|
|
538
|
+
<span className="font-medium text-slate-800">
|
|
539
|
+
{run.status || "run"}
|
|
540
|
+
</span>
|
|
541
|
+
<span className="text-slate-400">
|
|
542
|
+
{formatTs(run.finished_at || run.started_at)}
|
|
543
|
+
</span>
|
|
544
|
+
</div>
|
|
545
|
+
{run.summary ? (
|
|
546
|
+
<p className="mt-1 text-slate-600">{run.summary}</p>
|
|
547
|
+
) : null}
|
|
548
|
+
{run.error ? (
|
|
549
|
+
<p className="mt-1 text-rose-600">{run.error}</p>
|
|
550
|
+
) : null}
|
|
551
|
+
</div>
|
|
552
|
+
))}
|
|
553
|
+
</section>
|
|
554
|
+
) : null}
|
|
555
|
+
</div>
|
|
556
|
+
) : null}
|
|
557
|
+
|
|
558
|
+
{tab === "timeline" ? (
|
|
559
|
+
<div className="space-y-0">
|
|
560
|
+
{(hermes?.events?.length || 0) === 0 ? (
|
|
561
|
+
<p className="text-sm text-slate-500">
|
|
562
|
+
Aucun événement Hermes pour l'instant.
|
|
563
|
+
</p>
|
|
564
|
+
) : (
|
|
565
|
+
<ol className="relative space-y-0 border-l border-slate-200 pl-4">
|
|
566
|
+
{[...(hermes?.events || [])]
|
|
567
|
+
.slice()
|
|
568
|
+
.reverse()
|
|
569
|
+
.map((ev, i) => (
|
|
570
|
+
<li key={ev.id ?? i} className="relative pb-4">
|
|
571
|
+
<span className="absolute -left-[21px] top-1 h-2.5 w-2.5 rounded-full border-2 border-white bg-sky-500 shadow" />
|
|
572
|
+
<p className="text-xs font-medium text-slate-800">
|
|
573
|
+
{eventLabel(ev.kind)}
|
|
574
|
+
</p>
|
|
575
|
+
<p className="text-[11px] text-slate-400">
|
|
576
|
+
{formatTs(ev.created_at)}
|
|
577
|
+
</p>
|
|
578
|
+
{ev.payload && Object.keys(ev.payload).length ? (
|
|
579
|
+
<pre className="mt-1 max-h-28 overflow-auto rounded-lg bg-slate-50 p-2 font-mono text-[10px] text-slate-600">
|
|
580
|
+
{JSON.stringify(ev.payload, null, 2)}
|
|
581
|
+
</pre>
|
|
582
|
+
) : null}
|
|
583
|
+
</li>
|
|
584
|
+
))}
|
|
585
|
+
</ol>
|
|
586
|
+
)}
|
|
587
|
+
</div>
|
|
588
|
+
) : null}
|
|
589
|
+
</ScrollArea>
|
|
590
|
+
|
|
591
|
+
<div className="flex shrink-0 flex-wrap gap-2 border-t border-slate-100 bg-white px-4 py-3">
|
|
592
|
+
{task.executor_kind === "ai" ? (
|
|
593
|
+
<>
|
|
594
|
+
{!activeRun ? (
|
|
595
|
+
<Button size="sm" disabled={acting} onClick={() => void launchRun()}>
|
|
596
|
+
<Play className="mr-1.5 h-3.5 w-3.5" />
|
|
597
|
+
{runs.length ? "Relancer" : "Lancer"}
|
|
598
|
+
</Button>
|
|
599
|
+
) : null}
|
|
600
|
+
{task.assignee ? (
|
|
601
|
+
<Button
|
|
602
|
+
size="sm"
|
|
603
|
+
variant="outline"
|
|
604
|
+
disabled={acting}
|
|
605
|
+
onClick={() => void viewAsAi()}
|
|
606
|
+
>
|
|
607
|
+
<Eye className="mr-1.5 h-3.5 w-3.5" />
|
|
608
|
+
Voir comme IA
|
|
609
|
+
</Button>
|
|
610
|
+
) : null}
|
|
611
|
+
</>
|
|
612
|
+
) : null}
|
|
613
|
+
{task.executor_kind === "hermes" &&
|
|
614
|
+
task.status !== "in_progress" &&
|
|
615
|
+
task.status !== "done" ? (
|
|
616
|
+
<Button
|
|
617
|
+
size="sm"
|
|
618
|
+
variant="outline"
|
|
619
|
+
disabled={acting}
|
|
620
|
+
onClick={() => void act("in_progress")}
|
|
621
|
+
>
|
|
622
|
+
<Play className="mr-1.5 h-3.5 w-3.5" />
|
|
623
|
+
Envoyer à Hermes
|
|
624
|
+
</Button>
|
|
625
|
+
) : null}
|
|
626
|
+
{task.executor_kind === "human" && task.status === "backlog" ? (
|
|
627
|
+
<Button
|
|
628
|
+
size="sm"
|
|
629
|
+
variant="outline"
|
|
630
|
+
disabled={acting}
|
|
631
|
+
onClick={() => void act("in_progress")}
|
|
632
|
+
>
|
|
633
|
+
<Play className="mr-1.5 h-3.5 w-3.5" />
|
|
634
|
+
Commencer
|
|
635
|
+
</Button>
|
|
636
|
+
) : null}
|
|
637
|
+
{task.status !== "done" ? (
|
|
638
|
+
<Button size="sm" disabled={acting} onClick={() => void act("done")}>
|
|
639
|
+
<CheckCircle2 className="mr-1.5 h-3.5 w-3.5" />
|
|
640
|
+
Marquer terminée
|
|
641
|
+
</Button>
|
|
642
|
+
) : (
|
|
643
|
+
<Button
|
|
644
|
+
size="sm"
|
|
645
|
+
variant="outline"
|
|
646
|
+
disabled={acting}
|
|
647
|
+
onClick={() => void act("backlog")}
|
|
648
|
+
>
|
|
649
|
+
Rouvrir
|
|
650
|
+
</Button>
|
|
651
|
+
)}
|
|
652
|
+
{task.status !== "blocked" && task.status !== "done" ? (
|
|
653
|
+
<Button
|
|
654
|
+
size="sm"
|
|
655
|
+
variant="ghost"
|
|
656
|
+
disabled={acting}
|
|
657
|
+
onClick={() => void act("blocked")}
|
|
658
|
+
>
|
|
659
|
+
Bloquer
|
|
660
|
+
</Button>
|
|
661
|
+
) : null}
|
|
662
|
+
</div>
|
|
663
|
+
</>
|
|
664
|
+
)}
|
|
665
|
+
</SheetContent>
|
|
666
|
+
</Sheet>
|
|
667
|
+
);
|
|
668
|
+
}
|