@larkup/workspace-app 0.0.0-stage → 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 (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,1313 @@
1
+ 'use client';
2
+
3
+ import {
4
+ workspaceStorageKey,
5
+ workspaceStorageScope,
6
+ workspaceUrl,
7
+ workspaceFetch as fetch,
8
+ } from '../../host';
9
+ import { SourceUploadDropzone } from '@larkup/workspace-ui';
10
+
11
+ import { useEffect, useRef, useState } from 'react';
12
+ import { toast } from 'sonner';
13
+ import { formatErrorMessage } from '../../lib/shared/error-formatter';
14
+ import {
15
+ FileUp,
16
+ Loader2,
17
+ X,
18
+ Settings2,
19
+ Columns,
20
+ Plus,
21
+ Database,
22
+ Image as ImageIcon,
23
+ AlertTriangle,
24
+ Link2,
25
+ Eye,
26
+ } from 'lucide-react';
27
+ import { Button } from '@larkup/workspace-ui/button';
28
+ import { Input } from '../ui/input';
29
+ import { cn } from '../../lib/utils';
30
+ import { TabularPreview } from './tabular-preview';
31
+ import type { DataPrimaryAction } from './data-primary-action';
32
+ import Papa from 'papaparse';
33
+ import * as XLSX from 'xlsx';
34
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../ui/dialog';
35
+ import {
36
+ Select,
37
+ SelectContent,
38
+ SelectItem,
39
+ SelectTrigger,
40
+ SelectValue,
41
+ } from '@larkup/workspace-ui/select';
42
+ import { Checkbox } from '../ui/checkbox';
43
+ import { Label } from '@larkup/workspace-ui/label';
44
+ import { Switch } from '../ui/switch';
45
+ import { Progress } from '../ui/progress';
46
+ import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
47
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
48
+ import { DataEntryModeSwitch } from './data-entry-mode-switch';
49
+
50
+ const ACCEPT = '.txt,.md,.markdown,.toml,.json,.csv,.html,.htm,.log,.xlsx,.xls,.pdf,.doc,.docx';
51
+ const CONTENT_PREVIEW_LIMIT = 100_000;
52
+
53
+ type FileFormat = 'plain' | 'lines' | 'structured';
54
+
55
+ interface StagedFile {
56
+ id: string;
57
+ name: string;
58
+ size: number;
59
+ format: FileFormat;
60
+ rawContent?: string;
61
+ rows?: any[];
62
+ keys?: string[];
63
+ // mapping for structured
64
+ titleKey?: string;
65
+ contentKeys?: string[];
66
+ contentSeparator?: string;
67
+ metadataKeys?: string[];
68
+ globalMetadata?: { key: string; value: string }[];
69
+ /** When true, structured data is also saved as a TabularDataset for analytics */
70
+ indexAsTabular?: boolean;
71
+ /** Expanded preview state */
72
+ showPreview?: boolean;
73
+ /** When true, images from PDF will be extracted and indexed */
74
+ indexImages?: boolean;
75
+ /** The original file object for client-side processing */
76
+ fileObject?: File;
77
+ /** The public URL used to import this file, retained as source metadata. */
78
+ sourceUrl?: string;
79
+ }
80
+
81
+ interface ImageIndexingCapability {
82
+ available: boolean;
83
+ message?: string;
84
+ }
85
+
86
+ const stagedFilesByScope = new Map<string, StagedFile[]>();
87
+ const UPLOAD_QUEUE_KEY = 'larkup.upload-queue.v1';
88
+
89
+ function restoreQueue(key: string): StagedFile[] {
90
+ if (typeof window === 'undefined') return [];
91
+ try {
92
+ const saved = sessionStorage.getItem(key);
93
+ return saved ? (JSON.parse(saved) as StagedFile[]) : [];
94
+ } catch {
95
+ return [];
96
+ }
97
+ }
98
+
99
+ function persistQueue(files: StagedFile[], key: string) {
100
+ try {
101
+ const serializable = files.map(({ fileObject: _fileObject, ...file }) => file);
102
+ sessionStorage.setItem(key, JSON.stringify(serializable));
103
+ } catch {}
104
+ }
105
+
106
+ export function UploadPanel({
107
+ onAdded,
108
+ onActionChange,
109
+ groupId,
110
+ }: {
111
+ onAdded: () => void;
112
+ onActionChange?: (action: DataPrimaryAction | null) => void;
113
+ groupId?: string;
114
+ }) {
115
+ const inputRef = useRef<HTMLInputElement>(null);
116
+ const scope = useRef(workspaceStorageScope()).current;
117
+ const queueKey = useRef(workspaceStorageKey(UPLOAD_QUEUE_KEY)).current;
118
+ const initialStaged = stagedFilesByScope.get(scope) ?? restoreQueue(queueKey);
119
+ const [staged, setStagedState] = useState<StagedFile[]>(initialStaged);
120
+ const stagedRef = useRef(initialStaged);
121
+ const savingRef = useRef(false);
122
+ const groupIdRef = useRef(groupId ?? 'default');
123
+ groupIdRef.current = groupId ?? 'default';
124
+ const [indexAllImages, setIndexAllImages] = useState(false);
125
+ const [imageIndexingCapability, setImageIndexingCapability] =
126
+ useState<ImageIndexingCapability | null>(null);
127
+
128
+ useEffect(() => {
129
+ let active = true;
130
+ void fetch(workspaceUrl('/api/config'))
131
+ .then((response) => (response.ok ? response.json() : Promise.reject()))
132
+ .then((data) => {
133
+ if (!active) return;
134
+ const capability = data?.capabilities?.imageIndexing;
135
+ setImageIndexingCapability(
136
+ capability?.available
137
+ ? { available: true }
138
+ : {
139
+ available: false,
140
+ message:
141
+ capability?.message ??
142
+ 'No vision-capable model is configured. PDF images will not be indexed.',
143
+ },
144
+ );
145
+ })
146
+ .catch(() => {
147
+ if (active) {
148
+ setImageIndexingCapability({
149
+ available: false,
150
+ message: 'Could not verify a vision model. PDF images will not be indexed.',
151
+ });
152
+ }
153
+ });
154
+ return () => {
155
+ active = false;
156
+ };
157
+ }, []);
158
+
159
+ const setStaged = (val: React.SetStateAction<StagedFile[]>) => {
160
+ const next = typeof val === 'function' ? val(stagedRef.current) : val;
161
+ stagedRef.current = next;
162
+ stagedFilesByScope.set(scope, next);
163
+ persistQueue(next, queueKey);
164
+ setStagedState(next);
165
+ };
166
+ const imageIndexingAvailable = imageIndexingCapability?.available === true;
167
+
168
+ useEffect(() => {
169
+ if (!imageIndexingCapability || imageIndexingCapability.available) return;
170
+ setIndexAllImages(false);
171
+ setStaged((files) =>
172
+ files.map((file) =>
173
+ file.name.toLowerCase().endsWith('.pdf') ? { ...file, indexImages: false } : file,
174
+ ),
175
+ );
176
+ }, [imageIndexingCapability]);
177
+ const [dragging, setDragging] = useState(false);
178
+ const [entryTab, setEntryTab] = useState<'upload' | 'url'>('upload');
179
+ const [remoteUrl, setRemoteUrl] = useState('');
180
+ const [loadingRemoteFile, setLoadingRemoteFile] = useState(false);
181
+ const [saving, setSaving] = useState(false);
182
+ const [progress, setProgress] = useState<{
183
+ message?: string;
184
+ current: number;
185
+ total: number;
186
+ } | null>(null);
187
+
188
+ // Mapping state
189
+ const [editingFileId, setEditingFileId] = useState<string | null>(null);
190
+ const [previewingFileId, setPreviewingFileId] = useState<string | null>(null);
191
+ const editingFile = staged.find((f) => f.id === editingFileId);
192
+ const previewingFile = staged.find((f) => f.id === previewingFileId);
193
+ const isRemoteEntry = entryTab === 'url';
194
+
195
+ async function readFiles(files: FileList | File[], sourceUrl?: string) {
196
+ const joinedActiveSave = savingRef.current;
197
+ const next: StagedFile[] = [];
198
+ const stage = (file: StagedFile) => next.push({ ...file, sourceUrl });
199
+ for (const file of Array.from(files)) {
200
+ const ext = file.name.split('.').pop()?.toLowerCase();
201
+
202
+ // Prevent dragging media into the documents tab
203
+ const isRemotePlainText = Boolean(sourceUrl && file.type.startsWith('text/'));
204
+ if (ext && !ACCEPT.includes(`.${ext}`) && !isRemotePlainText) {
205
+ toast.error(
206
+ `Unsupported file type: ${file.name}. Please upload media files in the Media tab.`,
207
+ );
208
+ continue;
209
+ }
210
+
211
+ const id = Math.random().toString(36).slice(2);
212
+ try {
213
+ if (ext === 'csv') {
214
+ const content = await file.text();
215
+ const result = Papa.parse(content, {
216
+ header: true,
217
+ skipEmptyLines: true,
218
+ });
219
+ if (result.data.length > 0) {
220
+ const keys = Object.keys(result.data[0] as object);
221
+ stage({
222
+ id,
223
+ name: file.name,
224
+ size: file.size,
225
+ format: 'structured',
226
+ rows: result.data,
227
+ keys,
228
+ titleKey: keys[0],
229
+ contentKeys: keys,
230
+ contentSeparator: ', ',
231
+ metadataKeys: keys.slice(1),
232
+ globalMetadata: [],
233
+ indexAsTabular: true,
234
+ });
235
+ }
236
+ } else if (ext === 'xlsx' || ext === 'xls') {
237
+ const data = await file.arrayBuffer();
238
+ const workbook = XLSX.read(data, { type: 'array', cellDates: true });
239
+ const hasMultipleSheets = workbook.SheetNames.length > 1;
240
+ let indexedSheets = 0;
241
+ for (const sheetName of workbook.SheetNames) {
242
+ const sheet = workbook.Sheets[sheetName];
243
+ const rawRows = XLSX.utils.sheet_to_json(sheet, { defval: null }) as Record<
244
+ string,
245
+ any
246
+ >[];
247
+ const rows = rawRows.map((row) => {
248
+ const out: Record<string, any> = {};
249
+ for (const [k, v] of Object.entries(row)) {
250
+ out[k] = v instanceof Date ? v.toISOString().split('T')[0] : v;
251
+ }
252
+ return out;
253
+ });
254
+ if (rows.length === 0) continue;
255
+ const keys = Object.keys(rows[0] as object);
256
+ stage({
257
+ id: `${id}-${indexedSheets}`,
258
+ name: hasMultipleSheets ? `${file.name} — ${sheetName}` : file.name,
259
+ size: file.size,
260
+ format: 'structured',
261
+ rows,
262
+ keys,
263
+ titleKey: keys[0],
264
+ contentKeys: keys,
265
+ contentSeparator: ', ',
266
+ metadataKeys: keys.slice(1),
267
+ globalMetadata: [],
268
+ indexAsTabular: true,
269
+ });
270
+ indexedSheets++;
271
+ }
272
+ if (indexedSheets === 0) {
273
+ toast.error(`Could not find tabular rows in ${file.name}.`);
274
+ }
275
+ } else if (ext === 'json') {
276
+ const content = await file.text();
277
+ try {
278
+ const parsed = JSON.parse(content);
279
+ if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'object') {
280
+ const keys = Object.keys(parsed[0]);
281
+ stage({
282
+ id,
283
+ name: file.name,
284
+ size: file.size,
285
+ format: 'structured',
286
+ rows: parsed,
287
+ keys,
288
+ titleKey: keys[0],
289
+ contentKeys: keys,
290
+ contentSeparator: ', ',
291
+ metadataKeys: keys.slice(1),
292
+ globalMetadata: [],
293
+ indexAsTabular: true,
294
+ });
295
+ } else {
296
+ stage({
297
+ id,
298
+ name: file.name,
299
+ size: file.size,
300
+ format: 'plain',
301
+ rawContent: content,
302
+ });
303
+ }
304
+ } catch {
305
+ stage({
306
+ id,
307
+ name: file.name,
308
+ size: file.size,
309
+ format: 'plain',
310
+ rawContent: content,
311
+ });
312
+ }
313
+ } else if (ext === 'pdf' || ext === 'doc' || ext === 'docx') {
314
+ const formData = new FormData();
315
+ formData.append('file', file);
316
+
317
+ const res = await fetch(workspaceUrl('/api/parse-file'), {
318
+ method: 'POST',
319
+ body: formData,
320
+ });
321
+
322
+ const data = await res.json().catch(() => ({}));
323
+ if (!res.ok) {
324
+ throw new Error(data.error || `Could not extract text from ${file.name}`);
325
+ }
326
+
327
+ const { text } = data;
328
+ const hasReadableText = data.hasReadableText ?? Boolean(text?.trim());
329
+ const imageOnlyPdf = ext === 'pdf' && !hasReadableText;
330
+ if (!hasReadableText && !imageOnlyPdf) {
331
+ throw new Error(
332
+ 'This PDF does not contain selectable text. Try another copy of the file.',
333
+ );
334
+ }
335
+ if (imageOnlyPdf && imageIndexingCapability?.available === false) {
336
+ throw new Error(
337
+ 'This PDF contains scanned or image-only pages. Configure a vision model in Settings → AI Models to index it.',
338
+ );
339
+ }
340
+ if (imageOnlyPdf) {
341
+ toast.message(
342
+ `No selectable text found in ${file.name}; PDF image indexing was enabled automatically.`,
343
+ );
344
+ }
345
+ stage({
346
+ id,
347
+ name: file.name,
348
+ size: file.size,
349
+ format: 'plain',
350
+ rawContent: hasReadableText ? text : undefined,
351
+ fileObject: file,
352
+ indexImages: indexAllImages || imageOnlyPdf,
353
+ });
354
+ } else {
355
+ const content = await file.text();
356
+ stage({
357
+ id,
358
+ name: file.name,
359
+ size: file.size,
360
+ format: 'plain',
361
+ rawContent: content,
362
+ });
363
+ }
364
+ } catch (err) {
365
+ toast.error(`Could not read ${file.name}. ${formatErrorMessage(err)}`);
366
+ }
367
+ }
368
+ setStaged((prev) => [...prev, ...next]);
369
+ if (joinedActiveSave && next.length > 0) {
370
+ toast.message(`${next.length} file${next.length === 1 ? '' : 's'} added to the queue`);
371
+ window.setTimeout(() => void ingest(), 0);
372
+ }
373
+ return next;
374
+ }
375
+
376
+ async function loadRemoteFile() {
377
+ const requestedUrl = remoteUrl.trim();
378
+ if (!requestedUrl) {
379
+ toast.error('Paste a public link to a file first.');
380
+ return;
381
+ }
382
+
383
+ setLoadingRemoteFile(true);
384
+ try {
385
+ const response = await fetch(workspaceUrl('/api/files/remote'), {
386
+ method: 'POST',
387
+ headers: { 'Content-Type': 'application/json' },
388
+ body: JSON.stringify({ url: requestedUrl }),
389
+ });
390
+ if (!response.ok) {
391
+ const body = await response.json().catch(() => ({}));
392
+ throw new Error(body.error || 'Could not load this remote file.');
393
+ }
394
+
395
+ const fileName = decodeURIComponent(
396
+ response.headers.get('X-Larkup-File-Name') || 'remote-file',
397
+ );
398
+ const sourceUrl = decodeURIComponent(
399
+ response.headers.get('X-Larkup-Source-Url') || requestedUrl,
400
+ );
401
+ const bytes = await response.arrayBuffer();
402
+ if (!bytes.byteLength) throw new Error('This remote file is empty.');
403
+
404
+ const loadedFiles = await readFiles(
405
+ [new File([bytes], fileName, { type: response.headers.get('content-type') || '' })],
406
+ sourceUrl,
407
+ );
408
+ if (loadedFiles.length === 0) return;
409
+ setRemoteUrl('');
410
+ setEntryTab('upload');
411
+ await ingest(loadedFiles);
412
+ } catch (error) {
413
+ toast.error(formatErrorMessage(error));
414
+ } finally {
415
+ setLoadingRemoteFile(false);
416
+ }
417
+ }
418
+
419
+ async function ingest(files?: StagedFile[]) {
420
+ if (savingRef.current || (files?.length ?? stagedRef.current.length) === 0) return;
421
+ savingRef.current = true;
422
+ setSaving(true);
423
+ let ok = 0;
424
+
425
+ const filesToIngest = files ? [...files] : [...stagedRef.current];
426
+ const payloads: any[] = [];
427
+ const destinationGroupId = groupIdRef.current;
428
+ const tabularDatasetIds = new Map<string, string>();
429
+
430
+ for (const f of filesToIngest) {
431
+ if (f.format === 'structured' && f.rows && f.indexAsTabular) {
432
+ try {
433
+ const res = await fetch(workspaceUrl('/api/tabular'), {
434
+ method: 'POST',
435
+ headers: { 'Content-Type': 'application/json' },
436
+ body: JSON.stringify({
437
+ fileName: f.name,
438
+ rows: f.rows,
439
+ groupId: destinationGroupId,
440
+ }),
441
+ });
442
+ if (res.ok) {
443
+ const result = await res.json();
444
+ if (typeof result.id === 'string') tabularDatasetIds.set(f.id, result.id);
445
+ toast.success(
446
+ `Saved "${f.name}" as tabular dataset (${result.rowCount} rows, ${result.columns.length} columns)`,
447
+ );
448
+ }
449
+ } catch {
450
+ /* continue — still add as documents */
451
+ }
452
+ }
453
+ }
454
+
455
+ // Then: create document payloads
456
+ for (const f of filesToIngest) {
457
+ if (f.format === 'plain' && (f.rawContent || f.name.toLowerCase().endsWith('.pdf'))) {
458
+ let fileUrl = undefined;
459
+ // Upload the physical file so chat can link to it
460
+ if (f.fileObject) {
461
+ try {
462
+ const formData = new FormData();
463
+ formData.append('file', f.fileObject);
464
+ const uploadRes = await fetch(workspaceUrl('/api/upload-file'), {
465
+ method: 'POST',
466
+ body: formData,
467
+ });
468
+ if (uploadRes.ok) {
469
+ const { url } = await uploadRes.json();
470
+ fileUrl = url;
471
+ }
472
+ } catch (err) {
473
+ console.error('Failed to upload file for citation link', err);
474
+ }
475
+ }
476
+
477
+ const hasText = f.rawContent && f.rawContent.trim().length > 0;
478
+ const uploadedImages: any[] = [];
479
+ let skippedImageCount = 0;
480
+
481
+ // Extract images if requested (for PDFs)
482
+ if (
483
+ f.indexImages &&
484
+ imageIndexingAvailable &&
485
+ f.fileObject &&
486
+ f.name.toLowerCase().endsWith('.pdf')
487
+ ) {
488
+ try {
489
+ setProgress({ message: `Extracting images from ${f.name}...`, current: 0, total: 100 });
490
+ const { extractImagesFromPDF } = await import('../../lib/media/pdf');
491
+ const images = await extractImagesFromPDF(f.fileObject);
492
+ const BATCH_SIZE = 5;
493
+ for (let i = 0; i < images.length; i += BATCH_SIZE) {
494
+ const batch = images.slice(i, i + BATCH_SIZE);
495
+
496
+ await Promise.all(
497
+ batch.map(async (img, batchIdx) => {
498
+ const currentIdx = i + batchIdx;
499
+ setProgress({
500
+ message: `Analyzing image ${currentIdx + 1} of ${images.length} from ${
501
+ f.name
502
+ }...`,
503
+ current: currentIdx,
504
+ total: images.length || 1,
505
+ });
506
+
507
+ const res = await fetch(img.base64);
508
+ const blob = await res.blob();
509
+ const file = new File([blob], `image-${img.index}.jpg`, { type: 'image/jpeg' });
510
+
511
+ const formData = new FormData();
512
+ formData.append('file', file);
513
+ const uploadRes = await fetch(workspaceUrl('/api/upload-file'), {
514
+ method: 'POST',
515
+ body: formData,
516
+ });
517
+
518
+ if (uploadRes.ok) {
519
+ const { url } = await uploadRes.json();
520
+
521
+ let description = '';
522
+ try {
523
+ const descRes = await fetch(workspaceUrl('/api/describe-image'), {
524
+ method: 'POST',
525
+ headers: { 'Content-Type': 'application/json' },
526
+ body: JSON.stringify({
527
+ base64: img.base64,
528
+ prompt:
529
+ 'Create accurate retrieval notes for this rendered document page. Preserve captions, figure and table identifiers, equations and their grouping, readable labels, legends, headings, row/column relationships, and diagram connections. Do not invent unreadable text; say when text is unclear. Use concise labeled bullets so later questions can retrieve the correct page without re-reading the visual.',
530
+ }),
531
+ });
532
+ if (descRes.ok) {
533
+ const descData = await descRes.json();
534
+ if (descData.description) {
535
+ description = descData.description;
536
+ }
537
+ }
538
+ } catch (e) {
539
+ console.error('Failed to describe image:', e);
540
+ }
541
+
542
+ if (description.trim()) {
543
+ uploadedImages.push({
544
+ imageUrl: url,
545
+ pageNumber: img.pageNumber,
546
+ index: img.index,
547
+ description,
548
+ });
549
+ } else {
550
+ skippedImageCount++;
551
+ }
552
+ }
553
+ }),
554
+ );
555
+ }
556
+ // Ensure images remain in original order after concurrent processing
557
+ uploadedImages.sort((a, b) => a.index - b.index);
558
+ if (skippedImageCount > 0) {
559
+ toast.warning(
560
+ `${skippedImageCount} image${skippedImageCount === 1 ? '' : 's'} from ${f.name} could not be analyzed and were not indexed.`,
561
+ );
562
+ }
563
+ } catch (err) {
564
+ console.error('Failed to extract images from PDF:', err);
565
+ }
566
+ }
567
+
568
+ if (hasText || uploadedImages.length > 0) {
569
+ let contentStr = hasText ? f.rawContent : `Images extracted from ${f.name}`;
570
+ if (uploadedImages.length > 0) {
571
+ const descriptions = uploadedImages
572
+ .map(
573
+ (img) =>
574
+ `Image ${img.index + 1} (Page ${img.pageNumber}): ${
575
+ img.description || 'No description'
576
+ }`,
577
+ )
578
+ .join('\n');
579
+ contentStr += `\n\nImage Descriptions:\n${descriptions}`;
580
+ }
581
+
582
+ payloads.push({
583
+ title: f.name,
584
+ content: contentStr,
585
+ source: 'files',
586
+ url: fileUrl ?? f.sourceUrl,
587
+ metadata: {
588
+ ...(f.name.toLowerCase().endsWith('.pdf')
589
+ ? { mimeType: 'application/pdf', fileKind: 'pdf' }
590
+ : {}),
591
+ ...(f.sourceUrl ? { originalUrl: f.sourceUrl } : {}),
592
+ ...(uploadedImages.length > 0
593
+ ? {
594
+ images: uploadedImages.map((img) => ({
595
+ imageUrl: img.imageUrl,
596
+ pageNumber: img.pageNumber,
597
+ index: img.index,
598
+ description: img.description,
599
+ })),
600
+ }
601
+ : {}),
602
+ },
603
+ });
604
+ }
605
+ } else if (f.format === 'lines' && f.rawContent) {
606
+ payloads.push({
607
+ title: f.name,
608
+ content: f.rawContent,
609
+ source: 'files',
610
+ metadata: {
611
+ fileName: f.name,
612
+ format: 'lines',
613
+ ...(f.sourceUrl ? { originalUrl: f.sourceUrl } : {}),
614
+ },
615
+ });
616
+ } else if (f.format === 'structured' && f.rows) {
617
+ const rows = f.rows
618
+ .map((row) => {
619
+ const fields = f.contentKeys?.length
620
+ ? f.contentKeys.map((key) => `${key}: ${String(row[key] ?? '')}`)
621
+ : Object.entries(row).map(([key, value]) => `${key}: ${String(value ?? '')}`);
622
+ return fields.join(f.contentSeparator || ' | ');
623
+ })
624
+ .filter(Boolean);
625
+ const metadata: Record<string, unknown> = {
626
+ fileName: f.name,
627
+ rowCount: f.rows.length,
628
+ ...(tabularDatasetIds.get(f.id) ? { tabularDatasetId: tabularDatasetIds.get(f.id) } : {}),
629
+ ...(f.sourceUrl ? { originalUrl: f.sourceUrl } : {}),
630
+ };
631
+ for (const item of f.globalMetadata ?? []) {
632
+ if (item.key.trim()) metadata[item.key.trim()] = item.value;
633
+ }
634
+ if (rows.length) {
635
+ payloads.push({ title: f.name, content: rows.join('\n'), metadata, source: 'files' });
636
+ }
637
+ }
638
+ }
639
+
640
+ setProgress({ message: 'Uploading documents...', current: 0, total: payloads.length || 1 });
641
+
642
+ for (const [index, p] of payloads.entries()) {
643
+ try {
644
+ const res = await fetch(workspaceUrl('/api/documents'), {
645
+ method: 'POST',
646
+ headers: { 'Content-Type': 'application/json' },
647
+ body: JSON.stringify({ ...p, groupId: destinationGroupId }),
648
+ });
649
+ if (res.ok) ok++;
650
+ } catch {
651
+ /* continue */
652
+ }
653
+ setProgress({
654
+ message: 'Uploading documents...',
655
+ current: index + 1,
656
+ total: payloads.length || 1,
657
+ });
658
+ }
659
+
660
+ const stagedCount = filesToIngest.length;
661
+ savingRef.current = false;
662
+ setSaving(false);
663
+ setProgress(null);
664
+ const remaining = stagedRef.current.filter(
665
+ (pending) => !filesToIngest.some((ingested) => ingested.id === pending.id),
666
+ );
667
+ setStaged(remaining);
668
+ if (remaining.length === 0) sessionStorage.removeItem(queueKey);
669
+
670
+ // Also explicitly clear the input value just in case
671
+ if (inputRef.current) {
672
+ inputRef.current.value = '';
673
+ }
674
+
675
+ if (ok > 0) {
676
+ toast.success(
677
+ `Added ${ok} document${ok > 1 ? 's' : ''} from ${stagedCount} file${
678
+ stagedCount > 1 ? 's' : ''
679
+ }`,
680
+ );
681
+ onAdded();
682
+ } else {
683
+ toast.error('No text or images could be extracted from the uploaded files.');
684
+ }
685
+
686
+ if (!files && remaining.length > 0) window.setTimeout(() => void ingest(), 0);
687
+ }
688
+
689
+ useEffect(() => {
690
+ const actionBusy = saving || loadingRemoteFile;
691
+ onActionChange?.({
692
+ label: 'Add file',
693
+ onClick: isRemoteEntry ? () => void loadRemoteFile() : () => void ingest(),
694
+ disabled: actionBusy || (isRemoteEntry ? !remoteUrl.trim() : staged.length === 0),
695
+ loading: actionBusy,
696
+ });
697
+ return () => onActionChange?.(null);
698
+ }, [entryTab, staged, saving, remoteUrl, loadingRemoteFile, groupId, onActionChange]);
699
+
700
+ function updateEditingFile(patch: Partial<StagedFile>) {
701
+ setStaged((prev) => prev.map((f) => (f.id === editingFileId ? { ...f, ...patch } : f)));
702
+ }
703
+
704
+ return (
705
+ <div className="space-y-4 cursor-pointer">
706
+ <DataEntryModeSwitch
707
+ label="File source"
708
+ value={entryTab}
709
+ onValueChange={(value) => setEntryTab(value as 'upload' | 'url')}
710
+ options={[
711
+ { value: 'upload', label: 'Upload', icon: FileUp },
712
+ { value: 'url', label: 'From URL', icon: Link2 },
713
+ ]}
714
+ />
715
+
716
+ {entryTab === 'upload' ? (
717
+ <SourceUploadDropzone
718
+ onClick={() => inputRef.current?.click()}
719
+ onDragOver={(e) => {
720
+ e.preventDefault();
721
+ setDragging(true);
722
+ }}
723
+ onDragLeave={() => setDragging(false)}
724
+ onDrop={(e) => {
725
+ e.preventDefault();
726
+ setDragging(false);
727
+ if (e.dataTransfer.files?.length) readFiles(e.dataTransfer.files);
728
+ }}
729
+ dragging={dragging}
730
+ icon={<FileUp className="size-6 text-muted-foreground" />}
731
+ />
732
+ ) : (
733
+ <div className="space-y-1">
734
+ <Input
735
+ aria-label="Remote file URL"
736
+ type="url"
737
+ value={remoteUrl}
738
+ onChange={(event) => setRemoteUrl(event.target.value)}
739
+ onKeyDown={(event) => {
740
+ if (event.key === 'Enter') void loadRemoteFile();
741
+ }}
742
+ placeholder="https://example.com/my-report.pdf"
743
+ disabled={loadingRemoteFile}
744
+ className="bg-white"
745
+ />
746
+ <p className="text-[11px] pl-1 leading-relaxed text-muted-foreground">
747
+ Paste a public file URL, GitHub file link, or Hugging Face file link. PDF, Word, CSV,
748
+ JSON, Excel, and text files up to 50 MB are supported.
749
+ </p>
750
+ {!onActionChange && (
751
+ <Button
752
+ type="button"
753
+ onClick={() => void loadRemoteFile()}
754
+ disabled={!remoteUrl.trim() || loadingRemoteFile}
755
+ >
756
+ {loadingRemoteFile ? (
757
+ <Loader2 className="size-4 animate-spin" />
758
+ ) : (
759
+ <Link2 className="size-4" />
760
+ )}
761
+ Add file
762
+ </Button>
763
+ )}
764
+ </div>
765
+ )}
766
+ <input
767
+ ref={inputRef}
768
+ type="file"
769
+ multiple
770
+ accept={ACCEPT}
771
+ className="sr-only"
772
+ onChange={(e) => {
773
+ if (e.target.files?.length) readFiles(e.target.files);
774
+ e.target.value = '';
775
+ }}
776
+ />
777
+
778
+ {staged.length > 0 && (
779
+ <div className="space-y-2 pb-5">
780
+ {staged.some((file) => file.name.toLowerCase().endsWith('.pdf')) &&
781
+ imageIndexingCapability &&
782
+ !imageIndexingAvailable && (
783
+ <Alert className="border-amber-500/30 bg-amber-500/5">
784
+ <AlertTriangle className="size-4 text-amber-600 dark:text-amber-400" />
785
+ <AlertTitle>PDF image indexing is unavailable</AlertTitle>
786
+ <AlertDescription>
787
+ {imageIndexingCapability.message} Configure a vision model in Settings → AI Models
788
+ to index PDF visuals.
789
+ </AlertDescription>
790
+ </Alert>
791
+ )}
792
+ <div className="flex items-center justify-between px-1 ">
793
+ <span className="text-[13px] font-medium text-foreground">
794
+ {staged.length} file{staged.length !== 1 ? 's' : ''} staged
795
+ </span>
796
+ <div className="flex items-center gap-4 mt-5">
797
+ {staged.some((f) => f.name.toLowerCase().endsWith('.pdf')) && (
798
+ <div className="flex items-center gap-1.5">
799
+ <Switch
800
+ checked={indexAllImages}
801
+ disabled={!imageIndexingAvailable}
802
+ onCheckedChange={(val) => {
803
+ if (!imageIndexingAvailable) return;
804
+ setIndexAllImages(val);
805
+ setStaged((p) =>
806
+ p.map((item) =>
807
+ item.name.toLowerCase().endsWith('.pdf')
808
+ ? { ...item, indexImages: val }
809
+ : item,
810
+ ),
811
+ );
812
+ }}
813
+ id="global-index-images"
814
+ className="scale-75 origin-right"
815
+ />
816
+ <Label
817
+ htmlFor="global-index-images"
818
+ className="text-[11px] text-muted-foreground cursor-pointer"
819
+ >
820
+ Index all PDF images
821
+ </Label>
822
+ </div>
823
+ )}
824
+ <button
825
+ type="button"
826
+ onClick={() => setStaged([])}
827
+ className="text-[11px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
828
+ >
829
+ Clear All
830
+ </button>
831
+ </div>
832
+ </div>
833
+ <ul className="space-y-1.5">
834
+ {staged.map((f) => (
835
+ <li
836
+ key={f.id}
837
+ className="flex flex-col gap-2 rounded-md border border-border bg-card px-3 py-2 text-sm"
838
+ >
839
+ <div className="flex items-center justify-between gap-3">
840
+ <div className="min-w-0 flex-1 flex flex-col gap-0.5">
841
+ <span className="truncate font-mono text-xs">{f.name}</span>
842
+ <span className="text-[10px] text-muted-foreground uppercase tracking-wider">
843
+ {f.format === 'structured'
844
+ ? 'STRUCTURED'
845
+ : f.format === 'lines'
846
+ ? 'SPLIT BY LINE'
847
+ : 'PLAIN TEXT'}
848
+ {f.format === 'structured' && ` • ${f.rows?.length} ROWS`}
849
+ {f.indexAsTabular && ' • TABULAR'}
850
+ </span>
851
+ {f.sourceUrl && (
852
+ <span
853
+ className="truncate text-[10px] text-muted-foreground"
854
+ title={f.sourceUrl}
855
+ >
856
+ Imported from {new URL(f.sourceUrl).hostname.replace(/^www\./, '')}
857
+ </span>
858
+ )}
859
+ </div>
860
+ <span className="shrink-0 text-xs text-muted-foreground tabular-nums">
861
+ {(f.size / 1024).toFixed(1)} KB
862
+ </span>
863
+
864
+ <div className="flex items-center gap-1 shrink-0">
865
+ {/* Tabular indexing toggle for structured files */}
866
+ {f.format === 'structured' && (
867
+ <button
868
+ type="button"
869
+ aria-label="Toggle tabular indexing"
870
+ title={
871
+ f.indexAsTabular
872
+ ? 'Tabular indexing ON'
873
+ : 'Enable tabular indexing for data analysis'
874
+ }
875
+ onClick={() =>
876
+ setStaged((p) =>
877
+ p.map((item) =>
878
+ item.id === f.id
879
+ ? { ...item, indexAsTabular: !item.indexAsTabular }
880
+ : item,
881
+ ),
882
+ )
883
+ }
884
+ className={cn(
885
+ 'p-1.5 border rounded-md transition-colors cursor-pointer',
886
+ f.indexAsTabular
887
+ ? 'bg-primary/10 border-primary/30 text-primary'
888
+ : 'bg-secondary text-muted-foreground hover:bg-muted/50 hover:text-foreground',
889
+ )}
890
+ >
891
+ <Database className="size-3.5" />
892
+ </button>
893
+ )}
894
+ {/* Image indexing toggle for PDF files */}
895
+ {f.name.toLowerCase().endsWith('.pdf') && (
896
+ <TooltipProvider delay={0}>
897
+ <Tooltip>
898
+ <TooltipTrigger
899
+ type="button"
900
+ aria-label="Toggle image indexing"
901
+ aria-pressed={Boolean(f.indexImages)}
902
+ disabled={!imageIndexingAvailable}
903
+ onClick={() => {
904
+ if (!imageIndexingAvailable) return;
905
+ setStaged((p) =>
906
+ p.map((item) =>
907
+ item.id === f.id
908
+ ? { ...item, indexImages: !item.indexImages }
909
+ : item,
910
+ ),
911
+ );
912
+ }}
913
+ className={cn(
914
+ 'p-1.5 border rounded-md transition-colors cursor-pointer disabled:cursor-not-allowed disabled:opacity-50',
915
+ f.indexImages
916
+ ? 'bg-blue-100 border-blue-300 text-blue-600 dark:bg-blue-900/30 dark:border-blue-700 dark:text-blue-400'
917
+ : 'bg-secondary text-muted-foreground hover:bg-muted/50 hover:text-foreground',
918
+ )}
919
+ >
920
+ <ImageIcon className="size-3.5" />
921
+ </TooltipTrigger>
922
+ <TooltipContent side="top">
923
+ <p className="text-xs">
924
+ {f.indexImages
925
+ ? 'Image indexing ON'
926
+ : 'Extract and index images from PDF'}
927
+ </p>
928
+ </TooltipContent>
929
+ </Tooltip>
930
+ </TooltipProvider>
931
+ )}
932
+ {f.rawContent && (
933
+ <button
934
+ type="button"
935
+ aria-label={`Preview ${f.name}`}
936
+ title="Preview extracted content"
937
+ onClick={() => setPreviewingFileId(f.id)}
938
+ className="rounded-md border bg-secondary p-1.5 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
939
+ >
940
+ <Eye className="size-3.5" />
941
+ </button>
942
+ )}
943
+ <button
944
+ type="button"
945
+ aria-label={`Configure ${f.name}`}
946
+ onClick={() => setEditingFileId(f.id)}
947
+ className="p-1.5 bg-secondary cursor-pointer border text-muted-foreground hover:bg-muted/50 hover:text-foreground rounded-md transition-colors"
948
+ >
949
+ <Settings2 className="size-3.5" />
950
+ </button>
951
+ <button
952
+ type="button"
953
+ aria-label={`Remove ${f.name}`}
954
+ onClick={() => setStaged((p) => p.filter((item) => item.id !== f.id))}
955
+ className="p-1.5 border bg-muted hover:bg-muted/50 cursor-pointer text-muted-foreground hover:text-foreground rounded-md transition-colors"
956
+ >
957
+ <X className="size-3.5 text-red-500" />
958
+ </button>
959
+ </div>
960
+ </div>
961
+
962
+ {/* Tabular preview for structured files with tabular indexing */}
963
+ {f.format === 'structured' && f.indexAsTabular && f.rows && (
964
+ <div className="min-w-0 w-full mt-1">
965
+ <TabularPreview rows={f.rows} maxPreviewRows={5} />
966
+ </div>
967
+ )}
968
+ </li>
969
+ ))}
970
+ </ul>
971
+ </div>
972
+ )}
973
+
974
+ {progress && (
975
+ <div className="space-y-2 mb-4">
976
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
977
+ <span>{progress.message || 'Processing...'}</span>
978
+ <span className="tabular-nums font-mono">
979
+ {progress.current} / {progress.total}
980
+ </span>
981
+ </div>
982
+ <Progress value={(progress.current / progress.total) * 100} />
983
+ </div>
984
+ )}
985
+
986
+ {!onActionChange && (
987
+ <Button
988
+ onClick={isRemoteEntry ? () => void loadRemoteFile() : () => void ingest()}
989
+ disabled={
990
+ loadingRemoteFile || saving || (isRemoteEntry ? !remoteUrl.trim() : staged.length === 0)
991
+ }
992
+ >
993
+ {loadingRemoteFile || saving ? (
994
+ <Loader2 className="size-4 animate-spin" />
995
+ ) : (
996
+ <FileUp className="size-4" />
997
+ )}
998
+ Add file
999
+ </Button>
1000
+ )}
1001
+
1002
+ {/* Mapping Dialog */}
1003
+ <Dialog open={!!editingFile} onOpenChange={(open) => !open && setEditingFileId(null)}>
1004
+ <DialogContent className="sm:max-w-xl max-h-[80%]">
1005
+ <DialogHeader>
1006
+ <DialogTitle className="flex items-center gap-2">
1007
+ <Columns className="size-4 text-primary" />
1008
+ Configure Ingestion
1009
+ </DialogTitle>
1010
+ </DialogHeader>
1011
+
1012
+ {editingFile && (
1013
+ <div
1014
+ className="py-4 space-y-6 overflow-y-auto pr-2"
1015
+ style={{ maxHeight: 'calc(80vh - 120px)' }}
1016
+ >
1017
+ {editingFile.format === 'structured' ? (
1018
+ <>
1019
+ <div className="space-y-4">
1020
+ <div className="space-y-2">
1021
+ <Label>Title Column</Label>
1022
+ <Select
1023
+ value={editingFile.titleKey}
1024
+ onValueChange={(val) => updateEditingFile({ titleKey: val || undefined })}
1025
+ >
1026
+ <SelectTrigger>
1027
+ <SelectValue placeholder="Select column" />
1028
+ </SelectTrigger>
1029
+ <SelectContent>
1030
+ {editingFile.keys?.map((k) => (
1031
+ <SelectItem key={k} value={k}>
1032
+ {k}
1033
+ </SelectItem>
1034
+ ))}
1035
+ </SelectContent>
1036
+ </Select>
1037
+ <p className="text-[11px] text-muted-foreground">
1038
+ Used as the document name.
1039
+ </p>
1040
+ </div>
1041
+
1042
+ <div className="space-y-3 pt-2 border-t border-border">
1043
+ <div className="flex items-center justify-between">
1044
+ <Label>Content Columns</Label>
1045
+ <div className="flex items-center gap-2">
1046
+ <button
1047
+ type="button"
1048
+ className="text-[10px] uppercase font-medium text-primary hover:underline cursor-pointer"
1049
+ onClick={() => {
1050
+ updateEditingFile({
1051
+ contentKeys: editingFile.keys || [],
1052
+ });
1053
+ }}
1054
+ >
1055
+ Select All
1056
+ </button>
1057
+ <span className="text-muted-foreground text-[10px]">|</span>
1058
+ <button
1059
+ type="button"
1060
+ className="text-[10px] uppercase font-medium text-muted-foreground hover:underline cursor-pointer"
1061
+ onClick={() => {
1062
+ updateEditingFile({ contentKeys: [] });
1063
+ }}
1064
+ >
1065
+ Clear
1066
+ </button>
1067
+ </div>
1068
+ </div>
1069
+ <p className="text-[11px] text-muted-foreground mb-2 -mt-1">
1070
+ Selected columns will be combined to form the searchable content.
1071
+ </p>
1072
+ <div className="max-h-30 overflow-y-auto space-y-2 pr-2">
1073
+ {editingFile.keys?.map((k) => {
1074
+ const isSelected = editingFile.contentKeys?.includes(k);
1075
+ return (
1076
+ <div key={k} className="flex items-center space-x-2">
1077
+ <Checkbox
1078
+ id={`content-${k}`}
1079
+ checked={isSelected}
1080
+ onCheckedChange={(checked) => {
1081
+ const current = editingFile.contentKeys || [];
1082
+ if (checked) {
1083
+ updateEditingFile({
1084
+ contentKeys: [...current, k],
1085
+ });
1086
+ } else {
1087
+ updateEditingFile({
1088
+ contentKeys: current.filter((x) => x !== k),
1089
+ });
1090
+ }
1091
+ }}
1092
+ />
1093
+ <label
1094
+ htmlFor={`content-${k}`}
1095
+ className="text-sm font-medium leading-none"
1096
+ >
1097
+ {k}
1098
+ </label>
1099
+ </div>
1100
+ );
1101
+ })}
1102
+ </div>
1103
+
1104
+ {editingFile.contentKeys && editingFile.contentKeys.length > 1 && (
1105
+ <div className="pt-2">
1106
+ <Label className="mb-2 block">Separator</Label>
1107
+ <Select
1108
+ value={editingFile.contentSeparator || ' '}
1109
+ onValueChange={(val) =>
1110
+ updateEditingFile({
1111
+ contentSeparator: val || undefined,
1112
+ })
1113
+ }
1114
+ >
1115
+ <SelectTrigger>
1116
+ <SelectValue placeholder="Select separator" />
1117
+ </SelectTrigger>
1118
+ <SelectContent>
1119
+ <SelectItem value=" ">Space</SelectItem>
1120
+ <SelectItem value=", ">Comma</SelectItem>
1121
+ <SelectItem value="\n">Newline</SelectItem>
1122
+ <SelectItem value=" - ">Hyphen</SelectItem>
1123
+ </SelectContent>
1124
+ </Select>
1125
+ </div>
1126
+ )}
1127
+ </div>
1128
+
1129
+ <div className="space-y-3 pt-2 border-t border-border">
1130
+ <div className="flex items-center justify-between">
1131
+ <Label>Metadata Columns</Label>
1132
+ <div className="flex items-center gap-2">
1133
+ <button
1134
+ type="button"
1135
+ className="text-[10px] uppercase font-medium text-primary hover:underline cursor-pointer"
1136
+ onClick={() => {
1137
+ const availableKeys =
1138
+ editingFile.keys?.filter((k) => k !== editingFile.titleKey) || [];
1139
+ updateEditingFile({
1140
+ metadataKeys: availableKeys,
1141
+ });
1142
+ }}
1143
+ >
1144
+ Select All
1145
+ </button>
1146
+ <span className="text-muted-foreground text-[10px]">|</span>
1147
+ <button
1148
+ type="button"
1149
+ className="text-[10px] uppercase font-medium text-muted-foreground hover:underline cursor-pointer"
1150
+ onClick={() => {
1151
+ updateEditingFile({ metadataKeys: [] });
1152
+ }}
1153
+ >
1154
+ Clear
1155
+ </button>
1156
+ </div>
1157
+ </div>
1158
+ <p className="text-[11px] text-muted-foreground mb-2 -mt-1">
1159
+ These columns will be stored as searchable metadata.
1160
+ </p>
1161
+ <div className="max-h-30 overflow-y-auto space-y-2 pr-2">
1162
+ {editingFile.keys?.map((k) => {
1163
+ const isSelected = editingFile.metadataKeys?.includes(k);
1164
+ const disabled = k === editingFile.titleKey;
1165
+ return (
1166
+ <div key={k} className="flex items-center space-x-2">
1167
+ <Checkbox
1168
+ id={`meta-${k}`}
1169
+ checked={isSelected}
1170
+ disabled={disabled}
1171
+ onCheckedChange={(checked) => {
1172
+ const current = editingFile.metadataKeys || [];
1173
+ if (checked) {
1174
+ updateEditingFile({
1175
+ metadataKeys: [...current, k],
1176
+ });
1177
+ } else {
1178
+ updateEditingFile({
1179
+ metadataKeys: current.filter((x) => x !== k),
1180
+ });
1181
+ }
1182
+ }}
1183
+ />
1184
+ <label
1185
+ htmlFor={`meta-${k}`}
1186
+ className={cn(
1187
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
1188
+ disabled && 'text-muted-foreground',
1189
+ )}
1190
+ >
1191
+ {k} {disabled && '(used)'}
1192
+ </label>
1193
+ </div>
1194
+ );
1195
+ })}
1196
+ </div>
1197
+ </div>
1198
+ </div>
1199
+ </>
1200
+ ) : (
1201
+ <div className="space-y-4">
1202
+ <div className="flex items-center justify-between rounded-lg border border-border p-3">
1203
+ <div className="space-y-0.5">
1204
+ <Label>Split by Line</Label>
1205
+ <p className="text-[11px] text-muted-foreground">
1206
+ Treat each line as a completely separate document.
1207
+ </p>
1208
+ </div>
1209
+ <Switch
1210
+ checked={editingFile.format === 'lines'}
1211
+ onCheckedChange={(checked) =>
1212
+ updateEditingFile({
1213
+ format: checked ? 'lines' : 'plain',
1214
+ })
1215
+ }
1216
+ />
1217
+ </div>
1218
+ </div>
1219
+ )}
1220
+
1221
+ <div className="space-y-3 pt-4 border-t border-border">
1222
+ <div className="flex items-center justify-between">
1223
+ <Label>Global Custom Metadata</Label>
1224
+ <Button
1225
+ variant="ghost"
1226
+ size="sm"
1227
+ className="h-6 px-2 text-xs"
1228
+ onClick={() => {
1229
+ const current = editingFile.globalMetadata || [];
1230
+ updateEditingFile({
1231
+ globalMetadata: [...current, { key: '', value: '' }],
1232
+ });
1233
+ }}
1234
+ >
1235
+ <Plus className="size-3 mr-1" /> Add Field
1236
+ </Button>
1237
+ </div>
1238
+ <p className="text-[11px] text-muted-foreground mb-2">
1239
+ Apply custom key-value pairs to every document generated from this file.
1240
+ </p>
1241
+ <div className="space-y-2">
1242
+ {(editingFile.globalMetadata || []).map((gm, i) => (
1243
+ <div key={i} className="flex items-center gap-2">
1244
+ <Input
1245
+ placeholder="Key"
1246
+ value={gm.key}
1247
+ className="h-8 text-xs font-mono"
1248
+ onChange={(e) => {
1249
+ const next = [...(editingFile.globalMetadata || [])];
1250
+ next[i].key = e.target.value;
1251
+ updateEditingFile({ globalMetadata: next });
1252
+ }}
1253
+ />
1254
+ <Input
1255
+ placeholder="Value"
1256
+ value={gm.value}
1257
+ className="h-8 text-xs font-mono"
1258
+ onChange={(e) => {
1259
+ const next = [...(editingFile.globalMetadata || [])];
1260
+ next[i].value = e.target.value;
1261
+ updateEditingFile({ globalMetadata: next });
1262
+ }}
1263
+ />
1264
+ <Button
1265
+ variant="ghost"
1266
+ size="icon"
1267
+ className="h-8 w-8 text-muted-foreground shrink-0"
1268
+ onClick={() => {
1269
+ const next = [...(editingFile.globalMetadata || [])];
1270
+ next.splice(i, 1);
1271
+ updateEditingFile({ globalMetadata: next });
1272
+ }}
1273
+ >
1274
+ <X className="size-3.5" />
1275
+ </Button>
1276
+ </div>
1277
+ ))}
1278
+ {(!editingFile.globalMetadata || editingFile.globalMetadata.length === 0) && (
1279
+ <p className="text-xs text-muted-foreground italic text-center py-2">
1280
+ No custom metadata.
1281
+ </p>
1282
+ )}
1283
+ </div>
1284
+ </div>
1285
+ </div>
1286
+ )}
1287
+
1288
+ <DialogFooter className="pb-2! bg-muted ">
1289
+ <Button onClick={() => setEditingFileId(null)}>Done</Button>
1290
+ </DialogFooter>
1291
+ </DialogContent>
1292
+ </Dialog>
1293
+
1294
+ <Dialog open={!!previewingFile} onOpenChange={(open) => !open && setPreviewingFileId(null)}>
1295
+ <DialogContent className="max-w-7xl! ">
1296
+ <DialogHeader>
1297
+ <DialogTitle className="truncate">Preview: {previewingFile?.name}</DialogTitle>
1298
+ </DialogHeader>
1299
+ <div className="max-h-[65vh] overflow-auto rounded-md border bg-muted/30">
1300
+ <pre className="min-w-max p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap wrap-break-word">
1301
+ {previewingFile?.rawContent?.slice(0, CONTENT_PREVIEW_LIMIT)}
1302
+ </pre>
1303
+ </div>
1304
+ {(previewingFile?.rawContent?.length ?? 0) > CONTENT_PREVIEW_LIMIT && (
1305
+ <p className="text-xs text-muted-foreground">
1306
+ Preview is limited to the first {CONTENT_PREVIEW_LIMIT.toLocaleString()} characters.
1307
+ </p>
1308
+ )}
1309
+ </DialogContent>
1310
+ </Dialog>
1311
+ </div>
1312
+ );
1313
+ }