stagent 0.6.2 → 0.6.3
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/dist/cli.js +47 -1
- package/package.json +1 -2
- package/src/app/api/documents/[id]/route.ts +5 -1
- package/src/app/api/documents/[id]/versions/route.ts +53 -0
- package/src/app/api/documents/route.ts +5 -1
- package/src/app/api/projects/[id]/documents/route.ts +124 -0
- package/src/app/api/projects/[id]/route.ts +35 -3
- package/src/app/api/projects/__tests__/delete-project.test.ts +1 -0
- package/src/app/api/schedules/route.ts +19 -1
- package/src/app/api/tasks/[id]/route.ts +37 -2
- package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
- package/src/app/api/tasks/route.ts +8 -9
- package/src/app/api/workflows/[id]/documents/route.ts +209 -0
- package/src/app/api/workflows/[id]/execute/route.ts +6 -2
- package/src/app/api/workflows/[id]/route.ts +16 -3
- package/src/app/api/workflows/[id]/status/route.ts +18 -2
- package/src/app/api/workflows/route.ts +13 -2
- package/src/app/documents/page.tsx +5 -1
- package/src/app/layout.tsx +0 -1
- package/src/app/manifest.ts +3 -3
- package/src/app/projects/[id]/page.tsx +62 -2
- package/src/components/documents/document-chip-bar.tsx +17 -1
- package/src/components/documents/document-detail-view.tsx +51 -0
- package/src/components/documents/document-grid.tsx +5 -0
- package/src/components/documents/document-table.tsx +4 -0
- package/src/components/documents/types.ts +3 -0
- package/src/components/projects/project-form-sheet.tsx +133 -2
- package/src/components/schedules/schedule-form.tsx +113 -1
- package/src/components/shared/document-picker-sheet.tsx +283 -0
- package/src/components/tasks/task-card.tsx +8 -1
- package/src/components/tasks/task-create-panel.tsx +137 -14
- package/src/components/tasks/task-detail-view.tsx +47 -0
- package/src/components/tasks/task-edit-dialog.tsx +125 -2
- package/src/components/workflows/workflow-form-view.tsx +231 -7
- package/src/components/workflows/workflow-kanban-card.tsx +8 -1
- package/src/components/workflows/workflow-list.tsx +90 -45
- package/src/components/workflows/workflow-status-view.tsx +167 -22
- package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
- package/src/lib/agents/profiles/registry.ts +6 -3
- package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
- package/src/lib/book/chapter-generator.ts +4 -19
- package/src/lib/book/chapter-mapping.ts +17 -0
- package/src/lib/book/content.ts +5 -16
- package/src/lib/book/update-detector.ts +3 -16
- package/src/lib/chat/engine.ts +1 -0
- package/src/lib/chat/system-prompt.ts +9 -1
- package/src/lib/chat/tool-catalog.ts +1 -0
- package/src/lib/chat/tools/settings-tools.ts +109 -0
- package/src/lib/chat/tools/workflow-tools.ts +145 -2
- package/src/lib/data/clear.ts +12 -0
- package/src/lib/db/bootstrap.ts +48 -0
- package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
- package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
- package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
- package/src/lib/db/schema.ts +77 -0
- package/src/lib/docs/reader.ts +2 -3
- package/src/lib/documents/context-builder.ts +75 -2
- package/src/lib/documents/document-resolver.ts +119 -0
- package/src/lib/documents/processors/spreadsheet.ts +2 -1
- package/src/lib/schedules/scheduler.ts +31 -1
- package/src/lib/utils/app-root.ts +20 -0
- package/src/lib/validators/__tests__/task.test.ts +43 -10
- package/src/lib/validators/task.ts +7 -1
- package/src/lib/workflows/blueprints/registry.ts +3 -3
- package/src/lib/workflows/engine.ts +24 -8
- package/src/lib/workflows/types.ts +14 -0
- package/public/icon.svg +0 -13
- package/src/components/tasks/file-upload.tsx +0 -120
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from "react";
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
4
|
import {
|
|
5
5
|
Sheet,
|
|
6
6
|
SheetContent,
|
|
@@ -19,9 +19,12 @@ import {
|
|
|
19
19
|
SelectTrigger,
|
|
20
20
|
SelectValue,
|
|
21
21
|
} from "@/components/ui/select";
|
|
22
|
-
import { FolderOpen, AlignLeft, FolderCode, Trash2 } from "lucide-react";
|
|
22
|
+
import { FolderOpen, AlignLeft, FolderCode, Trash2, Paperclip, Plus, X } from "lucide-react";
|
|
23
23
|
import { toast } from "sonner";
|
|
24
|
+
import { Badge } from "@/components/ui/badge";
|
|
24
25
|
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
|
26
|
+
import { DocumentPickerSheet } from "@/components/shared/document-picker-sheet";
|
|
27
|
+
import { getFileIcon, formatSize } from "@/components/documents/utils";
|
|
25
28
|
|
|
26
29
|
interface Project {
|
|
27
30
|
id: string;
|
|
@@ -53,6 +56,9 @@ export function ProjectFormSheet({
|
|
|
53
56
|
const [loading, setLoading] = useState(false);
|
|
54
57
|
const [error, setError] = useState<string | null>(null);
|
|
55
58
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
59
|
+
const [selectedDocIds, setSelectedDocIds] = useState<Set<string>>(new Set());
|
|
60
|
+
const [selectedDocs, setSelectedDocs] = useState<Array<{ id: string; originalName: string; mimeType: string; size: number }>>([]);
|
|
61
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
56
62
|
|
|
57
63
|
// Pre-fill form in edit mode
|
|
58
64
|
useEffect(() => {
|
|
@@ -61,15 +67,68 @@ export function ProjectFormSheet({
|
|
|
61
67
|
setDescription(project.description ?? "");
|
|
62
68
|
setWorkingDirectory(project.workingDirectory ?? "");
|
|
63
69
|
setStatus(project.status);
|
|
70
|
+
// Load existing default documents
|
|
71
|
+
fetch(`/api/projects/${project.id}/documents`)
|
|
72
|
+
.then((r) => r.json())
|
|
73
|
+
.then((docs: Array<Record<string, unknown>>) => {
|
|
74
|
+
const ids = new Set(docs.map((d) => d.id as string));
|
|
75
|
+
setSelectedDocIds(ids);
|
|
76
|
+
setSelectedDocs(
|
|
77
|
+
docs.map((d) => ({
|
|
78
|
+
id: d.id as string,
|
|
79
|
+
originalName: d.originalName as string,
|
|
80
|
+
mimeType: d.mimeType as string,
|
|
81
|
+
size: d.size as number,
|
|
82
|
+
}))
|
|
83
|
+
);
|
|
84
|
+
})
|
|
85
|
+
.catch(() => {
|
|
86
|
+
setSelectedDocIds(new Set());
|
|
87
|
+
setSelectedDocs([]);
|
|
88
|
+
});
|
|
64
89
|
} else if (mode === "create") {
|
|
65
90
|
setName("");
|
|
66
91
|
setDescription("");
|
|
67
92
|
setWorkingDirectory("");
|
|
68
93
|
setStatus("active");
|
|
94
|
+
setSelectedDocIds(new Set());
|
|
95
|
+
setSelectedDocs([]);
|
|
69
96
|
}
|
|
70
97
|
setError(null);
|
|
71
98
|
}, [mode, project, open]);
|
|
72
99
|
|
|
100
|
+
const handleDocPickerConfirm = useCallback(
|
|
101
|
+
(ids: string[]) => {
|
|
102
|
+
setSelectedDocIds(new Set(ids));
|
|
103
|
+
const newIds = ids.filter(
|
|
104
|
+
(id) => !selectedDocs.some((d) => d.id === id)
|
|
105
|
+
);
|
|
106
|
+
if (newIds.length > 0) {
|
|
107
|
+
const params = new URLSearchParams({ status: "ready" });
|
|
108
|
+
if (project?.id) params.set("projectId", project.id);
|
|
109
|
+
fetch(`/api/documents?${params}`)
|
|
110
|
+
.then((r) => r.json())
|
|
111
|
+
.then((allDocs: Array<Record<string, unknown>>) => {
|
|
112
|
+
const idSet = new Set(ids);
|
|
113
|
+
setSelectedDocs(
|
|
114
|
+
allDocs
|
|
115
|
+
.filter((d) => idSet.has(d.id as string))
|
|
116
|
+
.map((d) => ({
|
|
117
|
+
id: d.id as string,
|
|
118
|
+
originalName: d.originalName as string,
|
|
119
|
+
mimeType: d.mimeType as string,
|
|
120
|
+
size: d.size as number,
|
|
121
|
+
}))
|
|
122
|
+
);
|
|
123
|
+
})
|
|
124
|
+
.catch(() => {});
|
|
125
|
+
} else {
|
|
126
|
+
setSelectedDocs((prev) => prev.filter((d) => ids.includes(d.id)));
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[project?.id, selectedDocs]
|
|
130
|
+
);
|
|
131
|
+
|
|
73
132
|
async function handleSubmit(e: React.FormEvent) {
|
|
74
133
|
e.preventDefault();
|
|
75
134
|
if (!name.trim()) return;
|
|
@@ -85,6 +144,7 @@ export function ProjectFormSheet({
|
|
|
85
144
|
name: name.trim(),
|
|
86
145
|
description: description.trim() || undefined,
|
|
87
146
|
workingDirectory: workingDirectory.trim() || undefined,
|
|
147
|
+
documentIds: selectedDocIds.size > 0 ? [...selectedDocIds] : undefined,
|
|
88
148
|
}),
|
|
89
149
|
});
|
|
90
150
|
if (res.ok) {
|
|
@@ -104,6 +164,7 @@ export function ProjectFormSheet({
|
|
|
104
164
|
description: description.trim() || undefined,
|
|
105
165
|
workingDirectory: workingDirectory.trim() || undefined,
|
|
106
166
|
status,
|
|
167
|
+
documentIds: [...selectedDocIds],
|
|
107
168
|
}),
|
|
108
169
|
});
|
|
109
170
|
if (res.ok) {
|
|
@@ -236,6 +297,76 @@ export function ProjectFormSheet({
|
|
|
236
297
|
</div>
|
|
237
298
|
)}
|
|
238
299
|
|
|
300
|
+
{/* Default Documents */}
|
|
301
|
+
{(isEdit || selectedDocs.length > 0) && (
|
|
302
|
+
<div className="space-y-2">
|
|
303
|
+
<Label className="flex items-center gap-1.5">
|
|
304
|
+
<Paperclip className="h-3.5 w-3.5 text-muted-foreground" />
|
|
305
|
+
Default Documents
|
|
306
|
+
</Label>
|
|
307
|
+
<p className="text-xs text-muted-foreground">
|
|
308
|
+
Auto-attached to new tasks and workflows in this project
|
|
309
|
+
</p>
|
|
310
|
+
{selectedDocs.length > 0 && (
|
|
311
|
+
<div className="flex flex-wrap gap-2">
|
|
312
|
+
{selectedDocs.map((doc) => {
|
|
313
|
+
const Icon = getFileIcon(doc.mimeType);
|
|
314
|
+
return (
|
|
315
|
+
<Badge
|
|
316
|
+
key={doc.id}
|
|
317
|
+
variant="secondary"
|
|
318
|
+
className="flex items-center gap-1.5 pl-2 pr-1 py-1"
|
|
319
|
+
>
|
|
320
|
+
<Icon className="h-3 w-3" />
|
|
321
|
+
<span className="text-xs max-w-[140px] truncate">
|
|
322
|
+
{doc.originalName}
|
|
323
|
+
</span>
|
|
324
|
+
<span className="text-[10px] text-muted-foreground">
|
|
325
|
+
{formatSize(doc.size)}
|
|
326
|
+
</span>
|
|
327
|
+
<button
|
|
328
|
+
type="button"
|
|
329
|
+
onClick={() => {
|
|
330
|
+
setSelectedDocIds((prev) => {
|
|
331
|
+
const next = new Set(prev);
|
|
332
|
+
next.delete(doc.id);
|
|
333
|
+
return next;
|
|
334
|
+
});
|
|
335
|
+
setSelectedDocs((prev) => prev.filter((d) => d.id !== doc.id));
|
|
336
|
+
}}
|
|
337
|
+
className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
|
|
338
|
+
aria-label={`Remove ${doc.originalName}`}
|
|
339
|
+
>
|
|
340
|
+
<X className="h-3 w-3" />
|
|
341
|
+
</button>
|
|
342
|
+
</Badge>
|
|
343
|
+
);
|
|
344
|
+
})}
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
<Button
|
|
348
|
+
type="button"
|
|
349
|
+
variant="outline"
|
|
350
|
+
size="sm"
|
|
351
|
+
onClick={() => setPickerOpen(true)}
|
|
352
|
+
className="gap-1.5"
|
|
353
|
+
>
|
|
354
|
+
<Plus className="h-3.5 w-3.5" />
|
|
355
|
+
{selectedDocs.length > 0 ? "Add More" : "Select Documents"}
|
|
356
|
+
</Button>
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
<DocumentPickerSheet
|
|
361
|
+
open={pickerOpen}
|
|
362
|
+
onOpenChange={setPickerOpen}
|
|
363
|
+
projectId={project?.id ?? null}
|
|
364
|
+
selectedIds={selectedDocIds}
|
|
365
|
+
onConfirm={handleDocPickerConfirm}
|
|
366
|
+
groupBy="source"
|
|
367
|
+
title="Select Default Documents"
|
|
368
|
+
/>
|
|
369
|
+
|
|
239
370
|
{error && (
|
|
240
371
|
<p className="text-sm text-destructive">{error}</p>
|
|
241
372
|
)}
|
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
SelectValue,
|
|
14
14
|
} from "@/components/ui/select";
|
|
15
15
|
import { Switch } from "@/components/ui/switch";
|
|
16
|
-
import { Clock, Bot, Heart, Plus, X, GripVertical, Sparkles, CheckCircle2, AlertCircle } from "lucide-react";
|
|
16
|
+
import { Clock, Bot, Heart, Plus, X, GripVertical, Sparkles, CheckCircle2, AlertCircle, Paperclip } from "lucide-react";
|
|
17
|
+
import { Badge } from "@/components/ui/badge";
|
|
18
|
+
import { DocumentPickerSheet } from "@/components/shared/document-picker-sheet";
|
|
19
|
+
import { getFileIcon, formatSize } from "@/components/documents/utils";
|
|
17
20
|
import {
|
|
18
21
|
type AgentRuntimeId,
|
|
19
22
|
DEFAULT_AGENT_RUNTIME,
|
|
@@ -59,6 +62,7 @@ export interface ScheduleFormValues {
|
|
|
59
62
|
activeHoursEnd: number | "";
|
|
60
63
|
activeTimezone: string;
|
|
61
64
|
heartbeatBudgetPerDay: number | "";
|
|
65
|
+
documentIds: string[];
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
export interface ScheduleFormInitialValues {
|
|
@@ -201,6 +205,43 @@ export function ScheduleForm({
|
|
|
201
205
|
const [activeTimezone, setActiveTimezone] = useState("UTC");
|
|
202
206
|
const [heartbeatBudgetPerDay, setHeartbeatBudgetPerDay] = useState<number | "">("");
|
|
203
207
|
|
|
208
|
+
// Document picker state
|
|
209
|
+
const [selectedDocIds, setSelectedDocIds] = useState<Set<string>>(new Set());
|
|
210
|
+
const [selectedDocs, setSelectedDocs] = useState<Array<{ id: string; originalName: string; mimeType: string; size: number }>>([]);
|
|
211
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
212
|
+
|
|
213
|
+
const handleDocPickerConfirm = useCallback(
|
|
214
|
+
(ids: string[]) => {
|
|
215
|
+
setSelectedDocIds(new Set(ids));
|
|
216
|
+
const newIds = ids.filter(
|
|
217
|
+
(id) => !selectedDocs.some((d) => d.id === id)
|
|
218
|
+
);
|
|
219
|
+
if (newIds.length > 0) {
|
|
220
|
+
const params = new URLSearchParams({ status: "ready" });
|
|
221
|
+
if (projectId) params.set("projectId", projectId);
|
|
222
|
+
fetch(`/api/documents?${params}`)
|
|
223
|
+
.then((r) => r.json())
|
|
224
|
+
.then((allDocs: Array<Record<string, unknown>>) => {
|
|
225
|
+
const idSet = new Set(ids);
|
|
226
|
+
setSelectedDocs(
|
|
227
|
+
allDocs
|
|
228
|
+
.filter((d) => idSet.has(d.id as string))
|
|
229
|
+
.map((d) => ({
|
|
230
|
+
id: d.id as string,
|
|
231
|
+
originalName: d.originalName as string,
|
|
232
|
+
mimeType: d.mimeType as string,
|
|
233
|
+
size: d.size as number,
|
|
234
|
+
}))
|
|
235
|
+
);
|
|
236
|
+
})
|
|
237
|
+
.catch(() => {});
|
|
238
|
+
} else {
|
|
239
|
+
setSelectedDocs((prev) => prev.filter((d) => ids.includes(d.id)));
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
[projectId, selectedDocs]
|
|
243
|
+
);
|
|
244
|
+
|
|
204
245
|
useEffect(() => {
|
|
205
246
|
fetch("/api/profiles")
|
|
206
247
|
.then((r) => r.json())
|
|
@@ -261,6 +302,7 @@ export function ScheduleForm({
|
|
|
261
302
|
activeHoursEnd,
|
|
262
303
|
activeTimezone,
|
|
263
304
|
heartbeatBudgetPerDay,
|
|
305
|
+
documentIds: [...selectedDocIds],
|
|
264
306
|
});
|
|
265
307
|
}
|
|
266
308
|
|
|
@@ -648,6 +690,76 @@ export function ScheduleForm({
|
|
|
648
690
|
</div>
|
|
649
691
|
)}
|
|
650
692
|
|
|
693
|
+
{/* Context Documents */}
|
|
694
|
+
<div className="space-y-2">
|
|
695
|
+
<Label className="flex items-center gap-1.5">
|
|
696
|
+
<Paperclip className="h-3.5 w-3.5 text-muted-foreground" />
|
|
697
|
+
Context Documents
|
|
698
|
+
</Label>
|
|
699
|
+
{selectedDocs.length > 0 && (
|
|
700
|
+
<div className="flex flex-wrap gap-2">
|
|
701
|
+
{selectedDocs.map((doc) => {
|
|
702
|
+
const Icon = getFileIcon(doc.mimeType);
|
|
703
|
+
return (
|
|
704
|
+
<Badge
|
|
705
|
+
key={doc.id}
|
|
706
|
+
variant="secondary"
|
|
707
|
+
className="flex items-center gap-1.5 pl-2 pr-1 py-1"
|
|
708
|
+
>
|
|
709
|
+
<Icon className="h-3 w-3" />
|
|
710
|
+
<span className="text-xs max-w-[140px] truncate">
|
|
711
|
+
{doc.originalName}
|
|
712
|
+
</span>
|
|
713
|
+
<span className="text-[10px] text-muted-foreground">
|
|
714
|
+
{formatSize(doc.size)}
|
|
715
|
+
</span>
|
|
716
|
+
<button
|
|
717
|
+
type="button"
|
|
718
|
+
onClick={() => {
|
|
719
|
+
setSelectedDocIds((prev) => {
|
|
720
|
+
const next = new Set(prev);
|
|
721
|
+
next.delete(doc.id);
|
|
722
|
+
return next;
|
|
723
|
+
});
|
|
724
|
+
setSelectedDocs((prev) => prev.filter((d) => d.id !== doc.id));
|
|
725
|
+
}}
|
|
726
|
+
className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
|
|
727
|
+
aria-label={`Remove ${doc.originalName}`}
|
|
728
|
+
>
|
|
729
|
+
<X className="h-3 w-3" />
|
|
730
|
+
</button>
|
|
731
|
+
</Badge>
|
|
732
|
+
);
|
|
733
|
+
})}
|
|
734
|
+
</div>
|
|
735
|
+
)}
|
|
736
|
+
<Button
|
|
737
|
+
type="button"
|
|
738
|
+
variant="outline"
|
|
739
|
+
size="sm"
|
|
740
|
+
onClick={() => setPickerOpen(true)}
|
|
741
|
+
className="gap-1.5"
|
|
742
|
+
>
|
|
743
|
+
<Plus className="h-3.5 w-3.5" />
|
|
744
|
+
{selectedDocs.length > 0 ? "Add More" : "Select Documents"}
|
|
745
|
+
</Button>
|
|
746
|
+
{selectedDocs.length > 0 && (
|
|
747
|
+
<p className="text-xs text-muted-foreground">
|
|
748
|
+
{selectedDocs.length} document{selectedDocs.length !== 1 ? "s" : ""} will be provided as context for each firing
|
|
749
|
+
</p>
|
|
750
|
+
)}
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<DocumentPickerSheet
|
|
754
|
+
open={pickerOpen}
|
|
755
|
+
onOpenChange={setPickerOpen}
|
|
756
|
+
projectId={projectId || null}
|
|
757
|
+
selectedIds={selectedDocIds}
|
|
758
|
+
onConfirm={handleDocPickerConfirm}
|
|
759
|
+
groupBy="source"
|
|
760
|
+
title="Select Context Documents"
|
|
761
|
+
/>
|
|
762
|
+
|
|
651
763
|
{/* Runtime */}
|
|
652
764
|
<div className="space-y-2">
|
|
653
765
|
<Label className="flex items-center gap-1.5">
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Sheet,
|
|
6
|
+
SheetContent,
|
|
7
|
+
SheetHeader,
|
|
8
|
+
SheetTitle,
|
|
9
|
+
SheetDescription,
|
|
10
|
+
} from "@/components/ui/sheet";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { Input } from "@/components/ui/input";
|
|
13
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
14
|
+
import { Badge } from "@/components/ui/badge";
|
|
15
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
16
|
+
import { FileText, Search, Loader2 } from "lucide-react";
|
|
17
|
+
import { getFileIcon, formatSize, getStatusDotColor } from "@/components/documents/utils";
|
|
18
|
+
|
|
19
|
+
export interface PickerDocument {
|
|
20
|
+
id: string;
|
|
21
|
+
originalName: string;
|
|
22
|
+
mimeType: string;
|
|
23
|
+
size: number;
|
|
24
|
+
direction: string;
|
|
25
|
+
status: string;
|
|
26
|
+
category: string | null;
|
|
27
|
+
taskTitle: string | null;
|
|
28
|
+
projectName: string | null;
|
|
29
|
+
/** Source workflow name (if document was produced by a workflow task) */
|
|
30
|
+
sourceWorkflow?: string;
|
|
31
|
+
/** Parent workflow name (joined from tasks → workflows) */
|
|
32
|
+
workflowName?: string | null;
|
|
33
|
+
createdAt: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DocumentPickerSheetProps {
|
|
37
|
+
open: boolean;
|
|
38
|
+
onOpenChange: (open: boolean) => void;
|
|
39
|
+
/** Scope documents to a project. Null = show all ready documents. */
|
|
40
|
+
projectId: string | null;
|
|
41
|
+
/** Currently selected document IDs (to pre-check) */
|
|
42
|
+
selectedIds: Set<string>;
|
|
43
|
+
/** Called when user confirms selection */
|
|
44
|
+
onConfirm: (selectedIds: string[]) => void;
|
|
45
|
+
/** Optional: scope to a step ID label */
|
|
46
|
+
stepLabel?: string;
|
|
47
|
+
/** Grouping mode: "workflow" groups by source workflow, "project" by project name, "source" by direction */
|
|
48
|
+
groupBy?: "workflow" | "project" | "source";
|
|
49
|
+
/** Override the sheet title */
|
|
50
|
+
title?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function DocumentPickerSheet({
|
|
54
|
+
open,
|
|
55
|
+
onOpenChange,
|
|
56
|
+
projectId,
|
|
57
|
+
selectedIds,
|
|
58
|
+
onConfirm,
|
|
59
|
+
stepLabel,
|
|
60
|
+
groupBy = "source",
|
|
61
|
+
title,
|
|
62
|
+
}: DocumentPickerSheetProps) {
|
|
63
|
+
const [documents, setDocuments] = useState<PickerDocument[]>([]);
|
|
64
|
+
const [loading, setLoading] = useState(false);
|
|
65
|
+
const [search, setSearch] = useState("");
|
|
66
|
+
const [localSelected, setLocalSelected] = useState<Set<string>>(new Set());
|
|
67
|
+
|
|
68
|
+
// Sync initial selection when sheet opens
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (open) {
|
|
71
|
+
setLocalSelected(new Set(selectedIds));
|
|
72
|
+
fetchDocuments();
|
|
73
|
+
}
|
|
74
|
+
}, [open, projectId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
75
|
+
|
|
76
|
+
async function fetchDocuments() {
|
|
77
|
+
setLoading(true);
|
|
78
|
+
try {
|
|
79
|
+
const params = new URLSearchParams({ status: "ready" });
|
|
80
|
+
if (projectId) params.set("projectId", projectId);
|
|
81
|
+
const res = await fetch(`/api/documents?${params}`);
|
|
82
|
+
if (res.ok) {
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
setDocuments(data);
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Silently fail — empty list shown
|
|
88
|
+
} finally {
|
|
89
|
+
setLoading(false);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const filtered = useMemo(() => {
|
|
94
|
+
if (!search.trim()) return documents;
|
|
95
|
+
const q = search.toLowerCase();
|
|
96
|
+
return documents.filter(
|
|
97
|
+
(doc) =>
|
|
98
|
+
doc.originalName.toLowerCase().includes(q) ||
|
|
99
|
+
doc.category?.toLowerCase().includes(q) ||
|
|
100
|
+
doc.taskTitle?.toLowerCase().includes(q)
|
|
101
|
+
);
|
|
102
|
+
}, [documents, search]);
|
|
103
|
+
|
|
104
|
+
// Group documents based on the groupBy mode
|
|
105
|
+
const grouped = useMemo(() => {
|
|
106
|
+
const groups: Record<string, PickerDocument[]> = {};
|
|
107
|
+
for (const doc of filtered) {
|
|
108
|
+
let key: string;
|
|
109
|
+
switch (groupBy) {
|
|
110
|
+
case "workflow":
|
|
111
|
+
key =
|
|
112
|
+
doc.direction === "output"
|
|
113
|
+
? doc.workflowName
|
|
114
|
+
? `From: ${doc.workflowName}`
|
|
115
|
+
: doc.taskTitle
|
|
116
|
+
? `From: ${doc.taskTitle}`
|
|
117
|
+
: "Agent Generated"
|
|
118
|
+
: "Uploaded";
|
|
119
|
+
break;
|
|
120
|
+
case "project":
|
|
121
|
+
key = doc.projectName ?? "No Project";
|
|
122
|
+
break;
|
|
123
|
+
case "source":
|
|
124
|
+
default:
|
|
125
|
+
key =
|
|
126
|
+
doc.direction === "output"
|
|
127
|
+
? doc.taskTitle
|
|
128
|
+
? "Task Output"
|
|
129
|
+
: "Agent Generated"
|
|
130
|
+
: "Uploaded";
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
if (!groups[key]) groups[key] = [];
|
|
134
|
+
groups[key].push(doc);
|
|
135
|
+
}
|
|
136
|
+
return groups;
|
|
137
|
+
}, [filtered, groupBy]);
|
|
138
|
+
|
|
139
|
+
function toggleDocument(id: string) {
|
|
140
|
+
setLocalSelected((prev) => {
|
|
141
|
+
const next = new Set(prev);
|
|
142
|
+
if (next.has(id)) {
|
|
143
|
+
next.delete(id);
|
|
144
|
+
} else {
|
|
145
|
+
next.add(id);
|
|
146
|
+
}
|
|
147
|
+
return next;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function handleConfirm() {
|
|
152
|
+
onConfirm([...localSelected]);
|
|
153
|
+
onOpenChange(false);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const sheetTitle = title
|
|
157
|
+
? title
|
|
158
|
+
: stepLabel
|
|
159
|
+
? `Select Documents for "${stepLabel}"`
|
|
160
|
+
: "Select Input Documents";
|
|
161
|
+
|
|
162
|
+
const emptyMessage = search
|
|
163
|
+
? "No documents match your search."
|
|
164
|
+
: projectId
|
|
165
|
+
? "No documents available in this project."
|
|
166
|
+
: "No documents available. Upload files in the Documents view.";
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
170
|
+
<SheetContent side="right" className="w-full sm:max-w-lg">
|
|
171
|
+
<SheetHeader className="p-4">
|
|
172
|
+
<SheetTitle className="flex items-center gap-2">
|
|
173
|
+
<FileText className="h-4 w-4" />
|
|
174
|
+
{sheetTitle}
|
|
175
|
+
</SheetTitle>
|
|
176
|
+
<SheetDescription>
|
|
177
|
+
Choose documents to provide as context.
|
|
178
|
+
</SheetDescription>
|
|
179
|
+
</SheetHeader>
|
|
180
|
+
|
|
181
|
+
<div className="px-6 pb-6 flex flex-col gap-4 flex-1 min-h-0">
|
|
182
|
+
{/* Search */}
|
|
183
|
+
<div className="relative">
|
|
184
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
185
|
+
<Input
|
|
186
|
+
placeholder="Search documents..."
|
|
187
|
+
value={search}
|
|
188
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
189
|
+
className="pl-9"
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Document list */}
|
|
194
|
+
<ScrollArea className="flex-1 min-h-0 -mx-2">
|
|
195
|
+
{loading ? (
|
|
196
|
+
<div className="flex items-center justify-center py-12">
|
|
197
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
198
|
+
</div>
|
|
199
|
+
) : Object.keys(grouped).length === 0 ? (
|
|
200
|
+
<div className="text-center text-sm text-muted-foreground py-12">
|
|
201
|
+
{emptyMessage}
|
|
202
|
+
</div>
|
|
203
|
+
) : (
|
|
204
|
+
<div className="space-y-4 px-2">
|
|
205
|
+
{Object.entries(grouped).map(([source, docs]) => (
|
|
206
|
+
<div key={source}>
|
|
207
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">
|
|
208
|
+
{source}
|
|
209
|
+
</p>
|
|
210
|
+
<div className="space-y-1">
|
|
211
|
+
{docs.map((doc) => {
|
|
212
|
+
const Icon = getFileIcon(doc.mimeType);
|
|
213
|
+
const isChecked = localSelected.has(doc.id);
|
|
214
|
+
return (
|
|
215
|
+
<div
|
|
216
|
+
key={doc.id}
|
|
217
|
+
role="button"
|
|
218
|
+
tabIndex={0}
|
|
219
|
+
onClick={() => toggleDocument(doc.id)}
|
|
220
|
+
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleDocument(doc.id); } }}
|
|
221
|
+
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-colors cursor-pointer ${
|
|
222
|
+
isChecked
|
|
223
|
+
? "bg-accent/50 border border-accent"
|
|
224
|
+
: "hover:bg-muted/50 border border-transparent"
|
|
225
|
+
}`}
|
|
226
|
+
>
|
|
227
|
+
<Checkbox
|
|
228
|
+
checked={isChecked}
|
|
229
|
+
aria-label={`Select ${doc.originalName}`}
|
|
230
|
+
/>
|
|
231
|
+
<Icon className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
|
232
|
+
<div className="flex-1 min-w-0">
|
|
233
|
+
<p className="text-sm font-medium truncate">
|
|
234
|
+
{doc.originalName}
|
|
235
|
+
</p>
|
|
236
|
+
<p className="text-xs text-muted-foreground">
|
|
237
|
+
{formatSize(doc.size)}
|
|
238
|
+
{doc.category && ` · ${doc.category}`}
|
|
239
|
+
</p>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="flex items-center gap-2">
|
|
242
|
+
<div
|
|
243
|
+
className={`h-2 w-2 rounded-full ${getStatusDotColor(doc.status)}`}
|
|
244
|
+
/>
|
|
245
|
+
{doc.direction === "output" && (
|
|
246
|
+
<Badge variant="outline" className="text-[10px] px-1.5">
|
|
247
|
+
output
|
|
248
|
+
</Badge>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
})}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
))}
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</ScrollArea>
|
|
260
|
+
|
|
261
|
+
{/* Footer */}
|
|
262
|
+
<div className="flex items-center justify-between pt-2 border-t">
|
|
263
|
+
<span className="text-sm text-muted-foreground">
|
|
264
|
+
{localSelected.size} selected
|
|
265
|
+
</span>
|
|
266
|
+
<div className="flex gap-2">
|
|
267
|
+
<Button
|
|
268
|
+
variant="outline"
|
|
269
|
+
size="sm"
|
|
270
|
+
onClick={() => onOpenChange(false)}
|
|
271
|
+
>
|
|
272
|
+
Cancel
|
|
273
|
+
</Button>
|
|
274
|
+
<Button size="sm" onClick={handleConfirm}>
|
|
275
|
+
Confirm
|
|
276
|
+
</Button>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</SheetContent>
|
|
281
|
+
</Sheet>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
@@ -5,7 +5,7 @@ import { useSortable } from "@dnd-kit/sortable";
|
|
|
5
5
|
import { CSS } from "@dnd-kit/utilities";
|
|
6
6
|
import { Card } from "@/components/ui/card";
|
|
7
7
|
import { Badge } from "@/components/ui/badge";
|
|
8
|
-
import { AlertCircle, Bot, ArrowUp, ArrowDown, Minus, Trash2, Check, X, Loader2, Square, CheckSquare, Pencil } from "lucide-react";
|
|
8
|
+
import { AlertCircle, Bot, ArrowUp, ArrowDown, Minus, Trash2, Check, X, Loader2, Square, CheckSquare, Pencil, FileText } from "lucide-react";
|
|
9
9
|
import type { TaskStatus } from "@/lib/constants/task-status";
|
|
10
10
|
|
|
11
11
|
export interface TaskItem {
|
|
@@ -28,6 +28,7 @@ export interface TaskItem {
|
|
|
28
28
|
resumeCount: number;
|
|
29
29
|
createdAt: string;
|
|
30
30
|
updatedAt: string;
|
|
31
|
+
docCount?: number;
|
|
31
32
|
usage?: {
|
|
32
33
|
inputTokens: number | null;
|
|
33
34
|
outputTokens: number | null;
|
|
@@ -165,6 +166,12 @@ export function TaskCard({
|
|
|
165
166
|
<span className="truncate">{task.assignedAgent}</span>
|
|
166
167
|
</Badge>
|
|
167
168
|
)}
|
|
169
|
+
{task.docCount != null && task.docCount > 0 && (
|
|
170
|
+
<Badge variant="outline" className="text-xs gap-1 h-5">
|
|
171
|
+
<FileText className="h-3 w-3 shrink-0" />
|
|
172
|
+
{task.docCount}
|
|
173
|
+
</Badge>
|
|
174
|
+
)}
|
|
168
175
|
{isFailed && <AlertCircle className="h-3.5 w-3.5 text-destructive" aria-label="Task failed" />}
|
|
169
176
|
{isRunning && (
|
|
170
177
|
<span className="flex h-2 w-2" aria-label="Task running">
|