stagent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +399 -0
  3. package/components.json +21 -0
  4. package/dist/cli.js +171 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.mjs +15 -0
  7. package/package.json +114 -0
  8. package/postcss.config.mjs +8 -0
  9. package/public/icon-512.png +0 -0
  10. package/public/icon.svg +13 -0
  11. package/public/readme/home-workspace.png +0 -0
  12. package/public/readme/inbox-approvals.png +0 -0
  13. package/public/readme/workflow-blueprints.png +0 -0
  14. package/public/stagent-s-128.png +0 -0
  15. package/public/stagent-s-64.png +0 -0
  16. package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
  17. package/src/app/api/blueprints/[id]/route.ts +39 -0
  18. package/src/app/api/blueprints/import/route.ts +68 -0
  19. package/src/app/api/blueprints/route.ts +29 -0
  20. package/src/app/api/command-palette/recent/route.ts +31 -0
  21. package/src/app/api/data/clear/route.ts +22 -0
  22. package/src/app/api/data/seed/route.ts +22 -0
  23. package/src/app/api/documents/[id]/file/route.ts +44 -0
  24. package/src/app/api/documents/[id]/route.ts +123 -0
  25. package/src/app/api/documents/route.ts +59 -0
  26. package/src/app/api/logs/stream/route.ts +101 -0
  27. package/src/app/api/notifications/[id]/route.ts +36 -0
  28. package/src/app/api/notifications/mark-all-read/route.ts +13 -0
  29. package/src/app/api/notifications/pending-approvals/route.ts +10 -0
  30. package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
  31. package/src/app/api/notifications/route.ts +34 -0
  32. package/src/app/api/permissions/route.ts +46 -0
  33. package/src/app/api/profiles/[id]/route.ts +79 -0
  34. package/src/app/api/profiles/[id]/test/route.ts +42 -0
  35. package/src/app/api/profiles/import/route.ts +108 -0
  36. package/src/app/api/profiles/route.ts +50 -0
  37. package/src/app/api/projects/[id]/route.ts +72 -0
  38. package/src/app/api/projects/route.ts +53 -0
  39. package/src/app/api/schedules/[id]/route.ts +185 -0
  40. package/src/app/api/schedules/route.ts +117 -0
  41. package/src/app/api/settings/budgets/route.ts +24 -0
  42. package/src/app/api/settings/openai/route.ts +24 -0
  43. package/src/app/api/settings/route.ts +21 -0
  44. package/src/app/api/settings/test/route.ts +26 -0
  45. package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +90 -0
  47. package/src/app/api/tasks/[id]/logs/route.ts +95 -0
  48. package/src/app/api/tasks/[id]/output/route.ts +47 -0
  49. package/src/app/api/tasks/[id]/respond/route.ts +64 -0
  50. package/src/app/api/tasks/[id]/resume/route.ts +76 -0
  51. package/src/app/api/tasks/[id]/route.ts +77 -0
  52. package/src/app/api/tasks/assist/route.ts +35 -0
  53. package/src/app/api/tasks/route.ts +82 -0
  54. package/src/app/api/uploads/[id]/route.ts +81 -0
  55. package/src/app/api/uploads/cleanup/route.ts +7 -0
  56. package/src/app/api/uploads/route.ts +66 -0
  57. package/src/app/api/workflows/[id]/execute/route.ts +82 -0
  58. package/src/app/api/workflows/[id]/route.ts +133 -0
  59. package/src/app/api/workflows/[id]/status/route.ts +54 -0
  60. package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
  61. package/src/app/api/workflows/route.ts +61 -0
  62. package/src/app/apple-icon.tsx +31 -0
  63. package/src/app/costs/page.tsx +256 -0
  64. package/src/app/dashboard/page.tsx +44 -0
  65. package/src/app/documents/[id]/page.tsx +46 -0
  66. package/src/app/documents/page.tsx +45 -0
  67. package/src/app/error.tsx +26 -0
  68. package/src/app/global-error.tsx +23 -0
  69. package/src/app/globals.css +733 -0
  70. package/src/app/icon.tsx +30 -0
  71. package/src/app/inbox/loading.tsx +15 -0
  72. package/src/app/inbox/page.tsx +35 -0
  73. package/src/app/layout.tsx +78 -0
  74. package/src/app/manifest.ts +32 -0
  75. package/src/app/monitor/page.tsx +37 -0
  76. package/src/app/page.tsx +162 -0
  77. package/src/app/profiles/[id]/edit/page.tsx +39 -0
  78. package/src/app/profiles/[id]/page.tsx +33 -0
  79. package/src/app/profiles/new/page.tsx +22 -0
  80. package/src/app/profiles/page.tsx +19 -0
  81. package/src/app/projects/[id]/page.tsx +134 -0
  82. package/src/app/projects/loading.tsx +17 -0
  83. package/src/app/projects/page.tsx +32 -0
  84. package/src/app/schedules/[id]/page.tsx +47 -0
  85. package/src/app/schedules/page.tsx +18 -0
  86. package/src/app/settings/loading.tsx +24 -0
  87. package/src/app/settings/page.tsx +27 -0
  88. package/src/app/tasks/[id]/page.tsx +45 -0
  89. package/src/app/tasks/new/page.tsx +27 -0
  90. package/src/app/workflows/[id]/edit/page.tsx +66 -0
  91. package/src/app/workflows/[id]/page.tsx +37 -0
  92. package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
  93. package/src/app/workflows/blueprints/new/page.tsx +20 -0
  94. package/src/app/workflows/blueprints/page.tsx +11 -0
  95. package/src/app/workflows/new/page.tsx +36 -0
  96. package/src/app/workflows/page.tsx +18 -0
  97. package/src/components/charts/donut-ring.tsx +64 -0
  98. package/src/components/charts/mini-bar.tsx +75 -0
  99. package/src/components/charts/sparkline.tsx +107 -0
  100. package/src/components/costs/cost-dashboard.tsx +877 -0
  101. package/src/components/costs/cost-filters.tsx +179 -0
  102. package/src/components/dashboard/activity-feed.tsx +95 -0
  103. package/src/components/dashboard/greeting.tsx +30 -0
  104. package/src/components/dashboard/priority-queue.tsx +79 -0
  105. package/src/components/dashboard/quick-actions.tsx +62 -0
  106. package/src/components/dashboard/recent-projects.tsx +79 -0
  107. package/src/components/dashboard/stats-cards.tsx +114 -0
  108. package/src/components/documents/document-browser.tsx +235 -0
  109. package/src/components/documents/document-detail-view.tsx +367 -0
  110. package/src/components/documents/document-grid.tsx +78 -0
  111. package/src/components/documents/document-preview.tsx +68 -0
  112. package/src/components/documents/document-table.tsx +119 -0
  113. package/src/components/documents/document-upload-dialog.tsx +153 -0
  114. package/src/components/documents/types.ts +6 -0
  115. package/src/components/documents/utils.ts +57 -0
  116. package/src/components/monitoring/connection-indicator.tsx +14 -0
  117. package/src/components/monitoring/log-entry.tsx +79 -0
  118. package/src/components/monitoring/log-filters.tsx +57 -0
  119. package/src/components/monitoring/log-stream.tsx +144 -0
  120. package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
  121. package/src/components/monitoring/monitor-overview.tsx +119 -0
  122. package/src/components/notifications/failure-action.tsx +38 -0
  123. package/src/components/notifications/inbox-list.tsx +165 -0
  124. package/src/components/notifications/message-response.tsx +196 -0
  125. package/src/components/notifications/notification-item.tsx +250 -0
  126. package/src/components/notifications/pending-approval-host.tsx +478 -0
  127. package/src/components/notifications/permission-action.tsx +37 -0
  128. package/src/components/notifications/permission-response-actions.tsx +126 -0
  129. package/src/components/notifications/unread-badge.tsx +35 -0
  130. package/src/components/profiles/profile-browser.tsx +117 -0
  131. package/src/components/profiles/profile-card.tsx +78 -0
  132. package/src/components/profiles/profile-detail-view.tsx +564 -0
  133. package/src/components/profiles/profile-form-view.tsx +480 -0
  134. package/src/components/profiles/profile-import-dialog.tsx +113 -0
  135. package/src/components/projects/project-card.tsx +58 -0
  136. package/src/components/projects/project-create-dialog.tsx +140 -0
  137. package/src/components/projects/project-detail.tsx +68 -0
  138. package/src/components/projects/project-edit-dialog.tsx +219 -0
  139. package/src/components/projects/project-list.tsx +108 -0
  140. package/src/components/schedules/schedule-create-dialog.tsx +403 -0
  141. package/src/components/schedules/schedule-detail-view.tsx +274 -0
  142. package/src/components/schedules/schedule-list.tsx +242 -0
  143. package/src/components/schedules/schedule-status-badge.tsx +16 -0
  144. package/src/components/settings/api-key-form.tsx +141 -0
  145. package/src/components/settings/auth-config-section.tsx +141 -0
  146. package/src/components/settings/auth-method-selector.tsx +67 -0
  147. package/src/components/settings/auth-status-badge.tsx +40 -0
  148. package/src/components/settings/auth-status-dot.tsx +59 -0
  149. package/src/components/settings/budget-guardrails-section.tsx +842 -0
  150. package/src/components/settings/data-management-section.tsx +141 -0
  151. package/src/components/settings/openai-runtime-section.tsx +104 -0
  152. package/src/components/settings/permissions-section.tsx +91 -0
  153. package/src/components/shared/app-sidebar.tsx +123 -0
  154. package/src/components/shared/card-skeleton.tsx +42 -0
  155. package/src/components/shared/command-palette.tsx +250 -0
  156. package/src/components/shared/confirm-dialog.tsx +52 -0
  157. package/src/components/shared/empty-state.tsx +24 -0
  158. package/src/components/shared/error-state.tsx +32 -0
  159. package/src/components/shared/form-section-card.tsx +33 -0
  160. package/src/components/shared/section-heading.tsx +14 -0
  161. package/src/components/shared/stagent-logo.tsx +21 -0
  162. package/src/components/shared/theme-toggle.tsx +46 -0
  163. package/src/components/tasks/ai-assist-panel.tsx +210 -0
  164. package/src/components/tasks/content-preview.tsx +89 -0
  165. package/src/components/tasks/empty-board.tsx +12 -0
  166. package/src/components/tasks/file-upload.tsx +120 -0
  167. package/src/components/tasks/kanban-board.tsx +275 -0
  168. package/src/components/tasks/kanban-column.tsx +75 -0
  169. package/src/components/tasks/skeleton-board.tsx +21 -0
  170. package/src/components/tasks/task-attachments.tsx +114 -0
  171. package/src/components/tasks/task-card.tsx +101 -0
  172. package/src/components/tasks/task-create-panel.tsx +360 -0
  173. package/src/components/tasks/task-detail-view.tsx +356 -0
  174. package/src/components/ui/alert-dialog.tsx +196 -0
  175. package/src/components/ui/badge.tsx +50 -0
  176. package/src/components/ui/button.tsx +71 -0
  177. package/src/components/ui/card.tsx +92 -0
  178. package/src/components/ui/checkbox.tsx +32 -0
  179. package/src/components/ui/command.tsx +184 -0
  180. package/src/components/ui/dialog.tsx +158 -0
  181. package/src/components/ui/dropdown-menu.tsx +257 -0
  182. package/src/components/ui/form.tsx +167 -0
  183. package/src/components/ui/input.tsx +21 -0
  184. package/src/components/ui/label.tsx +24 -0
  185. package/src/components/ui/popover.tsx +89 -0
  186. package/src/components/ui/progress.tsx +31 -0
  187. package/src/components/ui/radio-group.tsx +45 -0
  188. package/src/components/ui/scroll-area.tsx +58 -0
  189. package/src/components/ui/select.tsx +190 -0
  190. package/src/components/ui/separator.tsx +28 -0
  191. package/src/components/ui/sheet.tsx +143 -0
  192. package/src/components/ui/sidebar.tsx +726 -0
  193. package/src/components/ui/skeleton.tsx +13 -0
  194. package/src/components/ui/slider.tsx +63 -0
  195. package/src/components/ui/sonner.tsx +36 -0
  196. package/src/components/ui/switch.tsx +35 -0
  197. package/src/components/ui/table.tsx +116 -0
  198. package/src/components/ui/tabs.tsx +91 -0
  199. package/src/components/ui/textarea.tsx +18 -0
  200. package/src/components/ui/tooltip.tsx +57 -0
  201. package/src/components/workflows/blueprint-editor.tsx +109 -0
  202. package/src/components/workflows/blueprint-gallery.tsx +155 -0
  203. package/src/components/workflows/blueprint-preview.tsx +240 -0
  204. package/src/components/workflows/loop-status-view.tsx +272 -0
  205. package/src/components/workflows/swarm-dashboard.tsx +185 -0
  206. package/src/components/workflows/workflow-form-view.tsx +1376 -0
  207. package/src/components/workflows/workflow-list.tsx +230 -0
  208. package/src/components/workflows/workflow-status-view.tsx +477 -0
  209. package/src/hooks/use-mobile.ts +19 -0
  210. package/src/instrumentation.ts +7 -0
  211. package/src/lib/agents/claude-agent.ts +737 -0
  212. package/src/lib/agents/execution-manager.ts +27 -0
  213. package/src/lib/agents/profiles/assignment-validation.ts +75 -0
  214. package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
  215. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
  216. package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
  217. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
  218. package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
  219. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
  220. package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
  221. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
  222. package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
  223. package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
  224. package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
  225. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
  226. package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
  227. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
  228. package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
  229. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
  230. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
  231. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
  232. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
  233. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
  234. package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
  235. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
  236. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
  237. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
  238. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
  239. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
  240. package/src/lib/agents/profiles/compatibility.ts +109 -0
  241. package/src/lib/agents/profiles/registry.ts +293 -0
  242. package/src/lib/agents/profiles/test-runner.ts +18 -0
  243. package/src/lib/agents/profiles/test-types.ts +20 -0
  244. package/src/lib/agents/profiles/types.ts +43 -0
  245. package/src/lib/agents/router.ts +56 -0
  246. package/src/lib/agents/runtime/catalog.ts +85 -0
  247. package/src/lib/agents/runtime/claude-sdk.ts +12 -0
  248. package/src/lib/agents/runtime/claude.ts +370 -0
  249. package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
  250. package/src/lib/agents/runtime/index.ts +167 -0
  251. package/src/lib/agents/runtime/openai-codex.ts +1089 -0
  252. package/src/lib/agents/runtime/task-assist-types.ts +8 -0
  253. package/src/lib/agents/runtime/types.ts +30 -0
  254. package/src/lib/constants/settings.ts +13 -0
  255. package/src/lib/constants/status-colors.ts +44 -0
  256. package/src/lib/constants/task-status.ts +49 -0
  257. package/src/lib/data/clear.ts +63 -0
  258. package/src/lib/data/seed-data/documents.ts +715 -0
  259. package/src/lib/data/seed-data/logs.ts +195 -0
  260. package/src/lib/data/seed-data/notifications.ts +141 -0
  261. package/src/lib/data/seed-data/profiles.ts +175 -0
  262. package/src/lib/data/seed-data/projects.ts +61 -0
  263. package/src/lib/data/seed-data/schedules.ts +108 -0
  264. package/src/lib/data/seed-data/tasks.ts +341 -0
  265. package/src/lib/data/seed-data/usage-ledger.ts +130 -0
  266. package/src/lib/data/seed-data/workflows.ts +213 -0
  267. package/src/lib/data/seed.ts +129 -0
  268. package/src/lib/db/index.ts +221 -0
  269. package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
  270. package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
  271. package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
  272. package/src/lib/db/migrations/0003_add_settings.sql +5 -0
  273. package/src/lib/db/migrations/0004_add_documents.sql +20 -0
  274. package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
  275. package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
  276. package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
  277. package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
  278. package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
  279. package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
  280. package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
  281. package/src/lib/db/migrations/meta/_journal.json +27 -0
  282. package/src/lib/db/schema.ts +227 -0
  283. package/src/lib/documents/cleanup.ts +50 -0
  284. package/src/lib/documents/context-builder.ts +75 -0
  285. package/src/lib/documents/output-scanner.ts +166 -0
  286. package/src/lib/documents/processor.ts +120 -0
  287. package/src/lib/documents/processors/image.ts +21 -0
  288. package/src/lib/documents/processors/office.ts +36 -0
  289. package/src/lib/documents/processors/pdf.ts +12 -0
  290. package/src/lib/documents/processors/spreadsheet.ts +18 -0
  291. package/src/lib/documents/processors/text.ts +8 -0
  292. package/src/lib/documents/registry.ts +25 -0
  293. package/src/lib/notifications/actionable.ts +108 -0
  294. package/src/lib/notifications/permissions.ts +169 -0
  295. package/src/lib/queries/chart-data.ts +184 -0
  296. package/src/lib/schedules/interval-parser.ts +110 -0
  297. package/src/lib/schedules/scheduler.ts +220 -0
  298. package/src/lib/settings/auth.ts +98 -0
  299. package/src/lib/settings/budget-guardrails.ts +590 -0
  300. package/src/lib/settings/helpers.ts +23 -0
  301. package/src/lib/settings/openai-auth.ts +80 -0
  302. package/src/lib/settings/permissions.ts +102 -0
  303. package/src/lib/usage/ledger.ts +489 -0
  304. package/src/lib/usage/pricing.ts +68 -0
  305. package/src/lib/utils/crypto.ts +90 -0
  306. package/src/lib/utils/format-timestamp.ts +46 -0
  307. package/src/lib/utils/session-cleanup.ts +26 -0
  308. package/src/lib/utils/stagent-paths.ts +18 -0
  309. package/src/lib/utils.ts +6 -0
  310. package/src/lib/validators/blueprint.ts +43 -0
  311. package/src/lib/validators/profile.ts +64 -0
  312. package/src/lib/validators/project.ts +17 -0
  313. package/src/lib/validators/settings.ts +57 -0
  314. package/src/lib/validators/task.ts +30 -0
  315. package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
  316. package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
  317. package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
  318. package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
  319. package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
  320. package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
  321. package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
  322. package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
  323. package/src/lib/workflows/blueprints/instantiator.ts +131 -0
  324. package/src/lib/workflows/blueprints/registry.ts +128 -0
  325. package/src/lib/workflows/blueprints/template.ts +58 -0
  326. package/src/lib/workflows/blueprints/types.ts +38 -0
  327. package/src/lib/workflows/definition-validation.ts +121 -0
  328. package/src/lib/workflows/engine.ts +1113 -0
  329. package/src/lib/workflows/loop-executor.ts +270 -0
  330. package/src/lib/workflows/parallel.ts +55 -0
  331. package/src/lib/workflows/swarm.ts +97 -0
  332. package/src/lib/workflows/types.ts +112 -0
  333. package/tsconfig.json +41 -0
