@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,1552 @@
1
+ 'use client';
2
+
3
+ import { workspaceUrl, workspaceFetch as fetch } from '../../host';
4
+ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
5
+ import { useChat } from '@ai-sdk/react';
6
+ import { DefaultChatTransport } from 'ai';
7
+ import type { RagConfig } from '@larkup/core/types';
8
+ import {
9
+ ArrowUp,
10
+ SquarePen,
11
+ RotateCcw,
12
+ TriangleAlert,
13
+ Loader2,
14
+ ChevronDown,
15
+ MessageCircle,
16
+ Search,
17
+ X,
18
+ History,
19
+ Trash2,
20
+ Plus,
21
+ Globe,
22
+ Paperclip,
23
+ ListPlus,
24
+ } from 'lucide-react';
25
+ import useSWR, { mutate as globalMutate } from 'swr';
26
+ import { MessageItem } from './message-item';
27
+ import { MessageQueue, type QueuedMessage } from './message-queue';
28
+ import { JobsTrackingButton } from '../data/jobs-tracking-button';
29
+ import { shouldAutoOpenSupportingClip } from '../../lib/chat-supporting-clip';
30
+ import { useProject } from '../projects/project-provider';
31
+ import { cn } from '../../lib/utils';
32
+ import { getProviderMeta, ProviderIcon } from '@larkup/workspace-ui/provider-icon';
33
+ import { get, set, del, keys } from '../../storage';
34
+ import { useRouter } from 'next/navigation';
35
+ import { useChatStore } from '../../store/chat-store';
36
+ import { toast } from 'sonner';
37
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
38
+ import {
39
+ Sheet,
40
+ SheetClose,
41
+ SheetContent,
42
+ SheetHeader,
43
+ SheetTitle,
44
+ SheetTrigger,
45
+ } from '../ui/sheet';
46
+ import {
47
+ AlertDialog,
48
+ AlertDialogAction,
49
+ AlertDialogCancel,
50
+ AlertDialogContent,
51
+ AlertDialogDescription,
52
+ AlertDialogFooter,
53
+ AlertDialogHeader,
54
+ AlertDialogTitle,
55
+ AlertDialogTrigger,
56
+ } from '../ui/alert-dialog';
57
+ import { DocEditorProvider, useDocEditor } from './canvas/doc-editor-provider';
58
+ import { DocumentCanvas } from './canvas/document-canvas';
59
+ import { FileAttachmentButton } from './canvas/file-attachment-button';
60
+ import {
61
+ DropdownMenu,
62
+ DropdownMenuContent,
63
+ DropdownMenuItem,
64
+ DropdownMenuTrigger,
65
+ } from '../ui/dropdown-menu';
66
+ import { Switch } from '../ui/switch';
67
+ import { ChatComposerSurface, ChatEmptyState, ChatMessagesViewport } from '@larkup/workspace-ui';
68
+ import {
69
+ answerFeedbackForMessage,
70
+ groundedAnswerFeedbackContext,
71
+ updateAnswerFeedback,
72
+ type AnswerFeedback,
73
+ } from '../../lib/chat/message-feedback';
74
+
75
+ const fetcher = async <T,>(url: string): Promise<T> => {
76
+ const response = await fetch(url);
77
+ const payload = await response.json().catch(() => null);
78
+ if (!response.ok) {
79
+ const message =
80
+ payload &&
81
+ typeof payload === 'object' &&
82
+ 'error' in payload &&
83
+ typeof payload.error === 'string'
84
+ ? payload.error
85
+ : 'Could not load chat configuration.';
86
+ throw new Error(message);
87
+ }
88
+ return payload as T;
89
+ };
90
+
91
+ function compactToolResult(value: any): any {
92
+ if (!value || typeof value !== 'object') return value;
93
+ if (value.type === 'json' && 'value' in value) {
94
+ return { ...value, value: compactToolResult(value.value) };
95
+ }
96
+
97
+ const compact = { ...value };
98
+ if (Array.isArray(compact.rows)) compact.rows = compact.rows.slice(0, 50);
99
+ if (Array.isArray(compact.data)) compact.data = compact.data.slice(0, 100);
100
+ if (typeof compact.stdout === 'string') compact.stdout = compact.stdout.slice(0, 4_000);
101
+ if (typeof compact.stderr === 'string') compact.stderr = compact.stderr.slice(0, 1_000);
102
+ if (typeof compact.fileBase64 === 'string') delete compact.fileBase64;
103
+ if (Array.isArray(compact.pages)) delete compact.pages;
104
+ return compact;
105
+ }
106
+
107
+ /**
108
+ * Compact messages for the LLM context to prevent massive payloads (like chart data,
109
+ * table rows, and document base64s) from bloating the request and exceeding token limits.
110
+ */
111
+ function compactMessagesForLLM(messages: any[]) {
112
+ return messages.map((message, index) => {
113
+ // Keep the latest message completely intact
114
+ if (index === messages.length - 1) return message;
115
+
116
+ const { toolInvocations, experimental_attachments, parts, ...rest } = message;
117
+
118
+ // Keep tool invocations but strip heavy data fields from the results
119
+ const compactToolInvocations = Array.isArray(toolInvocations)
120
+ ? toolInvocations.map((ti) => {
121
+ if (ti.state === 'result' && ti.result) {
122
+ return { ...ti, result: compactToolResult(ti.result) };
123
+ }
124
+ return ti;
125
+ })
126
+ : toolInvocations;
127
+
128
+ // Do the same for parts
129
+ const compactParts = Array.isArray(parts)
130
+ ? parts.map((part) => {
131
+ if (part.type === 'tool-invocation') {
132
+ const ti = part.toolInvocation;
133
+ if (ti?.state === 'result' && ti.result) {
134
+ return { ...part, toolInvocation: { ...ti, result: compactToolResult(ti.result) } };
135
+ }
136
+ }
137
+ // AI SDK v5 serializes completed tool calls as `tool-<name>` parts.
138
+ // Keep the metadata needed by the server-side evidence ledger while
139
+ // bounding rows/chart points before the next chat request.
140
+ if (part.type?.startsWith('tool-')) {
141
+ return {
142
+ ...part,
143
+ ...(part.output !== undefined ? { output: compactToolResult(part.output) } : {}),
144
+ ...(part.result !== undefined ? { result: compactToolResult(part.result) } : {}),
145
+ };
146
+ }
147
+ return part;
148
+ })
149
+ : parts;
150
+
151
+ // Strip attachment payloads to just labels to save context window
152
+ const attachmentLabels = Array.isArray(experimental_attachments)
153
+ ? experimental_attachments.map((attachment) => ({
154
+ name: attachment?.name,
155
+ contentType: attachment?.contentType,
156
+ size: attachment?.size,
157
+ }))
158
+ : experimental_attachments;
159
+
160
+ return {
161
+ ...rest,
162
+ ...(compactParts ? { parts: compactParts } : {}),
163
+ ...(compactToolInvocations ? { toolInvocations: compactToolInvocations } : {}),
164
+ ...(attachmentLabels ? { experimental_attachments: attachmentLabels } : {}),
165
+ };
166
+ });
167
+ }
168
+
169
+ interface AvailableModel {
170
+ id: string;
171
+ label: string;
172
+ provider: string;
173
+ context_window?: number;
174
+ tags?: string[];
175
+ }
176
+
177
+ function getFileIconPath(file: File) {
178
+ const extension = file.name.split('.').pop()?.toLowerCase();
179
+ const mimeType = file.type;
180
+
181
+ if (['csv', 'xls', 'xlsx'].includes(extension || '')) return '/icons/excel.png';
182
+ if (['doc', 'docx'].includes(extension || '')) return '/icons/word.png';
183
+ if (['md', 'markdown'].includes(extension || '')) return '/icons/markdown.png';
184
+ if (['pdf'].includes(extension || '')) return '/icons/pdf.png';
185
+
186
+ if (mimeType.startsWith('video/')) return '/icons/video.png';
187
+ if (mimeType.startsWith('audio/')) return '/icons/audio.png';
188
+ if (mimeType.startsWith('image/')) return '/icons/image.png';
189
+
190
+ return '/icons/image.png'; // Fallback
191
+ }
192
+
193
+ function AttachmentCard({
194
+ file,
195
+ onClick,
196
+ onRemove,
197
+ }: {
198
+ file: File;
199
+ onClick?: () => void;
200
+ onRemove: () => void;
201
+ }) {
202
+ const iconPath = getFileIconPath(file);
203
+
204
+ return (
205
+ <div
206
+ className="relative mt-2 group flex items-center gap-2 bg-muted/50 border border-border/60 rounded-xl h-12 px-3 p-1 shrink-0 cursor-pointer hover:bg-muted/80 transition-colors"
207
+ onClick={onClick}
208
+ >
209
+ <div className="size-7 shrink-0 rounded-md overflow-hidden bg-background border border-border/90 flex items-center justify-center p-1">
210
+ <img src={iconPath} alt={file.name} className="w-full h-full object-contain" />
211
+ </div>
212
+ <div className="flex flex-col justify-center max-w-30">
213
+ <span className="text-xs font-medium truncate text-foreground">{file.name}</span>
214
+ <span className="text-[10px] text-muted-foreground">
215
+ {(file.size / 1024).toFixed(0)} KB
216
+ </span>
217
+ </div>
218
+ <button
219
+ type="button"
220
+ onClick={(e) => {
221
+ e.preventDefault();
222
+ e.stopPropagation();
223
+ onRemove();
224
+ }}
225
+ className="absolute cursor-pointer -top-3 -right-2 flex h-5 w-5 items-center justify-center rounded-full bg-background border border-border text-muted-foreground hover:bg-destructive hover:border-destructive hover:text-white transition opacity-0 group-hover:opacity-100"
226
+ >
227
+ <X className="size-3" />
228
+ </button>
229
+ </div>
230
+ );
231
+ }
232
+
233
+ interface ChatStatus {
234
+ ready: boolean;
235
+ indexed: boolean;
236
+ blockers: string[];
237
+ provider: string;
238
+ chatModelId: string;
239
+ availableModels: AvailableModel[];
240
+ }
241
+
242
+ export function ChatWorkspace({ chatId }: { chatId?: string } = {}) {
243
+ return (
244
+ <DocEditorProvider>
245
+ <DocumentCanvas>
246
+ <ChatWorkspaceInner chatId={chatId} />
247
+ </DocumentCanvas>
248
+ </DocEditorProvider>
249
+ );
250
+ }
251
+
252
+ function ChatWorkspaceInner({ chatId }: { chatId?: string }) {
253
+ const { activeProject } = useProject();
254
+ const { sessionId, parsedDocument, openCanvas, isLoading: isDocLoading } = useDocEditor();
255
+ const docEditorState = { sessionId, fields: parsedDocument?.fields || [] };
256
+ const projectId = activeProject?.id ?? null;
257
+
258
+ // Track dragging state
259
+ const dragCounter = useRef(0);
260
+ const [isDragging, setIsDragging] = useState(false);
261
+
262
+ const statusKey = projectId
263
+ ? workspaceUrl(`/api/chat/status?projectId=${encodeURIComponent(projectId)}`)
264
+ : workspaceUrl('/api/chat/status');
265
+ const {
266
+ data: status,
267
+ error: statusError,
268
+ isLoading: statusLoading,
269
+ } = useSWR<ChatStatus>(statusKey, fetcher, {
270
+ refreshInterval: 10000,
271
+ });
272
+
273
+ const { data: configData, mutate: mutateConfig } = useSWR<{ config: RagConfig }>(
274
+ workspaceUrl('/api/config'),
275
+ fetcher,
276
+ );
277
+ const router = useRouter();
278
+
279
+ const { selectedModel, setSelectedModel } = useChatStore();
280
+ const [showModelSelect, setShowModelSelect] = useState(false);
281
+ const [modelSearch, setModelSearch] = useState('');
282
+ const [input, setInput] = useState('');
283
+ const [attachments, setAttachments] = useState<File[]>([]);
284
+ const [pendingMessage, setPendingMessage] = useState<{ text: string; files?: any } | null>(null);
285
+ const [queuedMessages, setQueuedMessages] = useState<QueuedMessage[]>([]);
286
+ const searchInputRef = useRef<HTMLInputElement>(null);
287
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
288
+
289
+ useEffect(() => {
290
+ const el = textareaRef.current;
291
+ if (!el) return;
292
+ el.style.height = 'auto';
293
+ el.style.height = `${el.scrollHeight}px`;
294
+ }, [input]);
295
+
296
+ const [currentChatId, setCurrentChatId] = useState<string>(chatId || '');
297
+ const [history, setHistory] = useState<{ id: string; title: string; updatedAt: number }[]>([]);
298
+ const [isHistoryOpen, setIsHistoryOpen] = useState(false);
299
+ const [historySearch, setHistorySearch] = useState('');
300
+ const [isInitializingChat, setIsInitializingChat] = useState(!!chatId);
301
+
302
+ // Load history on mount
303
+ useEffect(() => {
304
+ get('chat_sessions').then((val) => {
305
+ if (val) setHistory(val);
306
+ });
307
+ }, []);
308
+
309
+ // Update selected model when status loads
310
+ useEffect(() => {
311
+ if (!status?.chatModelId) return;
312
+ const available = status.availableModels?.some((model) => model.id === selectedModel);
313
+ if (!selectedModel || !available) setSelectedModel(status.chatModelId);
314
+ }, [status?.chatModelId, status?.availableModels, selectedModel, setSelectedModel]);
315
+
316
+ // Focus search input when dropdown opens
317
+ useEffect(() => {
318
+ if (showModelSelect) {
319
+ // Small delay to let the DOM render
320
+ requestAnimationFrame(() => searchInputRef.current?.focus());
321
+ } else {
322
+ setModelSearch('');
323
+ }
324
+ }, [showModelSelect]);
325
+
326
+ const chatBody = useMemo(
327
+ () => ({
328
+ projectId,
329
+ chatModelId: selectedModel || undefined,
330
+ docSessionId: docEditorState.sessionId,
331
+ docFields: docEditorState.fields,
332
+ }),
333
+ [projectId, selectedModel, docEditorState.sessionId, JSON.stringify(docEditorState.fields)],
334
+ );
335
+
336
+ const chatBodyRef = useRef(chatBody);
337
+ useEffect(() => {
338
+ chatBodyRef.current = chatBody;
339
+ }, [chatBody]);
340
+
341
+ const transport = useMemo(
342
+ () =>
343
+ new DefaultChatTransport({
344
+ api: workspaceUrl('/api/chat'),
345
+ headers: {
346
+ 'Content-Type': 'application/json',
347
+ },
348
+ prepareSendMessagesRequest({ messages, id, body }) {
349
+ return {
350
+ body: {
351
+ messages: compactMessagesForLLM(messages),
352
+ id,
353
+ ...chatBodyRef.current,
354
+ ...body,
355
+ },
356
+ };
357
+ },
358
+ }),
359
+ [],
360
+ );
361
+
362
+ const {
363
+ messages,
364
+ sendMessage,
365
+ status: chatStatus,
366
+ setMessages,
367
+ error,
368
+ regenerate,
369
+ addToolResult,
370
+ } = useChat({
371
+ transport,
372
+ });
373
+
374
+ // Initialize specific chat if ID is provided via prop
375
+ useEffect(() => {
376
+ setQueuedMessages([]);
377
+ if (chatId) {
378
+ setIsInitializingChat(true);
379
+ get(`chat_messages_${chatId}`).then((msgs) => {
380
+ if (msgs) {
381
+ setMessages(msgs);
382
+ setCurrentChatId(chatId);
383
+ }
384
+ setIsInitializingChat(false);
385
+ });
386
+ } else {
387
+ setIsInitializingChat(false);
388
+ }
389
+ }, [chatId, setMessages]);
390
+
391
+ useEffect(() => {
392
+ if (pendingMessage && docEditorState.sessionId && !isDocLoading) {
393
+ sendMessage(pendingMessage);
394
+ setPendingMessage(null);
395
+ }
396
+ }, [pendingMessage, docEditorState.sessionId, isDocLoading, sendMessage]);
397
+
398
+ // Auto-open PDF if the AI uses a document tool and the canvas is not open
399
+ useEffect(() => {
400
+ if (!docEditorState.sessionId && openCanvas && messages.length > 0 && !isDocLoading) {
401
+ const lastMsg = messages[messages.length - 1] as any;
402
+ if (lastMsg.role === 'assistant' && lastMsg.toolInvocations) {
403
+ const hasDocTool = lastMsg.toolInvocations.some((t: any) =>
404
+ ['requestDocumentSignature', 'fillDocumentForm', 'editDocument'].includes(t.toolName),
405
+ );
406
+ if (hasDocTool) {
407
+ // Find the last PDF attachment in the chat
408
+ let targetAtt: any = null;
409
+ for (let i = messages.length - 1; i >= 0; i--) {
410
+ const m = messages[i] as any;
411
+ if (m.experimental_attachments) {
412
+ const pdfs = m.experimental_attachments.filter(
413
+ (a: any) =>
414
+ a.contentType?.includes('pdf') || a.name?.toLowerCase().endsWith('.pdf'),
415
+ );
416
+ if (pdfs.length > 0) {
417
+ targetAtt = pdfs[pdfs.length - 1];
418
+ break;
419
+ }
420
+ }
421
+ }
422
+
423
+ if (targetAtt && targetAtt.url) {
424
+ let fetchUrl = targetAtt.url;
425
+ if (!fetchUrl.startsWith('http') && !fetchUrl.startsWith('data:')) {
426
+ fetchUrl = `data:${targetAtt.contentType || 'application/pdf'};base64,${fetchUrl}`;
427
+ }
428
+ fetch(fetchUrl)
429
+ .then((r) => r.blob())
430
+ .then((blob) => {
431
+ const file = new File([blob], targetAtt.name || 'document.pdf', {
432
+ type: targetAtt.contentType || 'application/pdf',
433
+ });
434
+ openCanvas(file, { background: false });
435
+ })
436
+ .catch((err) => console.error('Auto-open failed', err));
437
+ }
438
+ }
439
+ }
440
+ }
441
+ }, [messages, docEditorState.sessionId, openCanvas, isDocLoading]);
442
+
443
+ // Reset chat context when switching workspaces to prevent overlap
444
+ useEffect(() => {
445
+ setMessages([]);
446
+ setCurrentChatId('');
447
+ setQueuedMessages([]);
448
+ }, [projectId, setMessages]);
449
+
450
+ const scrollRef = useRef<HTMLDivElement>(null);
451
+ const shouldAutoScrollRef = useRef(true);
452
+ const isBusy = chatStatus === 'submitted' || chatStatus === 'streaming';
453
+
454
+ // Drain the queue one message at a time once the assistant is free again
455
+ useEffect(() => {
456
+ if (queuedMessages.length === 0 || isBusy || isDocLoading || pendingMessage) return;
457
+ const [next, ...rest] = queuedMessages;
458
+ setQueuedMessages(rest);
459
+ shouldAutoScrollRef.current = true;
460
+ const dt = new DataTransfer();
461
+ (next.files ?? []).forEach((file) => dt.items.add(file));
462
+ sendMessage({ text: next.text, files: dt.files.length > 0 ? dt.files : undefined });
463
+ }, [queuedMessages, isBusy, isDocLoading, pendingMessage, sendMessage]);
464
+
465
+ const handleMessageScroll = useCallback(() => {
466
+ const el = scrollRef.current;
467
+ if (!el) return;
468
+ shouldAutoScrollRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
469
+ }, []);
470
+
471
+ useEffect(() => {
472
+ const el = scrollRef.current;
473
+ if (!el || !shouldAutoScrollRef.current) return;
474
+ const id = requestAnimationFrame(() => {
475
+ el.scrollTo({
476
+ top: el.scrollHeight,
477
+ behavior: chatStatus === 'streaming' ? 'auto' : 'smooth',
478
+ });
479
+ });
480
+ return () => cancelAnimationFrame(id);
481
+ }, [messages, chatStatus]);
482
+ // Save messages to history — debounced to prevent cascading re-renders during streaming
483
+ const historySaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
484
+ const messagesRef = useRef(messages);
485
+ messagesRef.current = messages;
486
+ const feedbackSyncRevision = useRef<Record<string, number>>({});
487
+
488
+ const handleMessageFeedback = useCallback(
489
+ (messageId: string, feedback: AnswerFeedback | undefined) => {
490
+ const currentMessages = messagesRef.current;
491
+ const currentMessage = currentMessages.find((message) => message.id === messageId);
492
+ const previousFeedback = currentMessage
493
+ ? answerFeedbackForMessage(currentMessage)
494
+ : undefined;
495
+ const cacheContext = groundedAnswerFeedbackContext(currentMessages, messageId);
496
+ const nextMessages = updateAnswerFeedback(currentMessages, messageId, feedback);
497
+ messagesRef.current = nextMessages;
498
+ setMessages(nextMessages);
499
+
500
+ // Persist immediately instead of waiting for the normal history debounce;
501
+ // a reload directly after clicking a thumb must not lose the selection.
502
+ const activeId = currentChatId || crypto.randomUUID();
503
+ if (!currentChatId) {
504
+ setCurrentChatId(activeId);
505
+ if (window.location.pathname === '/chat') {
506
+ window.history.replaceState(null, '', `/chat/${activeId}`);
507
+ }
508
+ }
509
+ void set(`chat_messages_${activeId}`, nextMessages).catch((storageError) => {
510
+ console.error('Failed to save answer feedback', storageError);
511
+ toast.error('Could not save this feedback. Please try again.');
512
+ });
513
+
514
+ // Only plain-document grounded answers enter the reusable answer cache.
515
+ // Media, exports, and multi-tool results keep their specialized paths.
516
+ if (!cacheContext) {
517
+ if (feedback === 'liked') {
518
+ toast.success(
519
+ 'Feedback saved. This answer type will use fresh tool results when repeated.',
520
+ );
521
+ }
522
+ return;
523
+ }
524
+ const revision = (feedbackSyncRevision.current[messageId] ?? 0) + 1;
525
+ feedbackSyncRevision.current[messageId] = revision;
526
+ void fetch(workspaceUrl('/api/chat/feedback'), {
527
+ method: 'POST',
528
+ headers: { 'Content-Type': 'application/json' },
529
+ keepalive: true,
530
+ body: JSON.stringify({
531
+ projectId,
532
+ feedback,
533
+ ...cacheContext,
534
+ }),
535
+ })
536
+ .then(async (response) => {
537
+ const result = (await response.json().catch(() => null)) as {
538
+ cached?: boolean;
539
+ removed?: boolean;
540
+ error?: string;
541
+ } | null;
542
+ if (!response.ok) {
543
+ throw new Error(result?.error || 'Could not update the answer cache.');
544
+ }
545
+ if (feedback === 'liked' && result?.cached) {
546
+ toast.success('Answer saved to Larkup cache');
547
+ } else if (feedback === 'disliked' && result?.removed) {
548
+ toast.success('Cached answer removed');
549
+ }
550
+ void globalMutate(workspaceUrl('/api/system/cache'));
551
+ })
552
+ .catch((feedbackError) => {
553
+ if (feedbackSyncRevision.current[messageId] !== revision) return;
554
+ const rolledBack = updateAnswerFeedback(messagesRef.current, messageId, previousFeedback);
555
+ messagesRef.current = rolledBack;
556
+ setMessages(rolledBack);
557
+ void set(`chat_messages_${activeId}`, rolledBack);
558
+ toast.error(
559
+ feedbackError instanceof Error
560
+ ? feedbackError.message
561
+ : 'Could not update the answer cache.',
562
+ );
563
+ });
564
+ },
565
+ [currentChatId, projectId, setMessages],
566
+ );
567
+
568
+ useEffect(() => {
569
+ if (messages.length === 0) return;
570
+ // Don't save while actively streaming — wait for it to settle
571
+ if (chatStatus === 'streaming' || chatStatus === 'submitted') return;
572
+
573
+ // Debounce: clear previous timer
574
+ if (historySaveTimer.current) clearTimeout(historySaveTimer.current);
575
+
576
+ historySaveTimer.current = setTimeout(() => {
577
+ const currentMessages = messagesRef.current;
578
+ if (currentMessages.length === 0) return;
579
+
580
+ let activeId = currentChatId;
581
+ if (!activeId) {
582
+ activeId = crypto.randomUUID();
583
+ setCurrentChatId(activeId);
584
+ }
585
+
586
+ // Save full messages to IDB so UI (charts, tables) can be restored on reload
587
+ set(`chat_messages_${activeId}`, currentMessages);
588
+
589
+ if (window.location.pathname === '/chat') {
590
+ window.history.replaceState(null, '', `/chat/${activeId}`);
591
+ }
592
+
593
+ setHistory((prev) => {
594
+ const existing = prev.find((p) => p.id === activeId);
595
+ const firstMsg = currentMessages[0];
596
+ const firstMsgText =
597
+ (firstMsg as any)?.parts
598
+ ?.filter((p: any) => p.type === 'text')
599
+ ?.map((p: any) => p.text)
600
+ ?.join('') ||
601
+ (firstMsg as any)?.content ||
602
+ (firstMsg as any)?.text;
603
+
604
+ const title = firstMsgText ? firstMsgText.substring(0, 40) : 'New Chat';
605
+
606
+ // Prevent update if nothing changed
607
+ if (existing && existing.title === title) {
608
+ return prev;
609
+ }
610
+
611
+ let next;
612
+ if (existing) {
613
+ next = prev.map((p) => (p.id === activeId ? { ...p, title, updatedAt: Date.now() } : p));
614
+ } else {
615
+ next = [{ id: activeId, title, updatedAt: Date.now() }, ...prev];
616
+ }
617
+
618
+ set('chat_sessions', next);
619
+ return next;
620
+ });
621
+ }, 500);
622
+
623
+ return () => {
624
+ if (historySaveTimer.current) clearTimeout(historySaveTimer.current);
625
+ };
626
+ }, [messages, currentChatId, chatStatus]);
627
+
628
+ // Group and filter models
629
+ const groupedModels = useMemo(() => {
630
+ const models = status?.availableModels ?? [];
631
+ const query = modelSearch.toLowerCase().trim();
632
+ const filtered = query
633
+ ? models.filter(
634
+ (m) =>
635
+ m.label.toLowerCase().includes(query) ||
636
+ m.id.toLowerCase().includes(query) ||
637
+ m.provider.toLowerCase().includes(query),
638
+ )
639
+ : models;
640
+
641
+ const groups = new Map<string, AvailableModel[]>();
642
+ for (const m of filtered) {
643
+ const existing = groups.get(m.provider) ?? [];
644
+ existing.push(m);
645
+ groups.set(m.provider, existing);
646
+ }
647
+ return groups;
648
+ }, [status?.availableModels, modelSearch]);
649
+
650
+ const filteredHistory = useMemo(() => {
651
+ const sorted = [...history].sort((a, b) => b.updatedAt - a.updatedAt);
652
+ const query = historySearch.toLowerCase().trim();
653
+ return query ? sorted.filter((chat) => chat.title.toLowerCase().includes(query)) : sorted;
654
+ }, [history, historySearch]);
655
+
656
+ function removeFromQueue(id: string) {
657
+ setQueuedMessages((prev) => prev.filter((m) => m.id !== id));
658
+ }
659
+
660
+ function moveQueueItem(id: string, direction: -1 | 1) {
661
+ setQueuedMessages((prev) => {
662
+ const index = prev.findIndex((m) => m.id === id);
663
+ const target = index + direction;
664
+ if (index === -1 || target < 0 || target >= prev.length) return prev;
665
+ const next = [...prev];
666
+ [next[index], next[target]] = [next[target], next[index]];
667
+ return next;
668
+ });
669
+ }
670
+
671
+ function editQueueItem(id: string) {
672
+ const item = queuedMessages.find((m) => m.id === id);
673
+ if (!item) return;
674
+ setQueuedMessages((prev) => prev.filter((m) => m.id !== id));
675
+ setInput(item.text);
676
+ setAttachments(item.files ?? []);
677
+ requestAnimationFrame(() => textareaRef.current?.focus());
678
+ }
679
+
680
+ function handleSubmit(e?: React.FormEvent) {
681
+ if (e) e.preventDefault();
682
+ const text = input.trim();
683
+ if (!text && attachments.length === 0) return;
684
+
685
+ if (isBusy || isDocLoading) {
686
+ setQueuedMessages((prev) => [
687
+ ...prev,
688
+ { id: crypto.randomUUID(), text, files: attachments.length > 0 ? attachments : undefined },
689
+ ]);
690
+ setInput('');
691
+ setAttachments([]);
692
+ return;
693
+ }
694
+
695
+ shouldAutoScrollRef.current = true;
696
+
697
+ const dt = new DataTransfer();
698
+ attachments.forEach((file) => dt.items.add(file));
699
+ const files = dt.files.length > 0 ? dt.files : undefined;
700
+
701
+ if (files) {
702
+ const docFile = Array.from(files).find(
703
+ (f) =>
704
+ f.type === 'application/pdf' ||
705
+ f.name.toLowerCase().endsWith('.pdf') ||
706
+ f.name.toLowerCase().endsWith('.doc') ||
707
+ f.name.toLowerCase().endsWith('.docx'),
708
+ );
709
+
710
+ // If we have a document that needs opening, open it BEFORE sending the message!
711
+ if (docFile && !docEditorState.sessionId) {
712
+ toast.info('Preparing document...');
713
+ setPendingMessage({ text, files });
714
+ openCanvas(docFile, { background: false }).catch((err) => {
715
+ toast.error(err.message || 'Failed to open document');
716
+ setPendingMessage(null);
717
+ // Send the message anyway so the user's text isn't lost
718
+ sendMessage({ text, files });
719
+ });
720
+ setInput('');
721
+ setAttachments([]);
722
+ return;
723
+ }
724
+
725
+ sendMessage({ text, files });
726
+ } else {
727
+ let docToOpen: any = null;
728
+ if (!docEditorState.sessionId) {
729
+ for (let i = messages.length - 1; i >= 0; i--) {
730
+ const m = messages[i] as any;
731
+ if (m.experimental_attachments) {
732
+ const docs = m.experimental_attachments.filter(
733
+ (a: any) =>
734
+ a.contentType?.includes('pdf') ||
735
+ a.name?.toLowerCase().endsWith('.pdf') ||
736
+ a.name?.toLowerCase().endsWith('.doc') ||
737
+ a.name?.toLowerCase().endsWith('.docx'),
738
+ );
739
+ if (docs.length > 0) {
740
+ docToOpen = docs[docs.length - 1];
741
+ break;
742
+ }
743
+ }
744
+ }
745
+ }
746
+
747
+ if (docToOpen) {
748
+ toast.info('Preparing document...');
749
+ let fetchUrl = docToOpen.url;
750
+ if (!fetchUrl.startsWith('http') && !fetchUrl.startsWith('data:')) {
751
+ fetchUrl = `data:${docToOpen.contentType || 'application/pdf'};base64,${fetchUrl}`;
752
+ }
753
+
754
+ setPendingMessage({ text });
755
+ fetch(fetchUrl)
756
+ .then((r) => r.blob())
757
+ .then((blob) => {
758
+ const file = new File([blob], docToOpen.name || 'document.pdf', {
759
+ type: docToOpen.contentType || 'application/pdf',
760
+ });
761
+ openCanvas(file, { background: false }).catch((err) => {
762
+ toast.error(err.message || 'Failed to open document');
763
+ setPendingMessage(null);
764
+ sendMessage({ text });
765
+ });
766
+ })
767
+ .catch((err) => {
768
+ console.error('Auto-open failed', err);
769
+ setPendingMessage(null);
770
+ sendMessage({ text });
771
+ });
772
+
773
+ setInput('');
774
+ setAttachments([]);
775
+ return;
776
+ }
777
+
778
+ sendMessage({ text });
779
+ }
780
+
781
+ setInput('');
782
+ setAttachments([]);
783
+ }
784
+
785
+ function newChat() {
786
+ setMessages([]);
787
+ setInput('');
788
+ setQueuedMessages([]);
789
+ setCurrentChatId(crypto.randomUUID());
790
+ window.history.pushState(null, '', '/chat');
791
+ }
792
+
793
+ useEffect(() => {
794
+ const handleNewChat = () => newChat();
795
+ window.addEventListener('new-chat', handleNewChat);
796
+ return () => window.removeEventListener('new-chat', handleNewChat);
797
+ }, []);
798
+
799
+ async function handleWebSearchToggle() {
800
+ const config = configData?.config;
801
+ if (!config) return;
802
+
803
+ const provider = config.webSearchProvider || 'tavily';
804
+ const apiKeys: Record<string, string | undefined> = {
805
+ tavily: config.tavilyApiKey,
806
+ serper: config.serperApiKey,
807
+ google: config.serperApiKey,
808
+ brave: config.braveApiKey,
809
+ bing: config.bingApiKey,
810
+ exa: config.exaApiKey,
811
+ };
812
+
813
+ if (provider !== 'local' && !apiKeys[provider]) {
814
+ toast.error('Web Search API key is missing. Please configure it in Settings.');
815
+ router.push('/settings?section=search-web');
816
+ return;
817
+ }
818
+
819
+ const nextState = !config.webSearchEnabled;
820
+ try {
821
+ await fetch(workspaceUrl('/api/config'), {
822
+ method: 'PUT',
823
+ headers: { 'Content-Type': 'application/json' },
824
+ body: JSON.stringify({ ...config, webSearchEnabled: nextState }),
825
+ });
826
+ await mutateConfig();
827
+ toast.success(`Web Search ${nextState ? 'enabled' : 'disabled'}`);
828
+ } catch (e: any) {
829
+ toast.error(e.message || 'Failed to update web search setting');
830
+ }
831
+ }
832
+
833
+ async function loadChat(id: string) {
834
+ const msgs = await get(`chat_messages_${id}`);
835
+ if (msgs) {
836
+ setMessages(msgs);
837
+ setCurrentChatId(id);
838
+ setIsHistoryOpen(false);
839
+ window.history.pushState(null, '', `/chat/${id}`);
840
+ }
841
+ }
842
+
843
+ async function deleteChat(id: string) {
844
+ await del(`chat_messages_${id}`);
845
+ setHistory((prev) => {
846
+ const next = prev.filter((p) => p.id !== id);
847
+ set('chat_sessions', next);
848
+ return next;
849
+ });
850
+ if (currentChatId === id) {
851
+ setMessages([]);
852
+ setInput('');
853
+ setCurrentChatId('');
854
+ window.history.pushState(null, '', '/chat');
855
+ }
856
+ }
857
+
858
+ async function deleteAllChats() {
859
+ try {
860
+ const chatStorageKeys = (await keys()).filter(
861
+ (key): key is string =>
862
+ typeof key === 'string' && (key === 'chat_sessions' || key.startsWith('chat_messages_')),
863
+ );
864
+ await Promise.all(chatStorageKeys.map((key) => del(key)));
865
+ setHistory([]);
866
+ setHistorySearch('');
867
+ setMessages([]);
868
+ setQueuedMessages([]);
869
+ setCurrentChatId('');
870
+ window.history.pushState(null, '', '/chat');
871
+ toast.success('Chat history cleared');
872
+ } catch (error) {
873
+ console.error('Failed to clear chat history', error);
874
+ toast.error('Could not clear chat history. Please try again.');
875
+ }
876
+ }
877
+
878
+ const handleDragEnter = useCallback((e: React.DragEvent) => {
879
+ e.preventDefault();
880
+ e.stopPropagation();
881
+ dragCounter.current++;
882
+ if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
883
+ if (Array.from(e.dataTransfer.items).some((item) => item.kind === 'file')) {
884
+ setIsDragging(true);
885
+ }
886
+ }
887
+ }, []);
888
+
889
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
890
+ e.preventDefault();
891
+ e.stopPropagation();
892
+ dragCounter.current--;
893
+ if (dragCounter.current === 0) {
894
+ setIsDragging(false);
895
+ }
896
+ }, []);
897
+
898
+ const handleDragOver = useCallback((e: React.DragEvent) => {
899
+ e.preventDefault();
900
+ e.stopPropagation();
901
+ }, []);
902
+
903
+ const handleDrop = useCallback(
904
+ async (e: React.DragEvent) => {
905
+ e.preventDefault();
906
+ e.stopPropagation();
907
+ setIsDragging(false);
908
+ dragCounter.current = 0;
909
+
910
+ const fileList = e.dataTransfer.files;
911
+ if (!fileList || fileList.length === 0) return;
912
+
913
+ const newAttachments = Array.from(fileList);
914
+
915
+ if (newAttachments.length === 0) {
916
+ toast.error('No valid files dropped.');
917
+ return;
918
+ }
919
+
920
+ setAttachments((prev) => [...prev, ...newAttachments]);
921
+
922
+ // Auto-open PDF if dropped
923
+ const pdfFile = newAttachments.find(
924
+ (f) => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'),
925
+ );
926
+ if (pdfFile) {
927
+ openCanvas(pdfFile, { background: false }).catch((err) => {
928
+ toast.error(err.message || 'Failed to auto-open document');
929
+ });
930
+ }
931
+ },
932
+ [openCanvas],
933
+ );
934
+
935
+ const isEmpty = messages.length === 0 && !isInitializingChat;
936
+ const ready = status?.ready ?? false;
937
+ const selectedModelData = status?.availableModels?.find((m) => m.id === selectedModel);
938
+
939
+ if (statusLoading) {
940
+ return (
941
+ <div className="flex flex-1 items-center justify-center">
942
+ <Loader2 className="size-6 animate-spin text-muted-foreground" />
943
+ </div>
944
+ );
945
+ }
946
+
947
+ return (
948
+ <div
949
+ className={cn(
950
+ 'flex h-full w-full flex-1 flex-col overflow-hidden relative',
951
+ isDragging && 'bg-primary/5',
952
+ )}
953
+ onDragEnter={handleDragEnter}
954
+ onDragLeave={handleDragLeave}
955
+ onDragOver={handleDragOver}
956
+ onDrop={handleDrop}
957
+ >
958
+ {isDragging && (
959
+ <div className="absolute inset-0 z-100 flex flex-col items-center justify-center bg-background/80 backdrop-blur-sm border-2 border-dashed border-primary/50 m-4 rounded-3xl pointer-events-none">
960
+ <div className="flex h-20 w-20 items-center justify-center rounded-full bg-primary/10 text-primary mb-4">
961
+ <Paperclip className="size-10" />
962
+ </div>
963
+ <h2 className="text-xl font-semibold tracking-tight text-foreground">
964
+ Drop document to attach
965
+ </h2>
966
+ <p className="text-sm text-muted-foreground mt-1">PDF, Word, PPT, TXT</p>
967
+ </div>
968
+ )}
969
+ {/* Header bar */}
970
+ <div className="relative z-40 flex items-center justify-between px-4 py-2">
971
+ <div className="flex items-center gap-3">{/* We moved model selector to the right */}</div>
972
+ <div className="flex items-center gap-2">
973
+ {/* Model selector with search */}
974
+ {status?.availableModels && status.availableModels.length > 0 && (
975
+ <div className="relative">
976
+ <button
977
+ type="button"
978
+ onClick={() => setShowModelSelect((o) => !o)}
979
+ className="flex w-75 justify-between items-center gap-1.5 rounded-lg border border-border bg-white dark:bg-card px-3 py-1.5 text-xs text-foreground transition hover:bg-muted"
980
+ >
981
+ <span className="flex items-center gap-2 truncate">
982
+ {selectedModelData && (
983
+ <ProviderIcon
984
+ src={getProviderMeta(selectedModelData.provider).iconSrc}
985
+ alt={selectedModelData.provider}
986
+ pillBg={getProviderMeta(selectedModelData.provider).pillBg}
987
+ size={16}
988
+ />
989
+ )}
990
+ <span className="truncate">
991
+ {selectedModelData?.label || selectedModel || 'Select model'}
992
+ </span>
993
+ </span>
994
+ <ChevronDown
995
+ className={cn(
996
+ 'size-3 shrink-0 text-muted-foreground transition-transform',
997
+ showModelSelect && 'rotate-180',
998
+ )}
999
+ />
1000
+ </button>
1001
+ {showModelSelect && (
1002
+ <>
1003
+ <div className="fixed inset-0 z-40" onClick={() => setShowModelSelect(false)} />
1004
+ <div className="absolute right-0 top-full z-50 mt-1 w-75 rounded-lg border border-border bg-card ">
1005
+ {/* Search input */}
1006
+ <div className="flex items-center gap-2 border-b border-border px-3 py-2">
1007
+ <Search className="size-3.5 shrink-0 text-muted-foreground" />
1008
+ <input
1009
+ ref={searchInputRef}
1010
+ type="text"
1011
+ value={modelSearch}
1012
+ onChange={(e) => setModelSearch(e.target.value)}
1013
+ placeholder="Search models..."
1014
+ className="flex-1 bg-transparent text-xs text-foreground outline-none placeholder:text-muted-foreground"
1015
+ />
1016
+ {modelSearch && (
1017
+ <button
1018
+ type="button"
1019
+ onClick={() => setModelSearch('')}
1020
+ className="text-muted-foreground hover:text-foreground"
1021
+ >
1022
+ <X className="size-3" />
1023
+ </button>
1024
+ )}
1025
+ </div>
1026
+
1027
+ {/* Grouped model list */}
1028
+ <div className="max-h-72 overflow-y-auto p-1 [&::-webkit-scrollbar]:hidden">
1029
+ {groupedModels.size === 0 ? (
1030
+ <div className="px-3 py-4 text-center text-xs text-muted-foreground">
1031
+ No models found
1032
+ </div>
1033
+ ) : (
1034
+ Array.from(groupedModels.entries()).map(([provider, models]) => {
1035
+ const meta = getProviderMeta(provider);
1036
+ return (
1037
+ <div key={provider} className="mb-1">
1038
+ <div className="flex items-center gap-2 px-3 py-2 text-xs font-semibold uppercase tracking-wider text-foreground/70">
1039
+ {meta.label}
1040
+ <span className="text-[10px] text-muted-foreground/50">
1041
+ ({models.length})
1042
+ </span>
1043
+ </div>
1044
+
1045
+ {/* Models */}
1046
+ {models.map((m) => (
1047
+ <button
1048
+ key={m.id}
1049
+ type="button"
1050
+ onClick={() => {
1051
+ setSelectedModel(m.id);
1052
+ setShowModelSelect(false);
1053
+ }}
1054
+ className={cn(
1055
+ 'flex w-full items-center justify-between rounded-md px-3 py-1.5 text-left text-xs transition',
1056
+ m.id === selectedModel
1057
+ ? 'bg-primary/10 text-primary font-medium'
1058
+ : 'text-foreground hover:bg-muted',
1059
+ )}
1060
+ >
1061
+ <span className="flex items-center gap-2 truncate">
1062
+ <ProviderIcon
1063
+ src={meta.iconSrc}
1064
+ alt={meta.label}
1065
+ pillBg={meta.pillBg}
1066
+ size={14}
1067
+ />
1068
+ <span className="truncate">{m.label}</span>
1069
+ </span>
1070
+ <span className="flex items-center gap-1.5 shrink-0 ml-2">
1071
+ {m.context_window && (
1072
+ <span className="text-[9px] font-mono text-muted-foreground/60">
1073
+ {m.context_window >= 1_000_000
1074
+ ? `${(m.context_window / 1_000_000).toFixed(0)}M`
1075
+ : `${(m.context_window / 1000).toFixed(0)}K`}
1076
+ </span>
1077
+ )}
1078
+ {/* {m.tags?.includes("reasoning") && (
1079
+ <span className="rounded bg-violet-100 dark:bg-violet-900/30 px-1 py-0.5 text-[8px] font-medium text-violet-700 dark:text-violet-300">
1080
+ reasoning
1081
+ </span>
1082
+ )} */}
1083
+ </span>
1084
+ </button>
1085
+ ))}
1086
+ </div>
1087
+ );
1088
+ })
1089
+ )}
1090
+ </div>
1091
+ </div>
1092
+ </>
1093
+ )}
1094
+ </div>
1095
+ )}
1096
+
1097
+ <div className="h-4 w-px bg-border mx-1" />
1098
+
1099
+ <TooltipProvider delay={50}>
1100
+ <Sheet
1101
+ open={isHistoryOpen}
1102
+ onOpenChange={(open) => {
1103
+ setIsHistoryOpen(open);
1104
+ if (!open) setHistorySearch('');
1105
+ }}
1106
+ >
1107
+ <Tooltip>
1108
+ <TooltipTrigger
1109
+ render={
1110
+ <SheetTrigger
1111
+ render={
1112
+ <button
1113
+ type="button"
1114
+ aria-label="Chat history"
1115
+ className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-secondary hover:text-foreground"
1116
+ >
1117
+ <History className="h-4 w-4" />
1118
+ </button>
1119
+ }
1120
+ />
1121
+ }
1122
+ />
1123
+ <TooltipContent>Chat History</TooltipContent>
1124
+ </Tooltip>
1125
+ <SheetContent
1126
+ side="left"
1127
+ showCloseButton={false}
1128
+ className="w-75 sm:w-87.5 p-0 flex flex-col"
1129
+ >
1130
+ <SheetHeader className="p-4 pb-3 border-b">
1131
+ <div className="flex items-center justify-between gap-2">
1132
+ <SheetTitle className="text-sm font-semibold">Chat History</SheetTitle>
1133
+ <div className="flex items-center gap-1">
1134
+ <AlertDialog>
1135
+ <Tooltip>
1136
+ <TooltipTrigger
1137
+ render={
1138
+ <AlertDialogTrigger
1139
+ render={
1140
+ <button
1141
+ type="button"
1142
+ aria-label="Clear chat history"
1143
+ disabled={history.length === 0}
1144
+ className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-destructive/10 hover:text-destructive disabled:pointer-events-none disabled:opacity-40"
1145
+ >
1146
+ <Trash2 className="size-3.5" />
1147
+ </button>
1148
+ }
1149
+ />
1150
+ }
1151
+ />
1152
+ <TooltipContent>Clear all chat history</TooltipContent>
1153
+ </Tooltip>
1154
+ <AlertDialogContent>
1155
+ <AlertDialogHeader>
1156
+ <AlertDialogTitle>Delete all chat history?</AlertDialogTitle>
1157
+ <AlertDialogDescription>
1158
+ This will permanently delete all saved chats on this device. This
1159
+ action cannot be undone.
1160
+ </AlertDialogDescription>
1161
+ </AlertDialogHeader>
1162
+ <AlertDialogFooter>
1163
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
1164
+ <AlertDialogAction
1165
+ onClick={() => void deleteAllChats()}
1166
+ className="bg-destructive text-white hover:bg-destructive/90"
1167
+ >
1168
+ Delete all
1169
+ </AlertDialogAction>
1170
+ </AlertDialogFooter>
1171
+ </AlertDialogContent>
1172
+ </AlertDialog>
1173
+ <SheetClose
1174
+ render={
1175
+ <button
1176
+ type="button"
1177
+ aria-label="Close chat history"
1178
+ className="flex size-7 items-center justify-center rounded-md text-muted-foreground transition hover:bg-secondary hover:text-foreground"
1179
+ >
1180
+ <X className="size-3.5" />
1181
+ </button>
1182
+ }
1183
+ />
1184
+ </div>
1185
+ </div>
1186
+ <div className="mt-3 flex items-center gap-2 rounded-lg border border-border bg-muted/50 px-2.5 py-1.5">
1187
+ <Search className="size-3.5 shrink-0 text-muted-foreground" />
1188
+ <input
1189
+ type="text"
1190
+ value={historySearch}
1191
+ onChange={(e) => setHistorySearch(e.target.value)}
1192
+ placeholder="Search chats..."
1193
+ className="flex-1 bg-transparent text-xs text-foreground outline-none placeholder:text-muted-foreground"
1194
+ />
1195
+ {historySearch && (
1196
+ <button
1197
+ type="button"
1198
+ onClick={() => setHistorySearch('')}
1199
+ className="text-muted-foreground hover:text-foreground"
1200
+ >
1201
+ <X className="size-3" />
1202
+ </button>
1203
+ )}
1204
+ </div>
1205
+ </SheetHeader>
1206
+ <div className="flex-1 overflow-y-auto p-2">
1207
+ {history.length === 0 ? (
1208
+ <div className="p-4 text-center text-xs text-muted-foreground">
1209
+ No chat history
1210
+ </div>
1211
+ ) : filteredHistory.length === 0 ? (
1212
+ <div className="p-4 text-center text-xs text-muted-foreground">
1213
+ No chats match "{historySearch}"
1214
+ </div>
1215
+ ) : (
1216
+ filteredHistory.map((chat) => (
1217
+ <div
1218
+ key={chat.id}
1219
+ onClick={() => loadChat(chat.id)}
1220
+ className={cn(
1221
+ 'group flex items-center justify-between rounded-md px-3 py-2 text-sm cursor-pointer transition hover:bg-muted mb-1',
1222
+ currentChatId === chat.id && 'bg-muted font-medium',
1223
+ )}
1224
+ >
1225
+ <div className="truncate pr-4 flex-1 text-xs">{chat.title}</div>
1226
+ <AlertDialog>
1227
+ <AlertDialogTrigger
1228
+ render={
1229
+ <button
1230
+ onClick={(e) => e.stopPropagation()}
1231
+ className="text-muted-foreground opacity-0 transition group-hover:opacity-100 hover:text-destructive"
1232
+ >
1233
+ <Trash2 className="size-3.5" />
1234
+ </button>
1235
+ }
1236
+ />
1237
+ <AlertDialogContent onClick={(e) => e.stopPropagation()}>
1238
+ <AlertDialogHeader>
1239
+ <AlertDialogTitle>Delete Chat</AlertDialogTitle>
1240
+ <AlertDialogDescription>
1241
+ Are you sure you want to delete this chat? This action cannot be
1242
+ undone.
1243
+ </AlertDialogDescription>
1244
+ </AlertDialogHeader>
1245
+ <AlertDialogFooter>
1246
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
1247
+ <AlertDialogAction
1248
+ onClick={() => deleteChat(chat.id)}
1249
+ className="bg-destructive text-white hover:bg-destructive/90"
1250
+ >
1251
+ Delete
1252
+ </AlertDialogAction>
1253
+ </AlertDialogFooter>
1254
+ </AlertDialogContent>
1255
+ </AlertDialog>
1256
+ </div>
1257
+ ))
1258
+ )}
1259
+ </div>
1260
+ </SheetContent>
1261
+ </Sheet>
1262
+
1263
+ <Tooltip>
1264
+ <TooltipTrigger
1265
+ render={
1266
+ <button
1267
+ type="button"
1268
+ onClick={newChat}
1269
+ className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-secondary hover:text-foreground"
1270
+ >
1271
+ <SquarePen className="h-4 w-4" />
1272
+ </button>
1273
+ }
1274
+ />
1275
+ <TooltipContent>New Chat</TooltipContent>
1276
+ </Tooltip>
1277
+ <JobsTrackingButton />
1278
+ </TooltipProvider>
1279
+ </div>
1280
+ </div>
1281
+
1282
+ {/* Messages area */}
1283
+ <ChatMessagesViewport viewportRef={scrollRef} onScroll={handleMessageScroll}>
1284
+ {!ready ? (
1285
+ <div className="flex flex-col items-center justify-center gap-3 pt-[18vh] text-center">
1286
+ <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white border text-muted-foreground">
1287
+ <MessageCircle className="size-7" />
1288
+ </div>
1289
+ <h1 className="text-xl font-semibold tracking-tight text-foreground">
1290
+ {statusError ? 'Chat temporarily unavailable' : 'Setup Required'}
1291
+ </h1>
1292
+ <p className="max-w-md text-sm leading-relaxed text-muted-foreground mb-2">
1293
+ {statusError
1294
+ ? 'Chat services are temporarily unavailable. Larkup will retry automatically.'
1295
+ : status?.blockers?.join(' ') ||
1296
+ 'Connect a chat model in Settings to start chatting.'}
1297
+ </p>
1298
+ {!statusError && (
1299
+ <a
1300
+ href={
1301
+ status?.blockers?.some((b) => b.includes('API Key') || b.includes('Settings'))
1302
+ ? '/settings/models'
1303
+ : '/settings'
1304
+ }
1305
+ className="inline-flex h-9 items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
1306
+ >
1307
+ Go to Settings
1308
+ </a>
1309
+ )}
1310
+ </div>
1311
+ ) : isEmpty ? (
1312
+ <ChatEmptyState
1313
+ mark={
1314
+ <>
1315
+ <img src="/logo.png" alt="Logo" className="size-9 dark:hidden" />
1316
+ <img src="/logo-light.png" alt="Logo" className="size-9 dark:block hidden" />
1317
+ </>
1318
+ }
1319
+ />
1320
+ ) : (
1321
+ <div className="flex flex-col gap-6">
1322
+ {messages.map((m, idx) => (
1323
+ <MessageItem
1324
+ key={m.id}
1325
+ message={m}
1326
+ isLast={idx === messages.length - 1}
1327
+ isStreaming={chatStatus === 'streaming'}
1328
+ addToolResult={addToolResult}
1329
+ serverId={projectId}
1330
+ autoOpenSupportingClip={shouldAutoOpenSupportingClip(messages, idx)}
1331
+ regenerate={regenerate}
1332
+ isBusy={isBusy}
1333
+ onFeedback={handleMessageFeedback}
1334
+ />
1335
+ ))}
1336
+ {chatStatus === 'submitted' || isInitializingChat ? (
1337
+ <div className="flex items-center gap-2">
1338
+ <div className="size-7 bg-white border border-border rounded-full flex items-center justify-center p-1 animate-pulse">
1339
+ <img src="/logo.png" alt="logo" className="size-4 animate-spin" />
1340
+ </div>
1341
+ </div>
1342
+ ) : null}
1343
+ {error
1344
+ ? (() => {
1345
+ const raw = error.message || 'Something went wrong while generating a response.';
1346
+ // Sanitize: if the message contains URLs or is too long, clean it up
1347
+ let displayMessage = raw;
1348
+ if (
1349
+ raw.includes('rate-limited') ||
1350
+ raw.includes('rate_limit') ||
1351
+ raw.includes('429')
1352
+ ) {
1353
+ displayMessage =
1354
+ 'Vercel AI Gateway could not serve this model because it is rate-limited. We tried compatible backups. Try again shortly, choose another model, or add AI Gateway credits / a provider key in Settings.';
1355
+ } else if (raw.includes('unavailable tool') || raw.includes('NoSuchTool')) {
1356
+ displayMessage =
1357
+ 'The model tried an unavailable action. Please try your question again.';
1358
+ } else if (
1359
+ raw.length > 200 ||
1360
+ raw.includes('http://') ||
1361
+ raw.includes('https://')
1362
+ ) {
1363
+ displayMessage =
1364
+ 'Something went wrong while generating a response. Please try again.';
1365
+ }
1366
+ return (
1367
+ <div className="flex items-start gap-3 rounded-xl border border-destructive/30 bg-destructive/5 px-4 py-3">
1368
+ <TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
1369
+ <div className="flex-1">
1370
+ <p className="text-sm text-foreground">{displayMessage}</p>
1371
+ <button
1372
+ type="button"
1373
+ onClick={() => regenerate()}
1374
+ className="mt-2 inline-flex items-center gap-1.5 rounded-lg border border-border bg-card px-2.5 py-1.5 text-xs font-medium text-foreground transition hover:bg-secondary"
1375
+ >
1376
+ <RotateCcw className="h-3.5 w-3.5" />
1377
+ Try again
1378
+ </button>
1379
+ </div>
1380
+ </div>
1381
+ );
1382
+ })()
1383
+ : null}
1384
+ </div>
1385
+ )}
1386
+ </ChatMessagesViewport>
1387
+
1388
+ {/* Input area */}
1389
+ {ready && (
1390
+ <div className="relative z-10 mt-auto px-4 pb-5 pt-3 sm:px-6">
1391
+ {queuedMessages.length > 0 && (
1392
+ <MessageQueue
1393
+ items={queuedMessages}
1394
+ onRemove={removeFromQueue}
1395
+ onMoveUp={(id) => moveQueueItem(id, -1)}
1396
+ onMoveDown={(id) => moveQueueItem(id, 1)}
1397
+ onEdit={editQueueItem}
1398
+ />
1399
+ )}
1400
+ <form onSubmit={handleSubmit} className="mx-auto w-full max-w-3xl">
1401
+ <ChatComposerSurface>
1402
+ <DropdownMenu>
1403
+ <DropdownMenuTrigger
1404
+ render={
1405
+ <button
1406
+ type="button"
1407
+ className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-muted-foreground transition hover:bg-secondary hover:text-foreground mt-1"
1408
+ >
1409
+ <Plus className="h-4.5 w-4.5" />
1410
+ </button>
1411
+ }
1412
+ />
1413
+ <DropdownMenuContent
1414
+ side="top"
1415
+ align="start"
1416
+ className="w-56 gap-1 flex flex-col"
1417
+ sideOffset={8}
1418
+ >
1419
+ {/* File Attachment Button hidden input, clicked programmatically */}
1420
+ <DropdownMenuItem
1421
+ onClick={(e) => {
1422
+ e.preventDefault();
1423
+ const fileInput = document.createElement('input');
1424
+ fileInput.type = 'file';
1425
+ fileInput.multiple = true;
1426
+ fileInput.accept = '*/*';
1427
+ fileInput.onchange = async (ev: any) => {
1428
+ const fileList = ev.target.files;
1429
+ if (!fileList || fileList.length === 0) return;
1430
+ const newAttachments = Array.from(fileList) as File[];
1431
+ setAttachments((prev) => [...prev, ...newAttachments]);
1432
+
1433
+ const pdfFile = newAttachments.find(
1434
+ (f) =>
1435
+ f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'),
1436
+ );
1437
+ if (pdfFile) {
1438
+ openCanvas(pdfFile, { background: false }).catch((err) => {
1439
+ toast.error(err.message || 'Failed to auto-open document');
1440
+ });
1441
+ }
1442
+ };
1443
+ fileInput.click();
1444
+ }}
1445
+ className="gap-2 cursor-pointer"
1446
+ >
1447
+ <Paperclip className="size-4" />
1448
+ <span>Attach Document</span>
1449
+ </DropdownMenuItem>
1450
+ {/* <DropdownMenuSeparator /> */}
1451
+ <DropdownMenuItem
1452
+ onClick={(e) => {
1453
+ e.preventDefault();
1454
+ handleWebSearchToggle();
1455
+ }}
1456
+ className="gap-2 justify-between cursor-pointer"
1457
+ >
1458
+ <div className="flex items-center gap-2">
1459
+ <Globe className="size-4" />
1460
+ <span>Web Search</span>
1461
+ </div>
1462
+ <Switch
1463
+ checked={configData?.config?.webSearchEnabled || false}
1464
+ className="pointer-events-none"
1465
+ />
1466
+ </DropdownMenuItem>
1467
+ </DropdownMenuContent>
1468
+ </DropdownMenu>
1469
+ <div className="hidden">
1470
+ <FileAttachmentButton id="chat-file-attachment" />
1471
+ </div>
1472
+
1473
+ <div className="flex flex-col flex-1 gap-2">
1474
+ {/* Attachments List */}
1475
+ {attachments.length > 0 && (
1476
+ <div className="flex w-full overflow-x-auto items-center gap-2 px-1 pt-1 pb-2 [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/60">
1477
+ {attachments.map((file, idx) => (
1478
+ <AttachmentCard
1479
+ key={`${file.name}-${idx}`}
1480
+ file={file}
1481
+ onClick={() => {
1482
+ openCanvas(file, { background: false }).catch((err) => {
1483
+ toast.error(err.message || 'Failed to open document');
1484
+ });
1485
+ }}
1486
+ onRemove={() => {
1487
+ setAttachments((prev) => prev.filter((_, i) => i !== idx));
1488
+ }}
1489
+ />
1490
+ ))}
1491
+ </div>
1492
+ )}
1493
+
1494
+ <textarea
1495
+ ref={textareaRef}
1496
+ value={input}
1497
+ onChange={(e) => setInput(e.target.value)}
1498
+ onKeyDown={(e) => {
1499
+ if (e.key === 'Enter' && !e.shiftKey) {
1500
+ e.preventDefault();
1501
+ handleSubmit(e);
1502
+ }
1503
+ }}
1504
+ onPaste={(e) => {
1505
+ const items = e.clipboardData?.items;
1506
+ if (!items) return;
1507
+ const pastedFiles: File[] = [];
1508
+ for (let i = 0; i < items.length; i++) {
1509
+ if (items[i].kind === 'file') {
1510
+ const file = items[i].getAsFile();
1511
+ if (file) pastedFiles.push(file);
1512
+ }
1513
+ }
1514
+ if (pastedFiles.length > 0) {
1515
+ setAttachments((prev) => [...prev, ...pastedFiles]);
1516
+ const pdfFile = pastedFiles.find(
1517
+ (f) =>
1518
+ f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'),
1519
+ );
1520
+ if (pdfFile) {
1521
+ openCanvas(pdfFile, { background: false }).catch((err) => {
1522
+ toast.error(err.message || 'Failed to auto-open pasted document');
1523
+ });
1524
+ }
1525
+ }
1526
+ }}
1527
+ rows={1}
1528
+ disabled={!ready}
1529
+ placeholder="How can I help you…"
1530
+ className="max-h-48 min-h-11 w-full resize-none overflow-y-auto bg-transparent px-2 py-2.5 text-[15px] leading-relaxed text-foreground outline-none placeholder:text-muted-foreground scrollbar-none [&::-webkit-scrollbar]:hidden disabled:opacity-50"
1531
+ />
1532
+ </div>
1533
+
1534
+ <button
1535
+ type="submit"
1536
+ disabled={!input.trim() && attachments.length === 0}
1537
+ title={isBusy || isDocLoading ? 'Add to queue' : 'Send'}
1538
+ className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary text-primary-foreground transition hover:opacity-90 disabled:opacity-40"
1539
+ >
1540
+ {isBusy || isDocLoading ? (
1541
+ <ListPlus className="h-4.5 w-4.5" />
1542
+ ) : (
1543
+ <ArrowUp className="h-4.5 w-4.5" />
1544
+ )}
1545
+ </button>
1546
+ </ChatComposerSurface>
1547
+ </form>
1548
+ </div>
1549
+ )}
1550
+ </div>
1551
+ );
1552
+ }