stagent 0.6.2 → 0.7.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 +21 -2
- package/dist/cli.js +272 -1
- package/docs/.coverage-gaps.json +66 -16
- package/docs/.last-generated +1 -1
- package/docs/features/dashboard-kanban.md +13 -7
- package/docs/features/settings.md +15 -3
- package/docs/features/tables.md +122 -0
- package/docs/index.md +3 -2
- package/docs/journeys/developer.md +26 -16
- package/docs/journeys/personal-use.md +23 -9
- package/docs/journeys/power-user.md +40 -14
- package/docs/journeys/work-use.md +43 -15
- package/docs/manifest.json +27 -17
- package/package.json +3 -2
- package/src/app/api/chat/entities/search/route.ts +12 -3
- 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 +72 -3
- package/src/app/api/projects/__tests__/delete-project.test.ts +13 -0
- package/src/app/api/schedules/route.ts +19 -1
- package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
- package/src/app/api/snapshots/[id]/route.ts +44 -0
- package/src/app/api/snapshots/route.ts +54 -0
- package/src/app/api/snapshots/settings/route.ts +67 -0
- package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
- package/src/app/api/tables/[id]/charts/route.ts +72 -0
- package/src/app/api/tables/[id]/columns/route.ts +70 -0
- package/src/app/api/tables/[id]/export/route.ts +94 -0
- package/src/app/api/tables/[id]/history/route.ts +15 -0
- package/src/app/api/tables/[id]/import/route.ts +111 -0
- package/src/app/api/tables/[id]/route.ts +86 -0
- package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
- package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
- package/src/app/api/tables/[id]/rows/route.ts +101 -0
- package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
- package/src/app/api/tables/[id]/triggers/route.ts +122 -0
- package/src/app/api/tables/route.ts +65 -0
- package/src/app/api/tables/templates/route.ts +92 -0
- 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/app/settings/page.tsx +2 -0
- package/src/app/tables/[id]/page.tsx +67 -0
- package/src/app/tables/page.tsx +21 -0
- package/src/app/tables/templates/page.tsx +19 -0
- package/src/components/chat/chat-table-result.tsx +139 -0
- package/src/components/documents/document-browser.tsx +1 -1
- 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 +109 -2
- package/src/components/schedules/schedule-form.tsx +91 -1
- package/src/components/settings/data-management-section.tsx +17 -12
- package/src/components/settings/database-snapshots-section.tsx +469 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/document-picker-sheet.tsx +486 -0
- package/src/components/tables/table-browser.tsx +234 -0
- package/src/components/tables/table-cell-editor.tsx +226 -0
- package/src/components/tables/table-chart-builder.tsx +288 -0
- package/src/components/tables/table-chart-view.tsx +146 -0
- package/src/components/tables/table-column-header.tsx +103 -0
- package/src/components/tables/table-column-sheet.tsx +331 -0
- package/src/components/tables/table-create-sheet.tsx +240 -0
- package/src/components/tables/table-detail-sheet.tsx +144 -0
- package/src/components/tables/table-detail-tabs.tsx +278 -0
- package/src/components/tables/table-grid.tsx +61 -0
- package/src/components/tables/table-history-tab.tsx +148 -0
- package/src/components/tables/table-import-wizard.tsx +542 -0
- package/src/components/tables/table-list-table.tsx +95 -0
- package/src/components/tables/table-relation-combobox.tsx +217 -0
- package/src/components/tables/table-spreadsheet.tsx +499 -0
- package/src/components/tables/table-template-gallery.tsx +162 -0
- package/src/components/tables/table-template-preview.tsx +219 -0
- package/src/components/tables/table-toolbar.tsx +79 -0
- package/src/components/tables/table-triggers-tab.tsx +446 -0
- package/src/components/tables/types.ts +6 -0
- package/src/components/tables/use-spreadsheet-keys.ts +171 -0
- package/src/components/tables/utils.ts +29 -0
- package/src/components/tasks/task-card.tsx +8 -1
- package/src/components/tasks/task-create-panel.tsx +111 -14
- package/src/components/tasks/task-detail-view.tsx +47 -0
- package/src/components/tasks/task-edit-dialog.tsx +103 -2
- package/src/components/workflows/workflow-form-view.tsx +207 -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 +168 -23
- package/src/instrumentation.ts +3 -0
- package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
- package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
- package/src/lib/agents/claude-agent.ts +3 -1
- package/src/lib/agents/profiles/registry.ts +6 -3
- package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
- package/src/lib/agents/runtime/openai-direct.ts +29 -0
- 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/stagent-tools.ts +2 -0
- package/src/lib/chat/system-prompt.ts +9 -1
- package/src/lib/chat/tool-catalog.ts +35 -0
- package/src/lib/chat/tools/settings-tools.ts +109 -0
- package/src/lib/chat/tools/table-tools.ts +955 -0
- package/src/lib/chat/tools/workflow-tools.ts +145 -2
- package/src/lib/constants/table-status.ts +68 -0
- package/src/lib/data/__tests__/clear.test.ts +1 -1
- package/src/lib/data/clear.ts +57 -0
- package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
- package/src/lib/data/seed-data/conversations.ts +350 -42
- package/src/lib/data/seed-data/documents.ts +564 -591
- package/src/lib/data/seed-data/learned-context.ts +101 -22
- package/src/lib/data/seed-data/notifications.ts +344 -70
- package/src/lib/data/seed-data/profile-test-results.ts +92 -11
- package/src/lib/data/seed-data/profiles.ts +144 -46
- package/src/lib/data/seed-data/projects.ts +50 -18
- package/src/lib/data/seed-data/repo-imports.ts +28 -13
- package/src/lib/data/seed-data/schedules.ts +208 -41
- package/src/lib/data/seed-data/table-templates.ts +234 -0
- package/src/lib/data/seed-data/tasks.ts +614 -116
- package/src/lib/data/seed-data/usage-ledger.ts +182 -103
- package/src/lib/data/seed-data/user-tables.ts +203 -0
- package/src/lib/data/seed-data/views.ts +52 -7
- package/src/lib/data/seed-data/workflows.ts +231 -84
- package/src/lib/data/seed.ts +55 -14
- package/src/lib/data/tables.ts +417 -0
- package/src/lib/db/bootstrap.ts +275 -0
- package/src/lib/db/index.ts +9 -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/migrations/0019_add_tables_feature.sql +160 -0
- package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
- package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
- package/src/lib/db/schema.ts +445 -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/snapshots/auto-backup.ts +132 -0
- package/src/lib/snapshots/retention.ts +64 -0
- package/src/lib/snapshots/snapshot-manager.ts +429 -0
- package/src/lib/tables/computed.ts +61 -0
- package/src/lib/tables/context-builder.ts +139 -0
- package/src/lib/tables/formula-engine.ts +415 -0
- package/src/lib/tables/history.ts +115 -0
- package/src/lib/tables/import.ts +343 -0
- package/src/lib/tables/query-builder.ts +152 -0
- package/src/lib/tables/trigger-evaluator.ts +146 -0
- package/src/lib/tables/types.ts +141 -0
- package/src/lib/tables/validation.ts +119 -0
- package/src/lib/utils/app-root.ts +20 -0
- package/src/lib/utils/stagent-paths.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/tsconfig.json +3 -1
- package/public/icon.svg +0 -13
- package/src/components/tasks/file-upload.tsx +0 -120
- /package/docs/features/{playbook.md → user-guide.md} +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { listTemplates } from "@/lib/data/tables";
|
|
2
|
+
import { PageShell } from "@/components/shared/page-shell";
|
|
3
|
+
import { TableTemplateGallery } from "@/components/tables/table-template-gallery";
|
|
4
|
+
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
|
|
7
|
+
export default async function TableTemplatesPage() {
|
|
8
|
+
const templates = await listTemplates();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<PageShell
|
|
12
|
+
title="Table Templates"
|
|
13
|
+
backHref="/tables"
|
|
14
|
+
backLabel="Tables"
|
|
15
|
+
>
|
|
16
|
+
<TableTemplateGallery templates={templates} />
|
|
17
|
+
</PageShell>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Table2, ChevronDown, ChevronUp, ExternalLink } from "lucide-react";
|
|
7
|
+
import Link from "next/link";
|
|
8
|
+
|
|
9
|
+
interface ChatTableResultProps {
|
|
10
|
+
tableId: string;
|
|
11
|
+
tableName: string;
|
|
12
|
+
columns: string[];
|
|
13
|
+
rows: Array<Record<string, unknown>>;
|
|
14
|
+
totalRows?: number;
|
|
15
|
+
aggregation?: {
|
|
16
|
+
operation: string;
|
|
17
|
+
column: string;
|
|
18
|
+
result: number;
|
|
19
|
+
count: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const MAX_VISIBLE_ROWS = 10;
|
|
24
|
+
|
|
25
|
+
export function ChatTableResult({
|
|
26
|
+
tableId,
|
|
27
|
+
tableName,
|
|
28
|
+
columns,
|
|
29
|
+
rows,
|
|
30
|
+
totalRows,
|
|
31
|
+
aggregation,
|
|
32
|
+
}: ChatTableResultProps) {
|
|
33
|
+
const [expanded, setExpanded] = useState(false);
|
|
34
|
+
const visibleRows = expanded ? rows : rows.slice(0, MAX_VISIBLE_ROWS);
|
|
35
|
+
const hasMore = rows.length > MAX_VISIBLE_ROWS;
|
|
36
|
+
|
|
37
|
+
if (aggregation) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="rounded-lg border p-3 my-2 space-y-2">
|
|
40
|
+
<div className="flex items-center gap-2">
|
|
41
|
+
<Table2 className="h-4 w-4 text-muted-foreground" />
|
|
42
|
+
<span className="text-sm font-medium">{tableName}</span>
|
|
43
|
+
<Badge variant="secondary" className="text-xs">
|
|
44
|
+
{aggregation.operation}
|
|
45
|
+
</Badge>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="flex items-baseline gap-2">
|
|
48
|
+
<span className="text-2xl font-semibold tabular-nums">
|
|
49
|
+
{typeof aggregation.result === "number"
|
|
50
|
+
? aggregation.result.toLocaleString()
|
|
51
|
+
: String(aggregation.result)}
|
|
52
|
+
</span>
|
|
53
|
+
<span className="text-xs text-muted-foreground">
|
|
54
|
+
{aggregation.column} · {aggregation.count} rows
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="rounded-lg border my-2 overflow-hidden">
|
|
63
|
+
<div className="flex items-center justify-between px-3 py-2 bg-muted/50">
|
|
64
|
+
<div className="flex items-center gap-2">
|
|
65
|
+
<Table2 className="h-4 w-4 text-muted-foreground" />
|
|
66
|
+
<span className="text-sm font-medium">{tableName}</span>
|
|
67
|
+
<Badge variant="secondary" className="text-xs">
|
|
68
|
+
{totalRows ?? rows.length} row{(totalRows ?? rows.length) !== 1 ? "s" : ""}
|
|
69
|
+
</Badge>
|
|
70
|
+
</div>
|
|
71
|
+
<Link
|
|
72
|
+
href={`/tables/${tableId}`}
|
|
73
|
+
className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
|
|
74
|
+
>
|
|
75
|
+
Open <ExternalLink className="h-3 w-3" />
|
|
76
|
+
</Link>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{columns.length > 0 && visibleRows.length > 0 && (
|
|
80
|
+
<div className="overflow-x-auto">
|
|
81
|
+
<table className="w-full text-xs">
|
|
82
|
+
<thead>
|
|
83
|
+
<tr className="border-b bg-muted/30">
|
|
84
|
+
{columns.slice(0, 8).map((col) => (
|
|
85
|
+
<th key={col} className="px-3 py-1.5 text-left font-medium text-muted-foreground">
|
|
86
|
+
{col}
|
|
87
|
+
</th>
|
|
88
|
+
))}
|
|
89
|
+
{columns.length > 8 && (
|
|
90
|
+
<th className="px-3 py-1.5 text-left font-medium text-muted-foreground">
|
|
91
|
+
+{columns.length - 8} more
|
|
92
|
+
</th>
|
|
93
|
+
)}
|
|
94
|
+
</tr>
|
|
95
|
+
</thead>
|
|
96
|
+
<tbody>
|
|
97
|
+
{visibleRows.map((row, i) => (
|
|
98
|
+
<tr key={i} className="border-b last:border-0">
|
|
99
|
+
{columns.slice(0, 8).map((col) => (
|
|
100
|
+
<td key={col} className="px-3 py-1.5 max-w-[200px] truncate">
|
|
101
|
+
{row[col] == null ? (
|
|
102
|
+
<span className="text-muted-foreground/40">—</span>
|
|
103
|
+
) : (
|
|
104
|
+
String(row[col])
|
|
105
|
+
)}
|
|
106
|
+
</td>
|
|
107
|
+
))}
|
|
108
|
+
</tr>
|
|
109
|
+
))}
|
|
110
|
+
</tbody>
|
|
111
|
+
</table>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{hasMore && (
|
|
116
|
+
<div className="px-3 py-2 border-t">
|
|
117
|
+
<Button
|
|
118
|
+
variant="ghost"
|
|
119
|
+
size="sm"
|
|
120
|
+
className="w-full text-xs"
|
|
121
|
+
onClick={() => setExpanded(!expanded)}
|
|
122
|
+
>
|
|
123
|
+
{expanded ? (
|
|
124
|
+
<>
|
|
125
|
+
<ChevronUp className="h-3 w-3 mr-1" />
|
|
126
|
+
Show less
|
|
127
|
+
</>
|
|
128
|
+
) : (
|
|
129
|
+
<>
|
|
130
|
+
<ChevronDown className="h-3 w-3 mr-1" />
|
|
131
|
+
Show all {rows.length} rows
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
</Button>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -89,7 +89,7 @@ export function DocumentBrowser({
|
|
|
89
89
|
let deleted = 0;
|
|
90
90
|
for (const id of selected) {
|
|
91
91
|
try {
|
|
92
|
-
const res = await fetch(`/api/documents/${id}`, { method: "DELETE" });
|
|
92
|
+
const res = await fetch(`/api/documents/${id}?cascadeDelete=true`, { method: "DELETE" });
|
|
93
93
|
if (res.ok) deleted++;
|
|
94
94
|
} catch {
|
|
95
95
|
// Continue with remaining
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
ArrowDownLeft,
|
|
19
19
|
Link2,
|
|
20
20
|
FolderKanban,
|
|
21
|
+
GitBranch,
|
|
21
22
|
} from "lucide-react";
|
|
22
23
|
import {
|
|
23
24
|
getFileIcon,
|
|
@@ -127,8 +128,23 @@ export function DocumentChipBar({
|
|
|
127
128
|
</Badge>
|
|
128
129
|
</div>
|
|
129
130
|
|
|
130
|
-
{/* Row 3: Links —
|
|
131
|
+
{/* Row 3: Links — workflow, task, project */}
|
|
131
132
|
<div className="flex flex-wrap items-center gap-2">
|
|
133
|
+
{/* Workflow source */}
|
|
134
|
+
{doc.workflowId && doc.workflowName && (
|
|
135
|
+
<Badge
|
|
136
|
+
variant="secondary"
|
|
137
|
+
className="text-xs cursor-pointer hover:bg-accent gap-1"
|
|
138
|
+
onClick={() => router.push(`/workflows/${doc.workflowId}`)}
|
|
139
|
+
>
|
|
140
|
+
<GitBranch className="h-3 w-3" />
|
|
141
|
+
{doc.workflowName}
|
|
142
|
+
{doc.workflowRunNumber != null && doc.workflowRunNumber > 0 && (
|
|
143
|
+
<span className="text-muted-foreground ml-1">Run #{doc.workflowRunNumber}</span>
|
|
144
|
+
)}
|
|
145
|
+
</Badge>
|
|
146
|
+
)}
|
|
147
|
+
|
|
132
148
|
{/* Task link */}
|
|
133
149
|
{doc.taskTitle ? (
|
|
134
150
|
<Badge
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
import { useState, useEffect, useCallback } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
5
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
6
|
+
import { Badge } from "@/components/ui/badge";
|
|
7
|
+
import { FileText } from "lucide-react";
|
|
6
8
|
import { toast } from "sonner";
|
|
7
9
|
import { DocumentChipBar } from "./document-chip-bar";
|
|
8
10
|
import { DocumentContentRenderer } from "./document-content-renderer";
|
|
11
|
+
import { formatSize, formatRelativeTime } from "./utils";
|
|
9
12
|
import type { DocumentWithRelations } from "./types";
|
|
10
13
|
|
|
11
14
|
/** Serialized version of DocumentWithRelations (Date fields become strings from server) */
|
|
@@ -28,6 +31,14 @@ export function DocumentDetailView({ documentId, initialDocument }: DocumentDeta
|
|
|
28
31
|
const [projects, setProjects] = useState<{ id: string; name: string }[]>([]);
|
|
29
32
|
const [deleting, setDeleting] = useState(false);
|
|
30
33
|
const [linking, setLinking] = useState(false);
|
|
34
|
+
const [versions, setVersions] = useState<Array<{
|
|
35
|
+
id: string;
|
|
36
|
+
version: number;
|
|
37
|
+
size: number;
|
|
38
|
+
status: string;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
workflowRunNumber: number | null;
|
|
41
|
+
}>>([]);
|
|
31
42
|
|
|
32
43
|
const refresh = useCallback(async () => {
|
|
33
44
|
try {
|
|
@@ -56,6 +67,16 @@ export function DocumentDetailView({ documentId, initialDocument }: DocumentDeta
|
|
|
56
67
|
.catch(() => {});
|
|
57
68
|
}, [refresh, initialDocument]);
|
|
58
69
|
|
|
70
|
+
// Fetch version history for output documents
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (doc?.direction === "output") {
|
|
73
|
+
fetch(`/api/documents/${documentId}/versions`)
|
|
74
|
+
.then((r) => r.ok ? r.json() : [])
|
|
75
|
+
.then(setVersions)
|
|
76
|
+
.catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
}, [doc?.direction, documentId]);
|
|
79
|
+
|
|
59
80
|
async function handleDelete() {
|
|
60
81
|
if (!doc) return;
|
|
61
82
|
setDeleting(true);
|
|
@@ -136,6 +157,36 @@ export function DocumentDetailView({ documentId, initialDocument }: DocumentDeta
|
|
|
136
157
|
deleting={deleting}
|
|
137
158
|
linking={linking}
|
|
138
159
|
/>
|
|
160
|
+
{/* Version History */}
|
|
161
|
+
{doc.direction === "output" && versions.length > 1 && (
|
|
162
|
+
<div className="space-y-2">
|
|
163
|
+
<h3 className="text-sm font-medium text-muted-foreground">Version History</h3>
|
|
164
|
+
<div className="surface-control rounded-lg divide-y divide-border">
|
|
165
|
+
{versions.map((v) => (
|
|
166
|
+
<button
|
|
167
|
+
key={v.id}
|
|
168
|
+
className={`w-full flex items-center gap-3 px-3 py-2 text-xs hover:bg-accent/50 transition-colors ${v.id === doc.id ? "bg-accent/30" : ""}`}
|
|
169
|
+
onClick={() => v.id !== doc.id && router.push(`/documents/${v.id}`)}
|
|
170
|
+
disabled={v.id === doc.id}
|
|
171
|
+
>
|
|
172
|
+
<FileText className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
173
|
+
<span className="font-medium">v{v.version}</span>
|
|
174
|
+
{v.id === doc.id && (
|
|
175
|
+
<Badge variant="outline" className="text-[10px] py-0 px-1.5">current</Badge>
|
|
176
|
+
)}
|
|
177
|
+
<span className="text-muted-foreground">{formatSize(v.size)}</span>
|
|
178
|
+
{v.workflowRunNumber != null && v.workflowRunNumber > 0 && (
|
|
179
|
+
<span className="text-muted-foreground">Run #{v.workflowRunNumber}</span>
|
|
180
|
+
)}
|
|
181
|
+
<span className="text-muted-foreground ml-auto">
|
|
182
|
+
{formatRelativeTime(typeof v.createdAt === "number" ? v.createdAt : new Date(v.createdAt).getTime())}
|
|
183
|
+
</span>
|
|
184
|
+
</button>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
|
|
139
190
|
<div className="prose-reader-surface">
|
|
140
191
|
<DocumentContentRenderer doc={doc} />
|
|
141
192
|
</div>
|
|
@@ -70,6 +70,11 @@ export function DocumentGrid({
|
|
|
70
70
|
<span className="capitalize">{doc.direction}</span>
|
|
71
71
|
{doc.direction === "output" && <span>v{doc.version}</span>}
|
|
72
72
|
</div>
|
|
73
|
+
{doc.workflowName && (
|
|
74
|
+
<p className="text-[10px] text-muted-foreground truncate mt-0.5">
|
|
75
|
+
{doc.workflowName}
|
|
76
|
+
</p>
|
|
77
|
+
)}
|
|
73
78
|
</Card>
|
|
74
79
|
);
|
|
75
80
|
})}
|
|
@@ -46,6 +46,7 @@ export function DocumentTable({
|
|
|
46
46
|
<TableHead>Name</TableHead>
|
|
47
47
|
<TableHead className="hidden md:table-cell">Size</TableHead>
|
|
48
48
|
<TableHead className="hidden md:table-cell">Direction</TableHead>
|
|
49
|
+
<TableHead className="hidden lg:table-cell">Workflow</TableHead>
|
|
49
50
|
<TableHead className="hidden lg:table-cell">Task</TableHead>
|
|
50
51
|
<TableHead className="hidden lg:table-cell">Project</TableHead>
|
|
51
52
|
<TableHead>Status</TableHead>
|
|
@@ -87,6 +88,9 @@ export function DocumentTable({
|
|
|
87
88
|
)}
|
|
88
89
|
</div>
|
|
89
90
|
</TableCell>
|
|
91
|
+
<TableCell className="hidden lg:table-cell text-muted-foreground text-sm truncate max-w-[140px]">
|
|
92
|
+
{doc.workflowName ?? "—"}
|
|
93
|
+
</TableCell>
|
|
90
94
|
<TableCell className="hidden lg:table-cell text-sm">
|
|
91
95
|
{doc.taskTitle ? (
|
|
92
96
|
<span className="truncate max-w-[150px] block">{doc.taskTitle}</span>
|
|
@@ -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,44 @@ 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[], meta: Array<{ id: string; originalName: string; mimeType: string; size: number }>) => {
|
|
102
|
+
setSelectedDocIds(new Set(ids));
|
|
103
|
+
setSelectedDocs(meta);
|
|
104
|
+
},
|
|
105
|
+
[]
|
|
106
|
+
);
|
|
107
|
+
|
|
73
108
|
async function handleSubmit(e: React.FormEvent) {
|
|
74
109
|
e.preventDefault();
|
|
75
110
|
if (!name.trim()) return;
|
|
@@ -85,6 +120,7 @@ export function ProjectFormSheet({
|
|
|
85
120
|
name: name.trim(),
|
|
86
121
|
description: description.trim() || undefined,
|
|
87
122
|
workingDirectory: workingDirectory.trim() || undefined,
|
|
123
|
+
documentIds: selectedDocIds.size > 0 ? [...selectedDocIds] : undefined,
|
|
88
124
|
}),
|
|
89
125
|
});
|
|
90
126
|
if (res.ok) {
|
|
@@ -104,6 +140,7 @@ export function ProjectFormSheet({
|
|
|
104
140
|
description: description.trim() || undefined,
|
|
105
141
|
workingDirectory: workingDirectory.trim() || undefined,
|
|
106
142
|
status,
|
|
143
|
+
documentIds: [...selectedDocIds],
|
|
107
144
|
}),
|
|
108
145
|
});
|
|
109
146
|
if (res.ok) {
|
|
@@ -236,6 +273,76 @@ export function ProjectFormSheet({
|
|
|
236
273
|
</div>
|
|
237
274
|
)}
|
|
238
275
|
|
|
276
|
+
{/* Default Documents */}
|
|
277
|
+
{(isEdit || selectedDocs.length > 0) && (
|
|
278
|
+
<div className="space-y-2">
|
|
279
|
+
<Label className="flex items-center gap-1.5">
|
|
280
|
+
<Paperclip className="h-3.5 w-3.5 text-muted-foreground" />
|
|
281
|
+
Default Documents
|
|
282
|
+
</Label>
|
|
283
|
+
<p className="text-xs text-muted-foreground">
|
|
284
|
+
Auto-attached to new tasks and workflows in this project
|
|
285
|
+
</p>
|
|
286
|
+
{selectedDocs.length > 0 && (
|
|
287
|
+
<div className="flex flex-wrap gap-2">
|
|
288
|
+
{selectedDocs.map((doc) => {
|
|
289
|
+
const Icon = getFileIcon(doc.mimeType);
|
|
290
|
+
return (
|
|
291
|
+
<Badge
|
|
292
|
+
key={doc.id}
|
|
293
|
+
variant="secondary"
|
|
294
|
+
className="flex items-center gap-1.5 pl-2 pr-1 py-1"
|
|
295
|
+
>
|
|
296
|
+
<Icon className="h-3 w-3" />
|
|
297
|
+
<span className="text-xs max-w-[140px] truncate">
|
|
298
|
+
{doc.originalName}
|
|
299
|
+
</span>
|
|
300
|
+
<span className="text-[10px] text-muted-foreground">
|
|
301
|
+
{formatSize(doc.size)}
|
|
302
|
+
</span>
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
onClick={() => {
|
|
306
|
+
setSelectedDocIds((prev) => {
|
|
307
|
+
const next = new Set(prev);
|
|
308
|
+
next.delete(doc.id);
|
|
309
|
+
return next;
|
|
310
|
+
});
|
|
311
|
+
setSelectedDocs((prev) => prev.filter((d) => d.id !== doc.id));
|
|
312
|
+
}}
|
|
313
|
+
className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
|
|
314
|
+
aria-label={`Remove ${doc.originalName}`}
|
|
315
|
+
>
|
|
316
|
+
<X className="h-3 w-3" />
|
|
317
|
+
</button>
|
|
318
|
+
</Badge>
|
|
319
|
+
);
|
|
320
|
+
})}
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
<Button
|
|
324
|
+
type="button"
|
|
325
|
+
variant="outline"
|
|
326
|
+
size="sm"
|
|
327
|
+
onClick={() => setPickerOpen(true)}
|
|
328
|
+
className="gap-1.5"
|
|
329
|
+
>
|
|
330
|
+
<Plus className="h-3.5 w-3.5" />
|
|
331
|
+
{selectedDocs.length > 0 ? "Add More" : "Select Documents"}
|
|
332
|
+
</Button>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
<DocumentPickerSheet
|
|
337
|
+
open={pickerOpen}
|
|
338
|
+
onOpenChange={setPickerOpen}
|
|
339
|
+
projectId={project?.id ?? null}
|
|
340
|
+
selectedIds={selectedDocIds}
|
|
341
|
+
onConfirm={handleDocPickerConfirm}
|
|
342
|
+
groupBy="source"
|
|
343
|
+
title="Select Default Documents"
|
|
344
|
+
/>
|
|
345
|
+
|
|
239
346
|
{error && (
|
|
240
347
|
<p className="text-sm text-destructive">{error}</p>
|
|
241
348
|
)}
|
|
@@ -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,19 @@ 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[], meta: Array<{ id: string; originalName: string; mimeType: string; size: number }>) => {
|
|
215
|
+
setSelectedDocIds(new Set(ids));
|
|
216
|
+
setSelectedDocs(meta);
|
|
217
|
+
},
|
|
218
|
+
[]
|
|
219
|
+
);
|
|
220
|
+
|
|
204
221
|
useEffect(() => {
|
|
205
222
|
fetch("/api/profiles")
|
|
206
223
|
.then((r) => r.json())
|
|
@@ -261,6 +278,7 @@ export function ScheduleForm({
|
|
|
261
278
|
activeHoursEnd,
|
|
262
279
|
activeTimezone,
|
|
263
280
|
heartbeatBudgetPerDay,
|
|
281
|
+
documentIds: [...selectedDocIds],
|
|
264
282
|
});
|
|
265
283
|
}
|
|
266
284
|
|
|
@@ -648,6 +666,78 @@ export function ScheduleForm({
|
|
|
648
666
|
</div>
|
|
649
667
|
)}
|
|
650
668
|
|
|
669
|
+
{/* Context Documents */}
|
|
670
|
+
<div className="space-y-2">
|
|
671
|
+
<Label className="flex items-center gap-1.5">
|
|
672
|
+
<Paperclip className="h-3.5 w-3.5 text-muted-foreground" />
|
|
673
|
+
Context Documents
|
|
674
|
+
</Label>
|
|
675
|
+
{selectedDocs.length > 0 && (
|
|
676
|
+
<div className="flex flex-wrap gap-2">
|
|
677
|
+
{selectedDocs.map((doc) => {
|
|
678
|
+
const Icon = getFileIcon(doc.mimeType);
|
|
679
|
+
return (
|
|
680
|
+
<Badge
|
|
681
|
+
key={doc.id}
|
|
682
|
+
variant="secondary"
|
|
683
|
+
className="flex items-center gap-1.5 pl-2 pr-1 py-1"
|
|
684
|
+
>
|
|
685
|
+
<Icon className="h-3 w-3" />
|
|
686
|
+
<span className="text-xs max-w-[140px] truncate">
|
|
687
|
+
{doc.originalName}
|
|
688
|
+
</span>
|
|
689
|
+
<span className="text-[10px] text-muted-foreground">
|
|
690
|
+
{formatSize(doc.size)}
|
|
691
|
+
</span>
|
|
692
|
+
<button
|
|
693
|
+
type="button"
|
|
694
|
+
onClick={() => {
|
|
695
|
+
setSelectedDocIds((prev) => {
|
|
696
|
+
const next = new Set(prev);
|
|
697
|
+
next.delete(doc.id);
|
|
698
|
+
return next;
|
|
699
|
+
});
|
|
700
|
+
setSelectedDocs((prev) => prev.filter((d) => d.id !== doc.id));
|
|
701
|
+
}}
|
|
702
|
+
className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
|
|
703
|
+
aria-label={`Remove ${doc.originalName}`}
|
|
704
|
+
>
|
|
705
|
+
<X className="h-3 w-3" />
|
|
706
|
+
</button>
|
|
707
|
+
</Badge>
|
|
708
|
+
);
|
|
709
|
+
})}
|
|
710
|
+
</div>
|
|
711
|
+
)}
|
|
712
|
+
<Button
|
|
713
|
+
type="button"
|
|
714
|
+
variant="outline"
|
|
715
|
+
size="sm"
|
|
716
|
+
onClick={() => setPickerOpen(true)}
|
|
717
|
+
className="gap-1.5"
|
|
718
|
+
>
|
|
719
|
+
<Plus className="h-3.5 w-3.5" />
|
|
720
|
+
{selectedDocs.length > 0 ? "Add More" : "Select Documents"}
|
|
721
|
+
</Button>
|
|
722
|
+
{selectedDocs.length > 0 && (
|
|
723
|
+
<p className="text-xs text-muted-foreground">
|
|
724
|
+
{selectedDocs.length} document{selectedDocs.length !== 1 ? "s" : ""} will be provided as context for each firing
|
|
725
|
+
</p>
|
|
726
|
+
)}
|
|
727
|
+
</div>
|
|
728
|
+
|
|
729
|
+
<DocumentPickerSheet
|
|
730
|
+
open={pickerOpen}
|
|
731
|
+
onOpenChange={setPickerOpen}
|
|
732
|
+
projectId={projectId || null}
|
|
733
|
+
selectedIds={selectedDocIds}
|
|
734
|
+
onConfirm={handleDocPickerConfirm}
|
|
735
|
+
groupBy="source"
|
|
736
|
+
title="Select Context Documents"
|
|
737
|
+
allowCrossProject
|
|
738
|
+
selectedDocumentMeta={selectedDocs}
|
|
739
|
+
/>
|
|
740
|
+
|
|
651
741
|
{/* Runtime */}
|
|
652
742
|
<div className="space-y-2">
|
|
653
743
|
<Label className="flex items-center gap-1.5">
|