stagent 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +226 -1
  3. package/docs/.coverage-gaps.json +66 -16
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/dashboard-kanban.md +13 -7
  6. package/docs/features/settings.md +15 -3
  7. package/docs/features/tables.md +122 -0
  8. package/docs/index.md +3 -2
  9. package/docs/journeys/developer.md +26 -16
  10. package/docs/journeys/personal-use.md +23 -9
  11. package/docs/journeys/power-user.md +40 -14
  12. package/docs/journeys/work-use.md +43 -15
  13. package/docs/manifest.json +27 -17
  14. package/package.json +3 -1
  15. package/src/app/api/chat/entities/search/route.ts +12 -3
  16. package/src/app/api/projects/[id]/route.ts +37 -0
  17. package/src/app/api/projects/__tests__/delete-project.test.ts +12 -0
  18. package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
  19. package/src/app/api/snapshots/[id]/route.ts +44 -0
  20. package/src/app/api/snapshots/route.ts +54 -0
  21. package/src/app/api/snapshots/settings/route.ts +67 -0
  22. package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
  23. package/src/app/api/tables/[id]/charts/route.ts +72 -0
  24. package/src/app/api/tables/[id]/columns/route.ts +70 -0
  25. package/src/app/api/tables/[id]/export/route.ts +94 -0
  26. package/src/app/api/tables/[id]/history/route.ts +15 -0
  27. package/src/app/api/tables/[id]/import/route.ts +111 -0
  28. package/src/app/api/tables/[id]/route.ts +86 -0
  29. package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
  30. package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
  31. package/src/app/api/tables/[id]/rows/route.ts +101 -0
  32. package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
  33. package/src/app/api/tables/[id]/triggers/route.ts +122 -0
  34. package/src/app/api/tables/route.ts +65 -0
  35. package/src/app/api/tables/templates/route.ts +92 -0
  36. package/src/app/globals.css +14 -0
  37. package/src/app/settings/page.tsx +2 -0
  38. package/src/app/tables/[id]/page.tsx +67 -0
  39. package/src/app/tables/page.tsx +21 -0
  40. package/src/app/tables/templates/page.tsx +19 -0
  41. package/src/components/book/book-reader.tsx +62 -9
  42. package/src/components/book/content-blocks.tsx +6 -1
  43. package/src/components/chat/chat-table-result.tsx +139 -0
  44. package/src/components/documents/document-browser.tsx +1 -1
  45. package/src/components/projects/project-form-sheet.tsx +3 -27
  46. package/src/components/schedules/schedule-form.tsx +5 -27
  47. package/src/components/settings/data-management-section.tsx +17 -12
  48. package/src/components/settings/database-snapshots-section.tsx +469 -0
  49. package/src/components/shared/app-sidebar.tsx +2 -0
  50. package/src/components/shared/document-picker-sheet.tsx +214 -11
  51. package/src/components/tables/table-browser.tsx +234 -0
  52. package/src/components/tables/table-cell-editor.tsx +226 -0
  53. package/src/components/tables/table-chart-builder.tsx +288 -0
  54. package/src/components/tables/table-chart-view.tsx +146 -0
  55. package/src/components/tables/table-column-header.tsx +103 -0
  56. package/src/components/tables/table-column-sheet.tsx +331 -0
  57. package/src/components/tables/table-create-sheet.tsx +240 -0
  58. package/src/components/tables/table-detail-sheet.tsx +144 -0
  59. package/src/components/tables/table-detail-tabs.tsx +278 -0
  60. package/src/components/tables/table-grid.tsx +61 -0
  61. package/src/components/tables/table-history-tab.tsx +148 -0
  62. package/src/components/tables/table-import-wizard.tsx +542 -0
  63. package/src/components/tables/table-list-table.tsx +95 -0
  64. package/src/components/tables/table-relation-combobox.tsx +217 -0
  65. package/src/components/tables/table-row-sheet.tsx +271 -0
  66. package/src/components/tables/table-spreadsheet.tsx +394 -0
  67. package/src/components/tables/table-template-gallery.tsx +162 -0
  68. package/src/components/tables/table-template-preview.tsx +219 -0
  69. package/src/components/tables/table-toolbar.tsx +79 -0
  70. package/src/components/tables/table-triggers-tab.tsx +446 -0
  71. package/src/components/tables/types.ts +6 -0
  72. package/src/components/tables/use-spreadsheet-keys.ts +171 -0
  73. package/src/components/tables/utils.ts +29 -0
  74. package/src/components/tasks/task-create-panel.tsx +5 -31
  75. package/src/components/tasks/task-edit-dialog.tsx +5 -27
  76. package/src/components/workflows/workflow-form-view.tsx +11 -35
  77. package/src/components/workflows/workflow-status-view.tsx +1 -1
  78. package/src/instrumentation.ts +3 -0
  79. package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
  80. package/src/lib/agents/claude-agent.ts +3 -1
  81. package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +23 -0
  82. package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +10 -0
  83. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +1 -1
  84. package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
  85. package/src/lib/agents/runtime/openai-direct.ts +29 -0
  86. package/src/lib/book/chapter-generator.ts +81 -5
  87. package/src/lib/book/chapter-mapping.ts +58 -24
  88. package/src/lib/book/content.ts +83 -47
  89. package/src/lib/book/markdown-parser.ts +1 -1
  90. package/src/lib/book/reading-paths.ts +8 -8
  91. package/src/lib/book/types.ts +1 -1
  92. package/src/lib/book/update-detector.ts +4 -1
  93. package/src/lib/chat/stagent-tools.ts +2 -0
  94. package/src/lib/chat/tool-catalog.ts +34 -0
  95. package/src/lib/chat/tools/table-tools.ts +955 -0
  96. package/src/lib/chat/tools/workflow-tools.ts +9 -1
  97. package/src/lib/constants/table-status.ts +68 -0
  98. package/src/lib/data/__tests__/clear.test.ts +1 -1
  99. package/src/lib/data/clear.ts +45 -0
  100. package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
  101. package/src/lib/data/seed-data/conversations.ts +350 -42
  102. package/src/lib/data/seed-data/documents.ts +564 -591
  103. package/src/lib/data/seed-data/learned-context.ts +101 -22
  104. package/src/lib/data/seed-data/notifications.ts +344 -70
  105. package/src/lib/data/seed-data/profile-test-results.ts +92 -11
  106. package/src/lib/data/seed-data/profiles.ts +144 -46
  107. package/src/lib/data/seed-data/projects.ts +50 -18
  108. package/src/lib/data/seed-data/repo-imports.ts +28 -13
  109. package/src/lib/data/seed-data/schedules.ts +208 -41
  110. package/src/lib/data/seed-data/table-templates.ts +234 -0
  111. package/src/lib/data/seed-data/tasks.ts +614 -116
  112. package/src/lib/data/seed-data/usage-ledger.ts +182 -103
  113. package/src/lib/data/seed-data/user-tables.ts +203 -0
  114. package/src/lib/data/seed-data/views.ts +52 -7
  115. package/src/lib/data/seed-data/workflows.ts +231 -84
  116. package/src/lib/data/seed.ts +55 -14
  117. package/src/lib/data/tables.ts +417 -0
  118. package/src/lib/db/bootstrap.ts +227 -0
  119. package/src/lib/db/index.ts +9 -0
  120. package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
  121. package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
  122. package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
  123. package/src/lib/db/schema.ts +368 -0
  124. package/src/lib/snapshots/auto-backup.ts +132 -0
  125. package/src/lib/snapshots/retention.ts +64 -0
  126. package/src/lib/snapshots/snapshot-manager.ts +429 -0
  127. package/src/lib/tables/computed.ts +61 -0
  128. package/src/lib/tables/context-builder.ts +139 -0
  129. package/src/lib/tables/formula-engine.ts +415 -0
  130. package/src/lib/tables/history.ts +115 -0
  131. package/src/lib/tables/import.ts +343 -0
  132. package/src/lib/tables/query-builder.ts +152 -0
  133. package/src/lib/tables/trigger-evaluator.ts +146 -0
  134. package/src/lib/tables/types.ts +141 -0
  135. package/src/lib/tables/validation.ts +119 -0
  136. package/src/lib/utils/stagent-paths.ts +20 -0
  137. package/src/lib/workflows/types.ts +1 -1
  138. package/tsconfig.json +3 -1
  139. /package/docs/features/{playbook.md → user-guide.md} +0 -0
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useMemo } from "react";
3
+ import { useState, useEffect, useMemo, useRef } from "react";
4
4
  import {
5
5
  Sheet,
6
6
  SheetContent,
@@ -8,13 +8,21 @@ import {
8
8
  SheetTitle,
9
9
  SheetDescription,
10
10
  } from "@/components/ui/sheet";
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from "@/components/ui/select";
11
18
  import { Button } from "@/components/ui/button";
12
19
  import { Input } from "@/components/ui/input";
13
20
  import { Checkbox } from "@/components/ui/checkbox";
14
21
  import { Badge } from "@/components/ui/badge";
15
22
  import { ScrollArea } from "@/components/ui/scroll-area";
16
- import { FileText, Search, Loader2 } from "lucide-react";
23
+ import { FileText, Search, Loader2, ChevronRight, X } from "lucide-react";
17
24
  import { getFileIcon, formatSize, getStatusDotColor } from "@/components/documents/utils";
25
+ import { cn } from "@/lib/utils";
18
26
 
19
27
  export interface PickerDocument {
20
28
  id: string;
@@ -33,6 +41,15 @@ export interface PickerDocument {
33
41
  createdAt: number;
34
42
  }
35
43
 
44
+ /** Lightweight metadata for a selected document (survives project switches) */
45
+ export interface PickerSelectedDoc {
46
+ id: string;
47
+ originalName: string;
48
+ mimeType: string;
49
+ size: number;
50
+ projectName?: string | null;
51
+ }
52
+
36
53
  interface DocumentPickerSheetProps {
37
54
  open: boolean;
38
55
  onOpenChange: (open: boolean) => void;
@@ -40,14 +57,18 @@ interface DocumentPickerSheetProps {
40
57
  projectId: string | null;
41
58
  /** Currently selected document IDs (to pre-check) */
42
59
  selectedIds: Set<string>;
43
- /** Called when user confirms selection */
44
- onConfirm: (selectedIds: string[]) => void;
60
+ /** Called when user confirms selection. Second arg provides metadata for display (avoids re-fetch). */
61
+ onConfirm: (selectedIds: string[], selectedMeta: PickerSelectedDoc[]) => void;
45
62
  /** Optional: scope to a step ID label */
46
63
  stepLabel?: string;
47
64
  /** Grouping mode: "workflow" groups by source workflow, "project" by project name, "source" by direction */
48
65
  groupBy?: "workflow" | "project" | "source";
49
66
  /** Override the sheet title */
50
67
  title?: string;
68
+ /** Enable cross-project browsing with a project dropdown. Default: false */
69
+ allowCrossProject?: boolean;
70
+ /** Metadata for pre-selected docs (enables tray display across project switches) */
71
+ selectedDocumentMeta?: Array<{ id: string; originalName: string; mimeType: string; size?: number; projectName?: string | null }>;
51
72
  }
52
73
 
53
74
  export function DocumentPickerSheet({
@@ -59,25 +80,78 @@ export function DocumentPickerSheet({
59
80
  stepLabel,
60
81
  groupBy = "source",
61
82
  title,
83
+ allowCrossProject = false,
84
+ selectedDocumentMeta,
62
85
  }: DocumentPickerSheetProps) {
63
86
  const [documents, setDocuments] = useState<PickerDocument[]>([]);
64
87
  const [loading, setLoading] = useState(false);
65
88
  const [search, setSearch] = useState("");
66
89
  const [localSelected, setLocalSelected] = useState<Set<string>>(new Set());
67
90
 
68
- // Sync initial selection when sheet opens
91
+ // Cross-project state
92
+ const [activeProjectId, setActiveProjectId] = useState<string | null>(projectId);
93
+ const [projects, setProjects] = useState<Array<{ id: string; name: string }>>([]);
94
+ const [projectsLoading, setProjectsLoading] = useState(false);
95
+ const [selectedDocMeta, setSelectedDocMeta] = useState<Map<string, PickerSelectedDoc>>(new Map());
96
+ const [trayExpanded, setTrayExpanded] = useState(true);
97
+ const initializedRef = useRef(false);
98
+
99
+ // Initialize state when sheet opens
69
100
  useEffect(() => {
70
101
  if (open) {
102
+ setActiveProjectId(projectId);
71
103
  setLocalSelected(new Set(selectedIds));
72
- fetchDocuments();
104
+ setSearch("");
105
+ setTrayExpanded(true);
106
+
107
+ // Seed selectedDocMeta from the prop
108
+ if (selectedDocumentMeta && selectedDocumentMeta.length > 0) {
109
+ const metaMap = new Map<string, PickerSelectedDoc>();
110
+ for (const doc of selectedDocumentMeta) {
111
+ if (selectedIds.has(doc.id)) {
112
+ metaMap.set(doc.id, {
113
+ id: doc.id,
114
+ originalName: doc.originalName,
115
+ mimeType: doc.mimeType,
116
+ size: doc.size ?? 0,
117
+ projectName: doc.projectName,
118
+ });
119
+ }
120
+ }
121
+ setSelectedDocMeta(metaMap);
122
+ } else {
123
+ setSelectedDocMeta(new Map());
124
+ }
125
+
126
+ // Fetch projects list for the dropdown
127
+ if (allowCrossProject) {
128
+ fetchProjects();
129
+ }
130
+ initializedRef.current = true;
131
+ } else {
132
+ initializedRef.current = false;
133
+ }
134
+ }, [open]); // eslint-disable-line react-hooks/exhaustive-deps
135
+
136
+ // Fetch documents when activeProjectId changes (and sheet is open)
137
+ useEffect(() => {
138
+ if (open && initializedRef.current) {
139
+ fetchDocuments(activeProjectId);
140
+ }
141
+ }, [activeProjectId]); // eslint-disable-line react-hooks/exhaustive-deps
142
+
143
+ // Initial document fetch when sheet opens
144
+ useEffect(() => {
145
+ if (open) {
146
+ fetchDocuments(projectId);
73
147
  }
74
148
  }, [open, projectId]); // eslint-disable-line react-hooks/exhaustive-deps
75
149
 
76
- async function fetchDocuments() {
150
+ async function fetchDocuments(forProjectId: string | null) {
77
151
  setLoading(true);
78
152
  try {
79
153
  const params = new URLSearchParams({ status: "ready" });
80
- if (projectId) params.set("projectId", projectId);
154
+ if (forProjectId) params.set("projectId", forProjectId);
81
155
  const res = await fetch(`/api/documents?${params}`);
82
156
  if (res.ok) {
83
157
  const data = await res.json();
@@ -90,6 +164,21 @@ export function DocumentPickerSheet({
90
164
  }
91
165
  }
92
166
 
167
+ async function fetchProjects() {
168
+ setProjectsLoading(true);
169
+ try {
170
+ const res = await fetch("/api/projects");
171
+ if (res.ok) {
172
+ const data = await res.json();
173
+ setProjects(data.map((p: { id: string; name: string }) => ({ id: p.id, name: p.name })));
174
+ }
175
+ } catch {
176
+ // Silently fail — no project dropdown options
177
+ } finally {
178
+ setProjectsLoading(false);
179
+ }
180
+ }
181
+
93
182
  const filtered = useMemo(() => {
94
183
  if (!search.trim()) return documents;
95
184
  const q = search.toLowerCase();
@@ -136,32 +225,71 @@ export function DocumentPickerSheet({
136
225
  return groups;
137
226
  }, [filtered, groupBy]);
138
227
 
228
+ // Compute cross-project tray items
229
+ const currentDocIds = useMemo(() => new Set(documents.map((d) => d.id)), [documents]);
230
+ const crossProjectSelected = useMemo(
231
+ () => [...selectedDocMeta.values()].filter((d) => localSelected.has(d.id) && !currentDocIds.has(d.id)),
232
+ [selectedDocMeta, localSelected, currentDocIds]
233
+ );
234
+ const showTray = allowCrossProject && crossProjectSelected.length > 0;
235
+
139
236
  function toggleDocument(id: string) {
140
237
  setLocalSelected((prev) => {
141
238
  const next = new Set(prev);
142
239
  if (next.has(id)) {
143
240
  next.delete(id);
241
+ setSelectedDocMeta((m) => {
242
+ const n = new Map(m);
243
+ n.delete(id);
244
+ return n;
245
+ });
144
246
  } else {
145
247
  next.add(id);
248
+ const doc = documents.find((d) => d.id === id);
249
+ if (doc) {
250
+ setSelectedDocMeta((m) =>
251
+ new Map(m).set(id, {
252
+ id: doc.id,
253
+ originalName: doc.originalName,
254
+ mimeType: doc.mimeType,
255
+ size: doc.size,
256
+ projectName: doc.projectName,
257
+ })
258
+ );
259
+ }
146
260
  }
147
261
  return next;
148
262
  });
149
263
  }
150
264
 
151
265
  function handleConfirm() {
152
- onConfirm([...localSelected]);
266
+ const ids = [...localSelected];
267
+ // Build metadata array from cached meta + current documents
268
+ const meta: PickerSelectedDoc[] = ids.map((id) => {
269
+ const cached = selectedDocMeta.get(id);
270
+ if (cached) return cached;
271
+ const doc = documents.find((d) => d.id === id);
272
+ if (doc) return { id: doc.id, originalName: doc.originalName, mimeType: doc.mimeType, size: doc.size, projectName: doc.projectName };
273
+ return { id, originalName: "Unknown", mimeType: "application/octet-stream", size: 0 };
274
+ });
275
+ onConfirm(ids, meta);
153
276
  onOpenChange(false);
154
277
  }
155
278
 
279
+ function handleProjectChange(value: string) {
280
+ setActiveProjectId(value === "__all__" ? null : value);
281
+ }
282
+
156
283
  const sheetTitle = title
157
284
  ? title
158
285
  : stepLabel
159
286
  ? `Select Documents for "${stepLabel}"`
160
287
  : "Select Input Documents";
161
288
 
289
+ const effectiveProjectId = allowCrossProject ? activeProjectId : projectId;
162
290
  const emptyMessage = search
163
291
  ? "No documents match your search."
164
- : projectId
292
+ : effectiveProjectId
165
293
  ? "No documents available in this project."
166
294
  : "No documents available. Upload files in the Documents view.";
167
295
 
@@ -179,6 +307,28 @@ export function DocumentPickerSheet({
179
307
  </SheetHeader>
180
308
 
181
309
  <div className="px-6 pb-6 flex flex-col gap-4 flex-1 min-h-0">
310
+ {/* Project selector (cross-project mode only) */}
311
+ {allowCrossProject && (
312
+ <Select
313
+ value={activeProjectId ?? "__all__"}
314
+ onValueChange={handleProjectChange}
315
+ disabled={projectsLoading}
316
+ >
317
+ <SelectTrigger className="h-9">
318
+ <SelectValue placeholder="All Projects" />
319
+ </SelectTrigger>
320
+ <SelectContent>
321
+ <SelectItem value="__all__">All Projects</SelectItem>
322
+ {projects.map((p) => (
323
+ <SelectItem key={p.id} value={p.id}>
324
+ {p.name}
325
+ {p.id === projectId ? " (current)" : ""}
326
+ </SelectItem>
327
+ ))}
328
+ </SelectContent>
329
+ </Select>
330
+ )}
331
+
182
332
  {/* Search */}
183
333
  <div className="relative">
184
334
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
@@ -187,9 +337,62 @@ export function DocumentPickerSheet({
187
337
  value={search}
188
338
  onChange={(e) => setSearch(e.target.value)}
189
339
  className="pl-9"
190
- />
340
+ />
191
341
  </div>
192
342
 
343
+ {/* Cross-project selected tray */}
344
+ {showTray && (
345
+ <div className="border rounded-lg p-2 space-y-1">
346
+ <button
347
+ type="button"
348
+ onClick={() => setTrayExpanded((v) => !v)}
349
+ className="w-full text-xs text-muted-foreground flex items-center gap-1 hover:text-foreground transition-colors"
350
+ >
351
+ <ChevronRight
352
+ className={cn(
353
+ "h-3 w-3 transition-transform",
354
+ trayExpanded && "rotate-90"
355
+ )}
356
+ />
357
+ {crossProjectSelected.length} selected from other projects
358
+ </button>
359
+ {trayExpanded && (
360
+ <div className="max-h-[120px] overflow-y-auto space-y-1">
361
+ {crossProjectSelected.map((doc) => {
362
+ const Icon = getFileIcon(doc.mimeType);
363
+ return (
364
+ <div
365
+ key={doc.id}
366
+ className="flex items-center gap-2 text-xs py-1.5 px-1"
367
+ >
368
+ <Icon className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
369
+ <span className="truncate flex-1">
370
+ {doc.originalName}
371
+ </span>
372
+ {doc.projectName && (
373
+ <Badge
374
+ variant="outline"
375
+ className="text-[10px] px-1.5 flex-shrink-0"
376
+ >
377
+ {doc.projectName}
378
+ </Badge>
379
+ )}
380
+ <button
381
+ type="button"
382
+ onClick={() => toggleDocument(doc.id)}
383
+ className="flex-shrink-0 text-muted-foreground hover:text-foreground transition-colors"
384
+ aria-label={`Remove ${doc.originalName}`}
385
+ >
386
+ <X className="h-3 w-3" />
387
+ </button>
388
+ </div>
389
+ );
390
+ })}
391
+ </div>
392
+ )}
393
+ </div>
394
+ )}
395
+
193
396
  {/* Document list */}
194
397
  <ScrollArea className="flex-1 min-h-0 -mx-2">
195
398
  {loading ? (
@@ -0,0 +1,234 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from "@/components/ui/select";
14
+ import { LayoutGrid, LayoutList, Plus, Trash2, Search, LayoutTemplate } from "lucide-react";
15
+ import { toast } from "sonner";
16
+ import { TableListTable } from "./table-list-table";
17
+ import { TableGrid } from "./table-grid";
18
+ import { TableCreateSheet } from "./table-create-sheet";
19
+ import { FilterBar } from "@/components/shared/filter-bar";
20
+ import { EmptyState } from "@/components/shared/empty-state";
21
+ import { Table2 } from "lucide-react";
22
+ import type { TableWithRelations } from "./types";
23
+
24
+ interface TableBrowserProps {
25
+ initialTables: TableWithRelations[];
26
+ projects: { id: string; name: string }[];
27
+ }
28
+
29
+ export function TableBrowser({ initialTables, projects }: TableBrowserProps) {
30
+ const [tables, setTables] = useState(initialTables);
31
+ const [view, setView] = useState<"table" | "grid">("table");
32
+ const [search, setSearch] = useState("");
33
+ const [sourceFilter, setSourceFilter] = useState<string>("all");
34
+ const [projectFilter, setProjectFilter] = useState<string>("all");
35
+ const [selected, setSelected] = useState<Set<string>>(new Set());
36
+ const [createOpen, setCreateOpen] = useState(false);
37
+ const [deleting, setDeleting] = useState(false);
38
+ const router = useRouter();
39
+
40
+ const refresh = useCallback(async () => {
41
+ try {
42
+ const res = await fetch("/api/tables");
43
+ if (res.ok) {
44
+ const data = await res.json();
45
+ setTables(data);
46
+ }
47
+ } catch {
48
+ // Silent refresh failure
49
+ }
50
+ }, []);
51
+
52
+ const filtered = tables.filter((t) => {
53
+ if (
54
+ search &&
55
+ !t.name.toLowerCase().includes(search.toLowerCase()) &&
56
+ !(t.description ?? "").toLowerCase().includes(search.toLowerCase())
57
+ ) {
58
+ return false;
59
+ }
60
+ if (sourceFilter !== "all" && t.source !== sourceFilter) return false;
61
+ if (projectFilter !== "all" && t.projectId !== projectFilter) return false;
62
+ return true;
63
+ });
64
+
65
+ function toggleSelect(id: string) {
66
+ setSelected((prev) => {
67
+ const next = new Set(prev);
68
+ if (next.has(id)) next.delete(id);
69
+ else next.add(id);
70
+ return next;
71
+ });
72
+ }
73
+
74
+ function toggleSelectAll() {
75
+ if (selected.size === filtered.length) {
76
+ setSelected(new Set());
77
+ } else {
78
+ setSelected(new Set(filtered.map((t) => t.id)));
79
+ }
80
+ }
81
+
82
+ async function handleBulkDelete() {
83
+ if (selected.size === 0) return;
84
+ setDeleting(true);
85
+ try {
86
+ const promises = Array.from(selected).map((id) =>
87
+ fetch(`/api/tables/${id}`, { method: "DELETE" })
88
+ );
89
+ await Promise.all(promises);
90
+ toast.success(`Deleted ${selected.size} table(s)`);
91
+ setSelected(new Set());
92
+ await refresh();
93
+ } catch {
94
+ toast.error("Failed to delete tables");
95
+ } finally {
96
+ setDeleting(false);
97
+ }
98
+ }
99
+
100
+ const activeFilters = [
101
+ sourceFilter !== "all",
102
+ projectFilter !== "all",
103
+ ].filter(Boolean).length;
104
+
105
+ const navigate = (id: string) => router.push(`/tables/${id}`);
106
+
107
+ return (
108
+ <div className="space-y-4">
109
+ <div className="flex items-center gap-2">
110
+ <div className="relative flex-1">
111
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
112
+ <Input
113
+ placeholder="Search tables..."
114
+ value={search}
115
+ onChange={(e) => setSearch(e.target.value)}
116
+ className="pl-9"
117
+ />
118
+ </div>
119
+ <div className="flex items-center gap-1 border rounded-lg p-0.5">
120
+ <Button
121
+ variant={view === "table" ? "secondary" : "ghost"}
122
+ size="icon"
123
+ className="h-8 w-8"
124
+ onClick={() => setView("table")}
125
+ >
126
+ <LayoutList className="h-4 w-4" />
127
+ </Button>
128
+ <Button
129
+ variant={view === "grid" ? "secondary" : "ghost"}
130
+ size="icon"
131
+ className="h-8 w-8"
132
+ onClick={() => setView("grid")}
133
+ >
134
+ <LayoutGrid className="h-4 w-4" />
135
+ </Button>
136
+ </div>
137
+ {selected.size > 0 && (
138
+ <Button
139
+ variant="destructive"
140
+ size="sm"
141
+ disabled={deleting}
142
+ onClick={handleBulkDelete}
143
+ >
144
+ <Trash2 className="h-4 w-4 mr-1" />
145
+ Delete ({selected.size})
146
+ </Button>
147
+ )}
148
+ <Button
149
+ variant="outline"
150
+ size="sm"
151
+ onClick={() => router.push("/tables/templates")}
152
+ >
153
+ <LayoutTemplate className="h-4 w-4 mr-1" />
154
+ Templates
155
+ </Button>
156
+ <Button size="sm" onClick={() => setCreateOpen(true)}>
157
+ <Plus className="h-4 w-4 mr-1" />
158
+ New Table
159
+ </Button>
160
+ </div>
161
+
162
+ <FilterBar
163
+ activeCount={activeFilters}
164
+ onClear={() => {
165
+ setSourceFilter("all");
166
+ setProjectFilter("all");
167
+ }}
168
+ >
169
+ <Select value={sourceFilter} onValueChange={setSourceFilter}>
170
+ <SelectTrigger className="w-[140px]">
171
+ <SelectValue placeholder="Source" />
172
+ </SelectTrigger>
173
+ <SelectContent>
174
+ <SelectItem value="all">All Sources</SelectItem>
175
+ <SelectItem value="manual">Manual</SelectItem>
176
+ <SelectItem value="imported">Imported</SelectItem>
177
+ <SelectItem value="agent">Agent</SelectItem>
178
+ <SelectItem value="template">Template</SelectItem>
179
+ </SelectContent>
180
+ </Select>
181
+
182
+ <Select value={projectFilter} onValueChange={setProjectFilter}>
183
+ <SelectTrigger className="w-[180px]">
184
+ <SelectValue placeholder="Project" />
185
+ </SelectTrigger>
186
+ <SelectContent>
187
+ <SelectItem value="all">All Projects</SelectItem>
188
+ {projects.map((p) => (
189
+ <SelectItem key={p.id} value={p.id}>
190
+ {p.name}
191
+ </SelectItem>
192
+ ))}
193
+ </SelectContent>
194
+ </Select>
195
+ </FilterBar>
196
+
197
+ {filtered.length === 0 ? (
198
+ <EmptyState
199
+ icon={Table2}
200
+ heading="No tables yet"
201
+ description="Create a table to start organizing your structured data, or browse templates for inspiration."
202
+ action={
203
+ <Button onClick={() => setCreateOpen(true)}>
204
+ <Plus className="h-4 w-4 mr-1" />
205
+ Create Table
206
+ </Button>
207
+ }
208
+ />
209
+ ) : view === "table" ? (
210
+ <TableListTable
211
+ tables={filtered}
212
+ selected={selected}
213
+ onToggleSelect={toggleSelect}
214
+ onToggleSelectAll={toggleSelectAll}
215
+ onSelect={navigate}
216
+ onOpen={navigate}
217
+ />
218
+ ) : (
219
+ <TableGrid
220
+ tables={filtered}
221
+ onSelect={navigate}
222
+ onOpen={navigate}
223
+ />
224
+ )}
225
+
226
+ <TableCreateSheet
227
+ open={createOpen}
228
+ onOpenChange={setCreateOpen}
229
+ projects={projects}
230
+ onCreated={refresh}
231
+ />
232
+ </div>
233
+ );
234
+ }