@@ -0,0 +1,235 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useRef } 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, Upload, Trash2, Search } from "lucide-react";
15
+ import { toast } from "sonner";
16
+ import { DocumentTable } from "./document-table";
17
+ import { DocumentGrid } from "./document-grid";
18
+ import { DocumentUploadDialog } from "./document-upload-dialog";
19
+ import type { DocumentWithRelations } from "./types";
20
+
21
+ interface DocumentBrowserProps {
22
+ initialDocuments: DocumentWithRelations[];
23
+ projects: { id: string; name: string }[];
24
+ }
25
+
26
+ export function DocumentBrowser({
27
+ initialDocuments,
28
+ projects,
29
+ }: DocumentBrowserProps) {
30
+ const [docs, setDocs] = useState(initialDocuments);
31
+ const [view, setView] = useState<"table" | "grid">("table");
32
+ const [search, setSearch] = useState("");
33
+ const [statusFilter, setStatusFilter] = useState<string>("all");
34
+ const [directionFilter, setDirectionFilter] = useState<string>("all");
35
+ const [projectFilter, setProjectFilter] = useState<string>("all");
36
+ const router = useRouter();
37
+ const [selected, setSelected] = useState<Set<string>>(new Set());
38
+ const [uploadOpen, setUploadOpen] = useState(false);
39
+ const [deleting, setDeleting] = useState(false);
40
+ const uploadButtonRef = useRef<HTMLButtonElement>(null);
41
+
42
+ const refresh = useCallback(async () => {
43
+ try {
44
+ const res = await fetch("/api/documents");
45
+ if (res.ok) {
46
+ const data = await res.json();
47
+ setDocs(data);
48
+ }
49
+ } catch {
50
+ // Silent refresh failure
51
+ }
52
+ }, []);
53
+
54
+ const filtered = docs.filter((doc) => {
55
+ if (
56
+ search &&
57
+ !doc.originalName.toLowerCase().includes(search.toLowerCase()) &&
58
+ !(doc.extractedText ?? "").toLowerCase().includes(search.toLowerCase())
59
+ ) {
60
+ return false;
61
+ }
62
+ if (statusFilter !== "all" && doc.status !== statusFilter) return false;
63
+ if (directionFilter !== "all" && doc.direction !== directionFilter) return false;
64
+ if (projectFilter !== "all" && doc.projectId !== projectFilter) return false;
65
+ return true;
66
+ });
67
+
68
+ function toggleSelect(id: string) {
69
+ setSelected((prev) => {
70
+ const next = new Set(prev);
71
+ if (next.has(id)) next.delete(id);
72
+ else next.add(id);
73
+ return next;
74
+ });
75
+ }
76
+
77
+ function toggleSelectAll() {
78
+ if (selected.size === filtered.length) {
79
+ setSelected(new Set());
80
+ } else {
81
+ setSelected(new Set(filtered.map((d) => d.id)));
82
+ }
83
+ }
84
+
85
+ async function handleBulkDelete() {
86
+ if (selected.size === 0) return;
87
+ setDeleting(true);
88
+ let deleted = 0;
89
+ for (const id of selected) {
90
+ try {
91
+ const res = await fetch(`/api/documents/${id}`, { method: "DELETE" });
92
+ if (res.ok) deleted++;
93
+ } catch {
94
+ // Continue with remaining
95
+ }
96
+ }
97
+ toast.success(`Deleted ${deleted} document${deleted !== 1 ? "s" : ""}`);
98
+ setSelected(new Set());
99
+ setDeleting(false);
100
+ await refresh();
101
+ }
102
+
103
+ return (
104
+ <>
105
+ <div className="flex items-center justify-between">
106
+ <h1 className="text-2xl font-bold tracking-tight">Documents</h1>
107
+ <Button ref={uploadButtonRef} onClick={() => setUploadOpen(true)} size="sm">
108
+ <Upload className="h-4 w-4 mr-1.5" />
109
+ Upload
110
+ </Button>
111
+ </div>
112
+
113
+ <div className="flex flex-wrap items-center gap-2">
114
+ <div className="relative flex-1 min-w-[200px]">
115
+ <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
116
+ <Input
117
+ placeholder="Search by name or content..."
118
+ value={search}
119
+ onChange={(e) => setSearch(e.target.value)}
120
+ className="pl-9"
121
+ />
122
+ </div>
123
+
124
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
125
+ <SelectTrigger className="w-[140px]">
126
+ <SelectValue placeholder="Status" />
127
+ </SelectTrigger>
128
+ <SelectContent>
129
+ <SelectItem value="all">All statuses</SelectItem>
130
+ <SelectItem value="uploaded">Uploaded</SelectItem>
131
+ <SelectItem value="processing">Processing</SelectItem>
132
+ <SelectItem value="ready">Ready</SelectItem>
133
+ <SelectItem value="error">Error</SelectItem>
134
+ </SelectContent>
135
+ </Select>
136
+
137
+ <Select value={directionFilter} onValueChange={setDirectionFilter}>
138
+ <SelectTrigger className="w-[150px]">
139
+ <SelectValue placeholder="Direction" />
140
+ </SelectTrigger>
141
+ <SelectContent>
142
+ <SelectItem value="all">All directions</SelectItem>
143
+ <SelectItem value="input">Inputs</SelectItem>
144
+ <SelectItem value="output">Outputs</SelectItem>
145
+ </SelectContent>
146
+ </Select>
147
+
148
+ <Select value={projectFilter} onValueChange={setProjectFilter}>
149
+ <SelectTrigger className="w-[160px]">
150
+ <SelectValue placeholder="Project" />
151
+ </SelectTrigger>
152
+ <SelectContent>
153
+ <SelectItem value="all">All projects</SelectItem>
154
+ {projects.map((p) => (
155
+ <SelectItem key={p.id} value={p.id}>
156
+ {p.name}
157
+ </SelectItem>
158
+ ))}
159
+ </SelectContent>
160
+ </Select>
161
+
162
+ <div className="flex items-center gap-1 border border-border rounded-md p-0.5">
163
+ <Button
164
+ variant={view === "table" ? "secondary" : "ghost"}
165
+ size="icon"
166
+ className="h-7 w-7"
167
+ onClick={() => setView("table")}
168
+ aria-label="Table view"
169
+ >
170
+ <LayoutList className="h-3.5 w-3.5" />
171
+ </Button>
172
+ <Button
173
+ variant={view === "grid" ? "secondary" : "ghost"}
174
+ size="icon"
175
+ className="h-7 w-7"
176
+ onClick={() => setView("grid")}
177
+ aria-label="Grid view"
178
+ >
179
+ <LayoutGrid className="h-3.5 w-3.5" />
180
+ </Button>
181
+ </div>
182
+
183
+ {selected.size > 0 && (
184
+ <Button
185
+ variant="destructive"
186
+ size="sm"
187
+ onClick={handleBulkDelete}
188
+ disabled={deleting}
189
+ >
190
+ <Trash2 className="h-3.5 w-3.5 mr-1.5" />
191
+ Delete {selected.size}
192
+ </Button>
193
+ )}
194
+ </div>
195
+
196
+ {filtered.length === 0 ? (
197
+ <div className="text-center py-16 text-muted-foreground">
198
+ {docs.length === 0 ? (
199
+ <>
200
+ <p className="text-lg font-medium mb-1">No documents yet</p>
201
+ <p className="text-sm">Upload files to get started.</p>
202
+ </>
203
+ ) : (
204
+ <>
205
+ <p className="text-lg font-medium mb-1">No matching documents</p>
206
+ <p className="text-sm">Try adjusting your filters or search.</p>
207
+ </>
208
+ )}
209
+ </div>
210
+ ) : view === "table" ? (
211
+ <DocumentTable
212
+ documents={filtered}
213
+ selected={selected}
214
+ onToggleSelect={toggleSelect}
215
+ onToggleSelectAll={toggleSelectAll}
216
+ onOpen={(doc) => router.push(`/documents/${doc.id}`)}
217
+ />
218
+ ) : (
219
+ <DocumentGrid
220
+ documents={filtered}
221
+ selected={selected}
222
+ onToggleSelect={toggleSelect}
223
+ onOpen={(doc) => router.push(`/documents/${doc.id}`)}
224
+ />
225
+ )}
226
+
227
+ <DocumentUploadDialog
228
+ open={uploadOpen}
229
+ onClose={() => setUploadOpen(false)}
230
+ onUploaded={refresh}
231
+ restoreFocusElement={uploadButtonRef.current}
232
+ />
233
+ </>
234
+ );
235
+ }
@@ -0,0 +1,367 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Skeleton } from "@/components/ui/skeleton";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "@/components/ui/select";
16
+ import {
17
+ Download,
18
+ Trash2,
19
+ Unlink,
20
+ HardDrive,
21
+ Clock,
22
+ ArrowUpRight,
23
+ ArrowDownLeft,
24
+ Link2,
25
+ FolderKanban,
26
+ FileText,
27
+ AlertTriangle,
28
+ } from "lucide-react";
29
+ import { toast } from "sonner";
30
+ import { DocumentPreview } from "./document-preview";
31
+ import { getFileIcon, formatSize, getStatusColor, formatRelativeTime } from "./utils";
32
+ import type { DocumentWithRelations } from "./types";
33
+
34
+ /** Serialized version of DocumentWithRelations (Date fields become strings from server) */
35
+ type SerializedDocument = Omit<DocumentWithRelations, "createdAt" | "updatedAt"> & {
36
+ createdAt: string | Date;
37
+ updatedAt: string | Date;
38
+ };
39
+
40
+ interface DocumentDetailViewProps {
41
+ documentId: string;
42
+ initialDocument?: SerializedDocument;
43
+ }
44
+
45
+ function getStatusDotColor(status: string): string {
46
+ switch (status) {
47
+ case "ready":
48
+ return "bg-status-completed";
49
+ case "processing":
50
+ return "bg-status-running";
51
+ case "error":
52
+ return "bg-status-failed";
53
+ case "uploaded":
54
+ return "bg-status-warning";
55
+ default:
56
+ return "bg-muted-foreground";
57
+ }
58
+ }
59
+
60
+ export function DocumentDetailView({ documentId, initialDocument }: DocumentDetailViewProps) {
61
+ const router = useRouter();
62
+ const [doc, setDoc] = useState<DocumentWithRelations | null>(
63
+ initialDocument ? (initialDocument as unknown as DocumentWithRelations) : null
64
+ );
65
+ const [loaded, setLoaded] = useState(!!initialDocument);
66
+ const [projects, setProjects] = useState<{ id: string; name: string }[]>([]);
67
+ const [deleting, setDeleting] = useState(false);
68
+ const [linking, setLinking] = useState(false);
69
+
70
+ const refresh = useCallback(async () => {
71
+ try {
72
+ const res = await fetch(`/api/documents/${documentId}`);
73
+ if (res.ok) {
74
+ setDoc(await res.json());
75
+ }
76
+ } catch {
77
+ // silent
78
+ }
79
+ setLoaded(true);
80
+ }, [documentId]);
81
+
82
+ useEffect(() => {
83
+ // If server provided initial data, only fetch supplementary data (projects list)
84
+ // and enrich with relation names in the background
85
+ if (!initialDocument) {
86
+ refresh();
87
+ } else {
88
+ // Background refresh to fill in taskTitle/projectName relation fields
89
+ refresh();
90
+ }
91
+ fetch("/api/projects")
92
+ .then((r) => r.ok ? r.json() : [])
93
+ .then(setProjects)
94
+ .catch(() => {});
95
+ }, [refresh, initialDocument]);
96
+
97
+ async function handleDelete() {
98
+ if (!doc) return;
99
+ setDeleting(true);
100
+ try {
101
+ const res = await fetch(`/api/documents/${doc.id}`, { method: "DELETE" });
102
+ if (res.ok) {
103
+ toast.success("Document deleted");
104
+ router.push("/documents");
105
+ } else {
106
+ toast.error("Failed to delete document");
107
+ }
108
+ } catch {
109
+ toast.error("Network error");
110
+ } finally {
111
+ setDeleting(false);
112
+ }
113
+ }
114
+
115
+ async function handleLinkProject(projectId: string) {
116
+ if (!doc) return;
117
+ setLinking(true);
118
+ try {
119
+ const value = projectId === "none" ? null : projectId;
120
+ const res = await fetch(`/api/documents/${doc.id}`, {
121
+ method: "PATCH",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify({ projectId: value }),
124
+ });
125
+ if (res.ok) {
126
+ toast.success(value ? "Linked to project" : "Unlinked from project");
127
+ refresh();
128
+ }
129
+ } catch {
130
+ toast.error("Failed to update link");
131
+ } finally {
132
+ setLinking(false);
133
+ }
134
+ }
135
+
136
+ async function handleUnlinkTask() {
137
+ if (!doc) return;
138
+ try {
139
+ const res = await fetch(`/api/documents/${doc.id}`, {
140
+ method: "PATCH",
141
+ headers: { "Content-Type": "application/json" },
142
+ body: JSON.stringify({ taskId: null }),
143
+ });
144
+ if (res.ok) {
145
+ toast.success("Unlinked from task");
146
+ refresh();
147
+ }
148
+ } catch {
149
+ toast.error("Failed to unlink");
150
+ }
151
+ }
152
+
153
+ if (!loaded) {
154
+ return (
155
+ <div className="space-y-4">
156
+ <Skeleton className="h-8 w-64" />
157
+ <Skeleton className="h-32 w-full" />
158
+ <Skeleton className="h-48 w-full" />
159
+ </div>
160
+ );
161
+ }
162
+
163
+ if (!doc) {
164
+ return <p className="text-muted-foreground">Document not found.</p>;
165
+ }
166
+
167
+ const Icon = getFileIcon(doc.mimeType);
168
+ const DirectionIcon = doc.direction === "output" ? ArrowUpRight : ArrowDownLeft;
169
+ const wordCount = doc.extractedText
170
+ ? doc.extractedText.split(/\s+/).filter(Boolean).length
171
+ : 0;
172
+
173
+ return (
174
+ <div className="space-y-6" aria-live="polite">
175
+ {/* Header */}
176
+ <div className="flex items-center justify-between">
177
+ <div className="flex items-center gap-3 min-w-0">
178
+ <Icon className="h-6 w-6 text-muted-foreground shrink-0" />
179
+ <h1 className="text-2xl font-bold truncate">{doc.originalName}</h1>
180
+ </div>
181
+ <div className="flex items-center gap-2">
182
+ <Button variant="outline" size="sm" asChild>
183
+ <a
184
+ href={`/api/documents/${doc.id}/file`}
185
+ target="_blank"
186
+ rel="noopener noreferrer"
187
+ >
188
+ <Download className="h-3.5 w-3.5 mr-1" />
189
+ Download
190
+ </a>
191
+ </Button>
192
+ <Button
193
+ variant="destructive"
194
+ size="sm"
195
+ onClick={handleDelete}
196
+ disabled={deleting}
197
+ >
198
+ <Trash2 className="h-3.5 w-3.5 mr-1" />
199
+ Delete
200
+ </Button>
201
+ </div>
202
+ </div>
203
+
204
+ {/* Bento Grid: Extracted Text + Metadata + Links */}
205
+ <div className="grid grid-cols-1 md:grid-cols-[2fr_1fr] gap-4">
206
+ {/* Extracted Text — spans 2 rows on desktop */}
207
+ {doc.extractedText ? (
208
+ <Card className="md:row-span-2">
209
+ <CardHeader className="pb-2">
210
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
211
+ <FileText className="h-4 w-4 text-muted-foreground" />
212
+ Extracted Text
213
+ <Badge variant="secondary" className="text-xs ml-auto">
214
+ {wordCount.toLocaleString()} words
215
+ </Badge>
216
+ </CardTitle>
217
+ </CardHeader>
218
+ <CardContent>
219
+ <pre className="text-xs bg-muted p-3 rounded-md max-h-80 overflow-y-auto whitespace-pre-wrap break-words">
220
+ {doc.extractedText.slice(0, 2000)}
221
+ {doc.extractedText.length > 2000 && "\n\n... (truncated)"}
222
+ </pre>
223
+ </CardContent>
224
+ </Card>
225
+ ) : (
226
+ <Card className="md:row-span-2 flex items-center justify-center">
227
+ <CardContent className="pt-4 text-center">
228
+ <FileText className="h-8 w-8 text-muted-foreground/40 mx-auto mb-2" />
229
+ <p className="text-sm text-muted-foreground">No extracted text</p>
230
+ </CardContent>
231
+ </Card>
232
+ )}
233
+
234
+ {/* Metadata Strip */}
235
+ <Card>
236
+ <CardHeader className="pb-2">
237
+ <CardTitle className="text-sm font-medium">Details</CardTitle>
238
+ </CardHeader>
239
+ <CardContent className="space-y-3">
240
+ {/* Type */}
241
+ <div className="flex items-center gap-2 text-sm">
242
+ <Icon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
243
+ <Badge variant="outline" className="text-xs font-normal">
244
+ {doc.mimeType}
245
+ </Badge>
246
+ </div>
247
+ {/* Size */}
248
+ <div className="flex items-center gap-2 text-sm">
249
+ <HardDrive className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
250
+ <span>{formatSize(doc.size)}</span>
251
+ </div>
252
+ {/* Status */}
253
+ <div className="flex items-center gap-2 text-sm">
254
+ <span className={`w-2 h-2 rounded-full shrink-0 ${getStatusDotColor(doc.status)}`} />
255
+ <Badge variant="outline" className={`text-xs ${getStatusColor(doc.status)}`}>
256
+ {doc.status}
257
+ </Badge>
258
+ </div>
259
+ {/* Direction */}
260
+ <div className="flex items-center gap-2 text-sm">
261
+ <DirectionIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
262
+ <span className="capitalize">{doc.direction}</span>
263
+ </div>
264
+ {doc.direction === "output" && (
265
+ <div className="flex items-center gap-2 text-sm">
266
+ <Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
267
+ <span>Version {doc.version}</span>
268
+ </div>
269
+ )}
270
+ {/* Date */}
271
+ <div className="flex items-center gap-2 text-sm">
272
+ <Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
273
+ <span title={new Date(doc.createdAt).toLocaleString()}>
274
+ {formatRelativeTime(typeof doc.createdAt === "number" ? doc.createdAt : new Date(doc.createdAt).getTime())}
275
+ </span>
276
+ </div>
277
+ </CardContent>
278
+ </Card>
279
+
280
+ {/* Links */}
281
+ <Card>
282
+ <CardHeader className="pb-2">
283
+ <CardTitle className="text-sm font-medium">Links</CardTitle>
284
+ </CardHeader>
285
+ <CardContent className="space-y-3">
286
+ {/* Task link */}
287
+ <div>
288
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
289
+ <Link2 className="h-3 w-3" />
290
+ <span>Task</span>
291
+ </div>
292
+ {doc.taskTitle ? (
293
+ <div className="flex items-center justify-between text-sm">
294
+ <span className="truncate">{doc.taskTitle}</span>
295
+ <Button
296
+ variant="ghost"
297
+ size="sm"
298
+ onClick={handleUnlinkTask}
299
+ aria-label="Unlink from task"
300
+ className="h-7 px-2"
301
+ >
302
+ <Unlink className="h-3 w-3" />
303
+ </Button>
304
+ </div>
305
+ ) : (
306
+ <p className="text-sm text-muted-foreground">Not linked</p>
307
+ )}
308
+ </div>
309
+ {/* Project selector */}
310
+ <div>
311
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
312
+ <FolderKanban className="h-3 w-3" />
313
+ <span>Project</span>
314
+ </div>
315
+ <Select
316
+ value={doc.projectId ?? "none"}
317
+ onValueChange={handleLinkProject}
318
+ disabled={linking}
319
+ >
320
+ <SelectTrigger className="w-full h-8 text-sm">
321
+ <SelectValue placeholder="Select project" />
322
+ </SelectTrigger>
323
+ <SelectContent>
324
+ <SelectItem value="none">No project</SelectItem>
325
+ {projects.map((p) => (
326
+ <SelectItem key={p.id} value={p.id}>
327
+ {p.name}
328
+ </SelectItem>
329
+ ))}
330
+ </SelectContent>
331
+ </Select>
332
+ </div>
333
+ </CardContent>
334
+ </Card>
335
+ </div>
336
+
337
+ {/* Preview — collapsible, full width */}
338
+ <details className="group">
339
+ <summary className="flex items-center gap-2 cursor-pointer list-none text-sm font-medium p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors">
340
+ <Icon className="h-4 w-4 text-muted-foreground" />
341
+ <span>Preview</span>
342
+ <span className="text-muted-foreground text-xs group-open:rotate-90 transition-transform">▶</span>
343
+ </summary>
344
+ <div className="mt-2">
345
+ <Card>
346
+ <CardContent className="pt-4">
347
+ <DocumentPreview document={doc} />
348
+ </CardContent>
349
+ </Card>
350
+ </div>
351
+ </details>
352
+
353
+ {/* Processing Error — conditional, red accent */}
354
+ {doc.processingError && (
355
+ <div className="rounded-lg border border-destructive/30 border-l-2 border-l-destructive bg-card p-4">
356
+ <div className="flex items-start gap-2">
357
+ <AlertTriangle className="h-4 w-4 text-destructive shrink-0 mt-0.5" />
358
+ <div>
359
+ <p className="text-sm font-medium text-destructive">Processing Error</p>
360
+ <p className="text-xs text-muted-foreground mt-1">{doc.processingError}</p>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ )}
365
+ </div>
366
+ );
367
+ }
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { Checkbox } from "@/components/ui/checkbox";
5
+ import { Card } from "@/components/ui/card";
6
+ import { getFileIcon, formatSize, getStatusColor } from "./utils";
7
+ import type { DocumentWithRelations } from "./types";
8
+
9
+ interface DocumentGridProps {
10
+ documents: DocumentWithRelations[];
11
+ selected: Set<string>;
12
+ onToggleSelect: (id: string) => void;
13
+ onOpen: (doc: DocumentWithRelations) => void;
14
+ }
15
+
16
+ export function DocumentGrid({
17
+ documents,
18
+ selected,
19
+ onToggleSelect,
20
+ onOpen,
21
+ }: DocumentGridProps) {
22
+ return (
23
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
24
+ {documents.map((doc) => {
25
+ const Icon = getFileIcon(doc.mimeType);
26
+ const isImage = doc.mimeType.startsWith("image/");
27
+
28
+ return (
29
+ <Card
30
+ key={doc.id}
31
+ className="group relative p-3 gap-0 cursor-pointer transition-colors"
32
+ onClick={() => onOpen(doc)}
33
+ >
34
+ <div
35
+ className="absolute top-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity"
36
+ onClick={(e) => e.stopPropagation()}
37
+ >
38
+ <Checkbox
39
+ checked={selected.has(doc.id)}
40
+ onCheckedChange={() => onToggleSelect(doc.id)}
41
+ aria-label={`Select ${doc.originalName}`}
42
+ />
43
+ </div>
44
+
45
+ <div className="flex flex-col items-center gap-2 py-3">
46
+ {isImage ? (
47
+ <img
48
+ src={`/api/documents/${doc.id}/file?inline=1`}
49
+ alt={doc.originalName}
50
+ className="h-16 w-16 object-cover rounded"
51
+ />
52
+ ) : (
53
+ <Icon className="h-10 w-10 text-muted-foreground" />
54
+ )}
55
+ </div>
56
+
57
+ <p className="text-sm font-medium truncate">{doc.originalName}</p>
58
+ <div className="flex items-center justify-between mt-1">
59
+ <span className="text-xs text-muted-foreground">
60
+ {formatSize(doc.size)}
61
+ </span>
62
+ <Badge
63
+ variant="outline"
64
+ className={`text-[10px] px-1 py-0 ${getStatusColor(doc.status)}`}
65
+ >
66
+ {doc.status}
67
+ </Badge>
68
+ </div>
69
+ <div className="mt-1 flex items-center justify-between text-[10px] text-muted-foreground">
70
+ <span className="capitalize">{doc.direction}</span>
71
+ {doc.direction === "output" && <span>v{doc.version}</span>}
72
+ </div>
73
+ </Card>
74
+ );
75
+ })}
76
+ </div>
77
+ );
78
+ }