@larkup/workspace-app 0.0.0-stage → 0.2.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 (440) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +4 -2
  3. package/assets/github.svg +3 -0
  4. package/assets/icons/Confluence.png +0 -0
  5. package/assets/icons/Instagram.png +0 -0
  6. package/assets/icons/Northflank.png +0 -0
  7. package/assets/icons/Salesforce.png +0 -0
  8. package/assets/icons/agentskills.png +0 -0
  9. package/assets/icons/airtable.png +0 -0
  10. package/assets/icons/asana.png +0 -0
  11. package/assets/icons/ashby.png +0 -0
  12. package/assets/icons/audio.png +0 -0
  13. package/assets/icons/aws-kendra.png +0 -0
  14. package/assets/icons/aws.svg +38 -0
  15. package/assets/icons/azure-ai-search.png +0 -0
  16. package/assets/icons/azure.svg +1 -0
  17. package/assets/icons/bigquery.png +0 -0
  18. package/assets/icons/bing.png +0 -0
  19. package/assets/icons/bitbucket.png +0 -0
  20. package/assets/icons/brave.png +0 -0
  21. package/assets/icons/browserbase.png +0 -0
  22. package/assets/icons/calendly.png +0 -0
  23. package/assets/icons/chroma.png +0 -0
  24. package/assets/icons/cloudflare.png +0 -0
  25. package/assets/icons/cohere.svg +30 -0
  26. package/assets/icons/databricks.png +0 -0
  27. package/assets/icons/daytonaio.png +0 -0
  28. package/assets/icons/daytonaio2.png +0 -0
  29. package/assets/icons/deepgram.png +0 -0
  30. package/assets/icons/deepl.png +0 -0
  31. package/assets/icons/digital-ocean.webp +0 -0
  32. package/assets/icons/discord.png +0 -0
  33. package/assets/icons/e2b-2.png +0 -0
  34. package/assets/icons/e2b.png +0 -0
  35. package/assets/icons/elevenlabs.png +0 -0
  36. package/assets/icons/email.png +0 -0
  37. package/assets/icons/exa.png +0 -0
  38. package/assets/icons/excel.png +0 -0
  39. package/assets/icons/fal.png +0 -0
  40. package/assets/icons/firecrawl.png +0 -0
  41. package/assets/icons/firecrawl2.png +0 -0
  42. package/assets/icons/flyio.jpeg +0 -0
  43. package/assets/icons/gcp.svg +1 -0
  44. package/assets/icons/gemini.svg +1 -0
  45. package/assets/icons/gmail.png +0 -0
  46. package/assets/icons/google-analytics.png +0 -0
  47. package/assets/icons/google-calender.png +0 -0
  48. package/assets/icons/google-docs.png +0 -0
  49. package/assets/icons/google-drive.png +0 -0
  50. package/assets/icons/google-maps.png +0 -0
  51. package/assets/icons/google-meet.png +0 -0
  52. package/assets/icons/google-sheets.png +0 -0
  53. package/assets/icons/google-slides.png +0 -0
  54. package/assets/icons/google-tasks.png +0 -0
  55. package/assets/icons/google.png +0 -0
  56. package/assets/icons/grok.png +0 -0
  57. package/assets/icons/hubspot.png +0 -0
  58. package/assets/icons/image.png +0 -0
  59. package/assets/icons/jina.svg +1 -0
  60. package/assets/icons/jira.png +0 -0
  61. package/assets/icons/lancedb2.png +0 -0
  62. package/assets/icons/linear.png +0 -0
  63. package/assets/icons/linux.png +0 -0
  64. package/assets/icons/looker.png +0 -0
  65. package/assets/icons/luma.png +0 -0
  66. package/assets/icons/mac.png +0 -0
  67. package/assets/icons/markdown.png +0 -0
  68. package/assets/icons/mcp.png +0 -0
  69. package/assets/icons/mcp.svg +1 -0
  70. package/assets/icons/messenger.svg +1 -0
  71. package/assets/icons/microsoft-intra.png +0 -0
  72. package/assets/icons/microsoft-onenote.png +0 -0
  73. package/assets/icons/microsoft-planner.png +0 -0
  74. package/assets/icons/microsoft-teams.png +0 -0
  75. package/assets/icons/miro.png +0 -0
  76. package/assets/icons/mistral.svg +469 -0
  77. package/assets/icons/modal.png +0 -0
  78. package/assets/icons/ngrok.png +0 -0
  79. package/assets/icons/nomic.png +0 -0
  80. package/assets/icons/onedrive.png +0 -0
  81. package/assets/icons/openai.svg +1 -0
  82. package/assets/icons/outlook-calendar.png +0 -0
  83. package/assets/icons/outlook-email.png +0 -0
  84. package/assets/icons/outlook.png +0 -0
  85. package/assets/icons/pdf.png +0 -0
  86. package/assets/icons/personio.png +0 -0
  87. package/assets/icons/pgvector2.png +0 -0
  88. package/assets/icons/phone.png +0 -0
  89. package/assets/icons/pinecone.png +0 -0
  90. package/assets/icons/pipedrive.png +0 -0
  91. package/assets/icons/power-pi.png +0 -0
  92. package/assets/icons/pylon.png +0 -0
  93. package/assets/icons/python.png +0 -0
  94. package/assets/icons/qdrant.svg +35 -0
  95. package/assets/icons/revai.png +0 -0
  96. package/assets/icons/revai.svg +1 -0
  97. package/assets/icons/sap.png +0 -0
  98. package/assets/icons/serpapi.svg +34 -0
  99. package/assets/icons/serper.png +0 -0
  100. package/assets/icons/servicenow.png +0 -0
  101. package/assets/icons/sharepoint.png +0 -0
  102. package/assets/icons/slack.png +0 -0
  103. package/assets/icons/snowflake.png +0 -0
  104. package/assets/icons/statistia.png +0 -0
  105. package/assets/icons/stripe.png +0 -0
  106. package/assets/icons/supabase.png +0 -0
  107. package/assets/icons/tableau.png +0 -0
  108. package/assets/icons/tavily.png +0 -0
  109. package/assets/icons/telegram.png +0 -0
  110. package/assets/icons/typescript.png +0 -0
  111. package/assets/icons/vercel copy.svg +1 -0
  112. package/assets/icons/vercel.svg +1 -0
  113. package/assets/icons/vertex-ai.png +0 -0
  114. package/assets/icons/video.png +0 -0
  115. package/assets/icons/voyage-light.png +0 -0
  116. package/assets/icons/weaviate.webp +0 -0
  117. package/assets/icons/webcontainers.svg +11 -0
  118. package/assets/icons/webhook.png +0 -0
  119. package/assets/icons/whatsapp copy.png +0 -0
  120. package/assets/icons/whatsapp.png +0 -0
  121. package/assets/icons/windows.png +0 -0
  122. package/assets/icons/word.png +0 -0
  123. package/assets/icons/zendesk.png +0 -0
  124. package/assets/icons/zoom.png +0 -0
  125. package/assets/notion-white.png +0 -0
  126. package/assets/notion.png +0 -0
  127. package/dist-types/asset-server.d.ts +3 -0
  128. package/dist-types/asset-server.d.ts.map +1 -0
  129. package/dist-types/components/alerts/generic-alert.d.ts +15 -0
  130. package/dist-types/components/alerts/generic-alert.d.ts.map +1 -0
  131. package/dist-types/components/chat/canvas/doc-editor-provider.d.ts +62 -0
  132. package/dist-types/components/chat/canvas/doc-editor-provider.d.ts.map +1 -0
  133. package/dist-types/components/chat/canvas/document-canvas.d.ts +7 -0
  134. package/dist-types/components/chat/canvas/document-canvas.d.ts.map +1 -0
  135. package/dist-types/components/chat/canvas/document-field.d.ts +13 -0
  136. package/dist-types/components/chat/canvas/document-field.d.ts.map +1 -0
  137. package/dist-types/components/chat/canvas/document-viewer.d.ts +2 -0
  138. package/dist-types/components/chat/canvas/document-viewer.d.ts.map +1 -0
  139. package/dist-types/components/chat/canvas/file-attachment-button.d.ts +9 -0
  140. package/dist-types/components/chat/canvas/file-attachment-button.d.ts.map +1 -0
  141. package/dist-types/components/chat/chat-workspace.d.ts +4 -0
  142. package/dist-types/components/chat/chat-workspace.d.ts.map +1 -0
  143. package/dist-types/components/chat/message-actions.d.ts +11 -0
  144. package/dist-types/components/chat/message-actions.d.ts.map +1 -0
  145. package/dist-types/components/chat/message-item.d.ts +20 -0
  146. package/dist-types/components/chat/message-item.d.ts.map +1 -0
  147. package/dist-types/components/chat/message-queue.d.ts +15 -0
  148. package/dist-types/components/chat/message-queue.d.ts.map +1 -0
  149. package/dist-types/components/chat/tools/chat-chart.d.ts +6 -0
  150. package/dist-types/components/chat/tools/chat-chart.d.ts.map +1 -0
  151. package/dist-types/components/chat/tools/chat-citations.d.ts +32 -0
  152. package/dist-types/components/chat/tools/chat-citations.d.ts.map +1 -0
  153. package/dist-types/components/chat/tools/chat-data-table.d.ts +14 -0
  154. package/dist-types/components/chat/tools/chat-data-table.d.ts.map +1 -0
  155. package/dist-types/components/chat/tools/chat-file-export.d.ts +5 -0
  156. package/dist-types/components/chat/tools/chat-file-export.d.ts.map +1 -0
  157. package/dist-types/components/chat/tools/chat-media-preview.d.ts +35 -0
  158. package/dist-types/components/chat/tools/chat-media-preview.d.ts.map +1 -0
  159. package/dist-types/components/chat/tools/chat-sandbox-result.d.ts +17 -0
  160. package/dist-types/components/chat/tools/chat-sandbox-result.d.ts.map +1 -0
  161. package/dist-types/components/chat/tools/chat-signature-request.d.ts +13 -0
  162. package/dist-types/components/chat/tools/chat-signature-request.d.ts.map +1 -0
  163. package/dist-types/components/chat/tools/chat-tabs.d.ts +17 -0
  164. package/dist-types/components/chat/tools/chat-tabs.d.ts.map +1 -0
  165. package/dist-types/components/chat/tools/corpus-data-result.d.ts +36 -0
  166. package/dist-types/components/chat/tools/corpus-data-result.d.ts.map +1 -0
  167. package/dist-types/components/chat/tools/knowledge-base-result.d.ts +14 -0
  168. package/dist-types/components/chat/tools/knowledge-base-result.d.ts.map +1 -0
  169. package/dist-types/components/chat/tools/visual-placement-modal.d.ts +23 -0
  170. package/dist-types/components/chat/tools/visual-placement-modal.d.ts.map +1 -0
  171. package/dist-types/components/configure/custom-model-modal.d.ts +13 -0
  172. package/dist-types/components/configure/custom-model-modal.d.ts.map +1 -0
  173. package/dist-types/components/configure/store-fields.d.ts +11 -0
  174. package/dist-types/components/configure/store-fields.d.ts.map +1 -0
  175. package/dist-types/components/data/corpus-panel.d.ts +12 -0
  176. package/dist-types/components/data/corpus-panel.d.ts.map +1 -0
  177. package/dist-types/components/data/data-entry-mode-switch.d.ts +13 -0
  178. package/dist-types/components/data/data-entry-mode-switch.d.ts.map +1 -0
  179. package/dist-types/components/data/data-primary-action.d.ts +7 -0
  180. package/dist-types/components/data/data-primary-action.d.ts.map +1 -0
  181. package/dist-types/components/data/data-workspace.d.ts +6 -0
  182. package/dist-types/components/data/data-workspace.d.ts.map +1 -0
  183. package/dist-types/components/data/groups-panel.d.ts +5 -0
  184. package/dist-types/components/data/groups-panel.d.ts.map +1 -0
  185. package/dist-types/components/data/integration-resources-panel.d.ts +9 -0
  186. package/dist-types/components/data/integration-resources-panel.d.ts.map +1 -0
  187. package/dist-types/components/data/integrations-panel.d.ts +5 -0
  188. package/dist-types/components/data/integrations-panel.d.ts.map +1 -0
  189. package/dist-types/components/data/jobs-panel.d.ts +6 -0
  190. package/dist-types/components/data/jobs-panel.d.ts.map +1 -0
  191. package/dist-types/components/data/jobs-tracking-button.d.ts +2 -0
  192. package/dist-types/components/data/jobs-tracking-button.d.ts.map +1 -0
  193. package/dist-types/components/data/live-media-progress.d.ts +9 -0
  194. package/dist-types/components/data/live-media-progress.d.ts.map +1 -0
  195. package/dist-types/components/data/media-indexing-jobs-panel.d.ts +34 -0
  196. package/dist-types/components/data/media-indexing-jobs-panel.d.ts.map +1 -0
  197. package/dist-types/components/data/media-panel.d.ts +58 -0
  198. package/dist-types/components/data/media-panel.d.ts.map +1 -0
  199. package/dist-types/components/data/notion-panel.d.ts +6 -0
  200. package/dist-types/components/data/notion-panel.d.ts.map +1 -0
  201. package/dist-types/components/data/paste-panel.d.ts +7 -0
  202. package/dist-types/components/data/paste-panel.d.ts.map +1 -0
  203. package/dist-types/components/data/project-data-page.d.ts +3 -0
  204. package/dist-types/components/data/project-data-page.d.ts.map +1 -0
  205. package/dist-types/components/data/scrape-panel.d.ts +9 -0
  206. package/dist-types/components/data/scrape-panel.d.ts.map +1 -0
  207. package/dist-types/components/data/tabular-preview.d.ts +10 -0
  208. package/dist-types/components/data/tabular-preview.d.ts.map +1 -0
  209. package/dist-types/components/data/tool-indexing-dialog.d.ts +54 -0
  210. package/dist-types/components/data/tool-indexing-dialog.d.ts.map +1 -0
  211. package/dist-types/components/data/upload-panel.d.ts +7 -0
  212. package/dist-types/components/data/upload-panel.d.ts.map +1 -0
  213. package/dist-types/components/data/web-crawler-status.d.ts +3 -0
  214. package/dist-types/components/data/web-crawler-status.d.ts.map +1 -0
  215. package/dist-types/components/projects/project-form-dialog.d.ts +10 -0
  216. package/dist-types/components/projects/project-form-dialog.d.ts.map +1 -0
  217. package/dist-types/components/projects/project-provider.d.ts +34 -0
  218. package/dist-types/components/projects/project-provider.d.ts.map +1 -0
  219. package/dist-types/components/settings/general/agent-customization-section.d.ts +2 -0
  220. package/dist-types/components/settings/general/agent-customization-section.d.ts.map +1 -0
  221. package/dist-types/components/settings/general/mcp-connections-section.d.ts +2 -0
  222. package/dist-types/components/settings/general/mcp-connections-section.d.ts.map +1 -0
  223. package/dist-types/components/settings/general/model-combobox.d.ts +24 -0
  224. package/dist-types/components/settings/general/model-combobox.d.ts.map +1 -0
  225. package/dist-types/components/settings/general/models-section.d.ts +2 -0
  226. package/dist-types/components/settings/general/models-section.d.ts.map +1 -0
  227. package/dist-types/components/settings/general/sandbox-section.d.ts +7 -0
  228. package/dist-types/components/settings/general/sandbox-section.d.ts.map +1 -0
  229. package/dist-types/components/settings/general/search-scraping-section.d.ts +2 -0
  230. package/dist-types/components/settings/general/search-scraping-section.d.ts.map +1 -0
  231. package/dist-types/components/settings/general/storage-indexing-section.d.ts +2 -0
  232. package/dist-types/components/settings/general/storage-indexing-section.d.ts.map +1 -0
  233. package/dist-types/components/ui/alert-dialog.d.ts +19 -0
  234. package/dist-types/components/ui/alert-dialog.d.ts.map +1 -0
  235. package/dist-types/components/ui/alert.d.ts +11 -0
  236. package/dist-types/components/ui/alert.d.ts.map +1 -0
  237. package/dist-types/components/ui/badge.d.ts +8 -0
  238. package/dist-types/components/ui/badge.d.ts.map +1 -0
  239. package/dist-types/components/ui/card.d.ts +2 -0
  240. package/dist-types/components/ui/card.d.ts.map +1 -0
  241. package/dist-types/components/ui/checkbox.d.ts +4 -0
  242. package/dist-types/components/ui/checkbox.d.ts.map +1 -0
  243. package/dist-types/components/ui/collapsible.d.ts +6 -0
  244. package/dist-types/components/ui/collapsible.d.ts.map +1 -0
  245. package/dist-types/components/ui/command.d.ts +20 -0
  246. package/dist-types/components/ui/command.d.ts.map +1 -0
  247. package/dist-types/components/ui/dialog.d.ts +18 -0
  248. package/dist-types/components/ui/dialog.d.ts.map +1 -0
  249. package/dist-types/components/ui/dropdown-menu.d.ts +30 -0
  250. package/dist-types/components/ui/dropdown-menu.d.ts.map +1 -0
  251. package/dist-types/components/ui/input-group.d.ts +19 -0
  252. package/dist-types/components/ui/input-group.d.ts.map +1 -0
  253. package/dist-types/components/ui/input.d.ts +4 -0
  254. package/dist-types/components/ui/input.d.ts.map +1 -0
  255. package/dist-types/components/ui/line-tabs.d.ts +15 -0
  256. package/dist-types/components/ui/line-tabs.d.ts.map +1 -0
  257. package/dist-types/components/ui/popover.d.ts +10 -0
  258. package/dist-types/components/ui/popover.d.ts.map +1 -0
  259. package/dist-types/components/ui/progress.d.ts +8 -0
  260. package/dist-types/components/ui/progress.d.ts.map +1 -0
  261. package/dist-types/components/ui/scroll-area.d.ts +6 -0
  262. package/dist-types/components/ui/scroll-area.d.ts.map +1 -0
  263. package/dist-types/components/ui/sheet.d.ts +15 -0
  264. package/dist-types/components/ui/sheet.d.ts.map +1 -0
  265. package/dist-types/components/ui/slider.d.ts +4 -0
  266. package/dist-types/components/ui/slider.d.ts.map +1 -0
  267. package/dist-types/components/ui/switch.d.ts +6 -0
  268. package/dist-types/components/ui/switch.d.ts.map +1 -0
  269. package/dist-types/components/ui/table.d.ts +2 -0
  270. package/dist-types/components/ui/table.d.ts.map +1 -0
  271. package/dist-types/components/ui/tabs.d.ts +11 -0
  272. package/dist-types/components/ui/tabs.d.ts.map +1 -0
  273. package/dist-types/components/ui/textarea.d.ts +2 -0
  274. package/dist-types/components/ui/textarea.d.ts.map +1 -0
  275. package/dist-types/components/ui/tooltip.d.ts +7 -0
  276. package/dist-types/components/ui/tooltip.d.ts.map +1 -0
  277. package/dist-types/hooks/use-copy-to-clipboard.d.ts +5 -0
  278. package/dist-types/hooks/use-copy-to-clipboard.d.ts.map +1 -0
  279. package/dist-types/hooks/use-integration-auth.d.ts +10 -0
  280. package/dist-types/hooks/use-integration-auth.d.ts.map +1 -0
  281. package/dist-types/hooks/use-live-media-progress.d.ts +14 -0
  282. package/dist-types/hooks/use-live-media-progress.d.ts.map +1 -0
  283. package/dist-types/hooks/use-notion-auth.d.ts +9 -0
  284. package/dist-types/hooks/use-notion-auth.d.ts.map +1 -0
  285. package/dist-types/host.d.ts +20 -0
  286. package/dist-types/host.d.ts.map +1 -0
  287. package/dist-types/lib/browser-cache.d.ts +7 -0
  288. package/dist-types/lib/browser-cache.d.ts.map +1 -0
  289. package/dist-types/lib/chat/chart-config.d.ts +2 -0
  290. package/dist-types/lib/chat/chart-config.d.ts.map +1 -0
  291. package/dist-types/lib/chat/data-export.d.ts +2 -0
  292. package/dist-types/lib/chat/data-export.d.ts.map +1 -0
  293. package/dist-types/lib/chat/live-tool-progress.d.ts +44 -0
  294. package/dist-types/lib/chat/live-tool-progress.d.ts.map +1 -0
  295. package/dist-types/lib/chat/message-feedback.d.ts +2 -0
  296. package/dist-types/lib/chat/message-feedback.d.ts.map +1 -0
  297. package/dist-types/lib/chat/table-presentation.d.ts +2 -0
  298. package/dist-types/lib/chat/table-presentation.d.ts.map +1 -0
  299. package/dist-types/lib/chat/tool-output.d.ts +3 -0
  300. package/dist-types/lib/chat/tool-output.d.ts.map +1 -0
  301. package/dist-types/lib/chat-supporting-clip.d.ts +11 -0
  302. package/dist-types/lib/chat-supporting-clip.d.ts.map +1 -0
  303. package/dist-types/lib/constants/stores.d.ts +6 -0
  304. package/dist-types/lib/constants/stores.d.ts.map +1 -0
  305. package/dist-types/lib/constants/tools.d.ts +27 -0
  306. package/dist-types/lib/constants/tools.d.ts.map +1 -0
  307. package/dist-types/lib/error-formatter.d.ts +2 -0
  308. package/dist-types/lib/error-formatter.d.ts.map +1 -0
  309. package/dist-types/lib/jobs-client.d.ts +7 -0
  310. package/dist-types/lib/jobs-client.d.ts.map +1 -0
  311. package/dist-types/lib/media/pdf.d.ts +6 -0
  312. package/dist-types/lib/media/pdf.d.ts.map +1 -0
  313. package/dist-types/lib/media/progress.d.ts +61 -0
  314. package/dist-types/lib/media/progress.d.ts.map +1 -0
  315. package/dist-types/lib/media/source-selection.d.ts +10 -0
  316. package/dist-types/lib/media/source-selection.d.ts.map +1 -0
  317. package/dist-types/lib/media/tool-indexing-estimate.d.ts +13 -0
  318. package/dist-types/lib/media/tool-indexing-estimate.d.ts.map +1 -0
  319. package/dist-types/lib/media/video-intelligence-capacity.d.ts +2 -0
  320. package/dist-types/lib/media/video-intelligence-capacity.d.ts.map +1 -0
  321. package/dist-types/lib/media/video-runtime-scope.d.ts +2 -0
  322. package/dist-types/lib/media/video-runtime-scope.d.ts.map +1 -0
  323. package/dist-types/lib/shared/error-formatter.d.ts +2 -0
  324. package/dist-types/lib/shared/error-formatter.d.ts.map +1 -0
  325. package/dist-types/lib/utils.d.ts +3 -0
  326. package/dist-types/lib/utils.d.ts.map +1 -0
  327. package/dist-types/storage.d.ts +5 -0
  328. package/dist-types/storage.d.ts.map +1 -0
  329. package/dist-types/store/chat-store.d.ts +19 -0
  330. package/dist-types/store/chat-store.d.ts.map +1 -0
  331. package/dist-types/store/scrape-store.d.ts +42 -0
  332. package/dist-types/store/scrape-store.d.ts.map +1 -0
  333. package/package.json +579 -3
  334. package/scripts/build-assets.mjs +12 -0
  335. package/src/asset-data.json +1 -0
  336. package/src/asset-server.ts +19 -0
  337. package/src/components/alerts/generic-alert.tsx +76 -0
  338. package/src/components/chat/canvas/doc-editor-provider.tsx +338 -0
  339. package/src/components/chat/canvas/document-canvas.tsx +98 -0
  340. package/src/components/chat/canvas/document-field.tsx +167 -0
  341. package/src/components/chat/canvas/document-viewer.tsx +446 -0
  342. package/src/components/chat/canvas/file-attachment-button.tsx +88 -0
  343. package/src/components/chat/chat-workspace.tsx +1552 -0
  344. package/src/components/chat/message-actions.tsx +96 -0
  345. package/src/components/chat/message-item.tsx +1398 -0
  346. package/src/components/chat/message-queue.tsx +86 -0
  347. package/src/components/chat/tools/chat-chart.tsx +558 -0
  348. package/src/components/chat/tools/chat-citations.tsx +152 -0
  349. package/src/components/chat/tools/chat-data-table.tsx +373 -0
  350. package/src/components/chat/tools/chat-file-export.tsx +49 -0
  351. package/src/components/chat/tools/chat-media-preview.tsx +466 -0
  352. package/src/components/chat/tools/chat-sandbox-result.tsx +130 -0
  353. package/src/components/chat/tools/chat-signature-request.tsx +810 -0
  354. package/src/components/chat/tools/chat-tabs.tsx +59 -0
  355. package/src/components/chat/tools/corpus-data-result.tsx +194 -0
  356. package/src/components/chat/tools/knowledge-base-result.tsx +345 -0
  357. package/src/components/chat/tools/visual-placement-modal.tsx +308 -0
  358. package/src/components/configure/custom-model-modal.tsx +209 -0
  359. package/src/components/configure/store-fields.tsx +129 -0
  360. package/src/components/data/corpus-panel.tsx +1223 -0
  361. package/src/components/data/data-entry-mode-switch.tsx +53 -0
  362. package/src/components/data/data-primary-action.ts +6 -0
  363. package/src/components/data/data-workspace.tsx +623 -0
  364. package/src/components/data/groups-panel.tsx +530 -0
  365. package/src/components/data/integration-resources-panel.tsx +282 -0
  366. package/src/components/data/integrations-panel.tsx +392 -0
  367. package/src/components/data/jobs-panel.tsx +490 -0
  368. package/src/components/data/jobs-tracking-button.tsx +224 -0
  369. package/src/components/data/live-media-progress.tsx +79 -0
  370. package/src/components/data/media-indexing-jobs-panel.tsx +190 -0
  371. package/src/components/data/media-panel.tsx +2905 -0
  372. package/src/components/data/notion-panel.tsx +408 -0
  373. package/src/components/data/paste-panel.tsx +82 -0
  374. package/src/components/data/project-data-page.tsx +55 -0
  375. package/src/components/data/scrape-panel.tsx +1290 -0
  376. package/src/components/data/tabular-preview.tsx +205 -0
  377. package/src/components/data/tool-indexing-dialog.tsx +210 -0
  378. package/src/components/data/upload-panel.tsx +1313 -0
  379. package/src/components/data/web-crawler-status.tsx +132 -0
  380. package/src/components/projects/project-form-dialog.tsx +97 -0
  381. package/src/components/projects/project-provider.tsx +143 -0
  382. package/src/components/settings/general/agent-customization-section.tsx +620 -0
  383. package/src/components/settings/general/mcp-connections-section.tsx +907 -0
  384. package/src/components/settings/general/model-combobox.tsx +235 -0
  385. package/src/components/settings/general/models-section.tsx +909 -0
  386. package/src/components/settings/general/sandbox-section.tsx +394 -0
  387. package/src/components/settings/general/search-scraping-section.tsx +757 -0
  388. package/src/components/settings/general/storage-indexing-section.tsx +394 -0
  389. package/src/components/ui/alert-dialog.tsx +162 -0
  390. package/src/components/ui/alert.tsx +69 -0
  391. package/src/components/ui/badge.tsx +49 -0
  392. package/src/components/ui/card.tsx +9 -0
  393. package/src/components/ui/checkbox.tsx +28 -0
  394. package/src/components/ui/collapsible.tsx +17 -0
  395. package/src/components/ui/command.tsx +175 -0
  396. package/src/components/ui/dialog.tsx +138 -0
  397. package/src/components/ui/dropdown-menu.tsx +267 -0
  398. package/src/components/ui/input-group.tsx +146 -0
  399. package/src/components/ui/input.tsx +19 -0
  400. package/src/components/ui/line-tabs.tsx +49 -0
  401. package/src/components/ui/popover.tsx +77 -0
  402. package/src/components/ui/progress.tsx +66 -0
  403. package/src/components/ui/scroll-area.tsx +51 -0
  404. package/src/components/ui/sheet.tsx +125 -0
  405. package/src/components/ui/slider.tsx +52 -0
  406. package/src/components/ui/switch.tsx +32 -0
  407. package/src/components/ui/table.tsx +10 -0
  408. package/src/components/ui/tabs.tsx +77 -0
  409. package/src/components/ui/textarea.tsx +1 -0
  410. package/src/components/ui/tooltip.tsx +54 -0
  411. package/src/hooks/use-copy-to-clipboard.ts +25 -0
  412. package/src/hooks/use-integration-auth.ts +39 -0
  413. package/src/hooks/use-live-media-progress.ts +71 -0
  414. package/src/hooks/use-notion-auth.ts +66 -0
  415. package/src/host.ts +55 -0
  416. package/src/lib/browser-cache.ts +17 -0
  417. package/src/lib/chat/chart-config.ts +1 -0
  418. package/src/lib/chat/data-export.ts +1 -0
  419. package/src/lib/chat/live-tool-progress.ts +69 -0
  420. package/src/lib/chat/message-feedback.ts +1 -0
  421. package/src/lib/chat/table-presentation.ts +1 -0
  422. package/src/lib/chat/tool-output.ts +14 -0
  423. package/src/lib/chat-supporting-clip.ts +16 -0
  424. package/src/lib/constants/stores.ts +12 -0
  425. package/src/lib/constants/tools.ts +178 -0
  426. package/src/lib/error-formatter.ts +30 -0
  427. package/src/lib/jobs-client.ts +27 -0
  428. package/src/lib/media/pdf.ts +57 -0
  429. package/src/lib/media/progress.ts +209 -0
  430. package/src/lib/media/source-selection.ts +47 -0
  431. package/src/lib/media/tool-indexing-estimate.ts +39 -0
  432. package/src/lib/media/video-intelligence-capacity.ts +1 -0
  433. package/src/lib/media/video-runtime-scope.ts +1 -0
  434. package/src/lib/shared/error-formatter.ts +1 -0
  435. package/src/lib/utils.ts +6 -0
  436. package/src/source.css +1 -0
  437. package/src/storage.ts +16 -0
  438. package/src/store/chat-store.ts +19 -0
  439. package/src/store/scrape-store.ts +76 -0
  440. package/src/styles.d.ts +1 -0
