@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,1398 @@
1
+ 'use client';
2
+
3
+ import { workspaceUrl, workspaceFetch as fetch } from '../../host';
4
+ import { useEffect, useMemo, useRef, useState } from 'react';
5
+ import { ChatUserBubble } from '@larkup/workspace-ui';
6
+ import useSWR from 'swr';
7
+ import type { UIMessage } from 'ai';
8
+ import katex from 'katex';
9
+ import 'katex/dist/katex.min.css';
10
+ import { MessageActions } from './message-actions';
11
+ import { ChatChart, type ChartConfig } from './tools/chat-chart';
12
+ import { ChatDataTable, type DataTableConfig } from './tools/chat-data-table';
13
+ import { ChatFileExport } from './tools/chat-file-export';
14
+ import { ChatSandboxResult, type SandboxResultConfig } from './tools/chat-sandbox-result';
15
+ import { ChatTabs } from './tools/chat-tabs';
16
+ import { CorpusDataResult, type CorpusDataConfig } from './tools/corpus-data-result';
17
+ import { ChatSignatureRequest } from './tools/chat-signature-request';
18
+ import { Sparkles, FileEdit, CheckCircle2, Globe, ChevronDown } from 'lucide-react';
19
+ import { ChatMediaPreview } from './tools/chat-media-preview';
20
+ import { ChatCitations, type ChatCitationsUi } from './tools/chat-citations';
21
+ import { KnowledgeBaseResult } from './tools/knowledge-base-result';
22
+ import { useDocEditor } from './canvas/doc-editor-provider';
23
+ import { CHAT_TOOL_BEHAVIORS, getChatToolBehavior } from '../../lib/constants/tools';
24
+ import { decodeToolOutput } from '../../lib/chat/tool-output';
25
+ import {
26
+ isIndeterminateProgress,
27
+ keepToolProgressMonotonic,
28
+ smoothLiveToolProgress,
29
+ smoothPendingToolProgress,
30
+ type LiveToolActivity,
31
+ } from '../../lib/chat/live-tool-progress';
32
+ import { answerFeedbackForMessage, type AnswerFeedback } from '../../lib/chat/message-feedback';
33
+
34
+ export function MessageItem({
35
+ message,
36
+ isLast,
37
+ isStreaming,
38
+ addToolResult,
39
+ serverId,
40
+ autoOpenSupportingClip = false,
41
+ regenerate,
42
+ isBusy,
43
+ onFeedback,
44
+ }: {
45
+ message: UIMessage;
46
+ isLast?: boolean;
47
+ isStreaming?: boolean;
48
+ addToolResult?: Function;
49
+ serverId?: string | null;
50
+ /** Only the first assistant message in a chat may expand a clip initially. */
51
+ autoOpenSupportingClip?: boolean;
52
+ regenerate?: (options?: { messageId?: string }) => void;
53
+ /** True while a request is in flight — hides "Regenerate" on a trailing user message that's still waiting on a reply. */
54
+ isBusy?: boolean;
55
+ onFeedback?: (messageId: string, feedback: AnswerFeedback | undefined) => void;
56
+ }) {
57
+ const isUser = message.role === 'user';
58
+ const [progressClock, setProgressClock] = useState(() => Date.now());
59
+ const pendingToolStartedAt = useRef<Record<string, number>>({});
60
+ const [lastToolActivity, setLastToolActivity] = useState<LiveToolActivity | null>(null);
61
+ const [toolProgressFloors, setToolProgressFloors] = useState<Record<string, number>>({});
62
+
63
+ // The server gives us authoritative stage updates; this lightweight clock
64
+ // only animates the safe in-between estimate above. It is scoped to the
65
+ // active streamed answer so completed chat history never re-renders.
66
+ useEffect(() => {
67
+ if (!isLast || !isStreaming) return;
68
+ setProgressClock(Date.now());
69
+ const timer = window.setInterval(() => setProgressClock(Date.now()), 500);
70
+ return () => window.clearInterval(timer);
71
+ }, [isLast, isStreaming]);
72
+
73
+ // Any long-running tool call (any installed tool, not just video) can
74
+ // report live progress keyed by its own toolCallId -- see
75
+ // gpu-activity-store.ts. Only the currently-streaming message ever has an
76
+ // executing tool part, so there's nothing to match against otherwise.
77
+ const { data: liveToolActivity } = useSWR<{ activity: LiveToolActivity | null }>(
78
+ isLast && isStreaming
79
+ ? workspaceUrl(
80
+ `/api/gpu-activity${serverId ? `?projectId=${encodeURIComponent(serverId)}` : ''}`,
81
+ )
82
+ : null,
83
+ (url: string) => fetch(url).then((r) => r.json()),
84
+ {
85
+ refreshInterval: 800,
86
+ dedupingInterval: 0,
87
+ keepPreviousData: true,
88
+ },
89
+ );
90
+ const currentToolActivity =
91
+ liveToolActivity?.activity?.toolCallId && liveToolActivity.activity.phase
92
+ ? liveToolActivity.activity
93
+ : lastToolActivity;
94
+ const smoothedToolPercent = currentToolActivity
95
+ ? smoothLiveToolProgress(currentToolActivity, progressClock)
96
+ : 0;
97
+ const priorToolPercent = currentToolActivity?.toolCallId
98
+ ? toolProgressFloors[currentToolActivity.toolCallId]
99
+ : undefined;
100
+ const visibleToolPercent = keepToolProgressMonotonic(priorToolPercent, smoothedToolPercent);
101
+
102
+ // A query may inspect several short source ranges in succession. The API
103
+ // clears each finished range before the next starts, but they are one chat
104
+ // action. Preserve its last visual floor and activity through that handoff
105
+ // so the inline bar never drops back to the beginning.
106
+ useEffect(() => {
107
+ if (!liveToolActivity?.activity?.toolCallId) return;
108
+ setLastToolActivity(liveToolActivity.activity);
109
+ const callId = liveToolActivity.activity.toolCallId;
110
+ const next = smoothLiveToolProgress(liveToolActivity.activity, progressClock);
111
+ setToolProgressFloors((current) => {
112
+ const floor = keepToolProgressMonotonic(current[callId], next);
113
+ return current[callId] === floor ? current : { ...current, [callId]: floor };
114
+ });
115
+ }, [liveToolActivity?.activity, progressClock]);
116
+ // Shown for both phases -- a silent "Working…" row for the whole time a
117
+ // cold GPU worker takes to wake up reads as hung, even though the
118
+ // dedicated bottom-right ring is also showing that same wait.
119
+ const activeToolProgress = currentToolActivity?.toolCallId
120
+ ? {
121
+ toolCallId: currentToolActivity.toolCallId,
122
+ percent: visibleToolPercent,
123
+ label: currentToolActivity.label,
124
+ message: currentToolActivity.message || currentToolActivity.label,
125
+ phase: currentToolActivity.phase,
126
+ }
127
+ : null;
128
+
129
+ let updateFromToolResult: ((result: any) => void) | undefined;
130
+ let openCanvas: ((file: File, options?: any) => Promise<void>) | undefined;
131
+
132
+ try {
133
+ const editor = useDocEditor();
134
+ updateFromToolResult = editor.updateFromToolResult;
135
+ openCanvas = editor.openCanvas;
136
+ } catch {}
137
+
138
+ const anyMessage = message as any;
139
+ const parts: any[] = useMemo(() => {
140
+ const p: any[] = message.parts ? [...message.parts] : [];
141
+ if (anyMessage.toolInvocations && Array.isArray(anyMessage.toolInvocations)) {
142
+ anyMessage.toolInvocations.forEach((t: any) => {
143
+ if (
144
+ !p.some(
145
+ (existing: any) =>
146
+ existing.type === 'tool-invocation' &&
147
+ existing.toolInvocation?.toolCallId === t.toolCallId,
148
+ )
149
+ ) {
150
+ p.push({ type: 'tool-invocation', toolInvocation: t });
151
+ }
152
+ });
153
+ }
154
+ return p;
155
+ }, [message.parts, anyMessage.toolInvocations]);
156
+
157
+ const appliedToolCallsRef = useRef<Set<string>>(new Set());
158
+
159
+ const docToolFingerprint = useMemo(() => {
160
+ if (!isLast) return '';
161
+ return parts
162
+ .filter((p: any) => {
163
+ const info = getToolInfo(p);
164
+ return (
165
+ info.isCompleted &&
166
+ info.output?.success &&
167
+ (info.toolName === 'fillDocumentForm' ||
168
+ info.toolName === 'editDocument' ||
169
+ info.toolName === 'requestDocumentSignature')
170
+ );
171
+ })
172
+ .map((p: any) => {
173
+ const ti = p.toolInvocation ?? p;
174
+ return ti.toolCallId || ti.id || '';
175
+ })
176
+ .join(',');
177
+ }, [parts, isLast]);
178
+
179
+ useEffect(() => {
180
+ if (!updateFromToolResult || !isLast || !docToolFingerprint) return;
181
+
182
+ const completedDocTools = parts.filter((p: any) => {
183
+ const info = getToolInfo(p);
184
+ return (
185
+ info.isCompleted &&
186
+ info.output?.success &&
187
+ (info.toolName === 'fillDocumentForm' ||
188
+ info.toolName === 'editDocument' ||
189
+ info.toolName === 'requestDocumentSignature')
190
+ );
191
+ });
192
+
193
+ if (completedDocTools.length > 0) {
194
+ completedDocTools.forEach((part: any) => {
195
+ const ti = part.toolInvocation ?? part;
196
+ const callId = ti.toolCallId || ti.id || '';
197
+
198
+ // Skip if we already applied this specific tool result
199
+ if (callId && appliedToolCallsRef.current.has(callId)) return;
200
+
201
+ const output = getToolInfo(part).output;
202
+ if (output && output.fileBase64) {
203
+ if (callId) appliedToolCallsRef.current.add(callId);
204
+ updateFromToolResult(output);
205
+ }
206
+ });
207
+ }
208
+ }, [docToolFingerprint, updateFromToolResult, isLast]);
209
+
210
+ useEffect(() => {
211
+ if (!isLast || !addToolResult) return;
212
+ const signatureTools = parts.filter(
213
+ (p: any) => getToolInfo(p).toolName === 'requestDocumentSignature',
214
+ );
215
+ if (signatureTools.length > 1) {
216
+ // Keep the first one, auto-resolve the rest
217
+ for (let i = 1; i < signatureTools.length; i++) {
218
+ const info = getToolInfo(signatureTools[i]);
219
+ if (info.isExecuting) {
220
+ const ti = signatureTools[i].toolInvocation ?? signatureTools[i];
221
+ const callId = ti.toolCallId || ti.id;
222
+ if (callId && !appliedToolCallsRef.current.has(callId)) {
223
+ appliedToolCallsRef.current.add(callId);
224
+ addToolResult({
225
+ toolCallId: callId,
226
+ result: { success: true, message: 'Duplicate signature request auto-resolved.' },
227
+ });
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }, [parts, isLast, addToolResult]);
233
+
234
+ if (isUser) {
235
+ const text = parts
236
+ .filter((p: any) => p.type === 'text')
237
+ .map((p: any) => p.text)
238
+ .join('');
239
+
240
+ const attachments = anyMessage.experimental_attachments || [];
241
+
242
+ const partAttachments = parts
243
+ .filter((p: any) => p.type === 'image' || p.type === 'file')
244
+ .map((p: any) => {
245
+ let name = p.name;
246
+ let cType = p.mimeType || p.contentType || (p.type === 'image' ? 'image/png' : '');
247
+
248
+ if (!cType && p.url && typeof p.url === 'string' && p.url.startsWith('data:')) {
249
+ const match = p.url.match(/^data:([^;]+);/);
250
+ if (match) cType = match[1];
251
+ }
252
+ if (!cType) cType = 'application/octet-stream';
253
+
254
+ if (!name) {
255
+ if (p.url && typeof p.url === 'string' && !p.url.startsWith('data:')) {
256
+ name = p.url.split('/').pop()?.split('?')[0];
257
+ } else {
258
+ name = p.type === 'image' ? 'image.png' : 'Document';
259
+ if (cType === 'application/pdf') name = 'Document.pdf';
260
+ else if (cType === 'text/plain') name = 'Text.txt';
261
+ else if (cType.includes('word')) name = 'Document.docx';
262
+ else if (cType.includes('excel') || cType.includes('spreadsheet')) name = 'Data.xlsx';
263
+ }
264
+ }
265
+ return {
266
+ url: p.image || p.data || p.url,
267
+ contentType: cType,
268
+ name,
269
+ };
270
+ });
271
+
272
+ // Deduplicate or combine (usually they are either in experimental_attachments or parts, rarely both)
273
+ const allAttachments = attachments.length > 0 ? attachments : partAttachments;
274
+
275
+ return (
276
+ <div className="message user-message group flex justify-end" data-role="user">
277
+ <div className="max-w-[85%] flex flex-col gap-2 items-end">
278
+ {allAttachments.length > 0 && (
279
+ <div className="flex flex-wrap justify-end gap-2">
280
+ {allAttachments.map((att: any, i: number) => {
281
+ let cType = att.contentType || att.type || '';
282
+ const isImage = cType.startsWith('image/');
283
+ const isVideo = cType.startsWith('video/');
284
+ const isAudio = cType.startsWith('audio/');
285
+ let nameStr = att.name;
286
+
287
+ if (
288
+ !cType &&
289
+ att.url &&
290
+ typeof att.url === 'string' &&
291
+ att.url.startsWith('data:')
292
+ ) {
293
+ const match = att.url.match(/^data:([^;]+);/);
294
+ if (match) cType = match[1];
295
+ }
296
+
297
+ // AI SDK sometimes loses the MIME type and defaults to application/octet-stream
298
+ // We can sniff PDF magic bytes `%PDF-` -> `JVBERi0` in base64
299
+ if (
300
+ (!cType || cType === 'application/octet-stream') &&
301
+ att.url &&
302
+ typeof att.url === 'string'
303
+ ) {
304
+ if (att.url.includes('JVBERi0')) {
305
+ cType = 'application/pdf';
306
+ }
307
+ }
308
+
309
+ if (!nameStr || nameStr === 'Attachment') {
310
+ nameStr = isImage ? 'image.png' : 'Document';
311
+ if (cType === 'application/pdf') nameStr = 'Document.pdf';
312
+ else if (cType === 'text/plain') nameStr = 'Text.txt';
313
+ else if (cType.includes('word')) nameStr = 'Document.docx';
314
+ else if (cType.includes('excel') || cType.includes('spreadsheet'))
315
+ nameStr = 'Data.xlsx';
316
+ } else if (cType === 'application/pdf' && !nameStr.toLowerCase().endsWith('.pdf')) {
317
+ nameStr += '.pdf';
318
+ }
319
+
320
+ const ext = nameStr.split('.').pop()?.toLowerCase() || '';
321
+ let iconPath = '/icons/image.png'; // default fallback for images and unknown files
322
+ if (
323
+ ['csv', 'xls', 'xlsx'].includes(ext) ||
324
+ cType.includes('excel') ||
325
+ cType.includes('spreadsheet')
326
+ )
327
+ iconPath = '/icons/excel.png';
328
+ else if (['doc', 'docx'].includes(ext) || cType.includes('word'))
329
+ iconPath = '/icons/word.png';
330
+ else if (['md', 'markdown'].includes(ext)) iconPath = '/icons/markdown.png';
331
+ else if (['pdf'].includes(ext) || cType === 'application/pdf')
332
+ iconPath = '/icons/pdf.png';
333
+ else if (isVideo) iconPath = '/icons/video.png';
334
+ else if (isAudio) iconPath = '/icons/audio.png';
335
+ else if (!isImage && !ext) iconPath = '/icons/word.png'; // Generic file icon
336
+
337
+ return (
338
+ <div
339
+ key={i}
340
+ onClick={() => {
341
+ if (openCanvas) {
342
+ if (att instanceof File || (att.name && att.size && !att.url)) {
343
+ // It's a File object provided directly from input
344
+ openCanvas(att as File, { background: false });
345
+ } else if (att.url) {
346
+ let fetchUrl = att.url;
347
+ // If it's a raw base64 string without data prefix, add it
348
+ if (!fetchUrl.startsWith('http') && !fetchUrl.startsWith('data:')) {
349
+ fetchUrl = `data:${
350
+ cType || 'application/octet-stream'
351
+ };base64,${fetchUrl}`;
352
+ }
353
+ fetch(fetchUrl)
354
+ .then((r) => r.blob())
355
+ .then((blob) => {
356
+ const file = new File([blob], nameStr, { type: cType });
357
+ openCanvas(file, { background: false });
358
+ })
359
+ .catch((err) => {
360
+ console.error('Failed to open attachment in canvas', err);
361
+ });
362
+ }
363
+ }
364
+ }}
365
+ className={`flex items-center gap-2 bg-muted/80 border border-border/60 rounded-xl h-12 px-3 p-1 shrink-0 ${
366
+ openCanvas && (att.url || att instanceof File || att.size)
367
+ ? 'cursor-pointer hover:bg-muted'
368
+ : ''
369
+ }`}
370
+ >
371
+ <div className="size-7 shrink-0 rounded-md overflow-hidden bg-background border border-border/90 flex items-center justify-center p-1">
372
+ <img src={iconPath} alt={nameStr} className="w-full h-full object-contain" />
373
+ </div>
374
+ <div className="flex flex-col justify-center max-w-37.5">
375
+ <span className="text-xs font-medium truncate text-foreground">
376
+ {nameStr}
377
+ </span>
378
+ </div>
379
+ </div>
380
+ );
381
+ })}
382
+ </div>
383
+ )}
384
+ {text && <ChatUserBubble>{text}</ChatUserBubble>}
385
+ {text && (
386
+ <MessageActions
387
+ text={text}
388
+ className="opacity-0 transition group-hover:opacity-100 focus-within:opacity-100"
389
+ regenerateLabel="Resend from here"
390
+ onRegenerate={
391
+ !isBusy && regenerate ? () => regenerate({ messageId: message.id }) : undefined
392
+ }
393
+ />
394
+ )}
395
+ </div>
396
+ </div>
397
+ );
398
+ }
399
+
400
+ // Assistant message
401
+ const kbParts = parts.filter((p: any) => {
402
+ const { toolName } = getToolInfo(p);
403
+ return getChatToolBehavior(toolName).placement === 'source';
404
+ });
405
+ // searchKnowledgeBase automatically enriches a video match with active
406
+ // evidence (queryVideoKnowledge's own result, ui.kind:'citations' included
407
+ // -- the same generic shape any tool's completed result can carry).
408
+ // Preserve that nested result as a visible citation card instead of
409
+ // making a completed GPU investigation look like ordinary RAG only.
410
+ const nestedVideoCitationsUi: ChatCitationsUi[] = kbParts.flatMap((part: any) => {
411
+ const output = getToolInfo(part).output;
412
+ const result = decodeToolOutput(output) as any;
413
+ return result?.videoEvidence?.success && result.videoEvidence.ui?.kind === 'citations'
414
+ ? [result.videoEvidence.ui as ChatCitationsUi]
415
+ : [];
416
+ });
417
+ const isKnowledgeSearchActive = kbParts.some((part: any) => getToolInfo(part).isExecuting);
418
+ const activeKnowledgeProgress =
419
+ activeToolProgress &&
420
+ kbParts.some((part: any) => {
421
+ const invocation = part.toolInvocation ?? part;
422
+ return (invocation.toolCallId || invocation.id || '') === activeToolProgress.toolCallId;
423
+ })
424
+ ? activeToolProgress
425
+ : null;
426
+
427
+ let signatureFound = false;
428
+ const toolParts = parts.filter((p: any) => {
429
+ const { toolName } = getToolInfo(p);
430
+ if (getChatToolBehavior(toolName).placement === 'source') return false;
431
+ if (toolName === 'requestDocumentSignature') {
432
+ if (signatureFound) return false;
433
+ signatureFound = true;
434
+ }
435
+ return true;
436
+ });
437
+
438
+ const textParts = parts.filter((p: any) => p.type === 'text');
439
+
440
+ const { allCleanTexts, hasThinkTags } = useMemo(() => {
441
+ let hasTags = false;
442
+ const cleanTexts: string[] = [];
443
+ for (const part of textParts) {
444
+ const raw = part.text || '';
445
+ const { cleanText, reasoningText } = stripThinkTags(raw);
446
+ cleanTexts.push(cleanText);
447
+ if (reasoningText) {
448
+ hasTags = true;
449
+ }
450
+ }
451
+ return { allCleanTexts: cleanTexts, hasThinkTags: hasTags };
452
+ }, [textParts]);
453
+
454
+ const answerText = useMemo(
455
+ () => allCleanTexts.filter(Boolean).join('\n\n').trim(),
456
+ [allCleanTexts],
457
+ );
458
+
459
+ const answerFeedback = answerFeedbackForMessage(message);
460
+ const liked = answerFeedback === 'liked';
461
+ const disliked = answerFeedback === 'disliked';
462
+
463
+ const handleLike = () => {
464
+ onFeedback?.(message.id, liked ? undefined : 'liked');
465
+ };
466
+
467
+ const handleDislike = () => {
468
+ onFeedback?.(message.id, disliked ? undefined : 'disliked');
469
+ };
470
+
471
+ const isShimmering =
472
+ textParts.every((p: any) => !p.text || p.text.trim().length === 0) && isLast && isStreaming;
473
+
474
+ const isVizPart = (p: any) => {
475
+ const { toolName, isCompleted } = getToolInfo(p);
476
+ return getChatToolBehavior(toolName).placement === 'visualization' && isCompleted;
477
+ };
478
+
479
+ const vizParts = toolParts.filter(isVizPart);
480
+ const mediaToolParts = toolParts.filter(
481
+ (p: any) => getChatToolBehavior(getToolInfo(p).toolName).placement === 'media',
482
+ );
483
+ const webSearchParts = toolParts.filter(
484
+ (p: any) => getChatToolBehavior(getToolInfo(p).toolName).placement === 'web-search',
485
+ );
486
+ const nonVizToolParts = toolParts.filter((p: any) => {
487
+ const placement = getChatToolBehavior(getToolInfo(p).toolName).placement;
488
+ return !isVizPart(p) && placement === 'inline';
489
+ });
490
+ const firstCitationToolPartIndex = toolParts.findIndex(
491
+ (part: any) => getToolInfo(part).output?.ui?.kind === 'citations',
492
+ );
493
+
494
+ const vizTabs =
495
+ vizParts.length > 1
496
+ ? vizParts.map((p: any, i: number) => {
497
+ const { output } = getToolInfo(p);
498
+ return {
499
+ label: (output as ChartConfig)?.title || `Chart ${i + 1}`,
500
+ content: renderToolPart(p, i),
501
+ };
502
+ })
503
+ : null;
504
+
505
+ const executingParts = useMemo(
506
+ () =>
507
+ toolParts.filter((p: any) => {
508
+ const { isExecuting, toolName } = getToolInfo(p);
509
+ return isExecuting && getChatToolBehavior(toolName).placement !== 'source';
510
+ }),
511
+ [toolParts],
512
+ );
513
+ const progressForExecutingPart = (part: any) => {
514
+ const invocation = part.toolInvocation ?? part;
515
+ const toolCallId = invocation.toolCallId || invocation.id || '';
516
+ if (activeToolProgress?.toolCallId === toolCallId) return activeToolProgress;
517
+ pendingToolStartedAt.current[toolCallId] ??= progressClock;
518
+ const behavior = getChatToolBehavior(getToolInfo(part).toolName);
519
+ return {
520
+ toolCallId,
521
+ percent: smoothPendingToolProgress(pendingToolStartedAt.current[toolCallId], progressClock),
522
+ label: behavior.pendingLabel || 'Working…',
523
+ message: behavior.pendingLabel || 'Working…',
524
+ };
525
+ };
526
+
527
+ return (
528
+ <div className="message assistant-message flex flex-col gap-2" data-role="assistant">
529
+ {kbParts.length > 0 && (
530
+ <KnowledgeBaseResult
531
+ parts={kbParts}
532
+ isShimmering={isShimmering}
533
+ activity={activeKnowledgeProgress}
534
+ />
535
+ )}
536
+
537
+ {nestedVideoCitationsUi.map((ui, i) => (
538
+ <ChatCitations
539
+ key={`nested-citations-${i}`}
540
+ ui={ui}
541
+ autoOpenSupportingClip={autoOpenSupportingClip && i === 0}
542
+ />
543
+ ))}
544
+
545
+ {webSearchParts.length > 0 && <WebSearchSummary parts={webSearchParts} />}
546
+
547
+ {nonVizToolParts
548
+ .filter((p: any) => getToolInfo(p).isCompleted)
549
+ .map((part: any, i: number) => {
550
+ const partIndex = toolParts.indexOf(part);
551
+ return renderToolPart(
552
+ part,
553
+ i,
554
+ addToolResult,
555
+ updateFromToolResult,
556
+ undefined,
557
+ autoOpenSupportingClip &&
558
+ nestedVideoCitationsUi.length === 0 &&
559
+ partIndex === firstCitationToolPartIndex,
560
+ );
561
+ })}
562
+
563
+ {vizTabs ? (
564
+ <ChatTabs config={{ tabs: vizTabs }} />
565
+ ) : (
566
+ vizParts.map((part: any, i: number) =>
567
+ renderToolPart(part, i, addToolResult, updateFromToolResult),
568
+ )
569
+ )}
570
+
571
+ {executingParts.map((part: any, i: number) =>
572
+ renderToolPart(
573
+ part,
574
+ i,
575
+ addToolResult,
576
+ updateFromToolResult,
577
+ progressForExecutingPart(part),
578
+ ),
579
+ )}
580
+
581
+ {isLast &&
582
+ isStreaming &&
583
+ executingParts.length === 0 &&
584
+ !isKnowledgeSearchActive &&
585
+ !hasThinkTags &&
586
+ textParts.every((p: any) => !p.text || p.text.trim().length === 0) && (
587
+ <div className="flex items-center gap-2">
588
+ <div className="size-6.5 bg-white border border-border rounded-full flex items-center justify-center p-1 animate-pulse">
589
+ <img src="/logo.png" alt="logo" className="size-4 animate-spin" />
590
+ </div>
591
+ </div>
592
+ )}
593
+
594
+ {/* Text parts — with markdown table detection + think-tag stripping */}
595
+ {textParts.map((part: any, i: number) => {
596
+ const rawText = part.text || '';
597
+ if (!rawText.trim()) return null;
598
+
599
+ const strippedText = allCleanTexts[i] ?? rawText;
600
+ const cleanText = strippedText.replace(/\[(?:IMAGE_REF|VIDEO_REF|AUDIO_REF):[^\]]+\]/g, '');
601
+
602
+ if (!cleanText.trim()) return null;
603
+
604
+ const segments = splitTextAndTables(cleanText);
605
+
606
+ return (
607
+ <div key={i} className="flex flex-col gap-4">
608
+ {segments.map((seg, j) => {
609
+ if (seg.type === 'table') {
610
+ const tableConfig: DataTableConfig = {
611
+ columns: seg.table.columns,
612
+ rows: seg.table.rows,
613
+ totalRows: seg.table.rows.length,
614
+ };
615
+ return <ChatDataTable key={`table-${j}`} config={tableConfig} />;
616
+ }
617
+ return (
618
+ <div
619
+ key={`text-${j}`}
620
+ className="assistant-text-content"
621
+ dangerouslySetInnerHTML={{
622
+ __html: renderMarkdown(seg.content),
623
+ }}
624
+ />
625
+ );
626
+ })}
627
+ </div>
628
+ );
629
+ })}
630
+
631
+ {mediaToolParts
632
+ .filter((p: any) => getToolInfo(p).isCompleted)
633
+ .map((part: any, i: number) =>
634
+ renderToolPart(part, i, addToolResult, updateFromToolResult),
635
+ )}
636
+
637
+ {answerText && !(isLast && isStreaming) && (
638
+ <MessageActions
639
+ text={answerText}
640
+ liked={liked}
641
+ disliked={disliked}
642
+ onLike={onFeedback ? handleLike : undefined}
643
+ onDislike={onFeedback ? handleDislike : undefined}
644
+ onRegenerate={regenerate ? () => regenerate({ messageId: message.id }) : undefined}
645
+ />
646
+ )}
647
+ </div>
648
+ );
649
+ }
650
+
651
+ interface ParsedTable {
652
+ columns: string[];
653
+ rows: Record<string, any>[];
654
+ }
655
+
656
+ function splitTextAndTables(
657
+ text: string,
658
+ ): Array<{ type: 'text'; content: string } | { type: 'table'; table: ParsedTable }> {
659
+ if (!text) return [];
660
+
661
+ const lines = text.split('\n');
662
+ const segments: Array<{ type: 'text'; content: string } | { type: 'table'; table: ParsedTable }> =
663
+ [];
664
+ let currentText: string[] = [];
665
+ let tableLines: string[] = [];
666
+ let inTable = false;
667
+
668
+ const isTableRow = (line: string) => {
669
+ const trimmed = line.trim();
670
+ return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 2;
671
+ };
672
+
673
+ const isSeparatorRow = (line: string) => /^\|[\s\-:|]+\|$/.test(line.trim());
674
+
675
+ const flushText = () => {
676
+ if (currentText.length > 0) {
677
+ const content = currentText.join('\n').trim();
678
+ if (content) {
679
+ segments.push({ type: 'text', content });
680
+ }
681
+ currentText = [];
682
+ }
683
+ };
684
+
685
+ const flushTable = () => {
686
+ if (tableLines.length < 2) {
687
+ currentText.push(...tableLines);
688
+ tableLines = [];
689
+ return;
690
+ }
691
+
692
+ const headerLine = tableLines[0];
693
+ const columns = headerLine
694
+ .split('|')
695
+ .map((c) => c.trim())
696
+ .filter(Boolean);
697
+
698
+ let dataStart = 1;
699
+ if (tableLines.length > 1 && isSeparatorRow(tableLines[1])) {
700
+ dataStart = 2;
701
+ }
702
+
703
+ const rows: Record<string, any>[] = [];
704
+ for (let i = dataStart; i < tableLines.length; i++) {
705
+ const rawCells = tableLines[i]
706
+ .split('|')
707
+ .slice(1, -1)
708
+ .map((c) => c.trim());
709
+
710
+ const row: Record<string, any> = {};
711
+ columns.forEach((col, j) => {
712
+ const val = rawCells[j] ?? '';
713
+ const num = Number(val);
714
+ row[col] = !isNaN(num) && val !== '' ? num : val;
715
+ });
716
+ rows.push(row);
717
+ }
718
+
719
+ if (columns.length > 0 && rows.length > 0) {
720
+ segments.push({
721
+ type: 'table',
722
+ table: { columns, rows },
723
+ });
724
+ }
725
+ tableLines = [];
726
+ };
727
+
728
+ for (const line of lines) {
729
+ if (isTableRow(line)) {
730
+ if (!inTable) {
731
+ flushText();
732
+ inTable = true;
733
+ }
734
+ if (!isSeparatorRow(line) || tableLines.length === 1) {
735
+ tableLines.push(line);
736
+ } else if (isSeparatorRow(line)) {
737
+ tableLines.push(line);
738
+ }
739
+ } else {
740
+ if (inTable) {
741
+ flushTable();
742
+ inTable = false;
743
+ }
744
+ currentText.push(line);
745
+ }
746
+ }
747
+
748
+ if (inTable) {
749
+ flushTable();
750
+ }
751
+ flushText();
752
+
753
+ return segments;
754
+ }
755
+
756
+ function stripThinkTags(text: string): { cleanText: string; reasoningText: string } {
757
+ if (!text) return { cleanText: '', reasoningText: '' };
758
+
759
+ const reasoningParts: string[] = [];
760
+ let cleaned = stripLeakedToolCalls(text);
761
+
762
+ // 1. Extract complete <think>…</think> or <thought>…</thought> blocks
763
+ const thinkRegex = /<(?:think|thought)>([\s\S]*?)<\/(?:think|thought)>/gi;
764
+ let match;
765
+ while ((match = thinkRegex.exec(cleaned)) !== null) {
766
+ reasoningParts.push(match[1].trim());
767
+ }
768
+ cleaned = cleaned.replace(thinkRegex, '');
769
+
770
+ // 2. Extract unclosed <think> or <thought> (streaming, no closing tag yet)
771
+ const unclosedThinkRegex = /<(?:think|thought)>([\s\S]*)$/i;
772
+ const unclosedThinkMatch = cleaned.match(unclosedThinkRegex);
773
+ if (unclosedThinkMatch) {
774
+ reasoningParts.push(unclosedThinkMatch[1].trim());
775
+ cleaned = cleaned.replace(unclosedThinkRegex, '');
776
+ }
777
+
778
+ // 3. Handle <result>...</result>
779
+ const resultRegex = /<result>([\s\S]*?)<\/result>/gi;
780
+ let resultMatch;
781
+ let foundResult = false;
782
+ let finalCleanText = '';
783
+ let textOutsideResult = cleaned;
784
+
785
+ while ((resultMatch = resultRegex.exec(cleaned)) !== null) {
786
+ foundResult = true;
787
+ finalCleanText += (finalCleanText ? '\n\n' : '') + resultMatch[1].trim();
788
+ textOutsideResult = textOutsideResult.replace(resultMatch[0], '');
789
+ }
790
+
791
+ // Handle unclosed <result>
792
+ const unclosedResultRegex = /<result>([\s\S]*)$/i;
793
+ const unclosedResultMatch = textOutsideResult.match(unclosedResultRegex);
794
+ if (unclosedResultMatch) {
795
+ foundResult = true;
796
+ finalCleanText += (finalCleanText ? '\n\n' : '') + unclosedResultMatch[1].trim();
797
+ textOutsideResult = textOutsideResult.replace(unclosedResultRegex, '');
798
+ }
799
+
800
+ if (foundResult) {
801
+ if (textOutsideResult.trim()) {
802
+ reasoningParts.push(textOutsideResult.trim());
803
+ }
804
+ return {
805
+ cleanText: finalCleanText.trim(),
806
+ reasoningText: reasoningParts.filter(Boolean).join('\n\n'),
807
+ };
808
+ }
809
+
810
+ return {
811
+ cleanText: cleaned.trim(),
812
+ reasoningText: reasoningParts.filter(Boolean).join('\n\n'),
813
+ };
814
+ }
815
+
816
+ /**
817
+ * Some providers occasionally place a tool request in assistant text after
818
+ * sending (or instead of sending) the structured tool part. Tool calls are
819
+ * implementation detail, never useful answer content. Restrict removal to
820
+ * registered first-party names so ordinary prose and code remain intact.
821
+ */
822
+ function stripLeakedToolCalls(text: string): string {
823
+ const names = new Set(Object.keys(CHAT_TOOL_BEHAVIORS));
824
+ const lines = text.split('\n');
825
+ const kept: string[] = [];
826
+ let callDepth = 0;
827
+
828
+ for (const line of lines) {
829
+ if (callDepth > 0) {
830
+ callDepth += countParentheses(line);
831
+ if (callDepth <= 0) callDepth = 0;
832
+ continue;
833
+ }
834
+ const name = line.match(/^\s*([A-Za-z_$][\w$]*)\s*\(/)?.[1];
835
+ if (name && names.has(name)) {
836
+ callDepth = countParentheses(line);
837
+ if (callDepth <= 0) callDepth = 0;
838
+ continue;
839
+ }
840
+ kept.push(line);
841
+ }
842
+ return kept.join('\n');
843
+ }
844
+
845
+ function countParentheses(text: string): number {
846
+ let count = 0;
847
+ for (const char of text) {
848
+ if (char === '(') count += 1;
849
+ if (char === ')') count -= 1;
850
+ }
851
+ return count;
852
+ }
853
+
854
+ function getToolInfo(part: any): {
855
+ toolName: string;
856
+ isExecuting: boolean;
857
+ isCompleted: boolean;
858
+ output: any;
859
+ input: any;
860
+ } {
861
+ if (part.type === 'tool-invocation') {
862
+ const ti = part.toolInvocation;
863
+ const state = ti.state;
864
+ return {
865
+ toolName: ti.toolName,
866
+ isExecuting: state === 'partial-call' || state === 'call',
867
+ isCompleted:
868
+ (state === 'result' || state === 'output' || state === 'output-available') &&
869
+ ti.result !== undefined,
870
+ output: decodeToolOutput(ti.result) ?? ti.result,
871
+ input: ti.args,
872
+ };
873
+ }
874
+
875
+ if (part.type?.startsWith('tool-') && part.type !== 'tool-invocation') {
876
+ const toolName = part.type.replace('tool-', '');
877
+ const state = part.state;
878
+ return {
879
+ toolName,
880
+ isExecuting: state === 'input-streaming' || state === 'input-available',
881
+ isCompleted:
882
+ (state === 'output' || state === 'output-available') && part.output !== undefined,
883
+ output: decodeToolOutput(part.output) ?? part.output,
884
+ input: part.input,
885
+ };
886
+ }
887
+
888
+ return {
889
+ toolName: '',
890
+ isExecuting: false,
891
+ isCompleted: false,
892
+ output: undefined,
893
+ input: undefined,
894
+ };
895
+ }
896
+
897
+ function renderToolPart(
898
+ part: any,
899
+ index: number,
900
+ addToolResult?: Function,
901
+ updateFromToolResult?: Function,
902
+ toolProgress?: {
903
+ toolCallId: string;
904
+ percent: number;
905
+ label: string;
906
+ message?: string;
907
+ } | null,
908
+ autoOpenSupportingClip = false,
909
+ ): React.ReactNode | null {
910
+ const { toolName, isExecuting, isCompleted, output, input } = getToolInfo(part);
911
+ const behavior = getChatToolBehavior(toolName);
912
+ const callId = part.toolInvocation?.toolCallId || part.toolCallId || part.id || '';
913
+
914
+ if (isExecuting) {
915
+ if (behavior.placement === 'source') return null;
916
+
917
+ if (behavior.resultView === 'signature') {
918
+ return (
919
+ <ChatSignatureRequest
920
+ key={index}
921
+ detectedLocations={input?.detectedLocations}
922
+ toolCallId={callId}
923
+ addToolResult={addToolResult}
924
+ />
925
+ );
926
+ }
927
+
928
+ const liveProgress = toolProgress && toolProgress.toolCallId === callId ? toolProgress : null;
929
+ // Past the point where the remaining work has a measurable size, a
930
+ // percentage is a claim we cannot back. Drop it and shuttle the bar
931
+ // instead, so the wait still reads as active rather than stalled.
932
+ const indeterminate = liveProgress ? isIndeterminateProgress(liveProgress.percent) : false;
933
+
934
+ return (
935
+ <div key={index} className="flex min-w-0 flex-col gap-1.5 py-3">
936
+ <div className="flex min-w-0 items-center gap-2">
937
+ <div className="size-6.5 bg-white dark:bg-card border border-border rounded-full flex items-center justify-center p-1">
938
+ <img src="/logo.png" alt="" className="size-4 animate-spin" />
939
+ </div>
940
+ <span
941
+ className="larkup-shimmer-text min-w-0 wrap-break-word text-[13px] font-medium leading-5"
942
+ aria-live="polite"
943
+ aria-atomic="true"
944
+ >
945
+ {liveProgress?.message || liveProgress?.label || behavior.pendingLabel || 'Working'}
946
+ </span>
947
+ </div>
948
+ {liveProgress && behavior.showProgressBar !== false ? (
949
+ <div className="ml-9 flex items-center gap-2">
950
+ <div className="h-1.5 w-48 overflow-hidden rounded-full bg-muted">
951
+ {indeterminate ? (
952
+ <div className="larkup-progress-shuttle h-full rounded-full bg-emerald-500" />
953
+ ) : (
954
+ <div
955
+ className="h-full rounded-full bg-emerald-500 transition-[width] duration-700 ease-out"
956
+ style={{ width: `${Math.max(6, Math.min(100, liveProgress.percent))}%` }}
957
+ />
958
+ )}
959
+ </div>
960
+ {!indeterminate ? (
961
+ <span className="text-[11px] tabular-nums text-muted-foreground">
962
+ {Math.round(liveProgress.percent)}%
963
+ </span>
964
+ ) : null}
965
+ </div>
966
+ ) : null}
967
+ </div>
968
+ );
969
+ }
970
+
971
+ if (isCompleted) {
972
+ const enterpriseUi = output?.ui;
973
+ if (enterpriseUi?.kind === 'notice') {
974
+ return (
975
+ <div
976
+ key={index}
977
+ className="rounded-xl border border-primary/20 bg-primary/5 px-4 py-3 text-sm"
978
+ >
979
+ <div className="font-medium">{enterpriseUi.title}</div>
980
+ <div className="mt-1 text-muted-foreground">{enterpriseUi.body}</div>
981
+ </div>
982
+ );
983
+ }
984
+ if (enterpriseUi?.kind === 'card') {
985
+ return (
986
+ <div key={index} className="rounded-xl border border-border bg-card px-4 py-3 text-sm">
987
+ <div className="font-medium">{enterpriseUi.title}</div>
988
+ <dl className="mt-3 grid gap-2">
989
+ {(enterpriseUi.facts ?? []).map((fact: { label: string; value: string }) => (
990
+ <div className="flex items-start justify-between gap-5" key={fact.label}>
991
+ <dt className="text-muted-foreground">{fact.label}</dt>
992
+ <dd className="text-right">{fact.value}</dd>
993
+ </div>
994
+ ))}
995
+ </dl>
996
+ </div>
997
+ );
998
+ }
999
+ if (enterpriseUi?.kind === 'table') {
1000
+ return (
1001
+ <div
1002
+ key={index}
1003
+ className="overflow-x-auto rounded-xl border border-border bg-card p-3 text-sm"
1004
+ >
1005
+ <div className="mb-3 font-medium">{enterpriseUi.title}</div>
1006
+ <table className="w-full text-left">
1007
+ <thead className="text-muted-foreground">
1008
+ <tr>
1009
+ {(enterpriseUi.columns ?? []).map((column: string) => (
1010
+ <th className="px-2 py-1" key={column}>
1011
+ {column}
1012
+ </th>
1013
+ ))}
1014
+ </tr>
1015
+ </thead>
1016
+ <tbody>
1017
+ {(enterpriseUi.rows ?? []).map((row: Record<string, string>, rowIndex: number) => (
1018
+ <tr className="border-t border-border/60" key={rowIndex}>
1019
+ {(enterpriseUi.columns ?? []).map((column: string) => (
1020
+ <td className="px-2 py-2" key={column}>
1021
+ {row[column]}
1022
+ </td>
1023
+ ))}
1024
+ </tr>
1025
+ ))}
1026
+ </tbody>
1027
+ </table>
1028
+ </div>
1029
+ );
1030
+ }
1031
+ if (enterpriseUi?.kind === 'citations') {
1032
+ return (
1033
+ <ChatCitations
1034
+ key={index}
1035
+ ui={enterpriseUi}
1036
+ assetId={output?.mediaAssetId}
1037
+ autoOpenSupportingClip={autoOpenSupportingClip}
1038
+ />
1039
+ );
1040
+ }
1041
+ switch (behavior.resultView) {
1042
+ case 'data-table': {
1043
+ if (output.error) return null;
1044
+ const tableConfig: DataTableConfig = {
1045
+ columns: output.columns ?? [],
1046
+ rows: output.rows ?? [],
1047
+ totalRows: output.totalRows ?? 0,
1048
+ aggregationResults: output.aggregationResults,
1049
+ };
1050
+ if (tableConfig.rows.length === 0 && !tableConfig.aggregationResults) return null;
1051
+ const visualization = output.visualization as ChartConfig | undefined;
1052
+ return (
1053
+ <div key={index}>
1054
+ <ChatDataTable config={tableConfig} compact={behavior.compactResult} />
1055
+ {visualization?.data?.length ? <ChatChart config={visualization} /> : null}
1056
+ </div>
1057
+ );
1058
+ }
1059
+
1060
+ case 'file-export': {
1061
+ if (!output?.success || !output?.fileBase64) return null;
1062
+ return <ChatFileExport key={index} artifact={output} />;
1063
+ }
1064
+
1065
+ case 'none': {
1066
+ if (behavior.placement !== 'visualization') return null;
1067
+ const chartConfig = output as ChartConfig;
1068
+ if (!chartConfig) return null;
1069
+ return <ChatChart key={index} config={chartConfig} />;
1070
+ }
1071
+
1072
+ case 'sandbox': {
1073
+ const result = output as SandboxResultConfig;
1074
+ return <ChatSandboxResult key={index} config={result} />;
1075
+ }
1076
+
1077
+ case 'corpus': {
1078
+ const corpusConfig = output as CorpusDataConfig;
1079
+ if (!corpusConfig) return null;
1080
+ return <CorpusDataResult key={index} config={corpusConfig} />;
1081
+ }
1082
+
1083
+ case 'document-edit': {
1084
+ if (!output.success) return null;
1085
+
1086
+ const fileName = output.fileName || 'Document';
1087
+ const cType = output.mimeType || '';
1088
+ const ext = fileName.split('.').pop()?.toLowerCase() || '';
1089
+ let iconPath = '/icons/image.png'; // default fallback for images and unknown files
1090
+ if (
1091
+ ['csv', 'xls', 'xlsx'].includes(ext) ||
1092
+ cType.includes('excel') ||
1093
+ cType.includes('spreadsheet')
1094
+ )
1095
+ iconPath = '/icons/excel.png';
1096
+ else if (['doc', 'docx'].includes(ext) || cType.includes('word'))
1097
+ iconPath = '/icons/word.png';
1098
+ else if (['md', 'markdown'].includes(ext)) iconPath = '/icons/markdown.png';
1099
+ else if (['pdf'].includes(ext) || cType === 'application/pdf') iconPath = '/icons/pdf.png';
1100
+ else if (!ext) iconPath = '/icons/word.png';
1101
+
1102
+ return (
1103
+ <div
1104
+ key={index}
1105
+ onClick={() => {
1106
+ if (updateFromToolResult && output.fileBase64) {
1107
+ updateFromToolResult(output);
1108
+ }
1109
+ }}
1110
+ className="group flex items-center gap-3 rounded-xl border border-border/60 bg-emerald-50/50 dark:bg-emerald-900/10 px-4 py-3 text-sm text-emerald-700 dark:text-emerald-400 cursor-pointer hover:bg-emerald-100/50 dark:hover:bg-emerald-900/20 transition"
1111
+ >
1112
+ <div className="size-8 shrink-0 rounded-md overflow-hidden bg-white border border-emerald-200/50 flex items-center justify-center p-1.5 35">
1113
+ <img src={iconPath} alt={fileName} className="w-full h-full object-contain" />
1114
+ </div>
1115
+ <div className="flex flex-col flex-1">
1116
+ <span className="font-medium text-emerald-800 dark:text-emerald-300">
1117
+ {toolName === 'fillDocumentForm' ? 'Form fields updated in ' : 'Edited '}
1118
+ <span className="font-bold">{fileName}</span>
1119
+ </span>
1120
+ <span className="text-xs opacity-80 mt-0.5">
1121
+ {output.updatedFields?.length || 0}{' '}
1122
+ {output.updatedFields?.length === 1 ? 'change' : 'changes'} applied. Preview updated
1123
+ in Canvas.
1124
+ </span>
1125
+ </div>
1126
+ {output.fileBase64 && (
1127
+ <div className="opacity-0 group-hover:opacity-100 text-xs font-medium text-emerald-600 dark:text-emerald-400 flex items-center gap-1 transition">
1128
+ <FileEdit className="size-3" />
1129
+ View this version
1130
+ </div>
1131
+ )}
1132
+ </div>
1133
+ );
1134
+ }
1135
+
1136
+ case 'signature': {
1137
+ if (!output?.success) return null;
1138
+ return (
1139
+ <div
1140
+ key={index}
1141
+ onClick={() => {
1142
+ if (updateFromToolResult && output.fileBase64) {
1143
+ updateFromToolResult(output);
1144
+ }
1145
+ }}
1146
+ className="group flex items-center gap-3 rounded-xl border border-border/60 bg-emerald-50/50 dark:bg-emerald-900/10 px-4 py-3 text-sm text-emerald-700 dark:text-emerald-400 mt-2 cursor-pointer hover:bg-emerald-100/50 dark:hover:bg-emerald-900/20 transition"
1147
+ >
1148
+ <CheckCircle2 className="size-5 shrink-0 text-emerald-500" />
1149
+ <div className="flex-1 flex items-center justify-between">
1150
+ <span className="font-medium text-emerald-800 dark:text-emerald-300">
1151
+ Document signed successfully.
1152
+ </span>
1153
+ {output.fileBase64 && (
1154
+ <div className="opacity-0 group-hover:opacity-100 text-xs font-medium text-emerald-600 dark:text-emerald-400 flex items-center gap-1 transition">
1155
+ <FileEdit className="size-3" />
1156
+ View this version
1157
+ </div>
1158
+ )}
1159
+ </div>
1160
+ </div>
1161
+ );
1162
+ }
1163
+
1164
+ case 'image-analysis': {
1165
+ if (output.error) return null;
1166
+ return (
1167
+ <div key={index} className="mb-2 w-full">
1168
+ <div className="inline-flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground bg-muted/30 rounded-md border border-border/50">
1169
+ <Sparkles className="h-3.5 w-3.5 shrink-0" />
1170
+ <span className="truncate max-w-62.5 sm:max-w-100">Analyzed indexed image</span>
1171
+ </div>
1172
+ </div>
1173
+ );
1174
+ }
1175
+
1176
+ case 'media': {
1177
+ if (
1178
+ !output?.success ||
1179
+ !output.assetId ||
1180
+ !['image', 'video', 'audio', 'frame-preview'].includes(output.mediaType)
1181
+ ) {
1182
+ return null;
1183
+ }
1184
+ return (
1185
+ <ChatMediaPreview
1186
+ key={index}
1187
+ assetId={output.assetId}
1188
+ mediaType={output.mediaType}
1189
+ fileName={output.fileName}
1190
+ mediaUrl={output.mediaUrl}
1191
+ sourceUrl={output.sourceUrl}
1192
+ startSecs={output.startSecs}
1193
+ endSecs={output.endSecs}
1194
+ />
1195
+ );
1196
+ }
1197
+
1198
+ default:
1199
+ return null;
1200
+ // return (
1201
+ // <div key={index} className="mb-2 w-full">
1202
+ // <div className="inline-flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground bg-muted/30 rounded-md border border-border/50">
1203
+ // <CheckCircle2 className="h-3.5 w-3.5 shrink-0" />
1204
+ // <span className="truncate max-w-62.5 sm:max-w-100">Used {toolName}</span>
1205
+ // </div>
1206
+ // </div>
1207
+ // );
1208
+ }
1209
+ }
1210
+
1211
+ return null;
1212
+ }
1213
+
1214
+ function WebSearchSummary({ parts }: { parts: any[] }) {
1215
+ const [open, setOpen] = useState(false);
1216
+ const searches = parts.map((part) => {
1217
+ const info = getToolInfo(part);
1218
+ const results = Array.isArray(info.output?.results) ? info.output.results : [];
1219
+ return { query: info.input?.query as string | undefined, results, running: info.isExecuting };
1220
+ });
1221
+ const resultCount = searches.reduce((count, search) => count + search.results.length, 0);
1222
+ const running = searches.some((search) => search.running);
1223
+
1224
+ return (
1225
+ <div className="mb-2 w-full">
1226
+ <button
1227
+ type="button"
1228
+ onClick={() => (resultCount > 0 || searches.length > 1) && setOpen((value) => !value)}
1229
+ className="inline-flex items-center gap-2 rounded-md border border-border/50 bg-muted/30 px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted/50"
1230
+ >
1231
+ <Globe className="size-3.5 shrink-0" />
1232
+ <span>{running ? 'Searching the web…' : 'Searched the web'}</span>
1233
+ {!running && resultCount > 0 ? (
1234
+ <span className="rounded-full bg-secondary px-1.5 py-0.5 text-[10px] font-medium text-foreground">
1235
+ {resultCount} result{resultCount === 1 ? '' : 's'}
1236
+ </span>
1237
+ ) : null}
1238
+ {!running && (resultCount > 0 || searches.length > 1) ? (
1239
+ <ChevronDown className={`size-3 transition-transform ${open ? 'rotate-180' : ''}`} />
1240
+ ) : null}
1241
+ </button>
1242
+
1243
+ {open ? (
1244
+ <div className="mt-2 max-w-xl overflow-hidden rounded-lg border border-border/60 bg-background text-xs">
1245
+ {searches
1246
+ .flatMap((search) => search.results)
1247
+ .slice(0, 5)
1248
+ .map((result: any, index) => (
1249
+ <a
1250
+ key={`${result.url ?? result.title ?? index}-${index}`}
1251
+ href={result.url}
1252
+ target="_blank"
1253
+ rel="noreferrer"
1254
+ className="block border-b border-border/50 px-3 py-2.5 last:border-0 hover:bg-muted/40"
1255
+ >
1256
+ <span className="block truncate font-medium text-foreground">
1257
+ {result.title || result.url}
1258
+ </span>
1259
+ {result.snippet ? (
1260
+ <span className="mt-0.5 block line-clamp-2 text-muted-foreground">
1261
+ {result.snippet}
1262
+ </span>
1263
+ ) : null}
1264
+ </a>
1265
+ ))}
1266
+ </div>
1267
+ ) : null}
1268
+ </div>
1269
+ );
1270
+ }
1271
+
1272
+ function normalizeRepeatedOrderedListMarkers(text: string): string {
1273
+ const lines = text.split('\n');
1274
+ let previousRepeatedMarker: number | null = null;
1275
+ let nextNumber = 2;
1276
+
1277
+ for (let index = 0; index < lines.length; index += 1) {
1278
+ const marker = lines[index].match(/^(\s*)1\.\s+(.+)$/);
1279
+ if (!marker) {
1280
+ if (/^\s*\d+\.\s+/.test(lines[index])) {
1281
+ previousRepeatedMarker = null;
1282
+ nextNumber = 2;
1283
+ }
1284
+ continue;
1285
+ }
1286
+
1287
+ const onlySupportingListContentSincePrevious =
1288
+ previousRepeatedMarker !== null &&
1289
+ lines
1290
+ .slice(previousRepeatedMarker + 1, index)
1291
+ .every((line) => !line.trim() || /^\s*[-*+]\s+/.test(line));
1292
+
1293
+ if (onlySupportingListContentSincePrevious) {
1294
+ lines[index] = `${marker[1]}${nextNumber}. ${marker[2]}`;
1295
+ nextNumber += 1;
1296
+ } else {
1297
+ nextNumber = 2;
1298
+ }
1299
+
1300
+ previousRepeatedMarker = index;
1301
+ }
1302
+
1303
+ return lines.join('\n');
1304
+ }
1305
+
1306
+ export function renderMarkdown(text: string): string {
1307
+ if (!text) return '';
1308
+
1309
+ let html = normalizeRepeatedOrderedListMarkers(text)
1310
+ .replace(/&/g, '&amp;')
1311
+ .replace(/</g, '&lt;')
1312
+ .replace(/>/g, '&gt;');
1313
+
1314
+ // Math blocks \[ ... \] and $$ ... $$
1315
+ html = html.replace(/\\\[([\s\S]+?)\\\]|\$\$([\s\S]+?)\$\$/g, (match, p1, p2) => {
1316
+ const math = p1 || p2;
1317
+ try {
1318
+ return katex.renderToString(math, { displayMode: true, throwOnError: false });
1319
+ } catch {
1320
+ return match;
1321
+ }
1322
+ });
1323
+
1324
+ // Inline math \( ... \) and $ ... $
1325
+ html = html.replace(/\\\(([\s\S]+?)\\\)|\$([^$\n]+?)\$/g, (match, p1, p2) => {
1326
+ const math = p1 || p2;
1327
+ try {
1328
+ return katex.renderToString(math, { displayMode: false, throwOnError: false });
1329
+ } catch {
1330
+ return match;
1331
+ }
1332
+ });
1333
+
1334
+ html = html.replace(
1335
+ /!\[([^\]]*)\]\([^)]*\)/g,
1336
+ (_match, alt) =>
1337
+ `<span class="text-muted-foreground italic">[Image unavailable${
1338
+ alt ? `: ${alt}` : ''
1339
+ }]</span>`,
1340
+ );
1341
+
1342
+ // Code blocks — styled premium
1343
+ html = html.replace(
1344
+ /```(\w*)\n([\s\S]*?)```/g,
1345
+ '<pre class="msg-code-block"><code>$2</code></pre>',
1346
+ );
1347
+
1348
+ // Inline code
1349
+ html = html.replace(/`([^`]+)`/g, '<code class="msg-inline-code">$1</code>');
1350
+
1351
+ // Bold
1352
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong class="msg-bold">$1</strong>');
1353
+
1354
+ // Italic
1355
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
1356
+
1357
+ // Links and Media (Video/Audio)
1358
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, label, url) => {
1359
+ const lowerUrl = url.toLowerCase();
1360
+ if (lowerUrl.match(/\.(mp4|webm|ogg)$/) || lowerUrl.includes('video')) {
1361
+ return `<video src="${url}" controls preload="metadata" class="max-w-90 w-full rounded-lg my-2 border border-border bg-card"></video>`;
1362
+ }
1363
+ if (lowerUrl.match(/\.(mp3|wav|m4a|oga)$/) || lowerUrl.includes('audio')) {
1364
+ return `<audio src="${url}" controls preload="metadata" class="max-w-80 w-full my-2"></audio>`;
1365
+ }
1366
+ return `<a href="${url}" target="_blank" rel="noreferrer" class="msg-link">${label}</a>`;
1367
+ });
1368
+
1369
+ html = html.replace(/^### (.+)$/gm, '<h3 class="msg-h3">$1</h3>');
1370
+ html = html.replace(/^## (.+)$/gm, '<h2 class="msg-h2">$1</h2>');
1371
+ html = html.replace(/^# (.+)$/gm, '<h1 class="msg-h1">$1</h1>');
1372
+
1373
+ // Unordered lists — collect consecutive lines into <ul>
1374
+ html = html.replace(/^- (.+)$/gm, '<li class="msg-li">$1</li>');
1375
+ // Wrap consecutive <li> in <ul> (including when separated by blank lines)
1376
+ html = html.replace(
1377
+ /(<li class="msg-li">.*?<\/li>\n?)+/g,
1378
+ (match) => `<ul class="msg-ul">${match}</ul>`,
1379
+ );
1380
+
1381
+ html = html.replace(/(^\d+\. .+$)(\n\n)((?=\d+\. ))/gm, '$1\n$3');
1382
+
1383
+ html = html.replace(/^(\d+)\. (.+)$/gm, '<li class="msg-li-ordered" data-order="$1">$2</li>');
1384
+ html = html.replace(/(<li class="msg-li-ordered" data-order="\d+">.*?<\/li>\n?)+/g, (match) => {
1385
+ const start = match.match(/data-order="(\d+)"/)?.[1] ?? '1';
1386
+ return `<ol class="msg-ol" start="${start}">${match.replace(/ data-order="\d+"/g, '')}</ol>`;
1387
+ });
1388
+
1389
+ // Paragraphs
1390
+ html = html.replace(/\n\n/g, '</p><p class="msg-p">');
1391
+
1392
+ html = html.replace(/(<?\/?(?:ul|ol|li|h1|h2|h3|pre)[^>]*>)\n+/g, '$1');
1393
+ html = html.replace(/\n+(<\/?(?:ul|ol|li|h1|h2|h3|pre)[^>]*>)/g, '$1');
1394
+
1395
+ html = html.replace(/\n/g, '<br/>');
1396
+
1397
+ return html;
1398
+ }