@@ -0,0 +1,1223 @@
1
+ 'use client';
2
+
3
+ import { workspaceUrl, workspaceFetch as fetch } from '../../host';
4
+ import { useEffect, useState, useRef, useMemo } from 'react';
5
+ import { toast } from 'sonner';
6
+ import { formatErrorMessage } from '../../lib/error-formatter';
7
+ import {
8
+ FileText,
9
+ Trash2,
10
+ FileUp,
11
+ Globe,
12
+ Pencil,
13
+ Loader2,
14
+ ChevronLeft,
15
+ ChevronRight,
16
+ Film,
17
+ Upload,
18
+ Type,
19
+ Image,
20
+ Plug,
21
+ ChevronDown,
22
+ Check,
23
+ X,
24
+ Eye,
25
+ EyeOff,
26
+ Monitor,
27
+ Cloud,
28
+ Info,
29
+ FolderInput,
30
+ } from 'lucide-react';
31
+ import {
32
+ Select,
33
+ SelectContent,
34
+ SelectItem,
35
+ SelectTrigger,
36
+ SelectValue,
37
+ } from '@larkup/workspace-ui/select';
38
+ import type { DocumentSource, SourceDocument, DataGroup } from '@larkup/core/types';
39
+ import { GenericAlert } from '../alerts/generic-alert';
40
+ import { Badge } from '../ui/badge';
41
+ import { Button } from '@larkup/workspace-ui/button';
42
+ import { Input } from '../ui/input';
43
+ import { Label } from '@larkup/workspace-ui/label';
44
+ import { Textarea } from '../ui/textarea';
45
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
46
+ import {
47
+ Dialog,
48
+ DialogContent,
49
+ DialogDescription,
50
+ DialogFooter,
51
+ DialogHeader,
52
+ DialogTitle,
53
+ } from '../ui/dialog';
54
+ import { ScrollArea } from '../ui/scroll-area';
55
+ import { Checkbox } from '../ui/checkbox';
56
+ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
57
+ import {
58
+ DropdownMenu,
59
+ DropdownMenuContent,
60
+ DropdownMenuItem,
61
+ DropdownMenuSeparator,
62
+ DropdownMenuSub,
63
+ DropdownMenuSubContent,
64
+ DropdownMenuSubTrigger,
65
+ DropdownMenuTrigger,
66
+ } from '../ui/dropdown-menu';
67
+ import { cn } from '../../lib/utils';
68
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
69
+ import {
70
+ videoRuntimeScopeFromMetadata,
71
+ videoRuntimeScopeLabel,
72
+ type VideoRuntimeScope,
73
+ } from '../../lib/media/video-runtime-scope';
74
+
75
+ const SOURCE_META: Record<DocumentSource, { label: string; icon: any }> = {
76
+ website: { label: 'Website', icon: Globe },
77
+ text: { label: 'Text', icon: Type },
78
+ files: { label: 'Files', icon: FileUp },
79
+ media: { label: 'Media', icon: Image },
80
+ integrations: { label: 'Integrations', icon: Plug },
81
+ };
82
+
83
+ export function CorpusPanel({
84
+ documents,
85
+ groups = [],
86
+ onChanged,
87
+ isIndexing,
88
+ activeVideoRuntimeScope,
89
+ initialGroupId,
90
+ isLoading,
91
+ }: {
92
+ documents: SourceDocument[];
93
+ groups?: DataGroup[];
94
+ onChanged: () => void | Promise<unknown>;
95
+ isIndexing?: boolean;
96
+ activeVideoRuntimeScope: VideoRuntimeScope;
97
+ initialGroupId?: string;
98
+ isLoading?: boolean;
99
+ }) {
100
+ const [active, setActive] = useState<SourceDocument | null>(null);
101
+ const [deleteTask, setDeleteTask] = useState<
102
+ { type: 'single'; doc: SourceDocument } | { type: 'all' } | { type: 'selected' } | null
103
+ >(null);
104
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
105
+ const [movingGroupId, setMovingGroupId] = useState<string | null>(null);
106
+ const [inspectorDoc, setInspectorDoc] = useState<SourceDocument | null>(null);
107
+
108
+ const [page, setPage] = useState(0);
109
+
110
+ const [sourceFilter, setSourceFilter] = useState<DocumentSource | 'all'>('all');
111
+ const [statusFilter, setStatusFilter] = useState<'all' | 'indexed' | 'unindexed'>('all');
112
+ const [selectedGroupIds, setSelectedGroupIds] = useState<Set<string>>(
113
+ () => new Set(initialGroupId ? [initialGroupId] : []),
114
+ );
115
+ const [groupDropdownOpen, setGroupDropdownOpen] = useState(false);
116
+
117
+ useEffect(() => {
118
+ setSelectedGroupIds(new Set(initialGroupId ? [initialGroupId] : []));
119
+ }, [initialGroupId]);
120
+
121
+ const groupedDocuments = useMemo(() => {
122
+ const grouped = new Map<string, SourceDocument>();
123
+ const others: SourceDocument[] = [];
124
+
125
+ for (const doc of documents) {
126
+ if (doc.source === 'media' && doc.metadata?.mediaAssetId) {
127
+ const id = doc.metadata.mediaAssetId;
128
+ if (!grouped.has(id)) {
129
+ grouped.set(id, {
130
+ ...doc,
131
+ id: `media-group-${id}`,
132
+ title: doc.metadata.fileName || doc.title || 'Media Asset',
133
+ charCount: doc.charCount,
134
+ metadata: { ...doc.metadata, isGroup: true, childIds: [doc.id] },
135
+ });
136
+ } else {
137
+ const existing = grouped.get(id)!;
138
+ existing.charCount += doc.charCount;
139
+ existing.metadata!.childIds.push(doc.id);
140
+ if (doc.status !== 'indexed') existing.status = doc.status;
141
+ }
142
+ } else {
143
+ others.push(doc);
144
+ }
145
+ }
146
+ return [...Array.from(grouped.values()), ...others].sort(
147
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
148
+ );
149
+ }, [documents]);
150
+
151
+ const filteredDocuments = groupedDocuments.filter(
152
+ (document) =>
153
+ (sourceFilter === 'all' || document.source === sourceFilter) &&
154
+ (statusFilter === 'all' || (document.status ?? 'unindexed') === statusFilter) &&
155
+ (selectedGroupIds.size === 0 || selectedGroupIds.has(document.groupId ?? 'default')),
156
+ );
157
+
158
+ useEffect(() => {
159
+ setPage(0);
160
+ }, [sourceFilter, statusFilter, selectedGroupIds]);
161
+
162
+ const PAGE_SIZE = 10;
163
+ const totalPages = Math.max(1, Math.ceil(filteredDocuments.length / PAGE_SIZE));
164
+ const safePage = Math.min(page, totalPages - 1);
165
+ const pageDocuments = filteredDocuments.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE);
166
+ const legacyVideoCount = groupedDocuments.filter(
167
+ (document) =>
168
+ document.source === 'media' &&
169
+ document.metadata?.isGroup &&
170
+ !videoRuntimeScopeFromMetadata(document.metadata),
171
+ ).length;
172
+
173
+ const allPageSelected =
174
+ pageDocuments.length > 0 && pageDocuments.every((d) => selectedIds.has(d.id));
175
+
176
+ async function deleteRequest(url: string) {
177
+ const response = await fetch(url, { method: 'DELETE' });
178
+ if (response.ok) return;
179
+
180
+ const body = await response.json().catch(() => ({}));
181
+ throw new Error(body.error ?? 'Could not delete this document.');
182
+ }
183
+
184
+ async function del(id: string, doc?: SourceDocument) {
185
+ try {
186
+ if (doc?.metadata?.isGroup && doc?.metadata?.mediaAssetId) {
187
+ await deleteRequest(workspaceUrl(`/api/media?id=${doc.metadata.mediaAssetId}`));
188
+ } else {
189
+ await deleteRequest(workspaceUrl(`/api/documents?id=${id}`));
190
+ }
191
+ setSelectedIds((prev) => {
192
+ const next = new Set(prev);
193
+ next.delete(id);
194
+ return next;
195
+ });
196
+ await onChanged();
197
+ toast.success('Document deleted');
198
+ } catch (error) {
199
+ toast.error(formatErrorMessage(error) || 'Could not delete this document.');
200
+ }
201
+ }
202
+ async function delSelected() {
203
+ if (selectedIds.size === 0) return;
204
+ const toDelete = groupedDocuments.filter((d) => selectedIds.has(d.id));
205
+ try {
206
+ for (const doc of toDelete) {
207
+ if (doc.metadata?.isGroup && doc.metadata?.mediaAssetId) {
208
+ await deleteRequest(workspaceUrl(`/api/media?id=${doc.metadata.mediaAssetId}`));
209
+ } else {
210
+ await deleteRequest(workspaceUrl(`/api/documents?id=${doc.id}`));
211
+ }
212
+ }
213
+ setSelectedIds(new Set());
214
+ await onChanged();
215
+ toast.success(`Deleted ${toDelete.length} item(s)`);
216
+ } catch (error) {
217
+ toast.error(formatErrorMessage(error) || 'Could not delete the selected documents.');
218
+ }
219
+ }
220
+ async function clearAll() {
221
+ try {
222
+ await deleteRequest(workspaceUrl('/api/documents'));
223
+ setSelectedIds(new Set());
224
+ await onChanged();
225
+ toast.success('Corpus and vector index cleared');
226
+ } catch (error) {
227
+ toast.error(formatErrorMessage(error) || 'Could not clear the corpus.');
228
+ }
229
+ }
230
+
231
+ const selectedDocuments = useMemo(
232
+ () => groupedDocuments.filter((document) => selectedIds.has(document.id)),
233
+ [groupedDocuments, selectedIds],
234
+ );
235
+ const moveTargets = groups.filter((group) =>
236
+ selectedDocuments.some((document) => (document.groupId ?? 'default') !== group.id),
237
+ );
238
+
239
+ async function moveSelectedToGroup(group: DataGroup) {
240
+ if (selectedDocuments.length === 0 || movingGroupId) return;
241
+ const documentIds = selectedDocuments.flatMap((document) => {
242
+ if (!document.metadata?.isGroup) return [document.id];
243
+ return (document.metadata.childIds ?? []).filter(
244
+ (id: unknown): id is string => typeof id === 'string',
245
+ );
246
+ });
247
+ const mediaAssetIds = selectedDocuments.flatMap((document) =>
248
+ document.metadata?.isGroup && typeof document.metadata.mediaAssetId === 'string'
249
+ ? [document.metadata.mediaAssetId]
250
+ : [],
251
+ );
252
+
253
+ setMovingGroupId(group.id);
254
+ try {
255
+ const response = await fetch(workspaceUrl('/api/documents/move'), {
256
+ method: 'PATCH',
257
+ headers: { 'Content-Type': 'application/json' },
258
+ body: JSON.stringify({ documentIds, mediaAssetIds, groupId: group.id }),
259
+ });
260
+ const body = await response.json().catch(() => ({}));
261
+ if (!response.ok) throw new Error(body.error ?? 'Could not move the selected data.');
262
+ setSelectedIds(new Set());
263
+ await onChanged();
264
+ toast.success(
265
+ `Moved ${selectedDocuments.length} source${selectedDocuments.length === 1 ? '' : 's'} to ${group.name}.`,
266
+ );
267
+ } catch (error) {
268
+ toast.error(formatErrorMessage(error) || 'Could not move the selected data.');
269
+ } finally {
270
+ setMovingGroupId(null);
271
+ }
272
+ }
273
+
274
+ const isAllSelected = selectedGroupIds.size === 0;
275
+ const selectedGroupLabels = useMemo(() => {
276
+ if (isAllSelected) return 'All Groups';
277
+ return Array.from(selectedGroupIds)
278
+ .map((id) => {
279
+ const g = groups.find((g) => g.id === id);
280
+ return g ? (g.icon ? `${g.icon} ${g.name}` : g.name) : id;
281
+ })
282
+ .join(', ');
283
+ }, [selectedGroupIds, groups, isAllSelected]);
284
+
285
+ function toggleGroupFilter(groupId: string) {
286
+ setSelectedGroupIds((prev) => {
287
+ const next = new Set(prev);
288
+ if (next.has(groupId)) next.delete(groupId);
289
+ else next.add(groupId);
290
+ return next;
291
+ });
292
+ }
293
+
294
+ function clearGroupFilter() {
295
+ setSelectedGroupIds(new Set());
296
+ }
297
+
298
+ if (isLoading) {
299
+ return <CorpusSkeleton />;
300
+ }
301
+
302
+ if (documents.length === 0) {
303
+ return (
304
+ <div className="flex flex-col items-center justify-center rounded-lg px-6 py-14 text-center">
305
+ <div className="size-10 p-1.5 bg-white/60 border border-border rounded-md flex items-center justify-center">
306
+ <FileText className="size-7 text-muted-foreground/60" />
307
+ </div>
308
+ <p className="mt-3 text-lg font-medium">Your corpus is empty</p>
309
+ <p className="mt-1 max-w-xl text-sm text-muted-foreground">
310
+ Scrape the web, paste text, or upload files. Everything you collect is stored locally and
311
+ ready to index in the next step.
312
+ </p>
313
+ </div>
314
+ );
315
+ }
316
+
317
+ return (
318
+ <TooltipProvider>
319
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center justify-between pb-3">
320
+ <div className="flex items-center gap-3">
321
+ <div className="flex flex-wrap items-center gap-2">
322
+ {[
323
+ { id: 'all', label: 'All Sources' },
324
+ ...Object.entries(SOURCE_META).map(([key, meta]) => ({ id: key, label: meta.label })),
325
+ ].map((tab) => {
326
+ const isActive = sourceFilter === tab.id;
327
+ return (
328
+ <button
329
+ key={tab.id}
330
+ type="button"
331
+ onClick={() => setSourceFilter(tab.id as any)}
332
+ className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
333
+ isActive
334
+ ? 'bg-muted text-foreground'
335
+ : 'border border-border bg-transparent text-muted-foreground hover:bg-muted/50'
336
+ }`}
337
+ >
338
+ {tab.label}
339
+ </button>
340
+ );
341
+ })}
342
+ </div>
343
+ </div>
344
+ <div className="flex items-center gap-2">
345
+ <Popover open={groupDropdownOpen} onOpenChange={setGroupDropdownOpen}>
346
+ <PopoverTrigger
347
+ render={
348
+ <button
349
+ type="button"
350
+ className={cn(
351
+ 'inline-flex h-9 items-center gap-1.5 rounded-md border border-border bg-white px-3 text-xs font-medium text-foreground transition-colors hover:bg-muted/50 shadow-none',
352
+ !isAllSelected && 'border-primary/40 bg-primary/5 text-primary',
353
+ )}
354
+ aria-label="Filter by group"
355
+ >
356
+ <span className="max-w-40 truncate">{selectedGroupLabels}</span>
357
+ <ChevronDown className="size-3 shrink-0 text-muted-foreground" />
358
+ </button>
359
+ }
360
+ />
361
+ <PopoverContent align="start" className="w-56 p-1.5 shadow-none">
362
+ <div className="space-y-0.5">
363
+ <button
364
+ type="button"
365
+ onClick={clearGroupFilter}
366
+ className={cn(
367
+ 'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs transition-colors hover:bg-accent',
368
+ isAllSelected && 'bg-accent/60 font-medium',
369
+ )}
370
+ >
371
+ <div
372
+ className={cn(
373
+ 'flex size-4 shrink-0 items-center justify-center rounded border border-primary/40',
374
+ isAllSelected ? 'bg-primary border-primary' : 'bg-white',
375
+ )}
376
+ >
377
+ {isAllSelected && <Check className="size-2.5 text-white" strokeWidth={3} />}
378
+ </div>
379
+ All Groups
380
+ </button>
381
+ {groups.length > 0 && <div className="my-1 border-t border-border" />}
382
+ {groups.map((group) => {
383
+ const isChecked = selectedGroupIds.has(group.id);
384
+ return (
385
+ <button
386
+ key={group.id}
387
+ type="button"
388
+ onClick={() => toggleGroupFilter(group.id)}
389
+ className={cn(
390
+ 'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs transition-colors hover:bg-accent',
391
+ isChecked && 'bg-accent/60',
392
+ )}
393
+ >
394
+ <div
395
+ className={cn(
396
+ 'flex size-4 shrink-0 items-center justify-center rounded border border-primary/40',
397
+ isChecked ? 'bg-primary border-primary' : 'bg-white',
398
+ )}
399
+ >
400
+ {isChecked && <Check className="size-2.5 text-white" strokeWidth={3} />}
401
+ </div>
402
+ <span className="flex-1 truncate text-left">
403
+ {group.icon ? `${group.icon} ` : ''}
404
+ {group.name}
405
+ </span>
406
+ </button>
407
+ );
408
+ })}
409
+ </div>
410
+ </PopoverContent>
411
+ </Popover>
412
+
413
+ <Select
414
+ value={statusFilter}
415
+ onValueChange={(v) => v && setStatusFilter(v as 'all' | 'indexed' | 'unindexed')}
416
+ >
417
+ <SelectTrigger className="w-29 shadow-none border-border/70 bg-white h-9! text-xs">
418
+ <SelectValue placeholder="All status" />
419
+ </SelectTrigger>
420
+ <SelectContent>
421
+ <SelectItem value="all">All status</SelectItem>
422
+ <SelectItem value="indexed">Indexed</SelectItem>
423
+ <SelectItem value="unindexed">Unindexed</SelectItem>
424
+ </SelectContent>
425
+ </Select>
426
+ {selectedIds.size > 0 && (
427
+ <DropdownMenu>
428
+ <DropdownMenuTrigger
429
+ render={
430
+ <Button
431
+ size="sm"
432
+ variant="secondary"
433
+ className="h-8 gap-1.5 text-xs"
434
+ disabled={movingGroupId !== null}
435
+ >
436
+ <FolderInput className="size-3.5" />
437
+ {movingGroupId ? 'Moving…' : `Actions (${selectedIds.size})`}
438
+ <ChevronDown className="size-3.5" />
439
+ </Button>
440
+ }
441
+ />
442
+ <DropdownMenuContent align="end" className="min-w-52">
443
+ <DropdownMenuSub>
444
+ <DropdownMenuSubTrigger>
445
+ <FolderInput className="size-3.5" />
446
+ Move to group
447
+ </DropdownMenuSubTrigger>
448
+ <DropdownMenuSubContent className="min-w-52">
449
+ {moveTargets.length > 0 ? (
450
+ moveTargets.map((group) => (
451
+ <DropdownMenuItem
452
+ key={group.id}
453
+ disabled={movingGroupId !== null}
454
+ onClick={() => void moveSelectedToGroup(group)}
455
+ >
456
+ <span className="w-5 text-center">{group.icon || '✦'}</span>
457
+ <span className="truncate">{group.name}</span>
458
+ </DropdownMenuItem>
459
+ ))
460
+ ) : (
461
+ <DropdownMenuItem disabled>Already in this group</DropdownMenuItem>
462
+ )}
463
+ </DropdownMenuSubContent>
464
+ </DropdownMenuSub>
465
+ <DropdownMenuSeparator />
466
+ <DropdownMenuItem
467
+ variant="destructive"
468
+ disabled={movingGroupId !== null}
469
+ onClick={() => setDeleteTask({ type: 'selected' })}
470
+ >
471
+ <Trash2 className="size-3.5" />
472
+ Delete selected
473
+ </DropdownMenuItem>
474
+ </DropdownMenuContent>
475
+ </DropdownMenu>
476
+ )}
477
+ <TooltipProvider>
478
+ <Tooltip>
479
+ <TooltipTrigger
480
+ render={
481
+ <Button
482
+ size="icon"
483
+ variant="destructive"
484
+ aria-label="Clear all data"
485
+ className="h-9 border border-border text-xs"
486
+ onClick={() => setDeleteTask({ type: 'all' })}
487
+ disabled={isIndexing}
488
+ >
489
+ <Trash2 className="size-3.5" />
490
+ </Button>
491
+ }
492
+ />
493
+ <TooltipContent>Clear all data</TooltipContent>
494
+ </Tooltip>
495
+ </TooltipProvider>
496
+ </div>
497
+ </div>
498
+
499
+ {legacyVideoCount > 0 && (
500
+ <div className="mb-3 flex items-start gap-2 rounded-lg border border-border bg-muted/25 px-3 py-2 text-xs text-muted-foreground">
501
+ <Info className="mt-0.5 size-3.5 shrink-0" />
502
+ <span>
503
+ {legacyVideoCount} existing video{legacyVideoCount === 1 ? '' : 's'} will stay available
504
+ until re-indexed. Re-index a video once to link it to{' '}
505
+ {videoRuntimeScopeLabel(activeVideoRuntimeScope)} and enable runtime-specific
506
+ visibility.
507
+ </span>
508
+ </div>
509
+ )}
510
+
511
+ <div className="overflow-hidden rounded-lg border border-border">
512
+ <Table>
513
+ <TableHeader>
514
+ <TableRow className="bg-muted/50">
515
+ <TableHead className="w-12">
516
+ <Checkbox
517
+ checked={allPageSelected}
518
+ onCheckedChange={(checked) => {
519
+ if (checked) {
520
+ const next = new Set(selectedIds);
521
+ pageDocuments.forEach((d) => next.add(d.id));
522
+ setSelectedIds(next);
523
+ } else {
524
+ const next = new Set(selectedIds);
525
+ pageDocuments.forEach((d) => next.delete(d.id));
526
+ setSelectedIds(next);
527
+ }
528
+ }}
529
+ aria-label="Select all on page"
530
+ />
531
+ </TableHead>
532
+ <TableHead>Title</TableHead>
533
+ <TableHead className="w-24">Status</TableHead>
534
+ <TableHead className="w-28">Source</TableHead>
535
+ <TableHead className="w-24 text-right">Chars</TableHead>
536
+ <TableHead className="w-20" />
537
+ </TableRow>
538
+ </TableHeader>
539
+ <TableBody className="bg-white/70">
540
+ {pageDocuments.map((doc) => {
541
+ const meta = SOURCE_META[doc.source as DocumentSource] || {
542
+ label: doc.source || 'Unknown',
543
+ icon: FileText,
544
+ };
545
+ const Icon = meta.icon;
546
+ const indexedRuntimeScope =
547
+ doc.source === 'media' ? videoRuntimeScopeFromMetadata(doc.metadata) : undefined;
548
+ const unavailableInActiveRuntime =
549
+ Boolean(indexedRuntimeScope) && indexedRuntimeScope !== activeVideoRuntimeScope;
550
+ const runtimeLabel = indexedRuntimeScope
551
+ ? videoRuntimeScopeLabel(indexedRuntimeScope)
552
+ : undefined;
553
+ const RuntimeIcon = indexedRuntimeScope === 'local' ? Monitor : Cloud;
554
+ return (
555
+ <TableRow
556
+ key={doc.id}
557
+ className={cn(
558
+ 'border-border/50 h-10.5! transition-opacity',
559
+ unavailableInActiveRuntime && 'opacity-45 hover:opacity-70',
560
+ )}
561
+ >
562
+ <TableCell>
563
+ <Checkbox
564
+ checked={selectedIds.has(doc.id)}
565
+ onCheckedChange={(checked) => {
566
+ const next = new Set(selectedIds);
567
+ if (checked) next.add(doc.id);
568
+ else next.delete(doc.id);
569
+ setSelectedIds(next);
570
+ }}
571
+ aria-label="Select document"
572
+ />
573
+ </TableCell>
574
+ <TableCell>
575
+ <button
576
+ type="button"
577
+ onClick={() => !doc.metadata?.isGroup && setActive(doc)}
578
+ className={cn(
579
+ 'group flex flex-col items-start text-left',
580
+ doc.metadata?.isGroup ? 'cursor-default' : 'cursor-pointer',
581
+ )}
582
+ >
583
+ <span
584
+ className={cn(
585
+ 'line-clamp-1 text-sm font-medium',
586
+ !doc.metadata?.isGroup && 'group-hover:text-primary',
587
+ )}
588
+ >
589
+ {doc.title}
590
+ </span>
591
+ {doc.metadata?.isGroup && (
592
+ <div className="mt-0.5 flex items-center gap-1.5 text-[10px] text-muted-foreground">
593
+ <span className="line-clamp-1">
594
+ Video ({doc.metadata.childIds?.length} chunks)
595
+ </span>
596
+ {unavailableInActiveRuntime && runtimeLabel && (
597
+ <Tooltip>
598
+ <TooltipTrigger
599
+ render={
600
+ <span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/5 px-1.5 py-0.5 text-[9px] font-medium text-amber-700 dark:text-amber-300">
601
+ <EyeOff className="size-2.5" />
602
+ {indexedRuntimeScope === 'local' ? 'Local only' : 'Cloud only'}
603
+ </span>
604
+ }
605
+ />
606
+ <TooltipContent className="max-w-64 text-center">
607
+ This video was indexed with {runtimeLabel}. It is not available to
608
+ the agent while {videoRuntimeScopeLabel(activeVideoRuntimeScope)} is
609
+ selected. Switch runtimes or re-index the video.
610
+ </TooltipContent>
611
+ </Tooltip>
612
+ )}
613
+ {!indexedRuntimeScope && (
614
+ <span className="shrink-0 text-[9px] text-muted-foreground/70">
615
+ Legacy index
616
+ </span>
617
+ )}
618
+ </div>
619
+ )}
620
+ {/* {doc.url && (
621
+ <span className="line-clamp-1 text-xs text-muted-foreground">
622
+ {doc.url}
623
+ </span>
624
+ )} */}
625
+ </button>
626
+ </TableCell>
627
+ <TableCell>
628
+ {doc.status === 'indexed' ? (
629
+ <Badge
630
+ variant="outline"
631
+ className="text-green-600 bg-green-50/10 border-green-500 font-normal"
632
+ >
633
+ Indexed
634
+ </Badge>
635
+ ) : isIndexing ? (
636
+ <Badge
637
+ variant="outline"
638
+ className="text-blue-500 bg-blue-50 border-blue-200 font-normal flex items-center gap-1.5"
639
+ >
640
+ <Loader2 className="size-3 animate-spin" />
641
+ Indexing
642
+ </Badge>
643
+ ) : (
644
+ <Badge
645
+ variant="outline"
646
+ className="text-orange-500 bg-orange-50 border-orange-200 font-normal"
647
+ >
648
+ Unindexed
649
+ </Badge>
650
+ )}
651
+ </TableCell>
652
+ <TableCell>
653
+ <div className="flex items-center gap-1.5">
654
+ <Badge variant="secondary" className="gap-1 font-normal">
655
+ <Icon className="size-3" />
656
+ {meta.label}
657
+ </Badge>
658
+ {doc.metadata?.isGroup &&
659
+ indexedRuntimeScope &&
660
+ !unavailableInActiveRuntime && (
661
+ <span className="inline-flex items-center gap-1 text-[10px] text-muted-foreground">
662
+ <RuntimeIcon className="size-2.5" />
663
+ {indexedRuntimeScope === 'local' ? 'Local' : 'Cloud'}
664
+ </span>
665
+ )}
666
+ </div>
667
+ </TableCell>
668
+ <TableCell className="text-right text-xs text-muted-foreground tabular-nums">
669
+ {doc.charCount.toLocaleString()}
670
+ </TableCell>
671
+ <TableCell>
672
+ <div className="flex items-center justify-end gap-4">
673
+ {!doc.metadata?.isGroup && (
674
+ <button
675
+ type="button"
676
+ aria-label="Edit document"
677
+ onClick={() => setActive(doc)}
678
+ className="text-muted-foreground transition-colors hover:text-primary"
679
+ >
680
+ <Pencil className="size-4" />
681
+ </button>
682
+ )}
683
+ {doc.metadata?.isGroup && (
684
+ <Tooltip>
685
+ <TooltipTrigger
686
+ render={
687
+ <button
688
+ type="button"
689
+ aria-label="View indexed video evidence"
690
+ onClick={() => setInspectorDoc(doc)}
691
+ className="text-muted-foreground transition-colors hover:text-primary"
692
+ >
693
+ <Eye className="size-4" />
694
+ </button>
695
+ }
696
+ />
697
+ <TooltipContent>View indexed video evidence</TooltipContent>
698
+ </Tooltip>
699
+ )}
700
+ <button
701
+ type="button"
702
+ aria-label="Delete document"
703
+ onClick={() => setDeleteTask({ type: 'single', doc })}
704
+ className="text-muted-foreground transition-colors hover:text-destructive"
705
+ >
706
+ <Trash2 className="size-4" />
707
+ </button>
708
+ </div>
709
+ </TableCell>
710
+ </TableRow>
711
+ );
712
+ })}
713
+ </TableBody>
714
+ </Table>
715
+ </div>
716
+
717
+ {/* Pagination */}
718
+ {totalPages > 1 && (
719
+ <div className="mt-3 flex items-center justify-between text-xs text-muted-foreground">
720
+ <span>
721
+ {filteredDocuments.length === 0 ? 0 : safePage * PAGE_SIZE + 1}–
722
+ {Math.min((safePage + 1) * PAGE_SIZE, filteredDocuments.length)} of{' '}
723
+ {filteredDocuments.length}
724
+ </span>
725
+ <div className="flex items-center gap-1">
726
+ <button
727
+ disabled={safePage === 0}
728
+ onClick={() => setPage((p) => Math.max(0, p - 1))}
729
+ className="rounded p-1 hover:bg-muted disabled:opacity-40 transition-colors cursor-pointer"
730
+ >
731
+ <ChevronLeft className="size-3.5" />
732
+ </button>
733
+ <span className="tabular-nums">
734
+ {safePage + 1} / {totalPages}
735
+ </span>
736
+ <button
737
+ disabled={safePage >= totalPages - 1}
738
+ onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
739
+ className="rounded p-1 hover:bg-muted disabled:opacity-40 transition-colors cursor-pointer"
740
+ >
741
+ <ChevronRight className="size-3.5" />
742
+ </button>
743
+ </div>
744
+ </div>
745
+ )}
746
+
747
+ <DocumentDialog
748
+ doc={active}
749
+ onOpenChange={(o) => !o && setActive(null)}
750
+ onSaved={() => {
751
+ setActive(null);
752
+ onChanged();
753
+ }}
754
+ onDelete={(id) => {
755
+ if (active) {
756
+ setDeleteTask({ type: 'single', doc: active });
757
+ }
758
+ }}
759
+ />
760
+
761
+ <VideoKnowledgeInspector
762
+ doc={inspectorDoc}
763
+ allDocuments={documents}
764
+ onClose={() => setInspectorDoc(null)}
765
+ />
766
+
767
+ <GenericAlert
768
+ open={deleteTask !== null}
769
+ onOpenChange={(open) => {
770
+ if (!open) setDeleteTask(null);
771
+ }}
772
+ title={
773
+ deleteTask?.type === 'all'
774
+ ? 'Clear corpus'
775
+ : deleteTask?.type === 'selected'
776
+ ? 'Delete selected documents'
777
+ : 'Delete document'
778
+ }
779
+ description={
780
+ deleteTask?.type === 'all' ? (
781
+ 'Delete every uploaded file and document from this knowledge base? This also clears the vector index and cannot be undone.'
782
+ ) : deleteTask?.type === 'selected' ? (
783
+ `Are you sure you want to delete ${selectedIds.size} selected documents?`
784
+ ) : (
785
+ <span className="wrap-break-word">
786
+ Are you sure you want to delete "
787
+ <span className="break-all font-medium">
788
+ {deleteTask?.type === 'single' ? deleteTask.doc.title : ''}
789
+ </span>
790
+ "?
791
+ </span>
792
+ )
793
+ }
794
+ actionText="Delete"
795
+ variant="destructive"
796
+ onAction={() => {
797
+ if (deleteTask?.type === 'all') {
798
+ void clearAll();
799
+ } else if (deleteTask?.type === 'selected') {
800
+ void delSelected();
801
+ } else if (deleteTask?.type === 'single') {
802
+ void del(deleteTask.doc.id, deleteTask.doc);
803
+ if (active?.id === deleteTask.doc.id) {
804
+ setActive(null);
805
+ }
806
+ }
807
+ }}
808
+ />
809
+ </TooltipProvider>
810
+ );
811
+ }
812
+
813
+ function VideoKnowledgeInspector({
814
+ doc,
815
+ allDocuments,
816
+ onClose,
817
+ }: {
818
+ doc: SourceDocument | null;
819
+ allDocuments: SourceDocument[];
820
+ onClose: () => void;
821
+ }) {
822
+ if (!doc || !doc.metadata?.isGroup) return null;
823
+
824
+ const childIds: string[] = doc.metadata?.childIds ?? [];
825
+ const children = childIds
826
+ .map((id: string) => allDocuments.find((d) => d.id === id))
827
+ .filter((d): d is SourceDocument => d !== undefined)
828
+ .sort((a, b) => {
829
+ const aStart = a.metadata?.startSecs ?? 0;
830
+ const bStart = b.metadata?.startSecs ?? 0;
831
+ return aStart - bStart;
832
+ });
833
+ const summaryDocument = children.find((child) => child.metadata?.isMediaSummary);
834
+ const mediaUrl = summaryDocument?.url || doc.url;
835
+ const previewUrl = mediaUrl ? workspaceUrl(mediaUrl) : undefined;
836
+ const mediaType = summaryDocument?.metadata?.mediaType || doc.metadata?.mediaType;
837
+
838
+ return (
839
+ <Dialog open={!!doc} onOpenChange={(open) => !open && onClose()}>
840
+ <DialogContent className="w-full pb-3 max-w-5xl! max-h-[90%]! h-full!">
841
+ <DialogHeader>
842
+ <DialogTitle className="flex items-center gap-2">
843
+ <Film className="size-4 text-muted-foreground" />
844
+ {doc.title}
845
+ </DialogTitle>
846
+ <DialogDescription>
847
+ {children.length} indexed segment{children.length !== 1 ? 's' : ''} ·{' '}
848
+ {doc.charCount.toLocaleString()} total characters
849
+ </DialogDescription>
850
+ </DialogHeader>
851
+ {/* {previewUrl && (mediaType === 'video' || mediaType === 'audio') && (
852
+ <div className="overflow-hidden rounded-lg border border-border bg-black/95">
853
+ {mediaType === 'video' ? (
854
+ <video
855
+ src={previewUrl}
856
+ controls
857
+ playsInline
858
+ preload="metadata"
859
+ className="max-h-[42vh] w-full object-contain"
860
+ />
861
+ ) : (
862
+ <audio src={previewUrl} controls preload="metadata" className="w-full" />
863
+ )}
864
+ </div>
865
+ )} */}
866
+ <ScrollArea className={previewUrl ? 'max-h-[70vh] h-full' : 'max-h-[70vh]'}>
867
+ <div className="space-y-4 pr-4">
868
+ {children.map((child, index) => {
869
+ const startSecs = child.metadata?.startSecs;
870
+ const hasTimestamp = typeof startSecs === 'number';
871
+ return (
872
+ <div
873
+ key={child.id}
874
+ className="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-1.5"
875
+ >
876
+ <div className="flex items-center justify-between">
877
+ <span className="text-[11px] font-medium text-muted-foreground">
878
+ {hasTimestamp
879
+ ? `${Math.floor(startSecs / 60)}:${String(
880
+ Math.floor(startSecs % 60),
881
+ ).padStart(2, '0')}`
882
+ : `Segment ${index + 1}`}
883
+ </span>
884
+ <span className="text-[10px] text-muted-foreground/60 tabular-nums">
885
+ {child.charCount.toLocaleString()} chars
886
+ </span>
887
+ </div>
888
+ <pre className="whitespace-pre-wrap wrap-break-word text-xs leading-relaxed text-foreground font-mono">
889
+ {child.content}
890
+ </pre>
891
+ </div>
892
+ );
893
+ })}
894
+ </div>
895
+ </ScrollArea>
896
+ <DialogFooter className="h-full">
897
+ <Button variant="outline" onClick={onClose} className={''}>
898
+ Close
899
+ </Button>
900
+ </DialogFooter>
901
+ </DialogContent>
902
+ </Dialog>
903
+ );
904
+ }
905
+
906
+ function DocumentDialog({
907
+ doc,
908
+ onOpenChange,
909
+ onSaved,
910
+ onDelete,
911
+ }: {
912
+ doc: SourceDocument | null;
913
+ onOpenChange: (open: boolean) => void;
914
+ onSaved: () => void;
915
+ onDelete: (id: string) => void;
916
+ }) {
917
+ const [editing, setEditing] = useState(false);
918
+ const [title, setTitle] = useState('');
919
+ const [url, setUrl] = useState('');
920
+ const [content, setContent] = useState('');
921
+ const [saving, setSaving] = useState(false);
922
+ const [uploadingMedia, setUploadingMedia] = useState(false);
923
+ const fileInputRef = useRef<HTMLInputElement>(null);
924
+
925
+ useEffect(() => {
926
+ if (doc) {
927
+ setEditing(false);
928
+ setTitle(doc.title);
929
+ setUrl(doc.url ?? '');
930
+ setContent(doc.content);
931
+ }
932
+ }, [doc]);
933
+
934
+ async function save() {
935
+ if (!doc) return;
936
+ if (!content.trim()) {
937
+ toast.error("Content can't be empty.");
938
+ return;
939
+ }
940
+ setSaving(true);
941
+ try {
942
+ const res = await fetch(workspaceUrl('/api/documents'), {
943
+ method: 'PATCH',
944
+ headers: { 'Content-Type': 'application/json' },
945
+ body: JSON.stringify({
946
+ id: doc.id,
947
+ title: title.trim() || 'Untitled',
948
+ url: url.trim(),
949
+ content,
950
+ }),
951
+ });
952
+ if (!res.ok) {
953
+ const body = await res.json().catch(() => ({}));
954
+ toast.error(
955
+ body.error ? formatErrorMessage(new Error(body.error)) : 'Failed to save document.',
956
+ );
957
+ return;
958
+ }
959
+ toast.success('Document saved');
960
+ onSaved();
961
+ } catch (err) {
962
+ toast.error(formatErrorMessage(err) || 'Could not reach the server.');
963
+ } finally {
964
+ setSaving(false);
965
+ }
966
+ }
967
+
968
+ async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
969
+ const file = e.target.files?.[0];
970
+ if (!file) return;
971
+
972
+ setUploadingMedia(true);
973
+ try {
974
+ const formData = new FormData();
975
+ formData.append('file', file);
976
+
977
+ const res = await fetch(workspaceUrl('/api/media'), {
978
+ method: 'POST',
979
+ body: formData,
980
+ });
981
+
982
+ if (!res.ok) {
983
+ const err = await res.json().catch(() => ({}));
984
+ throw new Error(err.error || 'Failed to upload file');
985
+ }
986
+
987
+ const data = await res.json();
988
+ if (data.assets && data.assets.length > 0) {
989
+ setUrl(workspaceUrl(`/api/media/${data.assets[0].id}`));
990
+ toast.success('File uploaded and URL updated');
991
+ }
992
+ } catch (err) {
993
+ toast.error(formatErrorMessage(err) || 'Failed to upload');
994
+ } finally {
995
+ setUploadingMedia(false);
996
+ if (fileInputRef.current) {
997
+ fileInputRef.current.value = '';
998
+ }
999
+ }
1000
+ }
1001
+
1002
+ return (
1003
+ <Dialog open={!!doc} onOpenChange={onOpenChange}>
1004
+ <DialogContent className="w-full max-w-5xl! ">
1005
+ <DialogHeader>
1006
+ <DialogTitle className="text-balance">
1007
+ {editing ? 'Edit document' : doc?.title}
1008
+ </DialogTitle>
1009
+ {!editing && doc?.url && (
1010
+ <DialogDescription className="break-all">{doc.url}</DialogDescription>
1011
+ )}
1012
+ {editing && (
1013
+ <DialogDescription>
1014
+ Changes are saved to this server&apos;s corpus. Re-index to apply them to search.
1015
+ </DialogDescription>
1016
+ )}
1017
+ </DialogHeader>
1018
+
1019
+ {editing ? (
1020
+ <div
1021
+ className="grid gap-4 overflow-y-auto pr-2 pb-2 "
1022
+ style={{ maxHeight: 'calc(80vh - 150px)' }}
1023
+ >
1024
+ <div className="grid gap-2">
1025
+ <Label htmlFor="doc-title">Title</Label>
1026
+ <Input
1027
+ id="doc-title"
1028
+ value={title}
1029
+ onChange={(e) => setTitle(e.target.value)}
1030
+ placeholder="Untitled"
1031
+ />
1032
+ </div>
1033
+ <div className="grid gap-2">
1034
+ <div className="flex items-center justify-between">
1035
+ <Label htmlFor="doc-url">URL (optional)</Label>
1036
+ <Button
1037
+ type="button"
1038
+ variant="ghost"
1039
+ size="sm"
1040
+ className="h-6 text-[10px] uppercase font-medium text-primary hover:bg-primary/10"
1041
+ onClick={() => fileInputRef.current?.click()}
1042
+ disabled={uploadingMedia}
1043
+ >
1044
+ {uploadingMedia ? (
1045
+ <Loader2 className="size-3 mr-1 animate-spin" />
1046
+ ) : (
1047
+ <Upload className="size-3 mr-1" />
1048
+ )}
1049
+ Upload File
1050
+ </Button>
1051
+ <input
1052
+ type="file"
1053
+ ref={fileInputRef}
1054
+ className="hidden"
1055
+ onChange={handleFileUpload}
1056
+ accept="image/*,video/*,audio/*"
1057
+ />
1058
+ </div>
1059
+ <Input
1060
+ id="doc-url"
1061
+ value={url}
1062
+ onChange={(e) => setUrl(e.target.value)}
1063
+ placeholder="https://…"
1064
+ />
1065
+ </div>
1066
+ <div className="grid gap-2">
1067
+ <div className="flex items-center justify-between">
1068
+ <Label htmlFor="doc-content">Content</Label>
1069
+ <span className="text-xs text-muted-foreground tabular-nums ">
1070
+ {content.length.toLocaleString()} chars
1071
+ </span>
1072
+ </div>
1073
+ <Textarea
1074
+ id="doc-content"
1075
+ value={content}
1076
+ onChange={(e) => setContent(e.target.value)}
1077
+ rows={14}
1078
+ className="resize-none font-mono text-xs leading-relaxed"
1079
+ />
1080
+ </div>
1081
+ </div>
1082
+ ) : (
1083
+ <div className="flex flex-col gap-4">
1084
+ {doc?.source === 'media' && doc?.url && (
1085
+ <div className="rounded-md border border-border bg-muted/30 overflow-hidden flex items-center justify-center p-4">
1086
+ {doc.metadata?.mediaType === 'video' ? (
1087
+ <video src={doc.url} controls className="max-h-[40vh] max-w-full" />
1088
+ ) : doc.metadata?.mediaType === 'audio' ? (
1089
+ <audio src={doc.url} controls className="w-full" />
1090
+ ) : (
1091
+ <img
1092
+ src={doc.url}
1093
+ alt={doc.title}
1094
+ className="max-h-[40vh] max-w-full object-contain"
1095
+ />
1096
+ )}
1097
+ </div>
1098
+ )}
1099
+ <ScrollArea className="max-h-[60vh] rounded-md border border-border bg-muted/30">
1100
+ <pre
1101
+ className={cn(
1102
+ 'whitespace-pre-wrap break-all px-4 py-3 font-mono text-xs leading-relaxed text-foreground',
1103
+ )}
1104
+ >
1105
+ {doc?.content}
1106
+ </pre>
1107
+ </ScrollArea>
1108
+ </div>
1109
+ )}
1110
+
1111
+ <DialogFooter className="flex-row items-center justify-between gap-2 sm:justify-between">
1112
+ {editing ? (
1113
+ <>
1114
+ <Button variant="ghost" onClick={() => setEditing(false)} disabled={saving}>
1115
+ <X className="size-4" />
1116
+ Cancel
1117
+ </Button>
1118
+ <Button onClick={() => void save()} disabled={saving}>
1119
+ {saving && <Loader2 className="size-4 animate-spin" />}
1120
+ Save changes
1121
+ </Button>
1122
+ </>
1123
+ ) : (
1124
+ <>
1125
+ <Button
1126
+ variant="destructive"
1127
+ className="text-white hover:text-white hover:bg-destructive/80"
1128
+ onClick={() => doc && onDelete(doc.id)}
1129
+ >
1130
+ <Trash2 className="size-4 mr-2" />
1131
+ Delete
1132
+ </Button>
1133
+ <Button onClick={() => setEditing(true)}>
1134
+ <Pencil className="size-4 mr-2" />
1135
+ Edit
1136
+ </Button>
1137
+ </>
1138
+ )}
1139
+ </DialogFooter>
1140
+ </DialogContent>
1141
+ </Dialog>
1142
+ );
1143
+ }
1144
+
1145
+ function CorpusSkeleton() {
1146
+ return (
1147
+ <div className="space-y-4 animate-in fade-in duration-150">
1148
+ {/* Filter bar skeleton */}
1149
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center justify-between pb-3">
1150
+ <div className="flex flex-wrap items-center gap-2">
1151
+ {['All Sources', 'Website', 'Text', 'Files', 'Media', 'Integrations'].map((label, i) => (
1152
+ <div
1153
+ key={label}
1154
+ className={cn(
1155
+ 'h-7 rounded-full px-3 text-xs font-medium flex items-center',
1156
+ i === 0 ? 'bg-muted/80 text-transparent w-22' : 'bg-muted/40 text-transparent w-16',
1157
+ 'animate-pulse',
1158
+ )}
1159
+ >
1160
+ &nbsp;
1161
+ </div>
1162
+ ))}
1163
+ </div>
1164
+ <div className="flex items-center gap-2">
1165
+ <div className="h-9 w-32 rounded-md bg-muted/50 animate-pulse border border-border/50" />
1166
+ <div className="h-9 w-28 rounded-md bg-muted/50 animate-pulse border border-border/50" />
1167
+ <div className="size-9 rounded-md bg-muted/50 animate-pulse border border-border/50" />
1168
+ </div>
1169
+ </div>
1170
+
1171
+ {/* Table skeleton */}
1172
+ <div className="overflow-hidden rounded-lg border border-border">
1173
+ <Table>
1174
+ <TableHeader>
1175
+ <TableRow className="bg-muted/50">
1176
+ <TableHead className="w-12">
1177
+ <div className="size-4 rounded bg-muted/70 animate-pulse" />
1178
+ </TableHead>
1179
+ <TableHead>Title</TableHead>
1180
+ <TableHead className="w-24">Status</TableHead>
1181
+ <TableHead className="w-28">Source</TableHead>
1182
+ <TableHead className="w-24 text-right">Chars</TableHead>
1183
+ <TableHead className="w-20" />
1184
+ </TableRow>
1185
+ </TableHeader>
1186
+ <TableBody className="bg-card">
1187
+ {Array.from({ length: 5 }).map((_, index) => (
1188
+ <TableRow key={index} className="border-t border-border/60">
1189
+ <TableCell className="w-12 py-3.5">
1190
+ <div className="size-4 rounded bg-muted/60 animate-pulse" />
1191
+ </TableCell>
1192
+ <TableCell className="py-3.5">
1193
+ <div className="space-y-1.5">
1194
+ <div
1195
+ className="h-4 rounded bg-muted/80 animate-pulse"
1196
+ style={{ width: `${Math.max(35, 75 - index * 10)}%` }}
1197
+ />
1198
+ <div className="h-3 w-40 rounded bg-muted/40 animate-pulse" />
1199
+ </div>
1200
+ </TableCell>
1201
+ <TableCell className="w-24 py-3.5">
1202
+ <div className="h-5 w-16 rounded-full bg-muted/60 animate-pulse" />
1203
+ </TableCell>
1204
+ <TableCell className="w-28 py-3.5">
1205
+ <div className="flex items-center gap-2">
1206
+ <div className="size-4 rounded bg-muted/60 animate-pulse" />
1207
+ <div className="h-3 w-14 rounded bg-muted/60 animate-pulse" />
1208
+ </div>
1209
+ </TableCell>
1210
+ <TableCell className="w-24 py-3.5 text-right">
1211
+ <div className="h-3 w-12 ml-auto rounded bg-muted/60 animate-pulse" />
1212
+ </TableCell>
1213
+ <TableCell className="w-20 py-3.5 text-right">
1214
+ <div className="size-6 ml-auto rounded bg-muted/40 animate-pulse" />
1215
+ </TableCell>
1216
+ </TableRow>
1217
+ ))}
1218
+ </TableBody>
1219
+ </Table>
1220
+ </div>
1221
+ </div>
1222
+ );
1223
+ }