larkup 0.4.12 → 0.4.14

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 (959) hide show
  1. package/.next/standalone/apps/web/.next/BUILD_ID +1 -0
  2. package/.next/standalone/apps/web/.next/app-path-routes-manifest.json +94 -0
  3. package/.next/standalone/apps/web/.next/build-manifest.json +20 -0
  4. package/.next/standalone/apps/web/.next/diagnostics/build-diagnostics.json +6 -0
  5. package/.next/standalone/apps/web/.next/diagnostics/framework.json +1 -0
  6. package/.next/standalone/apps/web/.next/lock +0 -0
  7. package/.next/standalone/apps/web/.next/package.json +1 -0
  8. package/.next/standalone/apps/web/.next/prerender-manifest.json +241 -0
  9. package/.next/standalone/apps/web/.next/react-loadable-manifest.json +31 -0
  10. package/.next/standalone/apps/web/.next/required-server-files.json +399 -0
  11. package/.next/standalone/apps/web/.next/routes-manifest.json +644 -0
  12. package/.next/standalone/apps/web/.next/server/app/_global-error/page.js +31 -0
  13. package/.next/standalone/apps/web/.next/server/app/_global-error/page.js.nft.json +1 -0
  14. package/.next/standalone/apps/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -0
  15. package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -0
  16. package/.next/standalone/apps/web/.next/server/app/_global-error.meta +35 -0
  17. package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +15 -0
  18. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +15 -0
  19. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +21 -0
  20. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -0
  21. package/.next/standalone/apps/web/.next/server/app/_not-found/page.js +1 -0
  22. package/.next/standalone/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -0
  23. package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  24. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -0
  25. package/.next/standalone/apps/web/.next/server/app/_not-found.meta +35 -0
  26. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +26 -0
  27. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +26 -0
  28. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +32 -0
  29. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -0
  30. package/.next/standalone/apps/web/.next/server/app/add/page.js +1 -0
  31. package/.next/standalone/apps/web/.next/server/app/add/page.js.nft.json +1 -0
  32. package/.next/standalone/apps/web/.next/server/app/add/page_client-reference-manifest.js +1 -0
  33. package/.next/standalone/apps/web/.next/server/app/add.html +1 -0
  34. package/.next/standalone/apps/web/.next/server/app/add.meta +34 -0
  35. package/.next/standalone/apps/web/.next/server/app/add.rsc +27 -0
  36. package/.next/standalone/apps/web/.next/server/app/add.segments/_full.segment.rsc +27 -0
  37. package/.next/standalone/apps/web/.next/server/app/add.segments/_tree.segment.rsc +2 -0
  38. package/.next/standalone/apps/web/.next/server/app/add.segments/add/__PAGE__.segment.rsc +33 -0
  39. package/.next/standalone/apps/web/.next/server/app/api/analytics/route.js +1 -0
  40. package/.next/standalone/apps/web/.next/server/app/api/analytics/route.js.nft.json +1 -0
  41. package/.next/standalone/apps/web/.next/server/app/api/analytics/route_client-reference-manifest.js +1 -0
  42. package/.next/standalone/apps/web/.next/server/app/api/chat/feedback/route.js +1 -0
  43. package/.next/standalone/apps/web/.next/server/app/api/chat/feedback/route.js.nft.json +1 -0
  44. package/.next/standalone/apps/web/.next/server/app/api/chat/feedback/route_client-reference-manifest.js +1 -0
  45. package/.next/standalone/apps/web/.next/server/app/api/chat/route.js +111 -0
  46. package/.next/standalone/apps/web/.next/server/app/api/chat/route.js.nft.json +1 -0
  47. package/.next/standalone/apps/web/.next/server/app/api/chat/route_client-reference-manifest.js +1 -0
  48. package/.next/standalone/apps/web/.next/server/app/api/chat/status/route.js +1 -0
  49. package/.next/standalone/apps/web/.next/server/app/api/chat/status/route.js.nft.json +1 -0
  50. package/.next/standalone/apps/web/.next/server/app/api/chat/status/route_client-reference-manifest.js +1 -0
  51. package/.next/standalone/apps/web/.next/server/app/api/config/route.js +1 -0
  52. package/.next/standalone/apps/web/.next/server/app/api/config/route.js.nft.json +1 -0
  53. package/.next/standalone/apps/web/.next/server/app/api/config/route_client-reference-manifest.js +1 -0
  54. package/.next/standalone/apps/web/.next/server/app/api/config/test/route.js +1 -0
  55. package/.next/standalone/apps/web/.next/server/app/api/config/test/route.js.nft.json +1 -0
  56. package/.next/standalone/apps/web/.next/server/app/api/config/test/route_client-reference-manifest.js +1 -0
  57. package/.next/standalone/apps/web/.next/server/app/api/config/test-embedding/route.js +10 -0
  58. package/.next/standalone/apps/web/.next/server/app/api/config/test-embedding/route.js.nft.json +1 -0
  59. package/.next/standalone/apps/web/.next/server/app/api/config/test-embedding/route_client-reference-manifest.js +1 -0
  60. package/.next/standalone/apps/web/.next/server/app/api/config/test-llm/route.js +10 -0
  61. package/.next/standalone/apps/web/.next/server/app/api/config/test-llm/route.js.nft.json +1 -0
  62. package/.next/standalone/apps/web/.next/server/app/api/config/test-llm/route_client-reference-manifest.js +1 -0
  63. package/.next/standalone/apps/web/.next/server/app/api/config/test-provider/route.js +1 -0
  64. package/.next/standalone/apps/web/.next/server/app/api/config/test-provider/route.js.nft.json +1 -0
  65. package/.next/standalone/apps/web/.next/server/app/api/config/test-provider/route_client-reference-manifest.js +1 -0
  66. package/.next/standalone/apps/web/.next/server/app/api/config/verify/route.js +1 -0
  67. package/.next/standalone/apps/web/.next/server/app/api/config/verify/route.js.nft.json +1 -0
  68. package/.next/standalone/apps/web/.next/server/app/api/config/verify/route_client-reference-manifest.js +1 -0
  69. package/.next/standalone/apps/web/.next/server/app/api/connections/[channelId]/oauth/callback/route.js +1 -0
  70. package/.next/standalone/apps/web/.next/server/app/api/connections/[channelId]/oauth/callback/route.js.nft.json +1 -0
  71. package/.next/standalone/apps/web/.next/server/app/api/connections/[channelId]/oauth/callback/route_client-reference-manifest.js +1 -0
  72. package/.next/standalone/apps/web/.next/server/app/api/connections/[channelId]/oauth/start/route.js +1 -0
  73. package/.next/standalone/apps/web/.next/server/app/api/connections/[channelId]/oauth/start/route.js.nft.json +1 -0
  74. package/.next/standalone/apps/web/.next/server/app/api/connections/[channelId]/oauth/start/route_client-reference-manifest.js +1 -0
  75. package/.next/standalone/apps/web/.next/server/app/api/connections/[channelId]/route.js +1 -0
  76. package/.next/standalone/apps/web/.next/server/app/api/connections/[channelId]/route.js.nft.json +1 -0
  77. package/.next/standalone/apps/web/.next/server/app/api/connections/[channelId]/route_client-reference-manifest.js +1 -0
  78. package/.next/standalone/apps/web/.next/server/app/api/connections/route.js +1 -0
  79. package/.next/standalone/apps/web/.next/server/app/api/connections/route.js.nft.json +1 -0
  80. package/.next/standalone/apps/web/.next/server/app/api/connections/route_client-reference-manifest.js +1 -0
  81. package/.next/standalone/apps/web/.next/server/app/api/connections/tunnel/route.js +1 -0
  82. package/.next/standalone/apps/web/.next/server/app/api/connections/tunnel/route.js.nft.json +1 -0
  83. package/.next/standalone/apps/web/.next/server/app/api/connections/tunnel/route_client-reference-manifest.js +1 -0
  84. package/.next/standalone/apps/web/.next/server/app/api/demo/route.js +1 -0
  85. package/.next/standalone/apps/web/.next/server/app/api/demo/route.js.nft.json +1 -0
  86. package/.next/standalone/apps/web/.next/server/app/api/demo/route_client-reference-manifest.js +1 -0
  87. package/.next/standalone/apps/web/.next/server/app/api/deploy/ssh/route.js +5 -0
  88. package/.next/standalone/apps/web/.next/server/app/api/deploy/ssh/route.js.nft.json +1 -0
  89. package/.next/standalone/apps/web/.next/server/app/api/deploy/ssh/route_client-reference-manifest.js +1 -0
  90. package/.next/standalone/apps/web/.next/server/app/api/deploy/verify/route.js +1 -0
  91. package/.next/standalone/apps/web/.next/server/app/api/deploy/verify/route.js.nft.json +1 -0
  92. package/.next/standalone/apps/web/.next/server/app/api/deploy/verify/route_client-reference-manifest.js +1 -0
  93. package/.next/standalone/apps/web/.next/server/app/api/describe-image/route.js +1 -0
  94. package/.next/standalone/apps/web/.next/server/app/api/describe-image/route.js.nft.json +1 -0
  95. package/.next/standalone/apps/web/.next/server/app/api/describe-image/route_client-reference-manifest.js +1 -0
  96. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/edit/route.js +1 -0
  97. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/edit/route.js.nft.json +1 -0
  98. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/edit/route_client-reference-manifest.js +1 -0
  99. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/export/route.js +1 -0
  100. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/export/route.js.nft.json +1 -0
  101. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/export/route_client-reference-manifest.js +1 -0
  102. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/extract-text/route.js +1 -0
  103. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/extract-text/route.js.nft.json +1 -0
  104. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/extract-text/route_client-reference-manifest.js +1 -0
  105. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/parse/route.js +1 -0
  106. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/parse/route.js.nft.json +1 -0
  107. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/parse/route_client-reference-manifest.js +1 -0
  108. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/restore/route.js +1 -0
  109. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/restore/route.js.nft.json +1 -0
  110. package/.next/standalone/apps/web/.next/server/app/api/doc-editor/restore/route_client-reference-manifest.js +1 -0
  111. package/.next/standalone/apps/web/.next/server/app/api/document/sign/route.js +1 -0
  112. package/.next/standalone/apps/web/.next/server/app/api/document/sign/route.js.nft.json +1 -0
  113. package/.next/standalone/apps/web/.next/server/app/api/document/sign/route_client-reference-manifest.js +1 -0
  114. package/.next/standalone/apps/web/.next/server/app/api/documents/move/route.js +1 -0
  115. package/.next/standalone/apps/web/.next/server/app/api/documents/move/route.js.nft.json +1 -0
  116. package/.next/standalone/apps/web/.next/server/app/api/documents/move/route_client-reference-manifest.js +1 -0
  117. package/.next/standalone/apps/web/.next/server/app/api/documents/route.js +1 -0
  118. package/.next/standalone/apps/web/.next/server/app/api/documents/route.js.nft.json +1 -0
  119. package/.next/standalone/apps/web/.next/server/app/api/documents/route_client-reference-manifest.js +1 -0
  120. package/.next/standalone/apps/web/.next/server/app/api/files/remote/route.js +1 -0
  121. package/.next/standalone/apps/web/.next/server/app/api/files/remote/route.js.nft.json +1 -0
  122. package/.next/standalone/apps/web/.next/server/app/api/files/remote/route_client-reference-manifest.js +1 -0
  123. package/.next/standalone/apps/web/.next/server/app/api/firecrawl/local/route.js +1 -0
  124. package/.next/standalone/apps/web/.next/server/app/api/firecrawl/local/route.js.nft.json +1 -0
  125. package/.next/standalone/apps/web/.next/server/app/api/firecrawl/local/route_client-reference-manifest.js +1 -0
  126. package/.next/standalone/apps/web/.next/server/app/api/gpu-activity/route.js +1 -0
  127. package/.next/standalone/apps/web/.next/server/app/api/gpu-activity/route.js.nft.json +1 -0
  128. package/.next/standalone/apps/web/.next/server/app/api/gpu-activity/route_client-reference-manifest.js +1 -0
  129. package/.next/standalone/apps/web/.next/server/app/api/groups/route.js +1 -0
  130. package/.next/standalone/apps/web/.next/server/app/api/groups/route.js.nft.json +1 -0
  131. package/.next/standalone/apps/web/.next/server/app/api/groups/route_client-reference-manifest.js +1 -0
  132. package/.next/standalone/apps/web/.next/server/app/api/index/route.js +1 -0
  133. package/.next/standalone/apps/web/.next/server/app/api/index/route.js.nft.json +1 -0
  134. package/.next/standalone/apps/web/.next/server/app/api/index/route_client-reference-manifest.js +1 -0
  135. package/.next/standalone/apps/web/.next/server/app/api/integrations/[integration]/callback/route.js +4 -0
  136. package/.next/standalone/apps/web/.next/server/app/api/integrations/[integration]/callback/route.js.nft.json +1 -0
  137. package/.next/standalone/apps/web/.next/server/app/api/integrations/[integration]/callback/route_client-reference-manifest.js +1 -0
  138. package/.next/standalone/apps/web/.next/server/app/api/integrations/[integration]/route.js +1 -0
  139. package/.next/standalone/apps/web/.next/server/app/api/integrations/[integration]/route.js.nft.json +1 -0
  140. package/.next/standalone/apps/web/.next/server/app/api/integrations/[integration]/route_client-reference-manifest.js +1 -0
  141. package/.next/standalone/apps/web/.next/server/app/api/integrations/notion/callback/route.js +19 -0
  142. package/.next/standalone/apps/web/.next/server/app/api/integrations/notion/callback/route.js.nft.json +1 -0
  143. package/.next/standalone/apps/web/.next/server/app/api/integrations/notion/callback/route_client-reference-manifest.js +1 -0
  144. package/.next/standalone/apps/web/.next/server/app/api/integrations/notion/route.js +11 -0
  145. package/.next/standalone/apps/web/.next/server/app/api/integrations/notion/route.js.nft.json +1 -0
  146. package/.next/standalone/apps/web/.next/server/app/api/integrations/notion/route_client-reference-manifest.js +1 -0
  147. package/.next/standalone/apps/web/.next/server/app/api/integrations/route.js +1 -0
  148. package/.next/standalone/apps/web/.next/server/app/api/integrations/route.js.nft.json +1 -0
  149. package/.next/standalone/apps/web/.next/server/app/api/integrations/route_client-reference-manifest.js +1 -0
  150. package/.next/standalone/apps/web/.next/server/app/api/jobs/[id]/route.js +1 -0
  151. package/.next/standalone/apps/web/.next/server/app/api/jobs/[id]/route.js.nft.json +1 -0
  152. package/.next/standalone/apps/web/.next/server/app/api/jobs/[id]/route_client-reference-manifest.js +1 -0
  153. package/.next/standalone/apps/web/.next/server/app/api/jobs/route.js +1 -0
  154. package/.next/standalone/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -0
  155. package/.next/standalone/apps/web/.next/server/app/api/jobs/route_client-reference-manifest.js +1 -0
  156. package/.next/standalone/apps/web/.next/server/app/api/marketplace/[toolId]/route.js +1 -0
  157. package/.next/standalone/apps/web/.next/server/app/api/marketplace/[toolId]/route.js.nft.json +1 -0
  158. package/.next/standalone/apps/web/.next/server/app/api/marketplace/[toolId]/route_client-reference-manifest.js +1 -0
  159. package/.next/standalone/apps/web/.next/server/app/api/marketplace/route.js +1 -0
  160. package/.next/standalone/apps/web/.next/server/app/api/marketplace/route.js.nft.json +1 -0
  161. package/.next/standalone/apps/web/.next/server/app/api/marketplace/route_client-reference-manifest.js +1 -0
  162. package/.next/standalone/apps/web/.next/server/app/api/mcp/[connectionId]/route.js +1 -0
  163. package/.next/standalone/apps/web/.next/server/app/api/mcp/[connectionId]/route.js.nft.json +1 -0
  164. package/.next/standalone/apps/web/.next/server/app/api/mcp/[connectionId]/route_client-reference-manifest.js +1 -0
  165. package/.next/standalone/apps/web/.next/server/app/api/mcp/[connectionId]/test/route.js +1 -0
  166. package/.next/standalone/apps/web/.next/server/app/api/mcp/[connectionId]/test/route.js.nft.json +1 -0
  167. package/.next/standalone/apps/web/.next/server/app/api/mcp/[connectionId]/test/route_client-reference-manifest.js +1 -0
  168. package/.next/standalone/apps/web/.next/server/app/api/mcp/route.js +1 -0
  169. package/.next/standalone/apps/web/.next/server/app/api/mcp/route.js.nft.json +1 -0
  170. package/.next/standalone/apps/web/.next/server/app/api/mcp/route_client-reference-manifest.js +1 -0
  171. package/.next/standalone/apps/web/.next/server/app/api/media/[id]/frame/route.js +1 -0
  172. package/.next/standalone/apps/web/.next/server/app/api/media/[id]/frame/route.js.nft.json +1 -0
  173. package/.next/standalone/apps/web/.next/server/app/api/media/[id]/frame/route_client-reference-manifest.js +1 -0
  174. package/.next/standalone/apps/web/.next/server/app/api/media/[id]/route.js +1 -0
  175. package/.next/standalone/apps/web/.next/server/app/api/media/[id]/route.js.nft.json +1 -0
  176. package/.next/standalone/apps/web/.next/server/app/api/media/[id]/route_client-reference-manifest.js +1 -0
  177. package/.next/standalone/apps/web/.next/server/app/api/media/inspect/route.js +1 -0
  178. package/.next/standalone/apps/web/.next/server/app/api/media/inspect/route.js.nft.json +1 -0
  179. package/.next/standalone/apps/web/.next/server/app/api/media/inspect/route_client-reference-manifest.js +1 -0
  180. package/.next/standalone/apps/web/.next/server/app/api/media/jobs/[id]/approval/route.js +1 -0
  181. package/.next/standalone/apps/web/.next/server/app/api/media/jobs/[id]/approval/route.js.nft.json +1 -0
  182. package/.next/standalone/apps/web/.next/server/app/api/media/jobs/[id]/approval/route_client-reference-manifest.js +1 -0
  183. package/.next/standalone/apps/web/.next/server/app/api/media/jobs/[id]/route.js +1 -0
  184. package/.next/standalone/apps/web/.next/server/app/api/media/jobs/[id]/route.js.nft.json +1 -0
  185. package/.next/standalone/apps/web/.next/server/app/api/media/jobs/[id]/route_client-reference-manifest.js +1 -0
  186. package/.next/standalone/apps/web/.next/server/app/api/media/process/route.js +2 -0
  187. package/.next/standalone/apps/web/.next/server/app/api/media/process/route.js.nft.json +1 -0
  188. package/.next/standalone/apps/web/.next/server/app/api/media/process/route_client-reference-manifest.js +1 -0
  189. package/.next/standalone/apps/web/.next/server/app/api/media/route.js +1 -0
  190. package/.next/standalone/apps/web/.next/server/app/api/media/route.js.nft.json +1 -0
  191. package/.next/standalone/apps/web/.next/server/app/api/media/route_client-reference-manifest.js +1 -0
  192. package/.next/standalone/apps/web/.next/server/app/api/media/stream/route.js +5 -0
  193. package/.next/standalone/apps/web/.next/server/app/api/media/stream/route.js.nft.json +1 -0
  194. package/.next/standalone/apps/web/.next/server/app/api/media/stream/route_client-reference-manifest.js +1 -0
  195. package/.next/standalone/apps/web/.next/server/app/api/models/route.js +1 -0
  196. package/.next/standalone/apps/web/.next/server/app/api/models/route.js.nft.json +1 -0
  197. package/.next/standalone/apps/web/.next/server/app/api/models/route_client-reference-manifest.js +1 -0
  198. package/.next/standalone/apps/web/.next/server/app/api/parse-file/route.js +1 -0
  199. package/.next/standalone/apps/web/.next/server/app/api/parse-file/route.js.nft.json +1 -0
  200. package/.next/standalone/apps/web/.next/server/app/api/parse-file/route_client-reference-manifest.js +1 -0
  201. package/.next/standalone/apps/web/.next/server/app/api/pdf-page/route.js +1 -0
  202. package/.next/standalone/apps/web/.next/server/app/api/pdf-page/route.js.nft.json +1 -0
  203. package/.next/standalone/apps/web/.next/server/app/api/pdf-page/route_client-reference-manifest.js +1 -0
  204. package/.next/standalone/apps/web/.next/server/app/api/projects/deployments/route.js +1 -0
  205. package/.next/standalone/apps/web/.next/server/app/api/projects/deployments/route.js.nft.json +1 -0
  206. package/.next/standalone/apps/web/.next/server/app/api/projects/deployments/route_client-reference-manifest.js +1 -0
  207. package/.next/standalone/apps/web/.next/server/app/api/projects/route.js +1 -0
  208. package/.next/standalone/apps/web/.next/server/app/api/projects/route.js.nft.json +1 -0
  209. package/.next/standalone/apps/web/.next/server/app/api/projects/route_client-reference-manifest.js +1 -0
  210. package/.next/standalone/apps/web/.next/server/app/api/projects/runtime/route.js +1 -0
  211. package/.next/standalone/apps/web/.next/server/app/api/projects/runtime/route.js.nft.json +1 -0
  212. package/.next/standalone/apps/web/.next/server/app/api/projects/runtime/route_client-reference-manifest.js +1 -0
  213. package/.next/standalone/apps/web/.next/server/app/api/proxy/verify/route.js +1 -0
  214. package/.next/standalone/apps/web/.next/server/app/api/proxy/verify/route.js.nft.json +1 -0
  215. package/.next/standalone/apps/web/.next/server/app/api/proxy/verify/route_client-reference-manifest.js +1 -0
  216. package/.next/standalone/apps/web/.next/server/app/api/sandbox/route.js +1 -0
  217. package/.next/standalone/apps/web/.next/server/app/api/sandbox/route.js.nft.json +1 -0
  218. package/.next/standalone/apps/web/.next/server/app/api/sandbox/route_client-reference-manifest.js +1 -0
  219. package/.next/standalone/apps/web/.next/server/app/api/sandbox/verify/route.js +1 -0
  220. package/.next/standalone/apps/web/.next/server/app/api/sandbox/verify/route.js.nft.json +1 -0
  221. package/.next/standalone/apps/web/.next/server/app/api/sandbox/verify/route_client-reference-manifest.js +1 -0
  222. package/.next/standalone/apps/web/.next/server/app/api/search/bing/route.js +1 -0
  223. package/.next/standalone/apps/web/.next/server/app/api/search/bing/route.js.nft.json +1 -0
  224. package/.next/standalone/apps/web/.next/server/app/api/search/bing/route_client-reference-manifest.js +1 -0
  225. package/.next/standalone/apps/web/.next/server/app/api/search/brave/route.js +1 -0
  226. package/.next/standalone/apps/web/.next/server/app/api/search/brave/route.js.nft.json +1 -0
  227. package/.next/standalone/apps/web/.next/server/app/api/search/brave/route_client-reference-manifest.js +1 -0
  228. package/.next/standalone/apps/web/.next/server/app/api/search/exa/route.js +1 -0
  229. package/.next/standalone/apps/web/.next/server/app/api/search/exa/route.js.nft.json +1 -0
  230. package/.next/standalone/apps/web/.next/server/app/api/search/exa/route_client-reference-manifest.js +1 -0
  231. package/.next/standalone/apps/web/.next/server/app/api/search/google/route.js +1 -0
  232. package/.next/standalone/apps/web/.next/server/app/api/search/google/route.js.nft.json +1 -0
  233. package/.next/standalone/apps/web/.next/server/app/api/search/google/route_client-reference-manifest.js +1 -0
  234. package/.next/standalone/apps/web/.next/server/app/api/search/route.js +1 -0
  235. package/.next/standalone/apps/web/.next/server/app/api/search/route.js.nft.json +1 -0
  236. package/.next/standalone/apps/web/.next/server/app/api/search/route_client-reference-manifest.js +1 -0
  237. package/.next/standalone/apps/web/.next/server/app/api/search/tavily/route.js +1 -0
  238. package/.next/standalone/apps/web/.next/server/app/api/search/tavily/route.js.nft.json +1 -0
  239. package/.next/standalone/apps/web/.next/server/app/api/search/tavily/route_client-reference-manifest.js +1 -0
  240. package/.next/standalone/apps/web/.next/server/app/api/search/verify/route.js +1 -0
  241. package/.next/standalone/apps/web/.next/server/app/api/search/verify/route.js.nft.json +1 -0
  242. package/.next/standalone/apps/web/.next/server/app/api/search/verify/route_client-reference-manifest.js +1 -0
  243. package/.next/standalone/apps/web/.next/server/app/api/system/cache/route.js +2 -0
  244. package/.next/standalone/apps/web/.next/server/app/api/system/cache/route.js.nft.json +1 -0
  245. package/.next/standalone/apps/web/.next/server/app/api/system/cache/route_client-reference-manifest.js +1 -0
  246. package/.next/standalone/apps/web/.next/server/app/api/tabular/query/route.js +1 -0
  247. package/.next/standalone/apps/web/.next/server/app/api/tabular/query/route.js.nft.json +1 -0
  248. package/.next/standalone/apps/web/.next/server/app/api/tabular/query/route_client-reference-manifest.js +1 -0
  249. package/.next/standalone/apps/web/.next/server/app/api/tabular/route.js +1 -0
  250. package/.next/standalone/apps/web/.next/server/app/api/tabular/route.js.nft.json +1 -0
  251. package/.next/standalone/apps/web/.next/server/app/api/tabular/route_client-reference-manifest.js +1 -0
  252. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/host/route.js +1 -0
  253. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/host/route.js.nft.json +1 -0
  254. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/host/route_client-reference-manifest.js +1 -0
  255. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/runtime/route.js +1 -0
  256. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/runtime/route.js.nft.json +1 -0
  257. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/runtime/route_client-reference-manifest.js +1 -0
  258. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/usage/route.js +1 -0
  259. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/usage/route.js.nft.json +1 -0
  260. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/usage/route_client-reference-manifest.js +1 -0
  261. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/usage-request/route.js +1 -0
  262. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/usage-request/route.js.nft.json +1 -0
  263. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/usage-request/route_client-reference-manifest.js +1 -0
  264. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/verify/route.js +1 -0
  265. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/verify/route.js.nft.json +1 -0
  266. package/.next/standalone/apps/web/.next/server/app/api/tools/[toolId]/verify/route_client-reference-manifest.js +1 -0
  267. package/.next/standalone/apps/web/.next/server/app/api/tools/video-intelligence/connection/route.js +1 -0
  268. package/.next/standalone/apps/web/.next/server/app/api/tools/video-intelligence/connection/route.js.nft.json +1 -0
  269. package/.next/standalone/apps/web/.next/server/app/api/tools/video-intelligence/connection/route_client-reference-manifest.js +1 -0
  270. package/.next/standalone/apps/web/.next/server/app/api/tools/video-intelligence/usage/route.js +1 -0
  271. package/.next/standalone/apps/web/.next/server/app/api/tools/video-intelligence/usage/route.js.nft.json +1 -0
  272. package/.next/standalone/apps/web/.next/server/app/api/tools/video-intelligence/usage/route_client-reference-manifest.js +1 -0
  273. package/.next/standalone/apps/web/.next/server/app/api/tools/video-intelligence/usage-request/route.js +1 -0
  274. package/.next/standalone/apps/web/.next/server/app/api/tools/video-intelligence/usage-request/route.js.nft.json +1 -0
  275. package/.next/standalone/apps/web/.next/server/app/api/tools/video-intelligence/usage-request/route_client-reference-manifest.js +1 -0
  276. package/.next/standalone/apps/web/.next/server/app/api/upload-file/route.js +1 -0
  277. package/.next/standalone/apps/web/.next/server/app/api/upload-file/route.js.nft.json +1 -0
  278. package/.next/standalone/apps/web/.next/server/app/api/upload-file/route_client-reference-manifest.js +1 -0
  279. package/.next/standalone/apps/web/.next/server/app/api/uploads/[...slug]/route.js +1 -0
  280. package/.next/standalone/apps/web/.next/server/app/api/uploads/[...slug]/route.js.nft.json +1 -0
  281. package/.next/standalone/apps/web/.next/server/app/api/uploads/[...slug]/route_client-reference-manifest.js +1 -0
  282. package/.next/standalone/apps/web/.next/server/app/api/vector-stores/install/route.js +1 -0
  283. package/.next/standalone/apps/web/.next/server/app/api/vector-stores/install/route.js.nft.json +1 -0
  284. package/.next/standalone/apps/web/.next/server/app/api/vector-stores/install/route_client-reference-manifest.js +1 -0
  285. package/.next/standalone/apps/web/.next/server/app/api/vector-stores/status/route.js +1 -0
  286. package/.next/standalone/apps/web/.next/server/app/api/vector-stores/status/route.js.nft.json +1 -0
  287. package/.next/standalone/apps/web/.next/server/app/api/vector-stores/status/route_client-reference-manifest.js +1 -0
  288. package/.next/standalone/apps/web/.next/server/app/chat/[id]/page.js +1 -0
  289. package/.next/standalone/apps/web/.next/server/app/chat/[id]/page.js.nft.json +1 -0
  290. package/.next/standalone/apps/web/.next/server/app/chat/[id]/page_client-reference-manifest.js +1 -0
  291. package/.next/standalone/apps/web/.next/server/app/chat/page.js +1 -0
  292. package/.next/standalone/apps/web/.next/server/app/chat/page.js.nft.json +1 -0
  293. package/.next/standalone/apps/web/.next/server/app/chat/page_client-reference-manifest.js +1 -0
  294. package/.next/standalone/apps/web/.next/server/app/chat.html +1 -0
  295. package/.next/standalone/apps/web/.next/server/app/chat.meta +34 -0
  296. package/.next/standalone/apps/web/.next/server/app/chat.rsc +28 -0
  297. package/.next/standalone/apps/web/.next/server/app/chat.segments/_full.segment.rsc +28 -0
  298. package/.next/standalone/apps/web/.next/server/app/chat.segments/_tree.segment.rsc +3 -0
  299. package/.next/standalone/apps/web/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +34 -0
  300. package/.next/standalone/apps/web/.next/server/app/data/page.js +1 -0
  301. package/.next/standalone/apps/web/.next/server/app/data/page.js.nft.json +1 -0
  302. package/.next/standalone/apps/web/.next/server/app/data/page_client-reference-manifest.js +1 -0
  303. package/.next/standalone/apps/web/.next/server/app/data.html +1 -0
  304. package/.next/standalone/apps/web/.next/server/app/data.meta +34 -0
  305. package/.next/standalone/apps/web/.next/server/app/data.rsc +27 -0
  306. package/.next/standalone/apps/web/.next/server/app/data.segments/_full.segment.rsc +27 -0
  307. package/.next/standalone/apps/web/.next/server/app/data.segments/_tree.segment.rsc +2 -0
  308. package/.next/standalone/apps/web/.next/server/app/data.segments/data/__PAGE__.segment.rsc +33 -0
  309. package/.next/standalone/apps/web/.next/server/app/favicon.ico/route.js +1 -0
  310. package/.next/standalone/apps/web/.next/server/app/favicon.ico/route.js.nft.json +1 -0
  311. package/.next/standalone/apps/web/.next/server/app/favicon.ico.body +0 -0
  312. package/.next/standalone/apps/web/.next/server/app/favicon.ico.meta +1 -0
  313. package/.next/standalone/apps/web/.next/server/app/index.html +1 -0
  314. package/.next/standalone/apps/web/.next/server/app/index.meta +29 -0
  315. package/.next/standalone/apps/web/.next/server/app/index.rsc +25 -0
  316. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +30 -0
  317. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +25 -0
  318. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -0
  319. package/.next/standalone/apps/web/.next/server/app/page.js +1 -0
  320. package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -0
  321. package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -0
  322. package/.next/standalone/apps/web/.next/server/app/settings/page.js +216 -0
  323. package/.next/standalone/apps/web/.next/server/app/settings/page.js.nft.json +1 -0
  324. package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -0
  325. package/.next/standalone/apps/web/.next/server/app/settings.html +1 -0
  326. package/.next/standalone/apps/web/.next/server/app/settings.meta +34 -0
  327. package/.next/standalone/apps/web/.next/server/app/settings.rsc +30 -0
  328. package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +30 -0
  329. package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -0
  330. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +36 -0
  331. package/.next/standalone/apps/web/.next/server/app-paths-manifest.json +94 -0
  332. package/.next/standalone/apps/web/.next/server/chunks/101.js +24 -0
  333. package/.next/standalone/apps/web/.next/server/chunks/1149.js +1 -0
  334. package/.next/standalone/apps/web/.next/server/chunks/1309.js +1 -0
  335. package/.next/standalone/apps/web/.next/server/chunks/1389.js +5 -0
  336. package/.next/standalone/apps/web/.next/server/chunks/1479.js +1 -0
  337. package/.next/standalone/apps/web/.next/server/chunks/1602.js +1 -0
  338. package/.next/standalone/apps/web/.next/server/chunks/1778.js +1 -0
  339. package/.next/standalone/apps/web/.next/server/chunks/1810.js +1 -0
  340. package/.next/standalone/apps/web/.next/server/chunks/1953.js +1 -0
  341. package/.next/standalone/apps/web/.next/server/chunks/1999.js +6 -0
  342. package/.next/standalone/apps/web/.next/server/chunks/2097.js +1 -0
  343. package/.next/standalone/apps/web/.next/server/chunks/2129.js +47 -0
  344. package/.next/standalone/apps/web/.next/server/chunks/2411.js +193 -0
  345. package/.next/standalone/apps/web/.next/server/chunks/2500.js +1 -0
  346. package/.next/standalone/apps/web/.next/server/chunks/2552.js +1 -0
  347. package/.next/standalone/apps/web/.next/server/chunks/2595.js +45 -0
  348. package/.next/standalone/apps/web/.next/server/chunks/2883.js +1 -0
  349. package/.next/standalone/apps/web/.next/server/chunks/2918.js +212 -0
  350. package/.next/standalone/apps/web/.next/server/chunks/309.js +2 -0
  351. package/.next/standalone/apps/web/.next/server/chunks/3122.js +1 -0
  352. package/.next/standalone/apps/web/.next/server/chunks/3257.js +1 -0
  353. package/.next/standalone/apps/web/.next/server/chunks/3287.js +1 -0
  354. package/.next/standalone/apps/web/.next/server/chunks/3304.js +1 -0
  355. package/.next/standalone/apps/web/.next/server/chunks/340.js +1 -0
  356. package/.next/standalone/apps/web/.next/server/chunks/3545.js +6 -0
  357. package/.next/standalone/apps/web/.next/server/chunks/3577.js +27 -0
  358. package/.next/standalone/apps/web/.next/server/chunks/3609.js +1 -0
  359. package/.next/standalone/apps/web/.next/server/chunks/364.js +36 -0
  360. package/.next/standalone/apps/web/.next/server/chunks/3642.js +1 -0
  361. package/.next/standalone/apps/web/.next/server/chunks/365.js +1 -0
  362. package/.next/standalone/apps/web/.next/server/chunks/4292.js +1 -0
  363. package/.next/standalone/apps/web/.next/server/chunks/4327.js +1 -0
  364. package/.next/standalone/apps/web/.next/server/chunks/4375.js +1 -0
  365. package/.next/standalone/apps/web/.next/server/chunks/4441.js +1 -0
  366. package/.next/standalone/apps/web/.next/server/chunks/4915.js +1 -0
  367. package/.next/standalone/apps/web/.next/server/chunks/5072.js +47 -0
  368. package/.next/standalone/apps/web/.next/server/chunks/5078.js +1 -0
  369. package/.next/standalone/apps/web/.next/server/chunks/5523.js +10 -0
  370. package/.next/standalone/apps/web/.next/server/chunks/5701.js +1 -0
  371. package/.next/standalone/apps/web/.next/server/chunks/5760.js +11 -0
  372. package/.next/standalone/apps/web/.next/server/chunks/5770.js +1 -0
  373. package/.next/standalone/apps/web/.next/server/chunks/588.js +1 -0
  374. package/.next/standalone/apps/web/.next/server/chunks/5884.js +1 -0
  375. package/.next/standalone/apps/web/.next/server/chunks/5937.js +1 -0
  376. package/.next/standalone/apps/web/.next/server/chunks/5995.js +1 -0
  377. package/.next/standalone/apps/web/.next/server/chunks/600.js +1 -0
  378. package/.next/standalone/apps/web/.next/server/chunks/6014.js +1 -0
  379. package/.next/standalone/apps/web/.next/server/chunks/6066.js +1 -0
  380. package/.next/standalone/apps/web/.next/server/chunks/6149.js +1 -0
  381. package/.next/standalone/apps/web/.next/server/chunks/6199.js +1 -0
  382. package/.next/standalone/apps/web/.next/server/chunks/620.js +78 -0
  383. package/.next/standalone/apps/web/.next/server/chunks/6207.js +14 -0
  384. package/.next/standalone/apps/web/.next/server/chunks/63.js +1 -0
  385. package/.next/standalone/apps/web/.next/server/chunks/6457.js +2 -0
  386. package/.next/standalone/apps/web/.next/server/chunks/6479.js +35 -0
  387. package/.next/standalone/apps/web/.next/server/chunks/657.js +84 -0
  388. package/.next/standalone/apps/web/.next/server/chunks/6573.js +1 -0
  389. package/.next/standalone/apps/web/.next/server/chunks/6814.js +18 -0
  390. package/.next/standalone/apps/web/.next/server/chunks/6909.js +1 -0
  391. package/.next/standalone/apps/web/.next/server/chunks/7010.js +1 -0
  392. package/.next/standalone/apps/web/.next/server/chunks/7289.js +1 -0
  393. package/.next/standalone/apps/web/.next/server/chunks/7324.js +1 -0
  394. package/.next/standalone/apps/web/.next/server/chunks/741.js +1 -0
  395. package/.next/standalone/apps/web/.next/server/chunks/7448.js +165 -0
  396. package/.next/standalone/apps/web/.next/server/chunks/7459.js +1 -0
  397. package/.next/standalone/apps/web/.next/server/chunks/7613.js +1 -0
  398. package/.next/standalone/apps/web/.next/server/chunks/7945.js +1 -0
  399. package/.next/standalone/apps/web/.next/server/chunks/7974.js +20 -0
  400. package/.next/standalone/apps/web/.next/server/chunks/8005.js +41 -0
  401. package/.next/standalone/apps/web/.next/server/chunks/8052.js +55 -0
  402. package/.next/standalone/apps/web/.next/server/chunks/8169.js +1 -0
  403. package/.next/standalone/apps/web/.next/server/chunks/8299.js +2 -0
  404. package/.next/standalone/apps/web/.next/server/chunks/8358.js +1 -0
  405. package/.next/standalone/apps/web/.next/server/chunks/843.js +1 -0
  406. package/.next/standalone/apps/web/.next/server/chunks/8437.js +12 -0
  407. package/.next/standalone/apps/web/.next/server/chunks/8840.js +1 -0
  408. package/.next/standalone/apps/web/.next/server/chunks/8952.js +1859 -0
  409. package/.next/standalone/apps/web/.next/server/chunks/9016.js +1 -0
  410. package/.next/standalone/apps/web/.next/server/chunks/9028.js +1 -0
  411. package/.next/standalone/apps/web/.next/server/chunks/9077.js +1 -0
  412. package/.next/standalone/apps/web/.next/server/chunks/915.js +1 -0
  413. package/.next/standalone/apps/web/.next/server/chunks/9248.js +256 -0
  414. package/.next/standalone/apps/web/.next/server/chunks/9301.js +1 -0
  415. package/.next/standalone/apps/web/.next/server/chunks/9319.js +1 -0
  416. package/.next/standalone/apps/web/.next/server/chunks/9439.js +24 -0
  417. package/.next/standalone/apps/web/.next/server/chunks/9480.js +1 -0
  418. package/.next/standalone/apps/web/.next/server/chunks/9579.js +1 -0
  419. package/.next/standalone/apps/web/.next/server/chunks/9768.js +4 -0
  420. package/.next/standalone/apps/web/.next/server/chunks/9837.js +121 -0
  421. package/.next/standalone/apps/web/.next/server/chunks/9851.js +1 -0
  422. package/.next/standalone/apps/web/.next/server/chunks/static/media/pdf.worker.min.63b810cd.mjs +6 -0
  423. package/.next/standalone/apps/web/.next/server/functions-config-manifest.json +82 -0
  424. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +1 -0
  425. package/.next/standalone/apps/web/.next/server/middleware-manifest.json +6 -0
  426. package/.next/standalone/apps/web/.next/server/middleware-react-loadable-manifest.js +1 -0
  427. package/.next/standalone/apps/web/.next/server/next-font-manifest.js +1 -0
  428. package/.next/standalone/apps/web/.next/server/next-font-manifest.json +1 -0
  429. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -0
  430. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -0
  431. package/.next/standalone/apps/web/.next/server/pages-manifest.json +4 -0
  432. package/.next/standalone/apps/web/.next/server/prefetch-hints.json +95 -0
  433. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -0
  434. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -0
  435. package/.next/standalone/apps/web/.next/server/webpack-runtime.js +1 -0
  436. package/.next/standalone/apps/web/.next/static/chunks/0ed6b8a9-7f39ffa51cd4d7d3.js +1 -0
  437. package/.next/standalone/apps/web/.next/static/chunks/1788-3ec11e3d6d6986e9.js +1 -0
  438. package/.next/standalone/apps/web/.next/static/chunks/1986-2eb6c09f2f556e84.js +1 -0
  439. package/.next/standalone/apps/web/.next/static/chunks/2038-1dc8a3028545edab.js +1 -0
  440. package/.next/standalone/apps/web/.next/static/chunks/217c94bb-b2b832568a17950f.js +1 -0
  441. package/.next/standalone/apps/web/.next/static/chunks/227-7f3dac471f0eb3b0.js +1 -0
  442. package/.next/standalone/apps/web/.next/static/chunks/2634.803b324122bd1d49.js +1 -0
  443. package/.next/standalone/apps/web/.next/static/chunks/2928-8a2d9ed34f45623a.js +1 -0
  444. package/.next/standalone/apps/web/.next/static/chunks/3182.74de4aadefc6e951.js +1 -0
  445. package/.next/standalone/apps/web/.next/static/chunks/3259.41d1a9e21c751dca.js +1 -0
  446. package/.next/standalone/apps/web/.next/static/chunks/3853-f4d7f7a9e7afd752.js +1 -0
  447. package/.next/standalone/apps/web/.next/static/chunks/4112ad79.b782044759df4bc8.js +45 -0
  448. package/.next/standalone/apps/web/.next/static/chunks/4765-48a3b66331719b80.js +1 -0
  449. package/.next/standalone/apps/web/.next/static/chunks/4975-4de53a9574e52468.js +1 -0
  450. package/.next/standalone/apps/web/.next/static/chunks/5833-f7b1cb2153cd52ab.js +1 -0
  451. package/.next/standalone/apps/web/.next/static/chunks/6462-9d67b69d990b1234.js +1 -0
  452. package/.next/standalone/apps/web/.next/static/chunks/6837-67b91a5109edf8a8.js +1 -0
  453. package/.next/standalone/apps/web/.next/static/chunks/6880.4300fd5c0fd7f17a.js +1 -0
  454. package/.next/standalone/apps/web/.next/static/chunks/6904-f77e06e821ecae13.js +52 -0
  455. package/.next/standalone/apps/web/.next/static/chunks/7534-8b910687515b8403.js +6 -0
  456. package/.next/standalone/apps/web/.next/static/chunks/7857.ce5356f660fb54a1.js +1 -0
  457. package/.next/standalone/apps/web/.next/static/chunks/8226-c6f5131375b6df6a.js +3 -0
  458. package/.next/standalone/apps/web/.next/static/chunks/9734-c7b94c9bf40c9b2e.js +1 -0
  459. package/.next/standalone/apps/web/.next/static/chunks/9779-5dc3db110e05c070.js +1 -0
  460. package/.next/standalone/apps/web/.next/static/chunks/9987-45c7dfa599210f7e.js +78 -0
  461. package/.next/standalone/apps/web/.next/static/chunks/app/_global-error/page-0d4f12c4c9cbf85f.js +1 -0
  462. package/.next/standalone/apps/web/.next/static/chunks/app/_not-found/page-7007da1b6a809c3d.js +1 -0
  463. package/.next/standalone/apps/web/.next/static/chunks/app/add/page-d423813ec2556ef0.js +1 -0
  464. package/.next/standalone/apps/web/.next/static/chunks/app/api/analytics/route-0d4f12c4c9cbf85f.js +1 -0
  465. package/.next/standalone/apps/web/.next/static/chunks/app/api/chat/feedback/route-0d4f12c4c9cbf85f.js +1 -0
  466. package/.next/standalone/apps/web/.next/static/chunks/app/api/chat/route-0d4f12c4c9cbf85f.js +1 -0
  467. package/.next/standalone/apps/web/.next/static/chunks/app/api/chat/status/route-0d4f12c4c9cbf85f.js +1 -0
  468. package/.next/standalone/apps/web/.next/static/chunks/app/api/config/route-0d4f12c4c9cbf85f.js +1 -0
  469. package/.next/standalone/apps/web/.next/static/chunks/app/api/config/test/route-0d4f12c4c9cbf85f.js +1 -0
  470. package/.next/standalone/apps/web/.next/static/chunks/app/api/config/test-embedding/route-0d4f12c4c9cbf85f.js +1 -0
  471. package/.next/standalone/apps/web/.next/static/chunks/app/api/config/test-llm/route-0d4f12c4c9cbf85f.js +1 -0
  472. package/.next/standalone/apps/web/.next/static/chunks/app/api/config/test-provider/route-0d4f12c4c9cbf85f.js +1 -0
  473. package/.next/standalone/apps/web/.next/static/chunks/app/api/config/verify/route-0d4f12c4c9cbf85f.js +1 -0
  474. package/.next/standalone/apps/web/.next/static/chunks/app/api/connections/[channelId]/oauth/callback/route-0d4f12c4c9cbf85f.js +1 -0
  475. package/.next/standalone/apps/web/.next/static/chunks/app/api/connections/[channelId]/oauth/start/route-0d4f12c4c9cbf85f.js +1 -0
  476. package/.next/standalone/apps/web/.next/static/chunks/app/api/connections/[channelId]/route-0d4f12c4c9cbf85f.js +1 -0
  477. package/.next/standalone/apps/web/.next/static/chunks/app/api/connections/route-0d4f12c4c9cbf85f.js +1 -0
  478. package/.next/standalone/apps/web/.next/static/chunks/app/api/connections/tunnel/route-0d4f12c4c9cbf85f.js +1 -0
  479. package/.next/standalone/apps/web/.next/static/chunks/app/api/demo/route-0d4f12c4c9cbf85f.js +1 -0
  480. package/.next/standalone/apps/web/.next/static/chunks/app/api/deploy/ssh/route-0d4f12c4c9cbf85f.js +1 -0
  481. package/.next/standalone/apps/web/.next/static/chunks/app/api/deploy/verify/route-0d4f12c4c9cbf85f.js +1 -0
  482. package/.next/standalone/apps/web/.next/static/chunks/app/api/describe-image/route-0d4f12c4c9cbf85f.js +1 -0
  483. package/.next/standalone/apps/web/.next/static/chunks/app/api/doc-editor/edit/route-0d4f12c4c9cbf85f.js +1 -0
  484. package/.next/standalone/apps/web/.next/static/chunks/app/api/doc-editor/export/route-0d4f12c4c9cbf85f.js +1 -0
  485. package/.next/standalone/apps/web/.next/static/chunks/app/api/doc-editor/extract-text/route-0d4f12c4c9cbf85f.js +1 -0
  486. package/.next/standalone/apps/web/.next/static/chunks/app/api/doc-editor/parse/route-0d4f12c4c9cbf85f.js +1 -0
  487. package/.next/standalone/apps/web/.next/static/chunks/app/api/doc-editor/restore/route-0d4f12c4c9cbf85f.js +1 -0
  488. package/.next/standalone/apps/web/.next/static/chunks/app/api/document/sign/route-0d4f12c4c9cbf85f.js +1 -0
  489. package/.next/standalone/apps/web/.next/static/chunks/app/api/documents/move/route-0d4f12c4c9cbf85f.js +1 -0
  490. package/.next/standalone/apps/web/.next/static/chunks/app/api/documents/route-0d4f12c4c9cbf85f.js +1 -0
  491. package/.next/standalone/apps/web/.next/static/chunks/app/api/files/remote/route-0d4f12c4c9cbf85f.js +1 -0
  492. package/.next/standalone/apps/web/.next/static/chunks/app/api/firecrawl/local/route-0d4f12c4c9cbf85f.js +1 -0
  493. package/.next/standalone/apps/web/.next/static/chunks/app/api/gpu-activity/route-0d4f12c4c9cbf85f.js +1 -0
  494. package/.next/standalone/apps/web/.next/static/chunks/app/api/groups/route-0d4f12c4c9cbf85f.js +1 -0
  495. package/.next/standalone/apps/web/.next/static/chunks/app/api/index/route-0d4f12c4c9cbf85f.js +1 -0
  496. package/.next/standalone/apps/web/.next/static/chunks/app/api/integrations/[integration]/callback/route-0d4f12c4c9cbf85f.js +1 -0
  497. package/.next/standalone/apps/web/.next/static/chunks/app/api/integrations/[integration]/route-0d4f12c4c9cbf85f.js +1 -0
  498. package/.next/standalone/apps/web/.next/static/chunks/app/api/integrations/notion/callback/route-0d4f12c4c9cbf85f.js +1 -0
  499. package/.next/standalone/apps/web/.next/static/chunks/app/api/integrations/notion/route-0d4f12c4c9cbf85f.js +1 -0
  500. package/.next/standalone/apps/web/.next/static/chunks/app/api/integrations/route-0d4f12c4c9cbf85f.js +1 -0
  501. package/.next/standalone/apps/web/.next/static/chunks/app/api/jobs/[id]/route-0d4f12c4c9cbf85f.js +1 -0
  502. package/.next/standalone/apps/web/.next/static/chunks/app/api/jobs/route-0d4f12c4c9cbf85f.js +1 -0
  503. package/.next/standalone/apps/web/.next/static/chunks/app/api/marketplace/[toolId]/route-0d4f12c4c9cbf85f.js +1 -0
  504. package/.next/standalone/apps/web/.next/static/chunks/app/api/marketplace/route-0d4f12c4c9cbf85f.js +1 -0
  505. package/.next/standalone/apps/web/.next/static/chunks/app/api/mcp/[connectionId]/route-0d4f12c4c9cbf85f.js +1 -0
  506. package/.next/standalone/apps/web/.next/static/chunks/app/api/mcp/[connectionId]/test/route-0d4f12c4c9cbf85f.js +1 -0
  507. package/.next/standalone/apps/web/.next/static/chunks/app/api/mcp/route-0d4f12c4c9cbf85f.js +1 -0
  508. package/.next/standalone/apps/web/.next/static/chunks/app/api/media/[id]/frame/route-0d4f12c4c9cbf85f.js +1 -0
  509. package/.next/standalone/apps/web/.next/static/chunks/app/api/media/[id]/route-0d4f12c4c9cbf85f.js +1 -0
  510. package/.next/standalone/apps/web/.next/static/chunks/app/api/media/inspect/route-0d4f12c4c9cbf85f.js +1 -0
  511. package/.next/standalone/apps/web/.next/static/chunks/app/api/media/jobs/[id]/approval/route-0d4f12c4c9cbf85f.js +1 -0
  512. package/.next/standalone/apps/web/.next/static/chunks/app/api/media/jobs/[id]/route-0d4f12c4c9cbf85f.js +1 -0
  513. package/.next/standalone/apps/web/.next/static/chunks/app/api/media/process/route-0d4f12c4c9cbf85f.js +1 -0
  514. package/.next/standalone/apps/web/.next/static/chunks/app/api/media/route-0d4f12c4c9cbf85f.js +1 -0
  515. package/.next/standalone/apps/web/.next/static/chunks/app/api/media/stream/route-0d4f12c4c9cbf85f.js +1 -0
  516. package/.next/standalone/apps/web/.next/static/chunks/app/api/models/route-0d4f12c4c9cbf85f.js +1 -0
  517. package/.next/standalone/apps/web/.next/static/chunks/app/api/parse-file/route-0d4f12c4c9cbf85f.js +1 -0
  518. package/.next/standalone/apps/web/.next/static/chunks/app/api/pdf-page/route-0d4f12c4c9cbf85f.js +1 -0
  519. package/.next/standalone/apps/web/.next/static/chunks/app/api/projects/deployments/route-0d4f12c4c9cbf85f.js +1 -0
  520. package/.next/standalone/apps/web/.next/static/chunks/app/api/projects/route-0d4f12c4c9cbf85f.js +1 -0
  521. package/.next/standalone/apps/web/.next/static/chunks/app/api/projects/runtime/route-0d4f12c4c9cbf85f.js +1 -0
  522. package/.next/standalone/apps/web/.next/static/chunks/app/api/proxy/verify/route-0d4f12c4c9cbf85f.js +1 -0
  523. package/.next/standalone/apps/web/.next/static/chunks/app/api/sandbox/route-0d4f12c4c9cbf85f.js +1 -0
  524. package/.next/standalone/apps/web/.next/static/chunks/app/api/sandbox/verify/route-0d4f12c4c9cbf85f.js +1 -0
  525. package/.next/standalone/apps/web/.next/static/chunks/app/api/search/bing/route-0d4f12c4c9cbf85f.js +1 -0
  526. package/.next/standalone/apps/web/.next/static/chunks/app/api/search/brave/route-0d4f12c4c9cbf85f.js +1 -0
  527. package/.next/standalone/apps/web/.next/static/chunks/app/api/search/exa/route-0d4f12c4c9cbf85f.js +1 -0
  528. package/.next/standalone/apps/web/.next/static/chunks/app/api/search/google/route-0d4f12c4c9cbf85f.js +1 -0
  529. package/.next/standalone/apps/web/.next/static/chunks/app/api/search/route-0d4f12c4c9cbf85f.js +1 -0
  530. package/.next/standalone/apps/web/.next/static/chunks/app/api/search/tavily/route-0d4f12c4c9cbf85f.js +1 -0
  531. package/.next/standalone/apps/web/.next/static/chunks/app/api/search/verify/route-0d4f12c4c9cbf85f.js +1 -0
  532. package/.next/standalone/apps/web/.next/static/chunks/app/api/system/cache/route-0d4f12c4c9cbf85f.js +1 -0
  533. package/.next/standalone/apps/web/.next/static/chunks/app/api/tabular/query/route-0d4f12c4c9cbf85f.js +1 -0
  534. package/.next/standalone/apps/web/.next/static/chunks/app/api/tabular/route-0d4f12c4c9cbf85f.js +1 -0
  535. package/.next/standalone/apps/web/.next/static/chunks/app/api/tools/[toolId]/host/route-0d4f12c4c9cbf85f.js +1 -0
  536. package/.next/standalone/apps/web/.next/static/chunks/app/api/tools/[toolId]/runtime/route-0d4f12c4c9cbf85f.js +1 -0
  537. package/.next/standalone/apps/web/.next/static/chunks/app/api/tools/[toolId]/usage/route-0d4f12c4c9cbf85f.js +1 -0
  538. package/.next/standalone/apps/web/.next/static/chunks/app/api/tools/[toolId]/usage-request/route-0d4f12c4c9cbf85f.js +1 -0
  539. package/.next/standalone/apps/web/.next/static/chunks/app/api/tools/[toolId]/verify/route-0d4f12c4c9cbf85f.js +1 -0
  540. package/.next/standalone/apps/web/.next/static/chunks/app/api/tools/video-intelligence/connection/route-0d4f12c4c9cbf85f.js +1 -0
  541. package/.next/standalone/apps/web/.next/static/chunks/app/api/tools/video-intelligence/usage/route-0d4f12c4c9cbf85f.js +1 -0
  542. package/.next/standalone/apps/web/.next/static/chunks/app/api/tools/video-intelligence/usage-request/route-0d4f12c4c9cbf85f.js +1 -0
  543. package/.next/standalone/apps/web/.next/static/chunks/app/api/upload-file/route-0d4f12c4c9cbf85f.js +1 -0
  544. package/.next/standalone/apps/web/.next/static/chunks/app/api/uploads/[...slug]/route-0d4f12c4c9cbf85f.js +1 -0
  545. package/.next/standalone/apps/web/.next/static/chunks/app/api/vector-stores/install/route-0d4f12c4c9cbf85f.js +1 -0
  546. package/.next/standalone/apps/web/.next/static/chunks/app/api/vector-stores/status/route-0d4f12c4c9cbf85f.js +1 -0
  547. package/.next/standalone/apps/web/.next/static/chunks/app/chat/[id]/page-579d3515fee9f89a.js +1 -0
  548. package/.next/standalone/apps/web/.next/static/chunks/app/chat/page-bec03ef7408245b1.js +1 -0
  549. package/.next/standalone/apps/web/.next/static/chunks/app/data/page-02e56e093071379d.js +1 -0
  550. package/.next/standalone/apps/web/.next/static/chunks/app/layout-4b8ce98cb78f7113.js +1 -0
  551. package/.next/standalone/apps/web/.next/static/chunks/app/page-0d4f12c4c9cbf85f.js +1 -0
  552. package/.next/standalone/apps/web/.next/static/chunks/app/settings/page-485ea820c1111580.js +214 -0
  553. package/.next/standalone/apps/web/.next/static/chunks/app/template-6c3f1e7c9216df6c.js +1 -0
  554. package/.next/standalone/apps/web/.next/static/chunks/ca017448-ac5fddb4e8d1ca39.js +1 -0
  555. package/.next/standalone/apps/web/.next/static/chunks/db534642-baa98c214832026c.js +1 -0
  556. package/.next/standalone/apps/web/.next/static/chunks/framework-ae648e3ae9d64766.js +1 -0
  557. package/.next/standalone/apps/web/.next/static/chunks/main-25c6b3b10ba0a4a3.js +5 -0
  558. package/.next/standalone/apps/web/.next/static/chunks/main-app-ecde5d6ff9b76409.js +1 -0
  559. package/.next/standalone/apps/web/.next/static/chunks/next/dist/client/components/builtin/app-error-0d4f12c4c9cbf85f.js +1 -0
  560. package/.next/standalone/apps/web/.next/static/chunks/next/dist/client/components/builtin/forbidden-0d4f12c4c9cbf85f.js +1 -0
  561. package/.next/standalone/apps/web/.next/static/chunks/next/dist/client/components/builtin/global-error-ab826e81c411f616.js +1 -0
  562. package/.next/standalone/apps/web/.next/static/chunks/next/dist/client/components/builtin/not-found-0d4f12c4c9cbf85f.js +1 -0
  563. package/.next/standalone/apps/web/.next/static/chunks/next/dist/client/components/builtin/unauthorized-0d4f12c4c9cbf85f.js +1 -0
  564. package/.next/standalone/apps/web/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  565. package/.next/standalone/apps/web/.next/static/chunks/webpack-8c15908ca85a5a46.js +1 -0
  566. package/.next/standalone/apps/web/.next/static/css/063aaab22a52e4b4.css +1 -0
  567. package/.next/standalone/apps/web/.next/static/css/a2d2ae3b76e6a5b6.css +1 -0
  568. package/.next/standalone/apps/web/.next/static/media/KaTeX_AMS-Regular.1608a09b.woff +0 -0
  569. package/.next/standalone/apps/web/.next/static/media/KaTeX_AMS-Regular.4aafdb68.ttf +0 -0
  570. package/.next/standalone/apps/web/.next/static/media/KaTeX_AMS-Regular.a79f1c31.woff2 +0 -0
  571. package/.next/standalone/apps/web/.next/static/media/KaTeX_Caligraphic-Bold.b6770918.woff +0 -0
  572. package/.next/standalone/apps/web/.next/static/media/KaTeX_Caligraphic-Bold.cce5b8ec.ttf +0 -0
  573. package/.next/standalone/apps/web/.next/static/media/KaTeX_Caligraphic-Bold.ec17d132.woff2 +0 -0
  574. package/.next/standalone/apps/web/.next/static/media/KaTeX_Caligraphic-Regular.07ef19e7.ttf +0 -0
  575. package/.next/standalone/apps/web/.next/static/media/KaTeX_Caligraphic-Regular.55fac258.woff2 +0 -0
  576. package/.next/standalone/apps/web/.next/static/media/KaTeX_Caligraphic-Regular.dad44a7f.woff +0 -0
  577. package/.next/standalone/apps/web/.next/static/media/KaTeX_Fraktur-Bold.9f256b85.woff +0 -0
  578. package/.next/standalone/apps/web/.next/static/media/KaTeX_Fraktur-Bold.b18f59e1.ttf +0 -0
  579. package/.next/standalone/apps/web/.next/static/media/KaTeX_Fraktur-Bold.d42a5579.woff2 +0 -0
  580. package/.next/standalone/apps/web/.next/static/media/KaTeX_Fraktur-Regular.7c187121.woff +0 -0
  581. package/.next/standalone/apps/web/.next/static/media/KaTeX_Fraktur-Regular.d3c882a6.woff2 +0 -0
  582. package/.next/standalone/apps/web/.next/static/media/KaTeX_Fraktur-Regular.ed38e79f.ttf +0 -0
  583. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-Bold.b74a1a8b.ttf +0 -0
  584. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-Bold.c3fb5ac2.woff2 +0 -0
  585. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-Bold.d181c465.woff +0 -0
  586. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-BoldItalic.6f2bb1df.woff2 +0 -0
  587. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-BoldItalic.70d8b0a5.ttf +0 -0
  588. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-BoldItalic.e3f82f9d.woff +0 -0
  589. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-Italic.47373d1e.ttf +0 -0
  590. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-Italic.8916142b.woff2 +0 -0
  591. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-Italic.9024d815.woff +0 -0
  592. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-Regular.0462f03b.woff2 +0 -0
  593. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-Regular.7f51fe03.woff +0 -0
  594. package/.next/standalone/apps/web/.next/static/media/KaTeX_Main-Regular.b7f8fe9b.ttf +0 -0
  595. package/.next/standalone/apps/web/.next/static/media/KaTeX_Math-BoldItalic.572d331f.woff2 +0 -0
  596. package/.next/standalone/apps/web/.next/static/media/KaTeX_Math-BoldItalic.a879cf83.ttf +0 -0
  597. package/.next/standalone/apps/web/.next/static/media/KaTeX_Math-BoldItalic.f1035d8d.woff +0 -0
  598. package/.next/standalone/apps/web/.next/static/media/KaTeX_Math-Italic.5295ba48.woff +0 -0
  599. package/.next/standalone/apps/web/.next/static/media/KaTeX_Math-Italic.939bc644.ttf +0 -0
  600. package/.next/standalone/apps/web/.next/static/media/KaTeX_Math-Italic.f28c23ac.woff2 +0 -0
  601. package/.next/standalone/apps/web/.next/static/media/KaTeX_SansSerif-Bold.8c5b5494.woff2 +0 -0
  602. package/.next/standalone/apps/web/.next/static/media/KaTeX_SansSerif-Bold.94e1e8dc.ttf +0 -0
  603. package/.next/standalone/apps/web/.next/static/media/KaTeX_SansSerif-Bold.bf59d231.woff +0 -0
  604. package/.next/standalone/apps/web/.next/static/media/KaTeX_SansSerif-Italic.3b1e59b3.woff2 +0 -0
  605. package/.next/standalone/apps/web/.next/static/media/KaTeX_SansSerif-Italic.7c9bc82b.woff +0 -0
  606. package/.next/standalone/apps/web/.next/static/media/KaTeX_SansSerif-Italic.b4c20c84.ttf +0 -0
  607. package/.next/standalone/apps/web/.next/static/media/KaTeX_SansSerif-Regular.74048478.woff +0 -0
  608. package/.next/standalone/apps/web/.next/static/media/KaTeX_SansSerif-Regular.ba21ed5f.woff2 +0 -0
  609. package/.next/standalone/apps/web/.next/static/media/KaTeX_SansSerif-Regular.d4d7ba48.ttf +0 -0
  610. package/.next/standalone/apps/web/.next/static/media/KaTeX_Script-Regular.03e9641d.woff2 +0 -0
  611. package/.next/standalone/apps/web/.next/static/media/KaTeX_Script-Regular.07505710.woff +0 -0
  612. package/.next/standalone/apps/web/.next/static/media/KaTeX_Script-Regular.fe9cbbe1.ttf +0 -0
  613. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size1-Regular.e1e279cb.woff +0 -0
  614. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size1-Regular.eae34984.woff2 +0 -0
  615. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size1-Regular.fabc004a.ttf +0 -0
  616. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size2-Regular.57727022.woff +0 -0
  617. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size2-Regular.5916a24f.woff2 +0 -0
  618. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size2-Regular.d6b476ec.ttf +0 -0
  619. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size3-Regular.9acaf01c.woff +0 -0
  620. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size3-Regular.a144ef58.ttf +0 -0
  621. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size3-Regular.b4230e7e.woff2 +0 -0
  622. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size4-Regular.10d95fd3.woff2 +0 -0
  623. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size4-Regular.7a996c9d.woff +0 -0
  624. package/.next/standalone/apps/web/.next/static/media/KaTeX_Size4-Regular.fbccdabe.ttf +0 -0
  625. package/.next/standalone/apps/web/.next/static/media/KaTeX_Typewriter-Regular.6258592b.woff +0 -0
  626. package/.next/standalone/apps/web/.next/static/media/KaTeX_Typewriter-Regular.a8709e36.woff2 +0 -0
  627. package/.next/standalone/apps/web/.next/static/media/KaTeX_Typewriter-Regular.d97aaf4a.ttf +0 -0
  628. package/.next/standalone/apps/web/.next/static/media/pdf.worker.min.b6d469c6.mjs +6 -0
  629. package/.next/standalone/apps/web/.next/static/omDbbCzcQ9kxlZ8Gz0yln/_buildManifest.js +1 -0
  630. package/.next/standalone/apps/web/.next/static/omDbbCzcQ9kxlZ8Gz0yln/_ssgManifest.js +1 -0
  631. package/.next/standalone/apps/web/.next/types/cache-life.d.ts +146 -0
  632. package/.next/standalone/apps/web/.next/types/root-params.d.ts +3 -0
  633. package/.next/standalone/apps/web/.next/types/routes.d.ts +161 -0
  634. package/.next/standalone/apps/web/.next/types/validator.ts +862 -0
  635. package/.next/standalone/apps/web/AGENTS.md +9 -0
  636. package/.next/standalone/apps/web/CLAUDE.md +1 -0
  637. package/.next/standalone/apps/web/app/api/groups/route.test.ts +86 -0
  638. package/.next/standalone/apps/web/app/api/index/route.test.ts +55 -0
  639. package/.next/standalone/apps/web/app/api/marketplace/[toolId]/route.test.ts +83 -0
  640. package/.next/standalone/apps/web/app/api/marketplace/route.test.ts +117 -0
  641. package/.next/standalone/apps/web/app/api/tools/[toolId]/verify/route.test.ts +66 -0
  642. package/.next/standalone/apps/web/app/favicon.ico +0 -0
  643. package/.next/standalone/apps/web/app/globals.css +406 -0
  644. package/.next/standalone/apps/web/app/themes.css +234 -0
  645. package/.next/standalone/apps/web/bin/larkup.js +528 -0
  646. package/.next/standalone/apps/web/components/alerts/generic-alert.tsx +76 -0
  647. package/.next/standalone/apps/web/components/analytics/analytics-dashboard.tsx +517 -0
  648. package/.next/standalone/apps/web/components/chat/canvas/doc-editor-provider.tsx +331 -0
  649. package/.next/standalone/apps/web/components/chat/canvas/document-canvas.tsx +98 -0
  650. package/.next/standalone/apps/web/components/chat/canvas/document-field.tsx +167 -0
  651. package/.next/standalone/apps/web/components/chat/canvas/document-viewer.tsx +446 -0
  652. package/.next/standalone/apps/web/components/chat/canvas/file-attachment-button.tsx +88 -0
  653. package/.next/standalone/apps/web/components/chat/canvas/file-chip.tsx +147 -0
  654. package/.next/standalone/apps/web/components/chat/message-actions.tsx +96 -0
  655. package/.next/standalone/apps/web/components/chat/message-item.tsx +1404 -0
  656. package/.next/standalone/apps/web/components/chat/message-queue.tsx +86 -0
  657. package/.next/standalone/apps/web/components/chat/reasoning.tsx +182 -0
  658. package/.next/standalone/apps/web/components/chat/tools/chat-card.tsx +97 -0
  659. package/.next/standalone/apps/web/components/chat/tools/chat-chart.tsx +554 -0
  660. package/.next/standalone/apps/web/components/chat/tools/chat-citations.tsx +152 -0
  661. package/.next/standalone/apps/web/components/chat/tools/chat-data-table.tsx +377 -0
  662. package/.next/standalone/apps/web/components/chat/tools/chat-file-export.tsx +49 -0
  663. package/.next/standalone/apps/web/components/chat/tools/chat-grid.tsx +99 -0
  664. package/.next/standalone/apps/web/components/chat/tools/chat-media-preview.tsx +465 -0
  665. package/.next/standalone/apps/web/components/chat/tools/chat-metrics.tsx +76 -0
  666. package/.next/standalone/apps/web/components/chat/tools/chat-sandbox-result.tsx +130 -0
  667. package/.next/standalone/apps/web/components/chat/tools/chat-signature-request.tsx +809 -0
  668. package/.next/standalone/apps/web/components/chat/tools/chat-tabs.tsx +59 -0
  669. package/.next/standalone/apps/web/components/chat/tools/corpus-data-result.tsx +194 -0
  670. package/.next/standalone/apps/web/components/chat/tools/knowledge-base-result.tsx +345 -0
  671. package/.next/standalone/apps/web/components/chat/tools/visual-placement-modal.tsx +314 -0
  672. package/.next/standalone/apps/web/components/chat/tools/visualization-tabs.tsx +37 -0
  673. package/.next/standalone/apps/web/components/configure/custom-model-modal.tsx +214 -0
  674. package/.next/standalone/apps/web/components/configure/store-fields.tsx +129 -0
  675. package/.next/standalone/apps/web/components/data/corpus-panel.tsx +1124 -0
  676. package/.next/standalone/apps/web/components/data/data-entry-mode-switch.tsx +53 -0
  677. package/.next/standalone/apps/web/components/data/data-primary-action.ts +6 -0
  678. package/.next/standalone/apps/web/components/data/groups-panel.tsx +526 -0
  679. package/.next/standalone/apps/web/components/data/integration-resources-panel.tsx +281 -0
  680. package/.next/standalone/apps/web/components/data/integrations-panel.tsx +397 -0
  681. package/.next/standalone/apps/web/components/data/jobs-panel.tsx +487 -0
  682. package/.next/standalone/apps/web/components/data/jobs-tracking-button.tsx +221 -0
  683. package/.next/standalone/apps/web/components/data/live-media-progress.tsx +79 -0
  684. package/.next/standalone/apps/web/components/data/media-indexing-jobs-panel.tsx +190 -0
  685. package/.next/standalone/apps/web/components/data/media-panel.tsx +2883 -0
  686. package/.next/standalone/apps/web/components/data/notion-panel.tsx +405 -0
  687. package/.next/standalone/apps/web/components/data/paste-panel.tsx +100 -0
  688. package/.next/standalone/apps/web/components/data/scrape-panel.tsx +1275 -0
  689. package/.next/standalone/apps/web/components/data/tabular-preview.tsx +205 -0
  690. package/.next/standalone/apps/web/components/data/tool-indexing-dialog.tsx +210 -0
  691. package/.next/standalone/apps/web/components/data/upload-panel.tsx +1306 -0
  692. package/.next/standalone/apps/web/components/data/web-crawler-status.tsx +131 -0
  693. package/.next/standalone/apps/web/components/index/index-action-dialog.tsx +545 -0
  694. package/.next/standalone/apps/web/components/index/index-workspace.tsx +406 -0
  695. package/.next/standalone/apps/web/components/onboarding/welcome-screen.tsx +596 -0
  696. package/.next/standalone/apps/web/components/projects/delete-project-dialog.tsx +63 -0
  697. package/.next/standalone/apps/web/components/projects/project-form-dialog.tsx +97 -0
  698. package/.next/standalone/apps/web/components/projects/project-switcher.tsx +136 -0
  699. package/.next/standalone/apps/web/components/settings/general/agent-customization-section.tsx +619 -0
  700. package/.next/standalone/apps/web/components/settings/general/cache-management-card.tsx +176 -0
  701. package/.next/standalone/apps/web/components/settings/general/general-section.tsx +333 -0
  702. package/.next/standalone/apps/web/components/settings/general/mcp-connections-section.tsx +893 -0
  703. package/.next/standalone/apps/web/components/settings/general/model-combobox.tsx +235 -0
  704. package/.next/standalone/apps/web/components/settings/general/models-section.tsx +918 -0
  705. package/.next/standalone/apps/web/components/settings/general/sandbox-section.tsx +382 -0
  706. package/.next/standalone/apps/web/components/settings/general/search-scraping-section.tsx +749 -0
  707. package/.next/standalone/apps/web/components/settings/general/storage-indexing-section.tsx +389 -0
  708. package/.next/standalone/apps/web/components/settings/hub/marketplace-section.tsx +574 -0
  709. package/.next/standalone/apps/web/components/settings/hub/marketplace-tools-settings.tsx +1247 -0
  710. package/.next/standalone/apps/web/components/settings/runtime/connections-tab.tsx +1045 -0
  711. package/.next/standalone/apps/web/components/settings/runtime/deploy/index.ts +4 -0
  712. package/.next/standalone/apps/web/components/settings/runtime/deploy/project-deploy-button.tsx +245 -0
  713. package/.next/standalone/apps/web/components/settings/runtime/deploy/project-deploy-view.tsx +1592 -0
  714. package/.next/standalone/apps/web/components/settings/runtime/deploy/use-vps-deploy.ts +114 -0
  715. package/.next/standalone/apps/web/components/settings/runtime/deploy/vps-terminal-dialog.tsx +391 -0
  716. package/.next/standalone/apps/web/components/settings/runtime/playground-tab.tsx +359 -0
  717. package/.next/standalone/apps/web/components/settings/runtime/runtime-section.tsx +1001 -0
  718. package/.next/standalone/apps/web/components/settings/runtime/widget-snippets.ts +15 -0
  719. package/.next/standalone/apps/web/components/settings/runtime/widget-tab.tsx +426 -0
  720. package/.next/standalone/apps/web/components/settings/settings-layout.tsx +123 -0
  721. package/.next/standalone/apps/web/components/sidebar.tsx +208 -0
  722. package/.next/standalone/apps/web/components/topnav.tsx +92 -0
  723. package/.next/standalone/apps/web/components/ui/alert-dialog.tsx +187 -0
  724. package/.next/standalone/apps/web/components/ui/alert.tsx +76 -0
  725. package/.next/standalone/apps/web/components/ui/badge.tsx +52 -0
  726. package/.next/standalone/apps/web/components/ui/button.tsx +58 -0
  727. package/.next/standalone/apps/web/components/ui/card.tsx +92 -0
  728. package/.next/standalone/apps/web/components/ui/checkbox.tsx +28 -0
  729. package/.next/standalone/apps/web/components/ui/collapsible.tsx +21 -0
  730. package/.next/standalone/apps/web/components/ui/command.tsx +181 -0
  731. package/.next/standalone/apps/web/components/ui/dialog.tsx +138 -0
  732. package/.next/standalone/apps/web/components/ui/dropdown-menu.tsx +267 -0
  733. package/.next/standalone/apps/web/components/ui/input-group.tsx +146 -0
  734. package/.next/standalone/apps/web/components/ui/input.tsx +20 -0
  735. package/.next/standalone/apps/web/components/ui/label.tsx +20 -0
  736. package/.next/standalone/apps/web/components/ui/line-tabs.tsx +59 -0
  737. package/.next/standalone/apps/web/components/ui/popover.tsx +77 -0
  738. package/.next/standalone/apps/web/components/ui/progress.tsx +83 -0
  739. package/.next/standalone/apps/web/components/ui/provider-icon.tsx +165 -0
  740. package/.next/standalone/apps/web/components/ui/scroll-area.tsx +55 -0
  741. package/.next/standalone/apps/web/components/ui/select.tsx +231 -0
  742. package/.next/standalone/apps/web/components/ui/separator.tsx +25 -0
  743. package/.next/standalone/apps/web/components/ui/sheet.tsx +125 -0
  744. package/.next/standalone/apps/web/components/ui/skeleton.tsx +14 -0
  745. package/.next/standalone/apps/web/components/ui/slider.tsx +52 -0
  746. package/.next/standalone/apps/web/components/ui/switch.tsx +32 -0
  747. package/.next/standalone/apps/web/components/ui/table.tsx +116 -0
  748. package/.next/standalone/apps/web/components/ui/tabs.tsx +77 -0
  749. package/.next/standalone/apps/web/components/ui/textarea.tsx +18 -0
  750. package/.next/standalone/apps/web/components/ui/tooltip.tsx +54 -0
  751. package/.next/standalone/apps/web/components.json +21 -0
  752. package/.next/standalone/apps/web/hooks/use-channel-oauth.ts +42 -0
  753. package/.next/standalone/apps/web/hooks/use-copy-to-clipboard.ts +25 -0
  754. package/.next/standalone/apps/web/hooks/use-integration-auth.ts +37 -0
  755. package/.next/standalone/apps/web/hooks/use-live-media-progress.ts +71 -0
  756. package/.next/standalone/apps/web/hooks/use-notion-auth.ts +63 -0
  757. package/.next/standalone/apps/web/lib/browser-cache.test.ts +32 -0
  758. package/.next/standalone/apps/web/lib/browser-cache.ts +17 -0
  759. package/.next/standalone/apps/web/lib/cache-size.test.ts +15 -0
  760. package/.next/standalone/apps/web/lib/cache-size.ts +11 -0
  761. package/.next/standalone/apps/web/lib/chat/assistant-source-scope.test.ts +43 -0
  762. package/.next/standalone/apps/web/lib/chat/chart-config.test.ts +144 -0
  763. package/.next/standalone/apps/web/lib/chat/chat-media-preview.test.ts +31 -0
  764. package/.next/standalone/apps/web/lib/chat/conversation-memory.test.ts +390 -0
  765. package/.next/standalone/apps/web/lib/chat/data-export.test.ts +224 -0
  766. package/.next/standalone/apps/web/lib/chat/live-tool-progress.test.ts +76 -0
  767. package/.next/standalone/apps/web/lib/chat/live-tool-progress.ts +69 -0
  768. package/.next/standalone/apps/web/lib/chat/media-retrieval-routing.test.ts +42 -0
  769. package/.next/standalone/apps/web/lib/chat/media-source-routing.test.ts +118 -0
  770. package/.next/standalone/apps/web/lib/chat/message-feedback.test.ts +102 -0
  771. package/.next/standalone/apps/web/lib/chat/message-input.test.ts +30 -0
  772. package/.next/standalone/apps/web/lib/chat/retrieval-ranking.test.ts +62 -0
  773. package/.next/standalone/apps/web/lib/chat/retrieval-routing.test.ts +80 -0
  774. package/.next/standalone/apps/web/lib/chat/tabular-dataset-routing.test.ts +83 -0
  775. package/.next/standalone/apps/web/lib/chat/tabular-query-plan.test.ts +148 -0
  776. package/.next/standalone/apps/web/lib/chat/tabular-routing.test.ts +109 -0
  777. package/.next/standalone/apps/web/lib/chat/tabular-sandbox-files.test.ts +100 -0
  778. package/.next/standalone/apps/web/lib/chat/tabular-visualization.test.ts +89 -0
  779. package/.next/standalone/apps/web/lib/chat/tool-call-repair.test.ts +64 -0
  780. package/.next/standalone/apps/web/lib/chat/tool-context.test.ts +978 -0
  781. package/.next/standalone/apps/web/lib/chat/tool-output.test.ts +23 -0
  782. package/.next/standalone/apps/web/lib/chat/tool-output.ts +14 -0
  783. package/.next/standalone/apps/web/lib/chat/tool-registry.test.ts +19 -0
  784. package/.next/standalone/apps/web/lib/chat/video-rag-routing.test.ts +158 -0
  785. package/.next/standalone/apps/web/lib/chat/visual-routing.test.ts +140 -0
  786. package/.next/standalone/apps/web/lib/chat-supporting-clip.test.ts +16 -0
  787. package/.next/standalone/apps/web/lib/chat-supporting-clip.ts +16 -0
  788. package/.next/standalone/apps/web/lib/constants/stores.ts +12 -0
  789. package/.next/standalone/apps/web/lib/constants/tools.test.ts +26 -0
  790. package/.next/standalone/apps/web/lib/constants/tools.ts +178 -0
  791. package/.next/standalone/apps/web/lib/error-formatter.ts +30 -0
  792. package/.next/standalone/apps/web/lib/jobs-client.ts +25 -0
  793. package/.next/standalone/apps/web/lib/marketplace/tool-runtime-config.test.ts +217 -0
  794. package/.next/standalone/apps/web/lib/media/knowledge.test.ts +36 -0
  795. package/.next/standalone/apps/web/lib/media/pdf-inspection.test.ts +63 -0
  796. package/.next/standalone/apps/web/lib/media/pdf.ts +57 -0
  797. package/.next/standalone/apps/web/lib/media/progress.test.ts +208 -0
  798. package/.next/standalone/apps/web/lib/media/progress.ts +209 -0
  799. package/.next/standalone/apps/web/lib/media/source-identity.test.ts +33 -0
  800. package/.next/standalone/apps/web/lib/media/source-selection.test.ts +36 -0
  801. package/.next/standalone/apps/web/lib/media/source-selection.ts +47 -0
  802. package/.next/standalone/apps/web/lib/media/source-utils.test.ts +231 -0
  803. package/.next/standalone/apps/web/lib/media/tool-indexing-estimate.test.ts +28 -0
  804. package/.next/standalone/apps/web/lib/media/tool-indexing-estimate.ts +39 -0
  805. package/.next/standalone/apps/web/lib/media/video/quick-look.test.ts +81 -0
  806. package/.next/standalone/apps/web/lib/media/video-intelligence-adapter.test.ts +739 -0
  807. package/.next/standalone/apps/web/lib/media/video-intelligence-capacity.test.ts +104 -0
  808. package/.next/standalone/apps/web/lib/media/video-runtime-scope.test.ts +43 -0
  809. package/.next/standalone/apps/web/lib/release-metadata.test.ts +118 -0
  810. package/.next/standalone/apps/web/lib/remote-files.test.ts +53 -0
  811. package/.next/standalone/apps/web/lib/system-cache.test.ts +173 -0
  812. package/.next/standalone/apps/web/lib/utils.ts +6 -0
  813. package/.next/standalone/apps/web/next-env.d.ts +7 -0
  814. package/.next/standalone/apps/web/next.config.mjs +82 -0
  815. package/.next/standalone/apps/web/package.json +96 -0
  816. package/.next/standalone/apps/web/parse_log.mjs +67 -0
  817. package/.next/standalone/apps/web/postcss.config.mjs +8 -0
  818. package/.next/standalone/apps/web/public/docker.png +0 -0
  819. package/.next/standalone/apps/web/public/favicon.ico +0 -0
  820. package/.next/standalone/apps/web/public/github.svg +3 -0
  821. package/.next/standalone/apps/web/public/hetzner.svg +1 -0
  822. package/.next/standalone/apps/web/public/icons/Confluence.png +0 -0
  823. package/.next/standalone/apps/web/public/icons/Instagram.png +0 -0
  824. package/.next/standalone/apps/web/public/icons/Northflank.png +0 -0
  825. package/.next/standalone/apps/web/public/icons/Salesforce.png +0 -0
  826. package/.next/standalone/apps/web/public/icons/agentskills.png +0 -0
  827. package/.next/standalone/apps/web/public/icons/airtable.png +0 -0
  828. package/.next/standalone/apps/web/public/icons/asana.png +0 -0
  829. package/.next/standalone/apps/web/public/icons/ashby.png +0 -0
  830. package/.next/standalone/apps/web/public/icons/audio.png +0 -0
  831. package/.next/standalone/apps/web/public/icons/aws-kendra.png +0 -0
  832. package/.next/standalone/apps/web/public/icons/aws.svg +38 -0
  833. package/.next/standalone/apps/web/public/icons/azure-ai-search.png +0 -0
  834. package/.next/standalone/apps/web/public/icons/azure.svg +1 -0
  835. package/.next/standalone/apps/web/public/icons/bigquery.png +0 -0
  836. package/.next/standalone/apps/web/public/icons/bing.png +0 -0
  837. package/.next/standalone/apps/web/public/icons/bitbucket.png +0 -0
  838. package/.next/standalone/apps/web/public/icons/brave.png +0 -0
  839. package/.next/standalone/apps/web/public/icons/browserbase.png +0 -0
  840. package/.next/standalone/apps/web/public/icons/calendly.png +0 -0
  841. package/.next/standalone/apps/web/public/icons/chroma.png +0 -0
  842. package/.next/standalone/apps/web/public/icons/cloudflare.png +0 -0
  843. package/.next/standalone/apps/web/public/icons/cohere.svg +30 -0
  844. package/.next/standalone/apps/web/public/icons/databricks.png +0 -0
  845. package/.next/standalone/apps/web/public/icons/daytonaio.png +0 -0
  846. package/.next/standalone/apps/web/public/icons/daytonaio2.png +0 -0
  847. package/.next/standalone/apps/web/public/icons/deepgram.png +0 -0
  848. package/.next/standalone/apps/web/public/icons/deepl.png +0 -0
  849. package/.next/standalone/apps/web/public/icons/digital-ocean.webp +0 -0
  850. package/.next/standalone/apps/web/public/icons/discord.png +0 -0
  851. package/.next/standalone/apps/web/public/icons/e2b-2.png +0 -0
  852. package/.next/standalone/apps/web/public/icons/e2b.png +0 -0
  853. package/.next/standalone/apps/web/public/icons/elevenlabs.png +0 -0
  854. package/.next/standalone/apps/web/public/icons/email.png +0 -0
  855. package/.next/standalone/apps/web/public/icons/exa.png +0 -0
  856. package/.next/standalone/apps/web/public/icons/excel.png +0 -0
  857. package/.next/standalone/apps/web/public/icons/fal.png +0 -0
  858. package/.next/standalone/apps/web/public/icons/firecrawl.png +0 -0
  859. package/.next/standalone/apps/web/public/icons/firecrawl2.png +0 -0
  860. package/.next/standalone/apps/web/public/icons/flyio.jpeg +0 -0
  861. package/.next/standalone/apps/web/public/icons/gcp.svg +1 -0
  862. package/.next/standalone/apps/web/public/icons/gemini.svg +1 -0
  863. package/.next/standalone/apps/web/public/icons/gmail.png +0 -0
  864. package/.next/standalone/apps/web/public/icons/google-analytics.png +0 -0
  865. package/.next/standalone/apps/web/public/icons/google-calender.png +0 -0
  866. package/.next/standalone/apps/web/public/icons/google-docs.png +0 -0
  867. package/.next/standalone/apps/web/public/icons/google-drive.png +0 -0
  868. package/.next/standalone/apps/web/public/icons/google-maps.png +0 -0
  869. package/.next/standalone/apps/web/public/icons/google-meet.png +0 -0
  870. package/.next/standalone/apps/web/public/icons/google-sheets.png +0 -0
  871. package/.next/standalone/apps/web/public/icons/google-slides.png +0 -0
  872. package/.next/standalone/apps/web/public/icons/google-tasks.png +0 -0
  873. package/.next/standalone/apps/web/public/icons/google.png +0 -0
  874. package/.next/standalone/apps/web/public/icons/grok.png +0 -0
  875. package/.next/standalone/apps/web/public/icons/hubspot.png +0 -0
  876. package/.next/standalone/apps/web/public/icons/image.png +0 -0
  877. package/.next/standalone/apps/web/public/icons/jina.svg +1 -0
  878. package/.next/standalone/apps/web/public/icons/jira.png +0 -0
  879. package/.next/standalone/apps/web/public/icons/lancedb2.png +0 -0
  880. package/.next/standalone/apps/web/public/icons/linear.png +0 -0
  881. package/.next/standalone/apps/web/public/icons/linux.png +0 -0
  882. package/.next/standalone/apps/web/public/icons/looker.png +0 -0
  883. package/.next/standalone/apps/web/public/icons/luma.png +0 -0
  884. package/.next/standalone/apps/web/public/icons/mac.png +0 -0
  885. package/.next/standalone/apps/web/public/icons/markdown.png +0 -0
  886. package/.next/standalone/apps/web/public/icons/mcp.png +0 -0
  887. package/.next/standalone/apps/web/public/icons/mcp.svg +1 -0
  888. package/.next/standalone/apps/web/public/icons/messenger.svg +1 -0
  889. package/.next/standalone/apps/web/public/icons/microsoft-intra.png +0 -0
  890. package/.next/standalone/apps/web/public/icons/microsoft-onenote.png +0 -0
  891. package/.next/standalone/apps/web/public/icons/microsoft-planner.png +0 -0
  892. package/.next/standalone/apps/web/public/icons/microsoft-teams.png +0 -0
  893. package/.next/standalone/apps/web/public/icons/miro.png +0 -0
  894. package/.next/standalone/apps/web/public/icons/mistral.svg +469 -0
  895. package/.next/standalone/apps/web/public/icons/modal.png +0 -0
  896. package/.next/standalone/apps/web/public/icons/ngrok.png +0 -0
  897. package/.next/standalone/apps/web/public/icons/nomic.png +0 -0
  898. package/.next/standalone/apps/web/public/icons/onedrive.png +0 -0
  899. package/.next/standalone/apps/web/public/icons/openai.svg +1 -0
  900. package/.next/standalone/apps/web/public/icons/outlook-calendar.png +0 -0
  901. package/.next/standalone/apps/web/public/icons/outlook-email.png +0 -0
  902. package/.next/standalone/apps/web/public/icons/outlook.png +0 -0
  903. package/.next/standalone/apps/web/public/icons/pdf.png +0 -0
  904. package/.next/standalone/apps/web/public/icons/personio.png +0 -0
  905. package/.next/standalone/apps/web/public/icons/pgvector2.png +0 -0
  906. package/.next/standalone/apps/web/public/icons/phone.png +0 -0
  907. package/.next/standalone/apps/web/public/icons/pinecone.png +0 -0
  908. package/.next/standalone/apps/web/public/icons/pipedrive.png +0 -0
  909. package/.next/standalone/apps/web/public/icons/power-pi.png +0 -0
  910. package/.next/standalone/apps/web/public/icons/pylon.png +0 -0
  911. package/.next/standalone/apps/web/public/icons/python.png +0 -0
  912. package/.next/standalone/apps/web/public/icons/qdrant.svg +35 -0
  913. package/.next/standalone/apps/web/public/icons/revai.png +0 -0
  914. package/.next/standalone/apps/web/public/icons/revai.svg +1 -0
  915. package/.next/standalone/apps/web/public/icons/sap.png +0 -0
  916. package/.next/standalone/apps/web/public/icons/serpapi.svg +34 -0
  917. package/.next/standalone/apps/web/public/icons/serper.png +0 -0
  918. package/.next/standalone/apps/web/public/icons/servicenow.png +0 -0
  919. package/.next/standalone/apps/web/public/icons/sharepoint.png +0 -0
  920. package/.next/standalone/apps/web/public/icons/slack.png +0 -0
  921. package/.next/standalone/apps/web/public/icons/snowflake.png +0 -0
  922. package/.next/standalone/apps/web/public/icons/statistia.png +0 -0
  923. package/.next/standalone/apps/web/public/icons/stripe.png +0 -0
  924. package/.next/standalone/apps/web/public/icons/supabase.png +0 -0
  925. package/.next/standalone/apps/web/public/icons/tableau.png +0 -0
  926. package/.next/standalone/apps/web/public/icons/tavily.png +0 -0
  927. package/.next/standalone/apps/web/public/icons/telegram.png +0 -0
  928. package/.next/standalone/apps/web/public/icons/typescript.png +0 -0
  929. package/.next/standalone/apps/web/public/icons/vercel copy.svg +1 -0
  930. package/.next/standalone/apps/web/public/icons/vercel.svg +1 -0
  931. package/.next/standalone/apps/web/public/icons/vertex-ai.png +0 -0
  932. package/.next/standalone/apps/web/public/icons/video.png +0 -0
  933. package/.next/standalone/apps/web/public/icons/voyage-light.png +0 -0
  934. package/.next/standalone/apps/web/public/icons/weaviate.webp +0 -0
  935. package/.next/standalone/apps/web/public/icons/webcontainers.svg +11 -0
  936. package/.next/standalone/apps/web/public/icons/webhook.png +0 -0
  937. package/.next/standalone/apps/web/public/icons/whatsapp copy.png +0 -0
  938. package/.next/standalone/apps/web/public/icons/whatsapp.png +0 -0
  939. package/.next/standalone/apps/web/public/icons/windows.png +0 -0
  940. package/.next/standalone/apps/web/public/icons/word.png +0 -0
  941. package/.next/standalone/apps/web/public/icons/zendesk.png +0 -0
  942. package/.next/standalone/apps/web/public/icons/zoom.png +0 -0
  943. package/.next/standalone/apps/web/public/logo-light.png +0 -0
  944. package/.next/standalone/apps/web/public/logo-light2.png +0 -0
  945. package/.next/standalone/apps/web/public/logo.png +0 -0
  946. package/.next/standalone/apps/web/public/logo9.png +0 -0
  947. package/.next/standalone/apps/web/public/notion-white.png +0 -0
  948. package/.next/standalone/apps/web/public/notion.png +0 -0
  949. package/.next/standalone/apps/web/public/vercel.svg +1 -0
  950. package/.next/standalone/apps/web/scripts/prepare-standalone.mjs +153 -0
  951. package/.next/standalone/apps/web/server.js +44 -0
  952. package/.next/standalone/apps/web/store/chat-store.ts +19 -0
  953. package/.next/standalone/apps/web/store/connections-store.ts +276 -0
  954. package/.next/standalone/apps/web/store/deploy-store.ts +90 -0
  955. package/.next/standalone/apps/web/store/scrape-store.ts +76 -0
  956. package/.next/standalone/apps/web/tsconfig.json +41 -0
  957. package/.next/standalone/apps/web/verify-models.mjs +133 -0
  958. package/.next/standalone/apps/web/vitest.config.ts +15 -0
  959. package/package.json +1 -1
@@ -0,0 +1,2883 @@
1
+ 'use client';
2
+
3
+ import { useRef, useState, useEffect, useMemo } from 'react';
4
+ import { motion } from 'framer-motion';
5
+ import useSWR from 'swr';
6
+ import { toast } from 'sonner';
7
+ import {
8
+ MEDIA_RECENT_URLS_STORAGE_KEY,
9
+ RECENT_ADD_HISTORY_CLEARED_EVENT,
10
+ } from '@/lib/browser-cache';
11
+ import { formatErrorMessage } from '@/lib/shared/error-formatter';
12
+ import {
13
+ describeActiveMediaStep,
14
+ completedMediaProcessingSeconds,
15
+ estimateMediaStepRemainingSeconds,
16
+ formatMediaProgressPercent,
17
+ isFinalizingMediaStep,
18
+ isMediaStepTelemetryStale,
19
+ mediaProcessingStartedAt,
20
+ primaryRunningMediaStep,
21
+ } from '@/lib/media/progress';
22
+ import {
23
+ Image as ImageIcon,
24
+ Video,
25
+ AudioLines,
26
+ Loader2,
27
+ X,
28
+ Upload,
29
+ FileUp,
30
+ Check,
31
+ AlertCircle,
32
+ Clock,
33
+ Trash2,
34
+ Store,
35
+ Link2,
36
+ Sparkles,
37
+ Pause,
38
+ Play,
39
+ ChevronLeft,
40
+ ChevronRight,
41
+ Search,
42
+ ExternalLink,
43
+ } from 'lucide-react';
44
+ import { Button } from '@/components/ui/button';
45
+ import { cn } from '@/lib/utils';
46
+ import { Progress } from '@/components/ui/progress';
47
+ import { Textarea } from '@/components/ui/textarea';
48
+ import { Input } from '@/components/ui/input';
49
+ import { useProject } from '@/components/projects/project-provider';
50
+ import { useLiveMediaProgress } from '@/hooks/use-live-media-progress';
51
+ import { LiveMediaProgress } from '@/components/data/live-media-progress';
52
+ import Link from 'next/link';
53
+ import { useRouter } from 'next/navigation';
54
+ import type { DataPrimaryAction } from '@/components/data/data-primary-action';
55
+ import {
56
+ Dialog,
57
+ DialogContent,
58
+ DialogHeader,
59
+ DialogTitle,
60
+ DialogTrigger,
61
+ } from '@/components/ui/dialog';
62
+ import {
63
+ AlertDialog,
64
+ AlertDialogAction,
65
+ AlertDialogCancel,
66
+ AlertDialogContent,
67
+ AlertDialogDescription,
68
+ AlertDialogFooter,
69
+ AlertDialogHeader,
70
+ AlertDialogTitle,
71
+ } from '@/components/ui/alert-dialog';
72
+ import { Alert, AlertAction, AlertDescription, AlertTitle } from '@/components/ui/alert';
73
+ import { GenericAlert } from '@/components/alerts/generic-alert';
74
+ import {
75
+ defaultToolIndexingValues,
76
+ ToolIndexingDialog,
77
+ type ToolIndexingSurface,
78
+ } from '@/components/data/tool-indexing-dialog';
79
+ import { isLocallyActiveVideoJob } from '@/lib/media/video-intelligence-capacity';
80
+ import { DataEntryModeSwitch } from '@/components/data/data-entry-mode-switch';
81
+ import {
82
+ selectedRemoteDuration,
83
+ selectedRemoteUrls,
84
+ type RemoteMediaEstimate,
85
+ } from '@/lib/media/source-selection';
86
+
87
+ type MediaSubTab = 'images' | 'video' | 'audio';
88
+ type MediaEntryTab = 'upload' | 'url';
89
+ type MediaPipelineStage =
90
+ 'download' | 'prepare' | 'extract' | 'transcribe' | 'vision' | 'synthesize' | 'index';
91
+
92
+ const MAX_RECENT_MEDIA_URLS = 10;
93
+
94
+ interface MediaProcessingStep {
95
+ stage: MediaPipelineStage;
96
+ status: 'waiting' | 'running' | 'completed' | 'skipped' | 'failed';
97
+ percent?: number;
98
+ current?: number;
99
+ total?: number;
100
+ unit?: string;
101
+ estimatedRemainingSeconds?: number;
102
+ elapsedSeconds?: number;
103
+ sequence?: number;
104
+ message?: string;
105
+ startedAt?: string;
106
+ updatedAt?: string;
107
+ finishedAt?: string;
108
+ }
109
+
110
+ interface MediaAsset {
111
+ id: string;
112
+ type: 'image' | 'video' | 'audio';
113
+ fileName: string;
114
+ mimeType: string;
115
+ storageUri: string;
116
+ thumbnailUri?: string;
117
+ fileSize: number;
118
+ processingStatus: 'pending' | 'processing' | 'completed' | 'failed';
119
+ processingError?: string;
120
+ processingMessage?: string;
121
+ processingPaused?: boolean;
122
+ processingProgress?: number;
123
+ processingSteps?: MediaProcessingStep[];
124
+ processingRevision?: number;
125
+ processingStartedAt?: string;
126
+ caption?: string;
127
+ documentIds: string[];
128
+ createdAt: string;
129
+ dimensions?: { width: number; height: number };
130
+ durationSecs?: number;
131
+ indexingInstructions?: string;
132
+ indexingQuality?: number;
133
+ toolInputs?: Record<string, unknown>;
134
+ activeVideoKnowledgeRevisionId?: string;
135
+ activeVideoKnowledgeManifestId?: string;
136
+ activeVideoKnowledgeJobId?: string;
137
+ activeVideoIntelligenceJobId?: string;
138
+ }
139
+
140
+ interface StagedMedia {
141
+ id: string;
142
+ file: File;
143
+ type: 'image' | 'video' | 'audio';
144
+ preview?: string;
145
+ durationSecs?: number;
146
+ }
147
+
148
+ interface RemoteEstimate extends RemoteMediaEstimate {
149
+ originalUrl: string;
150
+ title?: string;
151
+ durationSecs?: number;
152
+ singleItemDurationSecs?: number;
153
+ singleItemUrl?: string;
154
+ entryCount?: number;
155
+ mediaType: 'video' | 'audio' | 'unknown';
156
+ isYouTube?: boolean;
157
+ }
158
+
159
+ interface RecentMediaUrl {
160
+ url: string;
161
+ title?: string;
162
+ }
163
+
164
+ interface MediaApiResponse {
165
+ assets: MediaAsset[];
166
+ stats: {
167
+ total: number;
168
+ byType: Record<string, number>;
169
+ byStatus: Record<string, number>;
170
+ totalBytes: number;
171
+ };
172
+ storage: { usedBytes: number; fileCount: number };
173
+ }
174
+
175
+ interface VideoUsage {
176
+ sourceMinutesUsed: number;
177
+ sourceMinutesLimit: number | null;
178
+ activeJobs: number;
179
+ concurrentJobsLimit: number;
180
+ }
181
+
182
+ let globalStagedMedia: Record<string, StagedMedia[]> = { image: [], video: [], audio: [] };
183
+
184
+ function formatSize(bytes: number): string {
185
+ if (bytes < 1024) return `${bytes} B`;
186
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
187
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
188
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
189
+ }
190
+
191
+ function mediaAssetUrl(assetId: string, serverId?: string, thumbnail = false): string {
192
+ const query = new URLSearchParams();
193
+ if (thumbnail) query.set('thumb', 'true');
194
+ if (serverId) query.set('serverId', serverId);
195
+ const suffix = query.toString();
196
+ return `/api/media/${assetId}${suffix ? `?${suffix}` : ''}`;
197
+ }
198
+
199
+ function getMediaType(file: File): 'image' | 'video' | 'audio' | null {
200
+ if (file.type.startsWith('image/')) return 'image';
201
+ if (file.type.startsWith('video/')) return 'video';
202
+ if (file.type.startsWith('audio/')) return 'audio';
203
+ return null;
204
+ }
205
+
206
+ function getYouTubeVideoId(url: string): string | null {
207
+ try {
208
+ const parsed = new URL(url);
209
+ const host = parsed.hostname.replace(/^www\./, '').toLowerCase();
210
+ if (host === 'youtu.be') return parsed.pathname.split('/').filter(Boolean)[0] ?? null;
211
+ if (host !== 'youtube.com' && !host.endsWith('.youtube.com')) return null;
212
+
213
+ return (
214
+ parsed.searchParams.get('v') ??
215
+ parsed.pathname.match(/^\/(?:embed|shorts|live)\/([^/?#]+)/)?.[1] ??
216
+ null
217
+ );
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ function YouTubeUrlPreview({ estimate }: { estimate: RemoteEstimate }) {
224
+ const [thumbnailUnavailable, setThumbnailUnavailable] = useState(false);
225
+ const videoId = getYouTubeVideoId(estimate.originalUrl);
226
+ const thumbnailUrl = videoId ? `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg` : null;
227
+
228
+ return (
229
+ <div className="overflow-hidden rounded-xl border border-border/70 bg-muted/20">
230
+ <div className="relative flex aspect-video items-center justify-center overflow-hidden bg-black/90">
231
+ {thumbnailUrl && !thumbnailUnavailable ? (
232
+ <img
233
+ src={thumbnailUrl}
234
+ alt="YouTube video thumbnail"
235
+ className="size-full object-cover opacity-90"
236
+ onError={() => setThumbnailUnavailable(true)}
237
+ />
238
+ ) : (
239
+ <Video className="size-8 text-white/70" aria-hidden="true" />
240
+ )}
241
+ <div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent" />
242
+ <span className="absolute bottom-3 left-3 rounded-md bg-black/70 px-2 py-1 text-[11px] font-medium text-white">
243
+ YouTube video
244
+ </span>
245
+ </div>
246
+ <div className="flex flex-wrap items-center justify-between gap-3 p-3">
247
+ <div className="min-w-0">
248
+ <p className="truncate text-[13px] font-medium text-foreground">
249
+ {estimate.title || 'Preview this video on YouTube'}
250
+ </p>
251
+ <p className="mt-0.5 text-[11px] text-muted-foreground">
252
+ YouTube may block embedded playback. Open the source to preview it reliably.
253
+ </p>
254
+ </div>
255
+ <Button
256
+ variant="outline"
257
+ size="sm"
258
+ className="shrink-0 gap-1.5 text-xs"
259
+ nativeButton={false}
260
+ render={
261
+ // Styled as a button, but it navigates: Base UI's button
262
+ // primitive would otherwise announce this link as a button.
263
+ <a href={estimate.originalUrl} target="_blank" rel="noreferrer" role="link">
264
+ Open on YouTube
265
+ <ExternalLink className="size-3" />
266
+ </a>
267
+ }
268
+ />
269
+ </div>
270
+ </div>
271
+ );
272
+ }
273
+
274
+ function estimateMedia(durationSecs: number, isVideo: boolean, indexingMode = 'balanced') {
275
+ const minutes = durationSecs / 60;
276
+ const visualWindowSecs =
277
+ durationSecs >= 4 * 60 * 60 ? 15 * 60 : durationSecs > 60 * 60 ? 5 * 60 : 60;
278
+ const fastMinimumMinutes = Math.max(1, Math.ceil((60 + minutes * 4) / 60));
279
+ const fastMaximumMinutes = Math.max(fastMinimumMinutes, Math.ceil((60 + minutes * 5) / 60));
280
+ const processingMinutes = Math.max(1, Math.ceil(minutes * (isVideo ? 0.35 : 0.2)));
281
+ return {
282
+ transcriptionCost: minutes * 0.006,
283
+ processingMinutes:
284
+ isVideo && indexingMode === 'fast'
285
+ ? Math.ceil((fastMinimumMinutes + fastMaximumMinutes) / 2)
286
+ : processingMinutes,
287
+ minimumProcessingMinutes:
288
+ isVideo && indexingMode === 'fast' ? fastMinimumMinutes : processingMinutes,
289
+ maximumProcessingMinutes:
290
+ isVideo && indexingMode === 'fast' ? fastMaximumMinutes : processingMinutes,
291
+ visualScenes: isVideo ? Math.max(1, Math.ceil(durationSecs / visualWindowSecs)) : 0,
292
+ };
293
+ }
294
+
295
+ function formatEstimateRange(estimate: {
296
+ minimumProcessingMinutes: number;
297
+ maximumProcessingMinutes: number;
298
+ }): string {
299
+ return estimate.minimumProcessingMinutes === estimate.maximumProcessingMinutes
300
+ ? `${estimate.minimumProcessingMinutes} min`
301
+ : `${estimate.minimumProcessingMinutes}–${estimate.maximumProcessingMinutes} min`;
302
+ }
303
+
304
+ function qualityFrameEstimate(quality: number, durationSecs: number): number {
305
+ const intervalMap: Record<number, number> = { 20: 60, 40: 45, 60: 30, 80: 18, 100: 12 };
306
+ const maxMap: Record<number, number> = { 20: 100, 40: 250, 60: 600, 80: 900, 100: 1200 };
307
+ const tier =
308
+ quality <= 20 ? 20 : quality <= 40 ? 40 : quality <= 60 ? 60 : quality <= 80 ? 80 : 100;
309
+ const interval = intervalMap[tier];
310
+ const max = maxMap[tier];
311
+ return Math.min(max, Math.max(1, Math.ceil(durationSecs / interval)));
312
+ }
313
+
314
+ function qualityVisionCalls(quality: number, durationSecs: number): number {
315
+ const frames = qualityFrameEstimate(quality, durationSecs);
316
+ // Backend groups by 12 frames per vision API call
317
+ return Math.max(1, Math.ceil(frames / 12));
318
+ }
319
+
320
+ function qualityCostEstimate(quality: number, durationSecs: number): string {
321
+ const calls = qualityVisionCalls(quality, durationSecs);
322
+ // Transcription buffer (e.g. $0.008/min instead of $0.006)
323
+ const transcriptionCost = (durationSecs / 60) * 0.008;
324
+ // Vision buffer (e.g. $0.007/call instead of $0.003 to cover more expensive models)
325
+ const visionCost = calls * 0.007;
326
+ return (transcriptionCost + visionCost).toFixed(2);
327
+ }
328
+
329
+ function readMediaDuration(file: File): Promise<number | undefined> {
330
+ return new Promise((resolve) => {
331
+ const element = document.createElement(file.type.startsWith('video/') ? 'video' : 'audio');
332
+ const url = URL.createObjectURL(file);
333
+ let settled = false;
334
+ const finish = (duration?: number) => {
335
+ if (settled) return;
336
+ settled = true;
337
+ window.clearTimeout(timeout);
338
+ URL.revokeObjectURL(url);
339
+ resolve(duration);
340
+ };
341
+ // Some browsers never emit metadata or error events for incomplete media.
342
+ // Staging must stay responsive even when the eventual upload is rejected.
343
+ const timeout = window.setTimeout(() => finish(undefined), 3_000);
344
+ element.preload = 'metadata';
345
+ element.onloadedmetadata = () => {
346
+ finish(Number.isFinite(element.duration) ? element.duration : undefined);
347
+ };
348
+ element.onerror = () => {
349
+ finish(undefined);
350
+ };
351
+ element.src = url;
352
+ });
353
+ }
354
+
355
+ const SUB_TABS: { id: MediaSubTab; label: string; icon: typeof ImageIcon; accept: string }[] = [
356
+ { id: 'images', label: 'Images', icon: ImageIcon, accept: 'image/*' },
357
+ { id: 'video', label: 'Video', icon: Video, accept: 'video/*' },
358
+ { id: 'audio', label: 'Audio', icon: AudioLines, accept: 'audio/*' },
359
+ ];
360
+
361
+ const TAB_TO_TYPE: Record<MediaSubTab, 'image' | 'video' | 'audio'> = {
362
+ images: 'image',
363
+ video: 'video',
364
+ audio: 'audio',
365
+ };
366
+
367
+ const fetcher = (url: string) => fetch(url).then((r) => r.json() as Promise<MediaApiResponse>);
368
+
369
+ const toolFetcher = (url: string) => fetch(url).then((r) => r.json() as Promise<{ tools: any[] }>);
370
+
371
+ function isActiveJob(asset: MediaAsset): boolean {
372
+ return isLocallyActiveVideoJob(asset);
373
+ }
374
+
375
+ const INDEXING_PROGRESS_CLASS =
376
+ '[&_[data-slot=progress-indicator]]:bg-emerald-500 dark:[&_[data-slot=progress-indicator]]:bg-emerald-400';
377
+
378
+ export function MediaPanel({
379
+ onAdded,
380
+ onIndexed,
381
+ onProcessingStarted,
382
+ onActionChange,
383
+ groupId = 'default',
384
+ }: {
385
+ onAdded: () => void;
386
+ onIndexed: (asset: MediaAsset) => void;
387
+ onProcessingStarted?: () => void;
388
+ onActionChange?: (action: DataPrimaryAction | null) => void;
389
+ groupId?: string;
390
+ }) {
391
+ const [activeTab, setActiveTab] = useState<MediaSubTab>('images');
392
+ const [entryTab, setEntryTab] = useState<MediaEntryTab>('upload');
393
+ const [videoAudioToolJustInstalled, setVideoAudioToolJustInstalled] = useState(false);
394
+ const [usageRequestOpen, setUsageRequestOpen] = useState(false);
395
+ const [usageRequestEmail, setUsageRequestEmail] = useState('');
396
+ const [usageRequestNote, setUsageRequestNote] = useState('');
397
+ const [sendingUsageRequest, setSendingUsageRequest] = useState(false);
398
+ const mediaType = TAB_TO_TYPE[activeTab];
399
+ const previousStatusesRef = useRef<Map<string, MediaAsset['processingStatus']> | null>(null);
400
+ const { activeProject } = useProject();
401
+ const serverId = activeProject?.id;
402
+ const serverQuery = serverId ? `&serverId=${encodeURIComponent(serverId)}` : '';
403
+ const router = useRouter();
404
+
405
+ const { data, mutate, isLoading } = useSWR(`/api/media?type=all${serverQuery}`, fetcher, {
406
+ revalidateOnFocus: false,
407
+ keepPreviousData: true,
408
+ });
409
+
410
+ useEffect(() => {
411
+ previousStatusesRef.current = null;
412
+ const stream = new EventSource(`/api/media/stream?type=all${serverQuery}`);
413
+ let fallbackTimer: ReturnType<typeof setInterval> | undefined;
414
+ const handleUpdate = (event: MessageEvent<string>) => {
415
+ try {
416
+ const nextData = JSON.parse(event.data) as MediaApiResponse;
417
+ const previousStatuses = previousStatusesRef.current;
418
+ const nextStatuses = new Map(
419
+ nextData.assets.map((asset) => [asset.id, asset.processingStatus]),
420
+ );
421
+
422
+ // The first SSE snapshot establishes the baseline. Subsequent updates
423
+ // let us react only to a job that just finished, not old completed media.
424
+ if (previousStatuses) {
425
+ for (const asset of nextData.assets) {
426
+ const previousStatus = previousStatuses.get(asset.id);
427
+ if (
428
+ asset.processingStatus === 'completed' &&
429
+ (previousStatus === 'pending' || previousStatus === 'processing')
430
+ ) {
431
+ onIndexed(asset);
432
+ try {
433
+ const ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
434
+ const osc = ctx.createOscillator();
435
+ const gain = ctx.createGain();
436
+ osc.connect(gain);
437
+ gain.connect(ctx.destination);
438
+ osc.type = 'sine';
439
+ osc.frequency.setValueAtTime(880, ctx.currentTime);
440
+ gain.gain.setValueAtTime(0.05, ctx.currentTime);
441
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1);
442
+ osc.start(ctx.currentTime);
443
+ osc.stop(ctx.currentTime + 0.1);
444
+ } catch (e) {
445
+ // Ignore audio context errors
446
+ }
447
+ }
448
+ if (
449
+ asset.processingStatus === 'failed' &&
450
+ (previousStatus === 'pending' || previousStatus === 'processing')
451
+ ) {
452
+ const msg =
453
+ asset.processingError ||
454
+ asset.processingMessage ||
455
+ `Failed to index ${asset.fileName}`;
456
+ if (
457
+ msg.toLowerCase().includes('api key') ||
458
+ msg.toLowerCase().includes('configuration') ||
459
+ msg.toLowerCase().includes('missing provider') ||
460
+ msg.toLowerCase().includes('choose an audio provider')
461
+ ) {
462
+ toast.error(`Indexing failed: ${asset.fileName}`, {
463
+ description: msg,
464
+ duration: 10000,
465
+ action: {
466
+ label: 'Go to Settings',
467
+ onClick: () => router.push('/settings?section=tool-settings'),
468
+ },
469
+ });
470
+ } else {
471
+ toast.error(`Indexing failed: ${asset.fileName}`, {
472
+ description: msg,
473
+ duration: 8000,
474
+ });
475
+ }
476
+ }
477
+ }
478
+ }
479
+ previousStatusesRef.current = nextStatuses;
480
+ void mutate(nextData, { revalidate: false });
481
+ } catch {
482
+ // Ignore a malformed update and keep the last successful snapshot.
483
+ }
484
+ };
485
+
486
+ stream.onopen = () => {
487
+ if (fallbackTimer) clearInterval(fallbackTimer);
488
+ fallbackTimer = undefined;
489
+ };
490
+ stream.onerror = () => {
491
+ void mutate();
492
+ fallbackTimer ??= setInterval(() => void mutate(), 5_000);
493
+ };
494
+
495
+ // Removed manual visibilitychange mutate to prevent SWR cache invalidation
496
+ // The EventSource automatically reconnects and synchronizes state natively.
497
+ stream.addEventListener('media-update', handleUpdate);
498
+ return () => {
499
+ if (fallbackTimer) clearInterval(fallbackTimer);
500
+ stream.removeEventListener('media-update', handleUpdate);
501
+ stream.close();
502
+ };
503
+ }, [mediaType, mutate, onIndexed, serverQuery]);
504
+
505
+ const {
506
+ data: toolsData,
507
+ mutate: mutateTools,
508
+ isLoading: toolsLoading,
509
+ } = useSWR('/api/marketplace', toolFetcher);
510
+
511
+ const configUrl = serverId
512
+ ? `/api/config?serverId=${encodeURIComponent(serverId)}`
513
+ : '/api/config';
514
+ const { data: configData } = useSWR(configUrl, (url) => fetch(url).then((r) => r.json()));
515
+
516
+ // Check if the Video Intelligence tool is installed and enabled.
517
+ const videoAudioTool = toolsData?.tools?.find((t: any) => t.id === 'video-audio');
518
+ const videoIntelligenceTool = toolsData?.tools?.find(
519
+ (tool: any) => tool.id === 'video-intelligence',
520
+ );
521
+ const indexingSurface = useMemo(
522
+ () =>
523
+ toolsData?.tools
524
+ ?.filter((tool: any) => tool.status === 'installed')
525
+ .flatMap((tool: any) =>
526
+ (tool.ui?.surfaces ?? []).map((surface: ToolIndexingSurface) => ({
527
+ ...surface,
528
+ toolId: tool.id,
529
+ })),
530
+ )
531
+ .find(
532
+ (surface: ToolIndexingSurface & { toolId: string }) =>
533
+ surface.slot === 'data-indexing' && surface.appliesTo?.includes(mediaType),
534
+ ),
535
+ [mediaType, toolsData?.tools],
536
+ );
537
+ const modernVideoInstalled = videoIntelligenceTool?.status === 'installed';
538
+ const legacyVideoAudioInstalled =
539
+ videoAudioTool?.status === 'installed' || videoAudioToolJustInstalled;
540
+ const isToolInstalled = activeTab === 'video' ? modernVideoInstalled : legacyVideoAudioInstalled;
541
+ const enabledTools = configData?.config?.enabledTools;
542
+ const videoAudioConfig = configData?.config?.toolConfigs?.['video-audio'] ?? {};
543
+ const audioProvider =
544
+ typeof videoAudioConfig.audioProvider === 'string' ? videoAudioConfig.audioProvider : '';
545
+ const audioApiKey =
546
+ typeof videoAudioConfig.audioApiKey === 'string' ? videoAudioConfig.audioApiKey : '';
547
+ const audioConfigured = Boolean(audioProvider && (audioProvider === 'local' || audioApiKey));
548
+ const isToolEnabled = enabledTools
549
+ ? enabledTools.length === 0 ||
550
+ enabledTools.includes(
551
+ activeTab === 'video' && modernVideoInstalled ? 'video-intelligence' : 'video-audio',
552
+ )
553
+ : true;
554
+
555
+ const needsTool =
556
+ activeTab === 'video'
557
+ ? !modernVideoInstalled
558
+ : activeTab === 'audio' && !legacyVideoAudioInstalled;
559
+ const isToolDisabled = activeTab !== 'images' && isToolInstalled && !isToolEnabled;
560
+ const { data: videoUsage, mutate: mutateVideoUsage } = useSWR<VideoUsage>(
561
+ modernVideoInstalled
562
+ ? `/api/tools/video-intelligence/usage${
563
+ serverId ? `?serverId=${encodeURIComponent(serverId)}` : ''
564
+ }`
565
+ : null,
566
+ (url) => fetch(url).then((response) => (response.ok ? response.json() : null)),
567
+ { refreshInterval: modernVideoInstalled ? 30_000 : 0 },
568
+ );
569
+ const usageRatio =
570
+ videoUsage?.sourceMinutesLimit && videoUsage.sourceMinutesLimit > 0
571
+ ? videoUsage.sourceMinutesUsed / videoUsage.sourceMinutesLimit
572
+ : 0;
573
+ const usageWarning = usageRatio >= 0.8;
574
+
575
+ const assets = data?.assets ?? [];
576
+ const activeVideoAssets = assets.filter((asset) => asset.type === 'video' && isActiveJob(asset));
577
+ const videoConcurrencyLimit = Math.max(1, videoUsage?.concurrentJobsLimit ?? 1);
578
+ // A cached cloud count is advisory and may briefly remain at one after the
579
+ // prior job finished. Local live assets drive the button; the POST route
580
+ // performs the authoritative, reconciled concurrency check.
581
+ const videoIndexingAtCapacity = activeVideoAssets.length >= videoConcurrencyLimit;
582
+ const storageBytes = data?.storage?.usedBytes ?? 0;
583
+
584
+ async function installMediaTool(type: 'video' | 'audio') {
585
+ const toolId = type === 'video' ? 'video-intelligence' : 'video-audio';
586
+ const toolName = type === 'video' ? 'Video Intelligence' : 'Video & Audio';
587
+ const toastId = toast.loading(`Installing ${toolName}…`);
588
+ try {
589
+ const query = serverId ? `?serverId=${encodeURIComponent(serverId)}` : '';
590
+ const response = await fetch(`/api/marketplace/${toolId}${query}`, { method: 'POST' });
591
+ const body = await response.json();
592
+ if (!response.ok) throw new Error(body.error || 'Install failed');
593
+ if (type === 'audio') setVideoAudioToolJustInstalled(true);
594
+ await mutateTools();
595
+ toast.success(`${toolName} installed`, {
596
+ id: toastId,
597
+ description:
598
+ type === 'video'
599
+ ? 'Larkup Cloud is ready by default. No GPU or server setup is needed.'
600
+ : 'Choose an audio provider before indexing audio.',
601
+ action: {
602
+ label: 'Open settings',
603
+ onClick: () => router.push('/settings?section=tool-settings'),
604
+ },
605
+ });
606
+ } catch (error) {
607
+ toast.error(error instanceof Error ? error.message : 'Install failed', { id: toastId });
608
+ }
609
+ }
610
+
611
+ async function requestMoreVideoUsage() {
612
+ setSendingUsageRequest(true);
613
+ try {
614
+ const response = await fetch(
615
+ `/api/tools/video-intelligence/usage-request${
616
+ serverId ? `?serverId=${encodeURIComponent(serverId)}` : ''
617
+ }`,
618
+ {
619
+ method: 'POST',
620
+ headers: { 'Content-Type': 'application/json' },
621
+ body: JSON.stringify({
622
+ email: usageRequestEmail,
623
+ note: usageRequestNote,
624
+ usage: videoUsage,
625
+ }),
626
+ },
627
+ );
628
+ const body = await response.json().catch(() => ({}));
629
+ if (!response.ok) throw new Error(body.error || 'Could not send the usage request.');
630
+ toast.success('Usage request sent', {
631
+ description: 'We will contact you about additional Video Intelligence capacity.',
632
+ });
633
+ setUsageRequestOpen(false);
634
+ setUsageRequestNote('');
635
+ } catch (error) {
636
+ toast.error(error instanceof Error ? error.message : 'Could not send the usage request.');
637
+ } finally {
638
+ setSendingUsageRequest(false);
639
+ }
640
+ }
641
+
642
+ return (
643
+ <div className="space-y-5">
644
+ {/* Keep the task choice small. File type is detected after selection. */}
645
+ <div className="flex flex-wrap items-center justify-between gap-4 mb-2">
646
+ <DataEntryModeSwitch
647
+ label="Media source"
648
+ value={entryTab}
649
+ onValueChange={(value) => setEntryTab(value as MediaEntryTab)}
650
+ options={[
651
+ { value: 'upload', label: 'Upload', icon: Upload },
652
+ { value: 'url', label: 'From URL', icon: Link2 },
653
+ ]}
654
+ />
655
+
656
+ {/* Storage usage (subtle) */}
657
+ {storageBytes > 0 && (
658
+ <div className="text-[11px] text-muted-foreground/50 tabular-nums">
659
+ {formatSize(storageBytes)} used
660
+ </div>
661
+ )}
662
+ </div>
663
+
664
+ {!toolsLoading && !modernVideoInstalled && activeTab !== 'video' ? (
665
+ <VideoToolInstallCallout
666
+ toolName={videoIntelligenceTool?.name ?? 'Video Intelligence'}
667
+ onInstall={() => void installMediaTool('video')}
668
+ />
669
+ ) : null}
670
+
671
+ {activeTab === 'video' && usageWarning ? (
672
+ <Alert variant={usageRatio >= 1 ? 'destructive' : 'default'}>
673
+ <AlertCircle />
674
+ <AlertTitle>
675
+ {usageRatio >= 1
676
+ ? 'Video Intelligence allowance reached'
677
+ : 'Video Intelligence allowance is nearly used'}
678
+ </AlertTitle>
679
+ <AlertDescription>
680
+ {Math.ceil(videoUsage?.sourceMinutesUsed ?? 0)} of {videoUsage?.sourceMinutesLimit}{' '}
681
+ cloud video minutes have been used this month.
682
+ </AlertDescription>
683
+ <AlertAction>
684
+ <Button size="sm" variant="outline" onClick={() => setUsageRequestOpen(true)}>
685
+ Request more usage
686
+ </Button>
687
+ </AlertAction>
688
+ </Alert>
689
+ ) : null}
690
+
691
+ {/* Content: either install prompt or upload + gallery */}
692
+ {needsTool ? (
693
+ <InstallPrompt
694
+ toolId={activeTab === 'video' ? 'video-intelligence' : 'video-audio'}
695
+ toolName={
696
+ activeTab === 'video'
697
+ ? (videoIntelligenceTool?.name ?? 'Video Intelligence')
698
+ : (videoAudioTool?.name ?? 'Video Intelligence')
699
+ }
700
+ toolDescription={
701
+ activeTab === 'video'
702
+ ? (videoIntelligenceTool?.description ??
703
+ 'Index video with timestamped speech, OCR, objects, and tracks.')
704
+ : (videoAudioTool?.description ??
705
+ 'Process video and audio files with transcription and frame analysis.')
706
+ }
707
+ installSize={
708
+ activeTab === 'video'
709
+ ? (videoIntelligenceTool?.installSize ?? '~120 KB client')
710
+ : (videoAudioTool?.installSize ?? '~15 MB')
711
+ }
712
+ systemDeps={
713
+ activeTab === 'video' ? videoIntelligenceTool?.systemDeps : videoAudioTool?.systemDeps
714
+ }
715
+ successDescription={
716
+ activeTab === 'video'
717
+ ? 'Larkup Cloud is selected automatically. No GPU or server setup is needed.'
718
+ : 'Choose an audio provider in Installed Tools before indexing audio.'
719
+ }
720
+ serverId={serverId}
721
+ onInstallComplete={() => mutateTools()}
722
+ />
723
+ ) : isToolDisabled ? (
724
+ <DisabledPrompt />
725
+ ) : (
726
+ <MediaContent
727
+ mediaType={mediaType}
728
+ tab={activeTab}
729
+ assets={assets}
730
+ isLoading={isLoading}
731
+ storageUsedBytes={data?.storage?.usedBytes ?? 0}
732
+ serverId={serverId}
733
+ onMutate={() => mutate()}
734
+ onProcessingStarted={onProcessingStarted}
735
+ onUploadComplete={() => {
736
+ mutate();
737
+ onAdded();
738
+ }}
739
+ audioConfigured={audioConfigured}
740
+ onConfigureAudio={() => router.push('/settings?section=tool-settings')}
741
+ entryTab={entryTab}
742
+ onMediaTypeDetected={(type) => setActiveTab(type === 'image' ? 'images' : type)}
743
+ videoToolInstalled={modernVideoInstalled}
744
+ audioToolInstalled={legacyVideoAudioInstalled}
745
+ toolLoading={toolsLoading}
746
+ onToolRequired={(type) => void installMediaTool(type)}
747
+ onActionChange={onActionChange}
748
+ onRefreshToolConfiguration={() => void mutateTools()}
749
+ videoIndexingAtCapacity={videoIndexingAtCapacity}
750
+ videoConcurrencyLimit={videoConcurrencyLimit}
751
+ onVideoJobCancelled={() => void mutateVideoUsage()}
752
+ groupId={groupId}
753
+ indexingSurface={indexingSurface}
754
+ />
755
+ )}
756
+ <GenericAlert
757
+ open={usageRequestOpen}
758
+ onOpenChange={setUsageRequestOpen}
759
+ title="Request more video capacity"
760
+ description={
761
+ <div className="space-y-3">
762
+ <p>
763
+ Tell us where we should contact you. Your current usage summary is included
764
+ automatically.
765
+ </p>
766
+ <Input
767
+ aria-label="Work email"
768
+ type="email"
769
+ value={usageRequestEmail}
770
+ onChange={(event) => setUsageRequestEmail(event.target.value)}
771
+ placeholder="you@company.com"
772
+ />
773
+ <Textarea
774
+ aria-label="Capacity request details"
775
+ value={usageRequestNote}
776
+ onChange={(event) => setUsageRequestNote(event.target.value)}
777
+ placeholder="How many video minutes or concurrent jobs do you need?"
778
+ rows={3}
779
+ />
780
+ </div>
781
+ }
782
+ cancelText="Not now"
783
+ actionText={sendingUsageRequest ? 'Sending…' : 'Send request'}
784
+ onAction={() => void requestMoreVideoUsage()}
785
+ icon={<AlertCircle className="size-5 text-amber-600" />}
786
+ />
787
+ </div>
788
+ );
789
+ }
790
+
791
+ /* Install prompt (for Video & Audio tab when tool not installed) */
792
+ function InstallPrompt({
793
+ toolId,
794
+ toolName,
795
+ toolDescription,
796
+ installSize,
797
+ systemDeps,
798
+ successDescription,
799
+ serverId,
800
+ onInstallComplete,
801
+ }: {
802
+ toolId: string;
803
+ toolName: string;
804
+ toolDescription: string;
805
+ installSize: string;
806
+ systemDeps?: string[];
807
+ successDescription: string;
808
+ serverId?: string;
809
+ onInstallComplete: () => void;
810
+ }) {
811
+ const [installing, setInstalling] = useState(false);
812
+
813
+ const handleInstall = async () => {
814
+ setInstalling(true);
815
+ const toastId = toast.loading('Downloading and installing tool…', {
816
+ description: 'This may take a moment on first install.',
817
+ });
818
+ try {
819
+ const query = serverId ? `?serverId=${encodeURIComponent(serverId)}` : '';
820
+ const res = await fetch(`/api/marketplace/${toolId}${query}`, {
821
+ method: 'POST',
822
+ });
823
+ if (!res.ok) {
824
+ const err = await res.json();
825
+ throw new Error(err.error || 'Install failed');
826
+ }
827
+ toast.dismiss(toastId);
828
+ toast.success(`${toolName} installed`, {
829
+ description: successDescription,
830
+ });
831
+ onInstallComplete();
832
+ } catch (err) {
833
+ toast.dismiss(toastId);
834
+ toast.error(err instanceof Error ? err.message : 'Failed to install tool');
835
+ } finally {
836
+ setInstalling(false);
837
+ }
838
+ };
839
+
840
+ return (
841
+ <div className="flex flex-col items-center justify-center py-16 text-center">
842
+ <div className="flex size-14 items-center border border-border/90 justify-center rounded-xl bg-white">
843
+ <Store className="size-6 text-muted-foreground" />
844
+ </div>
845
+ <h3 className="mt-4 text-sm font-medium text-foreground">{toolName}</h3>
846
+ <p className="mt-1.5 max-w-sm text-[13px] leading-relaxed text-muted-foreground">
847
+ {toolDescription}
848
+ </p>
849
+ <div className="mt-3 flex items-center gap-3 text-[11px] text-muted-foreground/90">
850
+ <span>{installSize}</span>
851
+ {/* {systemDeps?.length ? (
852
+ <>
853
+ <span>·</span>
854
+ <span>Requires: {systemDeps.join(', ')}</span>
855
+ </>
856
+ ) : null} */}
857
+ </div>
858
+ <Button
859
+ variant="default"
860
+ size="sm"
861
+ className="mt-5 gap-1.5 text-[12px]"
862
+ disabled={installing}
863
+ onClick={handleInstall}
864
+ >
865
+ {installing ? (
866
+ <Loader2 className="size-3.5 animate-spin" />
867
+ ) : (
868
+ <Store className="size-3.5" />
869
+ )}
870
+ {installing ? 'Installing...' : 'Install directly'}
871
+ </Button>
872
+ </div>
873
+ );
874
+ }
875
+
876
+ function VideoToolInstallCallout({
877
+ toolName,
878
+ onInstall,
879
+ }: {
880
+ toolName: string;
881
+ onInstall: () => void;
882
+ }) {
883
+ return (
884
+ <div className="flex flex-col gap-3 rounded-xl border border-primary/20 bg-primary/5 p-4 sm:flex-row sm:items-center sm:justify-between">
885
+ <div className="flex min-w-0 items-start gap-3">
886
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
887
+ <Video className="size-4" />
888
+ </div>
889
+ <div>
890
+ <p className="text-sm font-medium text-foreground">Want to index a video?</p>
891
+ <p className="mt-0.5 text-xs leading-relaxed text-muted-foreground">
892
+ Install {toolName} first. It uses Larkup Cloud by default, so no local GPU is needed.
893
+ </p>
894
+ </div>
895
+ </div>
896
+ <Button size="sm" className="shrink-0 gap-1.5" onClick={onInstall}>
897
+ <Store className="size-3.5" />
898
+ Install video tool
899
+ </Button>
900
+ </div>
901
+ );
902
+ }
903
+
904
+ /* Media panel helpers. */
905
+ /* Disabled prompt (for Video & Audio tab when tool is disabled) */
906
+ /* End media panel helpers. */
907
+
908
+ function DisabledPrompt() {
909
+ return (
910
+ <div className="flex flex-col items-center justify-center py-16 text-center">
911
+ <div className="flex size-14 items-center border border-border/90 justify-center rounded-xl bg-white">
912
+ <AlertCircle className="size-6 text-muted-foreground" />
913
+ </div>
914
+ <h3 className="mt-4 text-sm font-medium text-foreground">
915
+ Video Intelligence Processing Disabled
916
+ </h3>
917
+ <p className="mt-1.5 max-w-sm text-[13px] leading-relaxed text-muted-foreground">
918
+ The Video Intelligence tool is currently disabled in your agent settings. You cannot index
919
+ new video or audio files until it is re-enabled.
920
+ </p>
921
+ <Button
922
+ variant="default"
923
+ size="sm"
924
+ className="mt-5 text-[12px]"
925
+ render={<Link href="/settings?tab=prompts">Go to Settings</Link>}
926
+ nativeButton={false}
927
+ />
928
+ </div>
929
+ );
930
+ }
931
+
932
+ function MediaContent({
933
+ mediaType,
934
+ tab,
935
+ assets,
936
+ isLoading,
937
+ storageUsedBytes,
938
+ serverId,
939
+ onMutate,
940
+ onProcessingStarted,
941
+ onUploadComplete,
942
+ audioConfigured,
943
+ onConfigureAudio,
944
+ entryTab,
945
+ onMediaTypeDetected,
946
+ videoToolInstalled,
947
+ audioToolInstalled,
948
+ toolLoading,
949
+ onToolRequired,
950
+ onActionChange,
951
+ onRefreshToolConfiguration,
952
+ videoIndexingAtCapacity,
953
+ videoConcurrencyLimit,
954
+ onVideoJobCancelled,
955
+ groupId,
956
+ indexingSurface,
957
+ }: {
958
+ mediaType: 'image' | 'video' | 'audio';
959
+ tab: MediaSubTab;
960
+ assets: MediaAsset[];
961
+ isLoading: boolean;
962
+ storageUsedBytes: number;
963
+ serverId?: string;
964
+ onMutate: () => void | Promise<unknown>;
965
+ onProcessingStarted?: () => void;
966
+ onUploadComplete: () => void;
967
+ audioConfigured: boolean;
968
+ onConfigureAudio: () => void;
969
+ entryTab: MediaEntryTab;
970
+ onMediaTypeDetected: (type: 'image' | 'video' | 'audio') => void;
971
+ videoToolInstalled: boolean;
972
+ audioToolInstalled: boolean;
973
+ toolLoading: boolean;
974
+ onToolRequired: (type: 'video' | 'audio') => void;
975
+ onActionChange?: (action: DataPrimaryAction | null) => void;
976
+ onRefreshToolConfiguration: () => void;
977
+ videoIndexingAtCapacity: boolean;
978
+ videoConcurrencyLimit: number;
979
+ onVideoJobCancelled: () => void;
980
+ groupId: string;
981
+ indexingSurface?: ToolIndexingSurface & { toolId: string };
982
+ }) {
983
+ const inputRef = useRef<HTMLInputElement>(null);
984
+ const groupIdRef = useRef(groupId);
985
+ groupIdRef.current = groupId;
986
+ const urlInputRef = useRef<HTMLInputElement>(null);
987
+ const recentUrlsRef = useRef<HTMLDivElement>(null);
988
+ const router = useRouter();
989
+ const [libraryTab, setLibraryTab] = useState<MediaSubTab>('images');
990
+ const [librarySearch, setLibrarySearch] = useState('');
991
+ const [libraryPage, setLibraryPage] = useState(0);
992
+ const [staged, setStagedState] = useState<StagedMedia[]>(globalStagedMedia[mediaType] || []);
993
+
994
+ useEffect(() => {
995
+ setStagedState(globalStagedMedia[mediaType] || []);
996
+ }, [mediaType]);
997
+
998
+ const setStaged = (val: React.SetStateAction<StagedMedia[]>) => {
999
+ setStagedState((prev) => {
1000
+ const next = typeof val === 'function' ? (val as Function)(prev) : val;
1001
+ globalStagedMedia[mediaType] = next;
1002
+ return next;
1003
+ });
1004
+ };
1005
+
1006
+ const [dragging, setDragging] = useState(false);
1007
+ const [uploading, setUploading] = useState(false);
1008
+ const [progress, setProgress] = useState<{
1009
+ message?: string;
1010
+ current: number;
1011
+ total: number;
1012
+ } | null>(null);
1013
+ const [urlsText, setUrlsText] = useState('');
1014
+ const [recentUrls, setRecentUrls] = useState<RecentMediaUrl[]>([]);
1015
+ const [showRecentUrls, setShowRecentUrls] = useState(false);
1016
+ const [remoteEstimates, setRemoteEstimates] = useState<RemoteEstimate[] | null>(null);
1017
+ const [remoteType, setRemoteType] = useState<'image' | 'video' | 'audio'>(mediaType);
1018
+ const [checkingUrls, setCheckingUrls] = useState(false);
1019
+ const [importingUrls, setImportingUrls] = useState(false);
1020
+ const [playlistAlertOpen, setPlaylistAlertOpen] = useState(false);
1021
+ const [assetToRemove, setAssetToRemove] = useState<MediaAsset | null>(null);
1022
+ const [indexingBriefOpen, setIndexingBriefOpen] = useState(false);
1023
+ const [indexingAssetIds, setIndexingAssetIds] = useState<string[] | null>(null);
1024
+ const [urlIndexingRequest, setUrlIndexingRequest] = useState<{ ignorePlaylist: boolean } | null>(
1025
+ null,
1026
+ );
1027
+ const [toolIndexingValues, setToolIndexingValues] = useState<Record<string, string | boolean>>(
1028
+ {},
1029
+ );
1030
+ const defaultIndexingValues = useMemo(
1031
+ () => (indexingSurface ? defaultToolIndexingValues(indexingSurface) : {}),
1032
+ [indexingSurface],
1033
+ );
1034
+ useEffect(() => {
1035
+ setToolIndexingValues((current) => {
1036
+ const nextEntries = Object.entries(defaultIndexingValues);
1037
+ const unchanged =
1038
+ Object.keys(current).length === nextEntries.length &&
1039
+ nextEntries.every(([key, value]) => current[key] === value);
1040
+ return unchanged ? current : defaultIndexingValues;
1041
+ });
1042
+ }, [defaultIndexingValues]);
1043
+ const mediaApiUrl = serverId
1044
+ ? `/api/media?serverId=${encodeURIComponent(serverId)}`
1045
+ : '/api/media';
1046
+
1047
+ const accept = 'image/*,video/*,audio/*';
1048
+
1049
+ useEffect(() => {
1050
+ try {
1051
+ const stored = JSON.parse(localStorage.getItem(MEDIA_RECENT_URLS_STORAGE_KEY) ?? '[]');
1052
+ if (!Array.isArray(stored)) return;
1053
+ setRecentUrls(
1054
+ stored
1055
+ .map((value): RecentMediaUrl | null => {
1056
+ // Keep histories saved by older releases usable while upgrading
1057
+ // them to titled entries on the next URL review.
1058
+ if (typeof value === 'string') return { url: value.trim() };
1059
+ if (!value || typeof value !== 'object' || typeof value.url !== 'string') return null;
1060
+ const title = typeof value.title === 'string' ? value.title.trim() : '';
1061
+ return { url: value.url.trim(), ...(title ? { title } : {}) };
1062
+ })
1063
+ .filter((value): value is RecentMediaUrl => Boolean(value?.url))
1064
+ .slice(0, MAX_RECENT_MEDIA_URLS),
1065
+ );
1066
+ } catch {
1067
+ // A malformed browser cache should never prevent adding media.
1068
+ }
1069
+ }, []);
1070
+
1071
+ useEffect(() => {
1072
+ const clearRecentUrls = () => {
1073
+ setRecentUrls([]);
1074
+ setShowRecentUrls(false);
1075
+ };
1076
+ const handleStorage = (event: StorageEvent) => {
1077
+ if (event.key === MEDIA_RECENT_URLS_STORAGE_KEY || event.key === null) clearRecentUrls();
1078
+ };
1079
+ window.addEventListener(RECENT_ADD_HISTORY_CLEARED_EVENT, clearRecentUrls);
1080
+ window.addEventListener('storage', handleStorage);
1081
+ return () => {
1082
+ window.removeEventListener(RECENT_ADD_HISTORY_CLEARED_EVENT, clearRecentUrls);
1083
+ window.removeEventListener('storage', handleStorage);
1084
+ };
1085
+ }, []);
1086
+
1087
+ useEffect(() => {
1088
+ function closeRecentUrls(event: MouseEvent) {
1089
+ if (
1090
+ !urlInputRef.current?.contains(event.target as Node) &&
1091
+ !recentUrlsRef.current?.contains(event.target as Node)
1092
+ ) {
1093
+ setShowRecentUrls(false);
1094
+ }
1095
+ }
1096
+ document.addEventListener('mousedown', closeRecentUrls);
1097
+ return () => document.removeEventListener('mousedown', closeRecentUrls);
1098
+ }, []);
1099
+
1100
+ function saveRecentUrls(entries: Array<string | RecentMediaUrl>) {
1101
+ const normalized = entries
1102
+ .map((entry): RecentMediaUrl | null => {
1103
+ const url = typeof entry === 'string' ? entry : entry.url;
1104
+ const title = typeof entry === 'string' ? '' : entry.title?.trim() || '';
1105
+ return url.trim() ? { url: url.trim(), ...(title ? { title } : {}) } : null;
1106
+ })
1107
+ .filter((entry): entry is RecentMediaUrl => Boolean(entry));
1108
+ if (normalized.length === 0) return;
1109
+ setRecentUrls((current) => {
1110
+ const titles = new Map(current.map((entry) => [entry.url, entry.title]));
1111
+ const next = [...normalized, ...current]
1112
+ .filter(
1113
+ (entry, index, values) => values.findIndex((value) => value.url === entry.url) === index,
1114
+ )
1115
+ .map((entry) => ({
1116
+ ...entry,
1117
+ ...(entry.title ? {} : titles.get(entry.url) ? { title: titles.get(entry.url) } : {}),
1118
+ }))
1119
+ .slice(0, MAX_RECENT_MEDIA_URLS);
1120
+ try {
1121
+ localStorage.setItem(MEDIA_RECENT_URLS_STORAGE_KEY, JSON.stringify(next));
1122
+ } catch {
1123
+ // Private-mode storage or a full quota should not block imports.
1124
+ }
1125
+ return next;
1126
+ });
1127
+ }
1128
+
1129
+ function removeRecentUrl(url: string, event: React.MouseEvent<HTMLButtonElement>) {
1130
+ event.stopPropagation();
1131
+ setRecentUrls((current) => {
1132
+ const next = current.filter((item) => item.url !== url);
1133
+ try {
1134
+ localStorage.setItem(MEDIA_RECENT_URLS_STORAGE_KEY, JSON.stringify(next));
1135
+ } catch {
1136
+ // Browser-local history is optional.
1137
+ }
1138
+ return next;
1139
+ });
1140
+ }
1141
+
1142
+ async function handleFiles(files: FileList | File[]) {
1143
+ const detectedType = getMediaType(Array.from(files)[0]);
1144
+ if (!detectedType) {
1145
+ toast.error('Choose an image, video, or audio file.');
1146
+ return;
1147
+ }
1148
+ const inspected = await Promise.all(
1149
+ Array.from(files)
1150
+ .filter((file) => getMediaType(file) === detectedType)
1151
+ .map(async (file): Promise<StagedMedia | null> => {
1152
+ const type = getMediaType(file);
1153
+ if (!type) return null;
1154
+ const id = Math.random().toString(36).slice(2);
1155
+ const preview =
1156
+ type === 'image' || type === 'audio' ? URL.createObjectURL(file) : undefined;
1157
+ const durationSecs = type === 'image' ? undefined : await readMediaDuration(file);
1158
+ return { id, file, type, preview, durationSecs };
1159
+ }),
1160
+ );
1161
+ const newFiles = inspected.filter((item): item is StagedMedia => item !== null);
1162
+ if (newFiles.length > 0) {
1163
+ if (detectedType === mediaType) {
1164
+ setStaged((prev) => [...prev, ...newFiles]);
1165
+ } else {
1166
+ globalStagedMedia[detectedType] = [...(globalStagedMedia[detectedType] || []), ...newFiles];
1167
+ onMediaTypeDetected(detectedType);
1168
+ }
1169
+ }
1170
+ }
1171
+
1172
+ function removeFile(id: string) {
1173
+ setStaged((prev) => {
1174
+ const file = prev.find((f) => f.id === id);
1175
+ if (file?.preview) URL.revokeObjectURL(file.preview);
1176
+ return prev.filter((f) => f.id !== id);
1177
+ });
1178
+ }
1179
+
1180
+ function showErrorToast(err: unknown) {
1181
+ const msg = formatErrorMessage(err);
1182
+ const normalizedMessage = msg.toLowerCase();
1183
+ if (
1184
+ normalizedMessage.includes('video intelligence tool needs an update') ||
1185
+ normalizedMessage.includes('video intelligence tool not properly installed') ||
1186
+ normalizedMessage.includes('video & audio tool needs an update') ||
1187
+ normalizedMessage.includes('video & audio tool not properly installed') ||
1188
+ normalizedMessage.includes('yt-dlp is required for youtube urls')
1189
+ ) {
1190
+ toast.error('Update Video Intelligence to continue.', {
1191
+ description:
1192
+ 'Your installed tool is out of date. Update it, then try adding the media again.',
1193
+ duration: 10_000,
1194
+ action: {
1195
+ label: 'Update tool',
1196
+ onClick: () => void updateVideoIntelligenceTool(),
1197
+ },
1198
+ });
1199
+ return;
1200
+ }
1201
+ if (
1202
+ normalizedMessage.includes('api key') ||
1203
+ normalizedMessage.includes('configuration') ||
1204
+ normalizedMessage.includes('missing provider') ||
1205
+ normalizedMessage.includes('choose an audio provider')
1206
+ ) {
1207
+ toast.error(msg, {
1208
+ duration: 10000,
1209
+ action: {
1210
+ label: 'Go to Settings',
1211
+ onClick: () => router.push('/settings?section=tool-settings'),
1212
+ },
1213
+ });
1214
+ } else {
1215
+ toast.error(msg);
1216
+ }
1217
+ }
1218
+
1219
+ async function updateVideoIntelligenceTool() {
1220
+ const toastId = toast.loading('Updating Video Intelligence…');
1221
+ try {
1222
+ const query = serverId ? `&serverId=${encodeURIComponent(serverId)}` : '';
1223
+ const response = await fetch(`/api/marketplace/video-intelligence?force=true${query}`, {
1224
+ method: 'POST',
1225
+ });
1226
+ const body = (await response.json().catch(() => null)) as { error?: string } | null;
1227
+ if (!response.ok) throw new Error(body?.error || 'Could not update Video Intelligence.');
1228
+ toast.success('Video Intelligence updated', {
1229
+ id: toastId,
1230
+ description: 'Try adding the media again.',
1231
+ });
1232
+ await onRefreshToolConfiguration();
1233
+ } catch (error) {
1234
+ toast.error(error instanceof Error ? error.message : 'Could not update Video Intelligence.', {
1235
+ id: toastId,
1236
+ });
1237
+ }
1238
+ }
1239
+
1240
+ function ensureAudioConfiguration(type = mediaType): boolean {
1241
+ if (type === 'image') return true;
1242
+ if (toolLoading) {
1243
+ toast.message('Checking Video Intelligence…', {
1244
+ description: 'Please wait a moment, then try again.',
1245
+ });
1246
+ return false;
1247
+ }
1248
+ const indexingToolInstalled = type === 'video' ? videoToolInstalled : audioToolInstalled;
1249
+ if (!indexingToolInstalled) {
1250
+ onToolRequired(type);
1251
+ return false;
1252
+ }
1253
+ if (type === 'video' || audioConfigured) return true;
1254
+ toast.error('Set up an audio provider to index audio.', {
1255
+ description: 'Select a provider and add its API key in Installed Tools.',
1256
+ duration: 10_000,
1257
+ action: { label: 'Set up audio', onClick: onConfigureAudio },
1258
+ });
1259
+ return false;
1260
+ }
1261
+
1262
+ function ensureVideoIndexingCapacity(type: 'image' | 'video' | 'audio'): boolean {
1263
+ if (type !== 'video' || !videoIndexingAtCapacity) return true;
1264
+ toast.error('A video is already being indexed.', {
1265
+ description: `Your plan allows ${videoConcurrencyLimit} video indexing job${
1266
+ videoConcurrencyLimit === 1 ? '' : 's'
1267
+ } at a time. Stop the current video or wait for it to finish.`,
1268
+ });
1269
+ return false;
1270
+ }
1271
+
1272
+ async function processAssets(assetIds: string[], toolInputs?: Record<string, unknown>) {
1273
+ const PROCESS_BATCH_SIZE = 4;
1274
+ for (let i = 0; i < assetIds.length; i += PROCESS_BATCH_SIZE) {
1275
+ const batch = assetIds.slice(i, i + PROCESS_BATCH_SIZE);
1276
+ const res = await fetch('/api/media/process', {
1277
+ method: 'POST',
1278
+ headers: { 'Content-Type': 'application/json' },
1279
+ body: JSON.stringify({ assetIds: batch, serverId, toolInputs }),
1280
+ });
1281
+
1282
+ if (!res.ok) {
1283
+ let err;
1284
+ try {
1285
+ err = await res.json();
1286
+ } catch {
1287
+ err = { error: 'Media processing failed' };
1288
+ }
1289
+ throw new Error(err.error ?? 'Media processing failed');
1290
+ }
1291
+ }
1292
+ await onMutate();
1293
+ onProcessingStarted?.();
1294
+ }
1295
+
1296
+ function parsedUrls() {
1297
+ return urlsText
1298
+ .split(/[\n,]/)
1299
+ .map((url) => url.trim())
1300
+ .filter(Boolean)
1301
+ .slice(0, 10);
1302
+ }
1303
+
1304
+ function mediaTypeFromUrl(url: string): 'image' | 'video' | 'audio' {
1305
+ const path = url.split('?')[0].toLowerCase();
1306
+ if (/\.(png|jpe?g|webp|gif|svg|avif)$/.test(path)) return 'image';
1307
+ if (/\.(mp3|wav|ogg|m4a|flac|aac)$/.test(path)) return 'audio';
1308
+ return 'video';
1309
+ }
1310
+
1311
+ async function reviewRemoteEstimate() {
1312
+ const urls = parsedUrls();
1313
+ if (urls.length === 0) return;
1314
+ const type = mediaTypeFromUrl(urls[0]);
1315
+ setRemoteType(type);
1316
+ onMediaTypeDetected(type);
1317
+ if (!ensureAudioConfiguration(type)) return;
1318
+ setCheckingUrls(true);
1319
+ try {
1320
+ if (type === 'image') {
1321
+ const estimates = urls.map((originalUrl) => ({
1322
+ originalUrl,
1323
+ mediaType: 'unknown' as const,
1324
+ }));
1325
+ setRemoteEstimates(estimates);
1326
+ saveRecentUrls(estimates.map(({ originalUrl }) => ({ url: originalUrl })));
1327
+ return;
1328
+ }
1329
+ const res = await fetch(mediaApiUrl, {
1330
+ method: 'POST',
1331
+ headers: { 'Content-Type': 'application/json' },
1332
+ body: JSON.stringify({ urls, estimateOnly: true, mediaType: type }),
1333
+ });
1334
+ const data = await res.json();
1335
+ if (!res.ok) throw new Error(data.error ?? 'Could not inspect media URLs');
1336
+ const estimates = data.estimates as RemoteEstimate[];
1337
+ setRemoteEstimates(estimates);
1338
+ saveRecentUrls(
1339
+ estimates.map((estimate) => ({ url: estimate.originalUrl, title: estimate.title })),
1340
+ );
1341
+ } catch (err) {
1342
+ showErrorToast(err);
1343
+ } finally {
1344
+ setCheckingUrls(false);
1345
+ }
1346
+ }
1347
+
1348
+ async function importRemoteUrls(
1349
+ ignorePlaylist = false,
1350
+ toolInputs?: Record<string, unknown>,
1351
+ ): Promise<boolean> {
1352
+ let urls = parsedUrls();
1353
+ if (urls.length === 0 || !remoteEstimates) return false;
1354
+ if (!ensureAudioConfiguration(remoteType)) return false;
1355
+
1356
+ urls = selectedRemoteUrls(urls, remoteEstimates, ignorePlaylist);
1357
+
1358
+ setImportingUrls(true);
1359
+ try {
1360
+ const surfaceValues = indexingSurface ? toolInputs?.[indexingSurface.toolId] : undefined;
1361
+ const indexingInstructions =
1362
+ surfaceValues &&
1363
+ typeof surfaceValues === 'object' &&
1364
+ typeof (surfaceValues as { goal?: unknown }).goal === 'string'
1365
+ ? (surfaceValues as { goal: string }).goal.trim()
1366
+ : undefined;
1367
+ const res = await fetch(mediaApiUrl, {
1368
+ method: 'POST',
1369
+ headers: { 'Content-Type': 'application/json' },
1370
+ body: JSON.stringify({
1371
+ urls,
1372
+ mediaType: remoteType,
1373
+ groupId: groupIdRef.current,
1374
+ toolInputs,
1375
+ ...(indexingInstructions ? { indexingInstructions } : {}),
1376
+ }),
1377
+ });
1378
+ const data = await res.json();
1379
+ if (!res.ok) throw new Error(data.error ?? 'Media import failed');
1380
+ const titlesByUrl = new Map(
1381
+ remoteEstimates.map((estimate) => [estimate.originalUrl, estimate.title] as const),
1382
+ );
1383
+ saveRecentUrls(urls.map((url) => ({ url, title: titlesByUrl.get(url) })));
1384
+ const ids = (data.assets as MediaAsset[]).map((asset) => asset.id);
1385
+ toast.success('Media import started', {
1386
+ description: 'Queued for download and indexing. You can safely leave in the meantime.',
1387
+ duration: 8_000,
1388
+ });
1389
+ await processAssets(ids);
1390
+ setUrlsText('');
1391
+ setRemoteEstimates(null);
1392
+ setProgress(null);
1393
+ onUploadComplete();
1394
+ return true;
1395
+ } catch (err) {
1396
+ showErrorToast(err);
1397
+ setProgress(null);
1398
+ return false;
1399
+ } finally {
1400
+ setImportingUrls(false);
1401
+ }
1402
+ }
1403
+
1404
+ async function uploadAll() {
1405
+ if (staged.length === 0) return;
1406
+ if (!ensureAudioConfiguration()) return;
1407
+ if (!ensureVideoIndexingCapacity(mediaType)) return;
1408
+ const destinationGroupId = groupIdRef.current;
1409
+ setUploading(true);
1410
+
1411
+ const BATCH_SIZE = 5;
1412
+ let uploaded = 0;
1413
+ const uploadedAssetIds: string[] = [];
1414
+ const total = staged.length;
1415
+ setProgress({ message: 'Starting upload...', current: 0, total });
1416
+
1417
+ try {
1418
+ for (let i = 0; i < staged.length; i += BATCH_SIZE) {
1419
+ setProgress({ message: 'Uploading files...', current: uploaded, total });
1420
+ const batch = staged.slice(i, i + BATCH_SIZE);
1421
+ const formData = new FormData();
1422
+ batch.forEach((item) => formData.append('file', item.file));
1423
+ formData.append(
1424
+ 'mediaDurations',
1425
+ JSON.stringify(batch.map((item) => item.durationSecs ?? null)),
1426
+ );
1427
+ const goal =
1428
+ typeof toolIndexingValues.goal === 'string' ? toolIndexingValues.goal.trim() : '';
1429
+ if (goal) {
1430
+ formData.append('indexingInstructions', goal);
1431
+ }
1432
+ formData.append('indexingQuality', '50');
1433
+ if (indexingSurface) {
1434
+ formData.append(
1435
+ 'toolInputs',
1436
+ JSON.stringify({
1437
+ [indexingSurface.toolId]: toolIndexingValues,
1438
+ }),
1439
+ );
1440
+ }
1441
+ formData.append('groupId', destinationGroupId);
1442
+
1443
+ const res = await fetch(mediaApiUrl, {
1444
+ method: 'POST',
1445
+ body: formData,
1446
+ });
1447
+
1448
+ const responseData = await res.json();
1449
+ if (!res.ok) {
1450
+ const err = responseData;
1451
+ throw new Error(err.error ?? 'Upload failed');
1452
+ }
1453
+ uploadedAssetIds.push(
1454
+ ...(responseData.assets as MediaAsset[]).map((asset: MediaAsset) => asset.id),
1455
+ );
1456
+
1457
+ uploaded += batch.length;
1458
+ setProgress({ message: 'Uploading files...', current: uploaded, total });
1459
+ }
1460
+
1461
+ if (uploadedAssetIds.length > 0) {
1462
+ setProgress({ message: 'Queueing files for indexing...', current: 0, total: uploaded });
1463
+ await processAssets(uploadedAssetIds);
1464
+ }
1465
+ const mediaLabel =
1466
+ mediaType === 'video' ? 'Video' : mediaType === 'audio' ? 'Audio' : 'Image';
1467
+ toast.success(`${mediaLabel} indexing started`, {
1468
+ description:
1469
+ mediaType === 'image'
1470
+ ? 'You can safely leave while we make it searchable.'
1471
+ : `Expected time: about ${formatEstimateRange(
1472
+ stagedEstimate,
1473
+ )}. You can safely leave while we index it.`,
1474
+ duration: 8_000,
1475
+ });
1476
+ setProgress(null);
1477
+ setIndexingBriefOpen(false);
1478
+
1479
+ staged.forEach((f) => {
1480
+ if (f.preview) URL.revokeObjectURL(f.preview);
1481
+ });
1482
+ globalStagedMedia[mediaType] = [];
1483
+ setStaged([]);
1484
+ setUploading(false);
1485
+
1486
+ onUploadComplete();
1487
+ } catch (err) {
1488
+ showErrorToast(err);
1489
+ setUploading(false);
1490
+ setProgress(null);
1491
+ }
1492
+ }
1493
+
1494
+ function beginUpload() {
1495
+ if (!ensureVideoIndexingCapacity(mediaType)) return;
1496
+ if (mediaType === 'video' && !indexingSurface) {
1497
+ onRefreshToolConfiguration();
1498
+ toast.message('Loading video indexing options…', {
1499
+ description:
1500
+ 'Wait a moment for the installed tool configuration, then try Add media again.',
1501
+ });
1502
+ return;
1503
+ }
1504
+ if (indexingSurface) {
1505
+ setIndexingAssetIds(null);
1506
+ setIndexingBriefOpen(true);
1507
+ return;
1508
+ }
1509
+ void uploadAll();
1510
+ }
1511
+
1512
+ function beginUrlIndexing(ignorePlaylist = false) {
1513
+ if (!ensureVideoIndexingCapacity(remoteType)) return;
1514
+ if (remoteType === 'video' && !indexingSurface) {
1515
+ onRefreshToolConfiguration();
1516
+ toast.message('Loading video indexing options…', {
1517
+ description:
1518
+ 'Wait a moment for the installed tool configuration, then try Add media again.',
1519
+ });
1520
+ return;
1521
+ }
1522
+ if (remoteType === 'video' && indexingSurface?.appliesTo.includes(remoteType)) {
1523
+ setIndexingAssetIds(null);
1524
+ setUrlIndexingRequest({ ignorePlaylist });
1525
+ setIndexingBriefOpen(true);
1526
+ return;
1527
+ }
1528
+ void importRemoteUrls(ignorePlaylist);
1529
+ }
1530
+
1531
+ async function handleDelete(assetId: string) {
1532
+ try {
1533
+ const query = new URLSearchParams({ id: assetId, force: 'true' });
1534
+ if (serverId) query.set('serverId', serverId);
1535
+ const response = await fetch(`/api/media?${query.toString()}`, { method: 'DELETE' });
1536
+ if (!response.ok) {
1537
+ const body = (await response.json().catch(() => null)) as { error?: string } | null;
1538
+ throw new Error(body?.error || 'Failed to delete media.');
1539
+ }
1540
+ toast.success('File removed');
1541
+ onMutate();
1542
+ onVideoJobCancelled();
1543
+ } catch (error) {
1544
+ showErrorToast(error);
1545
+ }
1546
+ }
1547
+
1548
+ async function togglePause(asset: MediaAsset) {
1549
+ try {
1550
+ const res = await fetch('/api/media/process', {
1551
+ method: 'POST',
1552
+ headers: { 'Content-Type': 'application/json' },
1553
+ body: JSON.stringify({
1554
+ action: asset.processingPaused ? 'resume' : 'pause',
1555
+ assetId: asset.id,
1556
+ serverId,
1557
+ }),
1558
+ });
1559
+ const body = await res.json();
1560
+ if (!res.ok) throw new Error(body.error || 'Could not update media indexing.');
1561
+ toast.success(asset.processingPaused ? 'Media indexing resumed.' : 'Media indexing paused.');
1562
+ await onMutate();
1563
+ } catch (error) {
1564
+ showErrorToast(error);
1565
+ }
1566
+ }
1567
+
1568
+ async function handleRetry(asset: MediaAsset) {
1569
+ if (asset.type === 'video' && indexingSurface) {
1570
+ setIndexingAssetIds([asset.id]);
1571
+ setIndexingBriefOpen(true);
1572
+ return;
1573
+ }
1574
+ setProgress({ message: 'Retrying media processing...', current: 0, total: 1 });
1575
+ try {
1576
+ await processAssets([asset.id]);
1577
+ toast.success(
1578
+ `${
1579
+ asset.type === 'video' ? 'Video' : asset.type === 'audio' ? 'Audio' : 'Image'
1580
+ } indexing started`,
1581
+ );
1582
+ onMutate();
1583
+ } catch (err) {
1584
+ showErrorToast(err);
1585
+ } finally {
1586
+ setProgress(null);
1587
+ }
1588
+ }
1589
+
1590
+ async function confirmExistingIndexing() {
1591
+ if (!indexingAssetIds?.length) return;
1592
+ setUploading(true);
1593
+ setProgress({
1594
+ message: 'Queueing media for indexing...',
1595
+ current: 0,
1596
+ total: indexingAssetIds.length,
1597
+ });
1598
+ try {
1599
+ await processAssets(
1600
+ indexingAssetIds,
1601
+ indexingSurface ? { [indexingSurface.toolId]: toolIndexingValues } : undefined,
1602
+ );
1603
+ toast.success('Video indexing started');
1604
+ setIndexingBriefOpen(false);
1605
+ setIndexingAssetIds(null);
1606
+ } catch (error) {
1607
+ showErrorToast(error);
1608
+ } finally {
1609
+ setUploading(false);
1610
+ setProgress(null);
1611
+ }
1612
+ }
1613
+
1614
+ async function confirmUrlIndexing() {
1615
+ if (!urlIndexingRequest) return;
1616
+ const imported = await importRemoteUrls(
1617
+ urlIndexingRequest.ignorePlaylist,
1618
+ indexingSurface ? { [indexingSurface.toolId]: toolIndexingValues } : undefined,
1619
+ );
1620
+ if (imported) {
1621
+ setIndexingBriefOpen(false);
1622
+ setUrlIndexingRequest(null);
1623
+ }
1624
+ }
1625
+
1626
+ const stagedDuration = staged.reduce((total, item) => total + (item.durationSecs ?? 0), 0);
1627
+ const remoteDuration = selectedRemoteDuration(remoteEstimates, false);
1628
+ const indexingDuration = indexingAssetIds
1629
+ ? assets
1630
+ .filter((asset) => indexingAssetIds.includes(asset.id))
1631
+ .reduce((total, asset) => total + (asset.durationSecs ?? 0), 0)
1632
+ : urlIndexingRequest
1633
+ ? selectedRemoteDuration(remoteEstimates, urlIndexingRequest.ignorePlaylist)
1634
+ : stagedDuration;
1635
+ const selectedIndexingMode =
1636
+ typeof toolIndexingValues.indexingMode === 'string'
1637
+ ? toolIndexingValues.indexingMode
1638
+ : 'balanced';
1639
+ const stagedEstimate = estimateMedia(stagedDuration, mediaType === 'video', selectedIndexingMode);
1640
+ const remoteEstimate = estimateMedia(remoteDuration, mediaType === 'video', selectedIndexingMode);
1641
+ const activeAssets = assets.filter(isActiveJob);
1642
+ const libraryPageSize = 12;
1643
+ const libraryAssets = assets.filter((asset) => {
1644
+ const matchesType = asset.type === TAB_TO_TYPE[libraryTab];
1645
+ const matchesSearch = asset.fileName.toLowerCase().includes(librarySearch.trim().toLowerCase());
1646
+ return matchesType && matchesSearch;
1647
+ });
1648
+ const libraryTotalPages = Math.max(1, Math.ceil(libraryAssets.length / libraryPageSize));
1649
+ const safeLibraryPage = Math.min(libraryPage, libraryTotalPages - 1);
1650
+ const paginatedLibraryAssets = libraryAssets.slice(
1651
+ safeLibraryPage * libraryPageSize,
1652
+ (safeLibraryPage + 1) * libraryPageSize,
1653
+ );
1654
+ const libraryStart = libraryAssets.length === 0 ? 0 : safeLibraryPage * libraryPageSize + 1;
1655
+ const libraryEnd = Math.min((safeLibraryPage + 1) * libraryPageSize, libraryAssets.length);
1656
+ const hasUrls = parsedUrls().length > 0;
1657
+ const videoConfigurationLoading = mediaType === 'video' && !indexingSurface;
1658
+
1659
+ function submitUrlMedia() {
1660
+ const totalEntries =
1661
+ remoteEstimates?.reduce((sum: number, item: any) => sum + (item.entryCount ?? 1), 0) ?? 0;
1662
+ if (totalEntries > 1) {
1663
+ setPlaylistAlertOpen(true);
1664
+ } else {
1665
+ beginUrlIndexing();
1666
+ }
1667
+ }
1668
+
1669
+ useEffect(() => {
1670
+ const fromUrl = entryTab === 'url';
1671
+ onActionChange?.({
1672
+ label: fromUrl ? (remoteEstimates ? 'Add media' : 'Check URL') : 'Add media',
1673
+ onClick: () => {
1674
+ if (fromUrl) {
1675
+ if (remoteEstimates) submitUrlMedia();
1676
+ else void reviewRemoteEstimate();
1677
+ } else {
1678
+ beginUpload();
1679
+ }
1680
+ },
1681
+ disabled: fromUrl
1682
+ ? !hasUrls ||
1683
+ (remoteEstimates !== null &&
1684
+ remoteType === 'video' &&
1685
+ (!indexingSurface || videoIndexingAtCapacity))
1686
+ : staged.length === 0 ||
1687
+ videoConfigurationLoading ||
1688
+ (mediaType === 'video' && videoIndexingAtCapacity),
1689
+ loading: fromUrl ? checkingUrls || importingUrls : uploading,
1690
+ });
1691
+ return () => onActionChange?.(null);
1692
+ }, [
1693
+ entryTab,
1694
+ remoteEstimates,
1695
+ hasUrls,
1696
+ urlsText,
1697
+ checkingUrls,
1698
+ importingUrls,
1699
+ staged,
1700
+ uploading,
1701
+ mediaType,
1702
+ remoteType,
1703
+ groupId,
1704
+ indexingSurface,
1705
+ videoIndexingAtCapacity,
1706
+ onActionChange,
1707
+ ]);
1708
+
1709
+ return (
1710
+ <div className="space-y-4">
1711
+ <ToolIndexingDialog
1712
+ surface={indexingSurface ?? null}
1713
+ open={indexingBriefOpen}
1714
+ values={toolIndexingValues}
1715
+ submitting={uploading || importingUrls}
1716
+ sourceDurationSecs={indexingDuration || undefined}
1717
+ onOpenChange={(open) => {
1718
+ setIndexingBriefOpen(open);
1719
+ if (!open) {
1720
+ setIndexingAssetIds(null);
1721
+ setUrlIndexingRequest(null);
1722
+ }
1723
+ }}
1724
+ onChange={(key, value) =>
1725
+ setToolIndexingValues((current) => ({ ...current, [key]: value }))
1726
+ }
1727
+ onConfirm={() =>
1728
+ void (indexingAssetIds
1729
+ ? confirmExistingIndexing()
1730
+ : urlIndexingRequest
1731
+ ? confirmUrlIndexing()
1732
+ : uploadAll())
1733
+ }
1734
+ />
1735
+ {progress && (
1736
+ <div className="space-y-1.5 rounded-lg border border-border bg-muted/20 px-3 py-2.5">
1737
+ <div className="flex items-center justify-between text-[11px] text-muted-foreground tabular-nums">
1738
+ <span>{progress.message || `Processing ${progress.current} of ${progress.total}`}</span>
1739
+ <span>{Math.round((progress.current / Math.max(progress.total, 1)) * 100)}%</span>
1740
+ </div>
1741
+ <Progress
1742
+ value={(progress.current / Math.max(progress.total, 1)) * 100}
1743
+ className="h-1.5"
1744
+ />
1745
+ </div>
1746
+ )}
1747
+
1748
+ {activeAssets.length > 0 ? (
1749
+ <ActiveIndexingList
1750
+ assets={activeAssets}
1751
+ onPause={togglePause}
1752
+ onRemove={setAssetToRemove}
1753
+ />
1754
+ ) : null}
1755
+
1756
+ {entryTab === 'url' ? (
1757
+ <div className="w-full">
1758
+ <div className="flex items-center gap-2">
1759
+ <div className="relative flex-1">
1760
+ <Input
1761
+ ref={urlInputRef}
1762
+ aria-label="Import media URL"
1763
+ autoComplete="off"
1764
+ value={urlsText}
1765
+ onChange={(event) => {
1766
+ setUrlsText(event.target.value);
1767
+ setRemoteEstimates(null);
1768
+ setShowRecentUrls(true);
1769
+ }}
1770
+ onFocus={() => setShowRecentUrls(true)}
1771
+ placeholder="Paste an image, video, or audio URL"
1772
+ className="h-10 bg-white pr-8 text-xs"
1773
+ />
1774
+ {showRecentUrls && recentUrls.length > 0 && (
1775
+ <div
1776
+ ref={recentUrlsRef}
1777
+ className="absolute top-full z-20 mt-1 w-full overflow-hidden rounded-md border border-border bg-popover"
1778
+ >
1779
+ <p className="px-3 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
1780
+ Recent URLs
1781
+ </p>
1782
+ <div className="max-h-52 overflow-y-auto py-1">
1783
+ {recentUrls.map(({ url, title }) => (
1784
+ <div
1785
+ key={url}
1786
+ className="group flex cursor-pointer items-center gap-2 px-3 py-1.5 hover:bg-muted"
1787
+ onClick={() => {
1788
+ setUrlsText(url);
1789
+ setRemoteEstimates(null);
1790
+ setShowRecentUrls(false);
1791
+ }}
1792
+ >
1793
+ <Clock
1794
+ className="size-3 shrink-0 text-muted-foreground"
1795
+ aria-hidden="true"
1796
+ />
1797
+ <span className="min-w-0 flex-1">
1798
+ <span className="block truncate text-xs text-foreground">
1799
+ {title || url}
1800
+ </span>
1801
+ {title ? (
1802
+ <span className="block truncate text-[10px] text-muted-foreground">
1803
+ {url}
1804
+ </span>
1805
+ ) : null}
1806
+ </span>
1807
+ <button
1808
+ type="button"
1809
+ aria-label={`Remove ${url} from recent URLs`}
1810
+ className="rounded p-0.5 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100 focus:opacity-100"
1811
+ onClick={(event) => removeRecentUrl(url, event)}
1812
+ >
1813
+ <X className="size-3" aria-hidden="true" />
1814
+ </button>
1815
+ </div>
1816
+ ))}
1817
+ </div>
1818
+ </div>
1819
+ )}
1820
+ </div>
1821
+ {!onActionChange && (
1822
+ <Button
1823
+ type="button"
1824
+ variant="outline"
1825
+ size="sm"
1826
+ className="h-9 text-xs"
1827
+ disabled={checkingUrls || importingUrls || parsedUrls().length === 0}
1828
+ onClick={reviewRemoteEstimate}
1829
+ >
1830
+ {checkingUrls ? (
1831
+ <Loader2 className="mr-1.5 size-3 animate-spin" />
1832
+ ) : (
1833
+ <Clock className="mr-1.5 size-3" />
1834
+ )}
1835
+ Review
1836
+ </Button>
1837
+ )}
1838
+ {!onActionChange && (
1839
+ <Button
1840
+ type="button"
1841
+ size="sm"
1842
+ className="h-9 text-xs"
1843
+ disabled={!remoteEstimates || importingUrls}
1844
+ onClick={() => {
1845
+ const totalEntries =
1846
+ remoteEstimates?.reduce(
1847
+ (sum: number, item: any) => sum + (item.entryCount ?? 1),
1848
+ 0,
1849
+ ) ?? 0;
1850
+ if (totalEntries > 1) {
1851
+ setPlaylistAlertOpen(true);
1852
+ } else {
1853
+ beginUrlIndexing();
1854
+ }
1855
+ }}
1856
+ >
1857
+ {importingUrls ? (
1858
+ <Loader2 className="mr-1.5 size-3 animate-spin" />
1859
+ ) : (
1860
+ <Sparkles className="mr-1.5 size-3" />
1861
+ )}
1862
+ Import
1863
+ </Button>
1864
+ )}
1865
+ </div>
1866
+
1867
+ <AlertDialog open={playlistAlertOpen} onOpenChange={setPlaylistAlertOpen}>
1868
+ <AlertDialogContent>
1869
+ <AlertDialogHeader>
1870
+ <AlertDialogTitle>Playlist Detected</AlertDialogTitle>
1871
+ <AlertDialogDescription>
1872
+ You entered a URL that points to a playlist. Do you want to download and index the
1873
+ entire playlist, or just the single video?
1874
+ </AlertDialogDescription>
1875
+ </AlertDialogHeader>
1876
+ <AlertDialogFooter>
1877
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
1878
+ <Button
1879
+ variant="outline"
1880
+ onClick={() => {
1881
+ setPlaylistAlertOpen(false);
1882
+ beginUrlIndexing(true);
1883
+ }}
1884
+ >
1885
+ Single Video
1886
+ </Button>
1887
+ <AlertDialogAction
1888
+ onClick={() => {
1889
+ setPlaylistAlertOpen(false);
1890
+ beginUrlIndexing(false);
1891
+ }}
1892
+ >
1893
+ Entire Playlist
1894
+ </AlertDialogAction>
1895
+ </AlertDialogFooter>
1896
+ </AlertDialogContent>
1897
+ </AlertDialog>
1898
+ <p className="mt-2 pl-1 text-[11px] leading-relaxed text-muted-foreground">
1899
+ Direct media links, YouTube videos, or playlists. Add up to 10 URLs separated by commas.
1900
+ </p>
1901
+ {remoteEstimates
1902
+ ? (() => {
1903
+ const totalEntries = remoteEstimates.reduce(
1904
+ (sum: number, item: any) => sum + (item.entryCount ?? 1),
1905
+ 0,
1906
+ );
1907
+ const isPlaylist = totalEntries > 1;
1908
+ return (
1909
+ <div className="mt-4 flex flex-col gap-4 border-t border-border/70 pt-4">
1910
+ {/* <div className="grid gap-2 sm:grid-cols-3">
1911
+ <EstimateMetric
1912
+ label="Media"
1913
+ value={`${totalEntries} item(s) · ${
1914
+ remoteDuration ? formatDuration(remoteDuration) : 'duration unavailable'
1915
+ }`}
1916
+ />
1917
+ <EstimateMetric
1918
+ label="Expected time"
1919
+ value={
1920
+ remoteDuration
1921
+ ? `~${remoteEstimate.processingMinutes} min`
1922
+ : 'Calculated during import'
1923
+ }
1924
+ />
1925
+ <EstimateMetric
1926
+ label="Estimated API cost"
1927
+ value={
1928
+ remoteDuration
1929
+ ? `~$${remoteEstimate.transcriptionCost.toFixed(2)} +`
1930
+ : 'Model-dependent'
1931
+ }
1932
+ />
1933
+ </div> */}
1934
+
1935
+ {/* Playlist warning moved to AlertDialog */}
1936
+
1937
+ {(mediaType === 'audio'
1938
+ ? remoteEstimates.slice(0, 1)
1939
+ : remoteEstimates.filter((est: any) => est.mediaType === 'video').slice(0, 1)
1940
+ ).map((est: any, i: number) => {
1941
+ if (mediaType === 'audio') {
1942
+ return (
1943
+ <div
1944
+ key={i}
1945
+ className="rounded-xl border border-border/70 bg-muted/20 p-3"
1946
+ >
1947
+ <p className="mb-2 truncate text-[11px] font-medium text-foreground">
1948
+ Preview before importing
1949
+ </p>
1950
+ <audio
1951
+ controls
1952
+ preload="metadata"
1953
+ src={est.originalUrl}
1954
+ className="h-9 w-full"
1955
+ />
1956
+ </div>
1957
+ );
1958
+ }
1959
+ if (est.isYouTube) {
1960
+ return <YouTubeUrlPreview key={i} estimate={est} />;
1961
+ } else {
1962
+ return (
1963
+ <video
1964
+ key={i}
1965
+ src={est.originalUrl}
1966
+ controls
1967
+ className="w-full max-w-sm rounded-md aspect-video bg-muted object-cover 35 border border-border"
1968
+ />
1969
+ );
1970
+ }
1971
+ })}
1972
+ </div>
1973
+ );
1974
+ })()
1975
+ : null}
1976
+ </div>
1977
+ ) : (
1978
+ <div
1979
+ onDragOver={(e) => {
1980
+ e.preventDefault();
1981
+ setDragging(true);
1982
+ }}
1983
+ onDragLeave={(e) => {
1984
+ e.preventDefault();
1985
+ setDragging(false);
1986
+ }}
1987
+ onDrop={(e) => {
1988
+ e.preventDefault();
1989
+ setDragging(false);
1990
+ if (e.dataTransfer.files.length > 0) handleFiles(e.dataTransfer.files);
1991
+ }}
1992
+ onClick={() => inputRef.current?.click()}
1993
+ className={cn(
1994
+ 'flex flex-col items-center justify-center gap-2.5 rounded-xl border-2 border-dashed px-6 py-10 cursor-pointer transition-colors',
1995
+ dragging
1996
+ ? 'border-primary bg-primary/5'
1997
+ : 'border-border hover:border-primary/40 hover:bg-muted/20',
1998
+ )}
1999
+ >
2000
+ <Upload className="size-5 text-muted-foreground" />
2001
+ <div className="text-center">
2002
+ <p className="text-[13px] font-medium text-foreground">
2003
+ Drop media files here or click to browse
2004
+ </p>
2005
+ <p className="mt-0.5 text-[11px] text-muted-foreground">Images, Video, Audio</p>
2006
+ </div>
2007
+ <input
2008
+ ref={inputRef}
2009
+ type="file"
2010
+ multiple
2011
+ accept={accept}
2012
+ className="hidden"
2013
+ onChange={(e) => e.target.files && handleFiles(e.target.files)}
2014
+ />
2015
+ </div>
2016
+ )}
2017
+
2018
+ {/* Staged files */}
2019
+ {staged.length > 0 && (
2020
+ <div className="space-y-3">
2021
+ {mediaType !== 'image' && stagedDuration > 0 ? (
2022
+ <div className="grid gap-2 rounded-lg border border-border bg-muted/15 px-3 py-2.5 sm:grid-cols-3">
2023
+ <EstimateMetric label="Duration" value={formatDuration(stagedDuration)} />
2024
+ <EstimateMetric
2025
+ label="Expected time"
2026
+ value={`~${formatEstimateRange(stagedEstimate)}`}
2027
+ />
2028
+ <EstimateMetric
2029
+ label="Estimated API cost"
2030
+ value={`~$${stagedEstimate.transcriptionCost.toFixed(2)}${
2031
+ mediaType === 'video'
2032
+ ? ` + up to ${stagedEstimate.visualScenes} vision calls`
2033
+ : ''
2034
+ }`}
2035
+ />
2036
+ </div>
2037
+ ) : null}
2038
+
2039
+ {indexingSurface && staged.length > 0 ? (
2040
+ <button
2041
+ type="button"
2042
+ onClick={() => setIndexingBriefOpen(true)}
2043
+ className="flex w-full items-center justify-between rounded-lg border border-border bg-muted/10 px-3 py-3 text-left hover:bg-muted/30"
2044
+ >
2045
+ <span>
2046
+ <span className="block text-xs font-medium">{indexingSurface.title}</span>
2047
+ <span className="mt-0.5 block text-[11px] text-muted-foreground">
2048
+ {typeof toolIndexingValues.goal === 'string' && toolIndexingValues.goal.trim()
2049
+ ? 'Custom indexing instructions are ready.'
2050
+ : (indexingSurface.description ?? 'Configure this tool before indexing.')}
2051
+ </span>
2052
+ </span>
2053
+ <Sparkles className="size-4 text-primary" />
2054
+ </button>
2055
+ ) : null}
2056
+
2057
+ <div className="flex items-center justify-between">
2058
+ <span className="text-[13px] font-medium text-foreground">
2059
+ {staged.length} file{staged.length !== 1 ? 's' : ''} ready
2060
+ </span>
2061
+ <div className="flex items-center gap-3">
2062
+ <button
2063
+ type="button"
2064
+ onClick={() => {
2065
+ staged.forEach((f) => {
2066
+ if (f.preview) URL.revokeObjectURL(f.preview);
2067
+ });
2068
+ setStaged([]);
2069
+ }}
2070
+ className="text-[11px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
2071
+ >
2072
+ Clear All
2073
+ </button>
2074
+ {!onActionChange && (
2075
+ <Button
2076
+ onClick={beginUpload}
2077
+ disabled={uploading || staged.length === 0}
2078
+ size="sm"
2079
+ className="h-7 text-xs px-3"
2080
+ >
2081
+ {uploading ? (
2082
+ <Loader2 className="size-3 animate-spin mr-1.5" />
2083
+ ) : (
2084
+ <FileUp className="size-3 mr-1.5" />
2085
+ )}
2086
+ Upload {staged.length} file{staged.length !== 1 ? 's' : ''}
2087
+ </Button>
2088
+ )}
2089
+ </div>
2090
+ </div>
2091
+
2092
+ <div className="max-h-87.5 overflow-y-auto pr-1">
2093
+ {/* Grid preview for staged images */}
2094
+ {mediaType === 'image' ? (
2095
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
2096
+ {staged.slice(0, 8).map((item) => (
2097
+ <div
2098
+ key={item.id}
2099
+ className="group relative aspect-4/3 rounded-xl overflow-hidden bg-muted/30 border border-border/50"
2100
+ >
2101
+ {item.preview && (
2102
+ <img
2103
+ src={item.preview}
2104
+ alt={item.file.name}
2105
+ className="size-full object-cover transition-transform duration-300 group-hover:scale-105"
2106
+ />
2107
+ )}
2108
+ <button
2109
+ type="button"
2110
+ onClick={(e) => {
2111
+ e.stopPropagation();
2112
+ removeFile(item.id);
2113
+ }}
2114
+ className="absolute right-2 top-2 rounded-full bg-black/50 p-1.5 text-white opacity-0 group-hover:opacity-100 transition-opacity"
2115
+ >
2116
+ <X className="size-3.5" />
2117
+ </button>
2118
+ </div>
2119
+ ))}
2120
+ {staged.length > 8 && (
2121
+ <div className="flex items-center justify-center rounded-xl bg-muted/30 border border-border/50 text-muted-foreground text-sm font-medium aspect-4/3">
2122
+ +{staged.length - 8} more
2123
+ </div>
2124
+ )}
2125
+ </div>
2126
+ ) : (
2127
+ <div className="flex flex-col gap-1.5">
2128
+ {staged.slice(0, 10).map((item) => {
2129
+ const Icon = mediaType === 'video' ? Video : AudioLines;
2130
+ return (
2131
+ <div
2132
+ key={item.id}
2133
+ className="flex items-center gap-3 rounded-lg border border-border px-3 py-2 group"
2134
+ >
2135
+ <div className="flex size-8 items-center justify-center rounded-md bg-muted">
2136
+ <Icon className="size-3.5 text-muted-foreground" />
2137
+ </div>
2138
+ <div className="min-w-0 flex-1">
2139
+ <p className="truncate text-[12px] font-medium text-foreground">
2140
+ {item.file.name}
2141
+ </p>
2142
+ <p className="text-[10px] text-muted-foreground">
2143
+ {formatSize(item.file.size)}
2144
+ </p>
2145
+ </div>
2146
+ {mediaType === 'audio' && item.preview ? (
2147
+ <audio
2148
+ aria-label={`Preview ${item.file.name}`}
2149
+ controls
2150
+ preload="metadata"
2151
+ src={item.preview}
2152
+ className="h-8 max-w-44"
2153
+ onClick={(event) => event.stopPropagation()}
2154
+ />
2155
+ ) : null}
2156
+ <button
2157
+ type="button"
2158
+ onClick={() => removeFile(item.id)}
2159
+ className="opacity-0 group-hover:opacity-100 p-1 text-muted-foreground hover:text-foreground transition-all"
2160
+ >
2161
+ <X className="size-3" />
2162
+ </button>
2163
+ </div>
2164
+ );
2165
+ })}
2166
+ {staged.length > 10 && (
2167
+ <div className="flex items-center justify-center py-2 text-xs text-muted-foreground border border-dashed rounded-lg bg-muted/10">
2168
+ +{staged.length - 10} more files
2169
+ </div>
2170
+ )}
2171
+ </div>
2172
+ )}
2173
+ </div>
2174
+ </div>
2175
+ )}
2176
+
2177
+ {/* Gallery of uploaded assets */}
2178
+ {isLoading && assets.length === 0 ? (
2179
+ <div className="flex justify-center py-10">
2180
+ <Loader2 className="size-4 animate-spin text-muted-foreground" />
2181
+ </div>
2182
+ ) : assets.length > 0 ? (
2183
+ <div className="space-y-2">
2184
+ <div className="flex items-center justify-between py-2 px-3 rounded-lg border border-border bg-muted/20">
2185
+ <h4 className="text-[13px] font-medium text-foreground">
2186
+ {assets.length} Uploaded Media
2187
+ </h4>
2188
+ <Dialog>
2189
+ <DialogTrigger
2190
+ render={
2191
+ <Button variant="outline" size="sm" className="h-7 text-xs px-3">
2192
+ View Uploads
2193
+ </Button>
2194
+ }
2195
+ />
2196
+ <DialogContent className="h-[calc(100vh-2rem)] max-h-190 w-[calc(100vw-2rem)] sm:max-w-5xl lg:max-w-6xl grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden p-0 bg-background">
2197
+ <DialogHeader className="gap-3 px-5 py-4 sm:px-6">
2198
+ <div>
2199
+ <DialogTitle>Uploaded media</DialogTitle>
2200
+ <p className="mt-1 text-xs text-muted-foreground">
2201
+ Browse, search, and manage every media upload in one place.
2202
+ </p>
2203
+ </div>
2204
+ <div className="relative w-full">
2205
+ <Search
2206
+ aria-hidden="true"
2207
+ className="pointer-events-none absolute left-3 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground"
2208
+ />
2209
+ <Input
2210
+ aria-label="Search uploaded media"
2211
+ value={librarySearch}
2212
+ onChange={(event) => {
2213
+ setLibrarySearch(event.target.value);
2214
+ setLibraryPage(0);
2215
+ }}
2216
+ placeholder="Search uploads"
2217
+ className="h-9 w-full pl-9 text-sm bg-card/90"
2218
+ />
2219
+ </div>
2220
+ <div className="flex flex-wrap items-center gap-2">
2221
+ {SUB_TABS.map((subTab) => {
2222
+ const Icon = subTab.icon;
2223
+ const isActive = libraryTab === subTab.id;
2224
+ const count = assets.filter(
2225
+ (asset) => asset.type === TAB_TO_TYPE[subTab.id],
2226
+ ).length;
2227
+ return (
2228
+ <button
2229
+ key={subTab.id}
2230
+ type="button"
2231
+ onClick={() => {
2232
+ setLibraryTab(subTab.id);
2233
+ setLibraryPage(0);
2234
+ }}
2235
+ className={cn(
2236
+ 'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors',
2237
+ isActive
2238
+ ? 'bg-muted text-foreground'
2239
+ : 'border border-border bg-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground',
2240
+ )}
2241
+ >
2242
+ <Icon className="size-3.5" strokeWidth={isActive ? 2 : 1.75} />
2243
+ {subTab.label}
2244
+ <span className="text-[10px] tabular-nums text-muted-foreground">
2245
+ {count}
2246
+ </span>
2247
+ </button>
2248
+ );
2249
+ })}
2250
+ </div>
2251
+ </DialogHeader>
2252
+ <div className="min-h-0 overflow-y-auto px-5 py-4 sm:px-6">
2253
+ {libraryAssets.length === 0 ? (
2254
+ <div className="flex min-h-48 flex-col items-center justify-center rounded-lg bg-muted/20 px-6 text-center">
2255
+ <Search className="size-4 text-muted-foreground" />
2256
+ <p className="mt-3 text-sm font-medium text-foreground">No uploads found</p>
2257
+ <p className="mt-1 text-xs text-muted-foreground">
2258
+ {librarySearch
2259
+ ? `No ${SUB_TABS.find(
2260
+ (tab) => tab.id === libraryTab,
2261
+ )?.label.toLowerCase()} match “${librarySearch}”.`
2262
+ : `There are no ${SUB_TABS.find(
2263
+ (tab) => tab.id === libraryTab,
2264
+ )?.label.toLowerCase()} uploads yet.`}
2265
+ </p>
2266
+ </div>
2267
+ ) : libraryTab === 'images' ? (
2268
+ <ImageGallery
2269
+ assets={paginatedLibraryAssets}
2270
+ onDelete={(id) =>
2271
+ setAssetToRemove(assets.find((asset) => asset.id === id) ?? null)
2272
+ }
2273
+ serverId={serverId}
2274
+ />
2275
+ ) : (
2276
+ <FileList
2277
+ assets={paginatedLibraryAssets}
2278
+ mediaType={TAB_TO_TYPE[libraryTab] as 'video' | 'audio'}
2279
+ onDelete={(id) =>
2280
+ setAssetToRemove(assets.find((asset) => asset.id === id) ?? null)
2281
+ }
2282
+ onProcess={handleRetry}
2283
+ />
2284
+ )}
2285
+ </div>
2286
+ {libraryAssets.length > 0 ? (
2287
+ <div className="flex items-center justify-between border-t px-5 py-3 text-xs text-muted-foreground sm:px-6">
2288
+ <span className="tabular-nums">
2289
+ {libraryStart}–{libraryEnd} of {libraryAssets.length}
2290
+ </span>
2291
+ <div className="flex items-center gap-1.5">
2292
+ <Button
2293
+ type="button"
2294
+ variant="ghost"
2295
+ size="icon-sm"
2296
+ aria-label="Previous uploads page"
2297
+ disabled={safeLibraryPage === 0}
2298
+ onClick={() => setLibraryPage((page) => Math.max(0, page - 1))}
2299
+ >
2300
+ <ChevronLeft className="size-4" />
2301
+ </Button>
2302
+ <span className="min-w-12 text-center tabular-nums">
2303
+ {safeLibraryPage + 1} / {libraryTotalPages}
2304
+ </span>
2305
+ <Button
2306
+ type="button"
2307
+ variant="ghost"
2308
+ size="icon-sm"
2309
+ aria-label="Next uploads page"
2310
+ disabled={safeLibraryPage >= libraryTotalPages - 1}
2311
+ onClick={() =>
2312
+ setLibraryPage((page) => Math.min(libraryTotalPages - 1, page + 1))
2313
+ }
2314
+ >
2315
+ <ChevronRight className="size-4" />
2316
+ </Button>
2317
+ </div>
2318
+ </div>
2319
+ ) : null}
2320
+ </DialogContent>
2321
+ </Dialog>
2322
+ </div>
2323
+ </div>
2324
+ ) : staged.length === 0 ? (
2325
+ <div className="py-10 text-center">
2326
+ <p className="text-[13px] text-muted-foreground">No media files yet</p>
2327
+ </div>
2328
+ ) : null}
2329
+
2330
+ <AlertDialog
2331
+ open={Boolean(assetToRemove)}
2332
+ onOpenChange={(open) => !open && setAssetToRemove(null)}
2333
+ >
2334
+ <AlertDialogContent>
2335
+ <AlertDialogHeader>
2336
+ <AlertDialogTitle>Remove this {assetToRemove?.type ?? 'media'} file?</AlertDialogTitle>
2337
+ <AlertDialogDescription>
2338
+ {assetToRemove && isActiveJob(assetToRemove)
2339
+ ? 'This will stop its indexing job and permanently remove any progress already made.'
2340
+ : 'This permanently removes the file and its searchable data from your knowledge base.'}
2341
+ </AlertDialogDescription>
2342
+ </AlertDialogHeader>
2343
+ <AlertDialogFooter>
2344
+ <AlertDialogCancel>Keep file</AlertDialogCancel>
2345
+ <AlertDialogAction
2346
+ className="bg-destructive text-white hover:bg-destructive/90"
2347
+ onClick={() => {
2348
+ if (assetToRemove) void handleDelete(assetToRemove.id);
2349
+ setAssetToRemove(null);
2350
+ }}
2351
+ >
2352
+ Remove file
2353
+ </AlertDialogAction>
2354
+ </AlertDialogFooter>
2355
+ </AlertDialogContent>
2356
+ </AlertDialog>
2357
+
2358
+ {/* Storage warning */}
2359
+ {/* {storageUsedBytes > 1024 * 1024 * 1024 && (
2360
+ <div className="flex items-start gap-2.5 rounded-lg border border-amber-200 bg-amber-50 px-3.5 py-3">
2361
+ <AlertCircle className="size-4 mt-0.5 shrink-0 text-amber-600" />
2362
+ <div>
2363
+ <p className="text-[12px] font-medium text-amber-800">
2364
+ Storage usage: {formatSize(storageUsedBytes)}
2365
+ </p>
2366
+ <p className="mt-0.5 text-[11px] text-amber-600">
2367
+ Media files are stored locally. Cloud storage support coming soon.
2368
+ </p>
2369
+ </div>
2370
+ </div>
2371
+ )} */}
2372
+ </div>
2373
+ );
2374
+ }
2375
+
2376
+ function ImageGallery({
2377
+ assets,
2378
+ onDelete,
2379
+ serverId,
2380
+ }: {
2381
+ assets: MediaAsset[];
2382
+ onDelete: (id: string) => void;
2383
+ serverId?: string;
2384
+ }) {
2385
+ const [expandedAsset, setExpandedAsset] = useState<MediaAsset | null>(null);
2386
+
2387
+ return (
2388
+ <div>
2389
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
2390
+ {assets.map((asset) => (
2391
+ <div
2392
+ key={asset.id}
2393
+ onClick={() => setExpandedAsset(asset)}
2394
+ className="group relative aspect-2/1 sm:aspect-21/9 rounded-xl overflow-hidden bg-muted/30 border border-border/50 cursor-pointer"
2395
+ >
2396
+ <img
2397
+ src={mediaAssetUrl(asset.id, serverId, true)}
2398
+ alt={asset.fileName}
2399
+ className="size-full object-cover transition-transform duration-300 group-hover:scale-105"
2400
+ loading="lazy"
2401
+ onError={(e) => {
2402
+ (e.target as HTMLImageElement).src = mediaAssetUrl(asset.id, serverId);
2403
+ }}
2404
+ />
2405
+
2406
+ {/* Processing status overlay */}
2407
+ {asset.processingStatus === 'processing' && (
2408
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-black/50 backdrop-blur-sm p-4 text-center">
2409
+ <Loader2 className="size-5 animate-spin text-white mb-2" />
2410
+ <span className="text-[10px] text-white/90 line-clamp-2 leading-tight">
2411
+ {asset.processingMessage || 'Processing...'}
2412
+ </span>
2413
+ <Progress
2414
+ value={asset.processingProgress ?? 0}
2415
+ className={cn('mt-2 h-1.5 w-4/5 bg-white/20', INDEXING_PROGRESS_CLASS)}
2416
+ />
2417
+ <span className="mt-1 text-[9px] tabular-nums text-white/75">
2418
+ {Math.round(asset.processingProgress ?? 0)}%
2419
+ </span>
2420
+ </div>
2421
+ )}
2422
+ {asset.processingStatus === 'failed' && (
2423
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-black/50 backdrop-blur-sm p-4 text-center">
2424
+ <AlertCircle className="size-5 text-red-400 mb-2" />
2425
+ <span className="text-[10px] text-red-200 line-clamp-2 leading-tight">
2426
+ {asset.processingError || 'Failed'}
2427
+ </span>
2428
+ </div>
2429
+ )}
2430
+ {asset.processingStatus === 'pending' && (
2431
+ <div className="absolute bottom-2 right-2 bg-black/50 backdrop-blur-sm px-2 py-1 rounded-sm flex items-center gap-1.5 max-w-[80%]">
2432
+ <Clock className="size-3 text-white/80 shrink-0" />
2433
+ <span className="text-[9px] text-white/90 truncate">
2434
+ {asset.processingMessage || 'Pending...'}
2435
+ </span>
2436
+ </div>
2437
+ )}
2438
+ {asset.processingStatus === 'completed' && asset.activeVideoKnowledgeRevisionId ? (
2439
+ <div className="absolute bottom-2 left-2 rounded-sm bg-emerald-600/85 px-2 py-1 text-[9px] text-white">
2440
+ Evidence revision active
2441
+ </div>
2442
+ ) : null}
2443
+
2444
+ {/* Hover actions */}
2445
+ <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors pointer-events-none">
2446
+ {!isActiveJob(asset) ? (
2447
+ <button
2448
+ type="button"
2449
+ aria-label={`Delete ${asset.fileName}`}
2450
+ onClick={(e) => {
2451
+ e.stopPropagation();
2452
+ onDelete(asset.id);
2453
+ }}
2454
+ className="absolute right-2 top-2 rounded-full bg-black/50 p-1.5 text-white opacity-0 group-hover:opacity-100 transition-opacity pointer-events-auto hover:bg-black/70"
2455
+ >
2456
+ <Trash2 className="size-3.5" />
2457
+ </button>
2458
+ ) : null}
2459
+ </div>
2460
+ </div>
2461
+ ))}
2462
+ </div>
2463
+
2464
+ {/* Lightbox Overlay */}
2465
+ {expandedAsset && (
2466
+ <div
2467
+ className="fixed inset-0 z-9999 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 md:p-8 animate-in fade-in duration-200"
2468
+ onClick={() => setExpandedAsset(null)}
2469
+ >
2470
+ <div className="relative max-w-full max-h-full">
2471
+ <img
2472
+ src={mediaAssetUrl(expandedAsset.id, serverId)}
2473
+ alt={expandedAsset.fileName}
2474
+ className="max-w-full max-h-[90vh] rounded-lg object-contain"
2475
+ onClick={(e) => e.stopPropagation()} // Prevent click from closing immediately
2476
+ />
2477
+ <button
2478
+ onClick={() => setExpandedAsset(null)}
2479
+ className="absolute -top-3 -right-3 md:-top-5 md:-right-5 rounded-full bg-black/50 border border-white/20 p-2 text-white hover:bg-black/80 transition-colors"
2480
+ >
2481
+ <X className="size-5" />
2482
+ </button>
2483
+ <div className="absolute bottom-0 inset-x-0 p-4 bg-linear-to-t from-black/80 to-transparent text-white rounded-b-lg opacity-0 hover:opacity-100 transition-opacity flex justify-between items-end">
2484
+ <div>
2485
+ <p className="text-sm font-medium">{expandedAsset.fileName}</p>
2486
+ <p className="text-xs text-white/70 mt-1">{formatSize(expandedAsset.fileSize)}</p>
2487
+ </div>
2488
+ </div>
2489
+ </div>
2490
+ </div>
2491
+ )}
2492
+ </div>
2493
+ );
2494
+ }
2495
+
2496
+ function FileList({
2497
+ assets,
2498
+ mediaType,
2499
+ onDelete,
2500
+ onProcess,
2501
+ }: {
2502
+ assets: MediaAsset[];
2503
+ mediaType: 'video' | 'audio';
2504
+ onDelete: (id: string) => void;
2505
+ onProcess: (asset: MediaAsset) => void;
2506
+ }) {
2507
+ const Icon = mediaType === 'video' ? Video : AudioLines;
2508
+
2509
+ return (
2510
+ <div className="flex flex-col gap-1.5">
2511
+ {assets.map((asset) => (
2512
+ <div
2513
+ key={asset.id}
2514
+ className="flex items-center gap-3 rounded-lg border border-border px-3 py-2.5 group"
2515
+ >
2516
+ <div className="flex size-9 items-center justify-center rounded-md bg-muted shrink-0">
2517
+ <Icon className="size-4 text-muted-foreground" />
2518
+ </div>
2519
+ <div className="min-w-0 flex-1">
2520
+ <p className="truncate text-[12px] font-medium text-foreground">{asset.fileName}</p>
2521
+ <div className="flex items-center gap-2 mt-0.5">
2522
+ <span className="text-[10px] text-muted-foreground">
2523
+ {formatSize(asset.fileSize)}
2524
+ </span>
2525
+ {asset.durationSecs ? (
2526
+ <span className="text-[10px] text-muted-foreground">
2527
+ · {Math.floor(asset.durationSecs / 60)}:
2528
+ {String(Math.floor(asset.durationSecs % 60)).padStart(2, '0')}
2529
+ </span>
2530
+ ) : null}
2531
+ <StatusBadge asset={asset} />
2532
+ {mediaType === 'video' ? <IndexingTime asset={asset} /> : null}
2533
+ </div>
2534
+ {isActiveJob(asset) ? (
2535
+ <Progress
2536
+ value={asset.processingProgress ?? 0}
2537
+ aria-label="Overall indexing progress"
2538
+ className={cn('mt-2 h-1', INDEXING_PROGRESS_CLASS)}
2539
+ />
2540
+ ) : null}
2541
+ </div>
2542
+ <div className="flex items-center gap-1">
2543
+ {asset.processingStatus === 'failed' ? (
2544
+ <Button
2545
+ type="button"
2546
+ variant="secondary"
2547
+ size="sm"
2548
+ className="h-7 px-2 text-[11px]"
2549
+ onClick={() => onProcess(asset)}
2550
+ >
2551
+ Retry
2552
+ </Button>
2553
+ ) : null}
2554
+ {asset.processingStatus === 'completed' ? (
2555
+ <Button
2556
+ type="button"
2557
+ variant="ghost"
2558
+ size="sm"
2559
+ className="h-7 px-2 text-[11px] text-muted-foreground"
2560
+ onClick={() => onProcess(asset)}
2561
+ >
2562
+ Re-index
2563
+ </Button>
2564
+ ) : null}
2565
+ {!isActiveJob(asset) ? (
2566
+ <button
2567
+ type="button"
2568
+ aria-label={`Delete ${asset.fileName}`}
2569
+ onClick={() => onDelete(asset.id)}
2570
+ className="text-red-500 p-1.5 rounded cursor-pointer hover:text-red-600 transition-all"
2571
+ >
2572
+ <Trash2 className="size-3.5" />
2573
+ </button>
2574
+ ) : null}
2575
+ </div>
2576
+ </div>
2577
+ ))}
2578
+ </div>
2579
+ );
2580
+ }
2581
+
2582
+ function IndexingTime({ asset }: { asset: MediaAsset }) {
2583
+ const seconds = completedMediaProcessingSeconds(asset);
2584
+ if (seconds === null) return null;
2585
+
2586
+ const minutes = Math.floor(seconds / 60);
2587
+ const remainder = seconds % 60;
2588
+ const duration = minutes > 0 ? `${minutes}m ${remainder}s` : `${remainder}s`;
2589
+ return (
2590
+ <span
2591
+ className="flex items-center gap-0.5 text-[10px] text-muted-foreground"
2592
+ title="Time taken to index this video"
2593
+ >
2594
+ · <Clock className="size-2.5" /> Indexed in {duration}
2595
+ </span>
2596
+ );
2597
+ }
2598
+
2599
+ function StatusBadge({ asset }: { asset: MediaAsset }) {
2600
+ const status = asset.processingStatus;
2601
+
2602
+ if (status === 'completed') {
2603
+ return (
2604
+ <span className="flex items-center gap-0.5 text-[10px] text-emerald-600">
2605
+ <Check className="size-2.5" />
2606
+ Indexed
2607
+ </span>
2608
+ );
2609
+ }
2610
+ if (status === 'processing') {
2611
+ return (
2612
+ <span className="flex items-center gap-1 text-[10px] text-blue-600 bg-blue-50/50 px-1.5 py-0.5 rounded-sm">
2613
+ <Loader2 className="size-2.5 animate-spin shrink-0" />
2614
+ <span
2615
+ className="truncate max-w-30 sm:max-w-50"
2616
+ title={asset.processingMessage || 'Processing...'}
2617
+ >
2618
+ {asset.processingMessage || 'Processing...'}
2619
+ </span>
2620
+ <span className="tabular-nums">
2621
+ {formatMediaProgressPercent(asset.processingProgress ?? 0)}% overall
2622
+ </span>
2623
+ </span>
2624
+ );
2625
+ }
2626
+ if (status === 'pending' && asset.processingMessage) {
2627
+ return (
2628
+ <span className="flex items-center gap-1 rounded-sm bg-blue-50/50 px-1.5 py-0.5 text-[10px] text-blue-600">
2629
+ <Loader2 className="size-2.5 shrink-0 animate-spin" />
2630
+ <span className="max-w-30 truncate sm:max-w-50" title={asset.processingMessage}>
2631
+ {asset.processingMessage}
2632
+ </span>
2633
+ <span className="tabular-nums">
2634
+ {formatMediaProgressPercent(asset.processingProgress ?? 0)}%
2635
+ </span>
2636
+ </span>
2637
+ );
2638
+ }
2639
+ if (status === 'failed') {
2640
+ return (
2641
+ <span
2642
+ className="flex items-center gap-0.5 text-[10px] text-red-500"
2643
+ title={asset.processingError}
2644
+ >
2645
+ <AlertCircle className="size-2.5 shrink-0" />
2646
+ <span className="truncate max-w-30">{asset.processingError || 'Failed'}</span>
2647
+ </span>
2648
+ );
2649
+ }
2650
+ return (
2651
+ <span className="flex items-center gap-1 text-[10px] text-muted-foreground/60">
2652
+ <Clock className="size-2.5 shrink-0" />
2653
+ <span className="truncate max-w-30" title={asset.processingMessage || 'Pending...'}>
2654
+ {asset.processingMessage || 'Pending...'}
2655
+ </span>
2656
+ </span>
2657
+ );
2658
+ }
2659
+
2660
+ function ActiveIndexingList({
2661
+ assets,
2662
+ onPause,
2663
+ onRemove,
2664
+ }: {
2665
+ assets: MediaAsset[];
2666
+ onPause: (asset: MediaAsset) => void;
2667
+ onRemove: (asset: MediaAsset) => void;
2668
+ }) {
2669
+ return (
2670
+ <section
2671
+ aria-live="polite"
2672
+ aria-label="Active media indexing jobs"
2673
+ className="space-y-3 rounded-xl border border-emerald-500 bg-emerald-50/20 p-3 dark:border-emerald-700/60 dark:bg-emerald-950/20"
2674
+ >
2675
+ <div className="flex items-center gap-2">
2676
+ <Loader2 className="size-3.5 animate-spin text-emerald-600 dark:text-emerald-400" />
2677
+ <div className="min-w-0 flex-1">
2678
+ <p className="text-[12px] font-medium text-foreground">
2679
+ Indexing {assets.length} {assets.length === 1 ? 'file' : 'files'}
2680
+ </p>
2681
+ </div>
2682
+ </div>
2683
+
2684
+ <div className="space-y-2">
2685
+ {assets.map((asset) => (
2686
+ <ActiveIndexingJob key={asset.id} asset={asset} onPause={onPause} onRemove={onRemove} />
2687
+ ))}
2688
+ </div>
2689
+ </section>
2690
+ );
2691
+ }
2692
+
2693
+ function ActiveIndexingJob({
2694
+ asset,
2695
+ onPause,
2696
+ onRemove,
2697
+ }: {
2698
+ asset: MediaAsset;
2699
+ onPause: (asset: MediaAsset) => void;
2700
+ onRemove: (asset: MediaAsset) => void;
2701
+ }) {
2702
+ const progress = useLiveMediaProgress(asset);
2703
+ const activeStep = primaryRunningMediaStep(asset.processingSteps);
2704
+ const isFinalizing = isFinalizingMediaStep(activeStep);
2705
+ return (
2706
+ <div className="group relative space-y-2 rounded-lg border border-transparent bg-background/40 p-2.5">
2707
+ <div className="flex items-start justify-between gap-3">
2708
+ <div className="min-w-0 pr-6">
2709
+ <p className="truncate text-[11px] font-medium text-foreground">{asset.fileName}</p>
2710
+ </div>
2711
+ <div className="flex items-center gap-2">
2712
+ <span className="shrink-0 text-[10px] font-medium tabular-nums text-emerald-600 dark:text-emerald-400">
2713
+ {isFinalizing ? 'Finalizing' : `${formatMediaProgressPercent(progress)}%`}
2714
+ </span>
2715
+ {asset.type !== 'image' ? (
2716
+ <button
2717
+ type="button"
2718
+ title={asset.processingPaused ? 'Resume Indexing' : 'Pause Indexing'}
2719
+ aria-label={
2720
+ asset.processingPaused ? `Resume ${asset.fileName}` : `Pause ${asset.fileName}`
2721
+ }
2722
+ onClick={() => onPause(asset)}
2723
+ className="rounded-full p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
2724
+ >
2725
+ {asset.processingPaused ? <Play className="size-3" /> : <Pause className="size-3" />}
2726
+ </button>
2727
+ ) : null}
2728
+ <button
2729
+ type="button"
2730
+ title="Remove file"
2731
+ aria-label={`Remove ${asset.fileName}`}
2732
+ onClick={() => onRemove(asset)}
2733
+ className="-mr-1 rounded-full p-1 text-muted-foreground transition-colors hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-900/30"
2734
+ >
2735
+ <X className="size-3" />
2736
+ </button>
2737
+ </div>
2738
+ </div>
2739
+ <ActiveIndexingDescription asset={asset} />
2740
+ {isFinalizing ? (
2741
+ <IndeterminateIndexingProgress />
2742
+ ) : (
2743
+ <LiveMediaProgress value={progress} step={activeStep} paused={asset.processingPaused} />
2744
+ )}
2745
+ {asset.activeVideoKnowledgeJobId ? (
2746
+ <p
2747
+ className="truncate text-[9px] text-muted-foreground"
2748
+ title={asset.activeVideoKnowledgeJobId}
2749
+ >
2750
+ Knowledge job {asset.activeVideoKnowledgeJobId.slice(0, 8)}
2751
+ </p>
2752
+ ) : null}
2753
+ </div>
2754
+ );
2755
+ }
2756
+
2757
+ function IndeterminateIndexingProgress() {
2758
+ return (
2759
+ <div
2760
+ role="progressbar"
2761
+ aria-label="Finalizing video indexing"
2762
+ aria-valuetext="Finalizing verified results"
2763
+ className="relative h-2 min-h-2 w-full overflow-hidden rounded-full bg-emerald-100 dark:bg-emerald-950/60"
2764
+ >
2765
+ <motion.div
2766
+ className="absolute inset-y-0 left-0 w-2/5 rounded-full bg-emerald-500 dark:bg-emerald-400"
2767
+ initial={{ x: '-120%' }}
2768
+ animate={{ x: ['-120%', '270%'] }}
2769
+ transition={{ duration: 1.15, ease: 'easeInOut', repeat: Infinity }}
2770
+ />
2771
+ </div>
2772
+ );
2773
+ }
2774
+
2775
+ function ActiveIndexingDescription({ asset }: { asset: MediaAsset }) {
2776
+ const step = primaryRunningMediaStep(asset.processingSteps);
2777
+ const stepDescription = describeActiveMediaStep(step);
2778
+
2779
+ return (
2780
+ <div className="min-w-0 space-y-1">
2781
+ <div className="flex min-w-0 items-center gap-1.5">
2782
+ <ElapsedTime startTime={mediaProcessingStartedAt(asset)} />
2783
+ <p className="truncate text-[10px] text-muted-foreground animate-pulse">
2784
+ {asset.processingPaused
2785
+ ? 'Paused — resume when you are ready.'
2786
+ : asset.processingMessage || 'Preparing media indexing...'}
2787
+ </p>
2788
+ </div>
2789
+ {!asset.processingPaused && stepDescription ? (
2790
+ <LiveIndexingStepDescription step={step} description={stepDescription} />
2791
+ ) : null}
2792
+ </div>
2793
+ );
2794
+ }
2795
+
2796
+ function LiveIndexingStepDescription({
2797
+ step,
2798
+ description,
2799
+ }: {
2800
+ step: MediaProcessingStep | undefined;
2801
+ description: string;
2802
+ }) {
2803
+ const [now, setNow] = useState(() => Date.now());
2804
+ useEffect(() => {
2805
+ const interval = setInterval(() => setNow(Date.now()), 5_000);
2806
+ return () => clearInterval(interval);
2807
+ }, []);
2808
+ const stale = isMediaStepTelemetryStale(step, now);
2809
+
2810
+ return (
2811
+ <div
2812
+ className="flex min-w-0 items-center gap-1.5 text-[9px] text-muted-foreground/80"
2813
+ aria-live="polite"
2814
+ >
2815
+ <span className="truncate">
2816
+ {stale ? 'Waiting for a fresh worker update…' : `Worker active · ${description}`}
2817
+ </span>
2818
+ {!stale ? <EstimatedStepTime step={step} /> : null}
2819
+ </div>
2820
+ );
2821
+ }
2822
+
2823
+ function EstimatedStepTime({ step }: { step: MediaProcessingStep | undefined }) {
2824
+ const [now, setNow] = useState(() => Date.now());
2825
+ useEffect(() => {
2826
+ const interval = setInterval(() => setNow(Date.now()), 15_000);
2827
+ return () => clearInterval(interval);
2828
+ }, []);
2829
+
2830
+ const remainingSeconds = estimateMediaStepRemainingSeconds(step, now);
2831
+ if (remainingSeconds === null) return null;
2832
+ return (
2833
+ <span className="shrink-0 whitespace-nowrap text-muted-foreground/70">
2834
+ ~{formatShortDuration(remainingSeconds)} left
2835
+ </span>
2836
+ );
2837
+ }
2838
+
2839
+ function formatShortDuration(seconds: number): string {
2840
+ const minutes = Math.round(seconds / 60);
2841
+ if (minutes < 1) return '<1 min';
2842
+ if (minutes < 60) return `${minutes} min`;
2843
+ const hours = Math.floor(minutes / 60);
2844
+ return `${hours}h ${minutes % 60}m`;
2845
+ }
2846
+
2847
+ function ElapsedTime({ startTime }: { startTime: string }) {
2848
+ const [elapsed, setElapsed] = useState(0);
2849
+ useEffect(() => {
2850
+ const start = new Date(startTime).getTime();
2851
+ const update = () => setElapsed(Math.max(0, Math.floor((Date.now() - start) / 1000)));
2852
+ update();
2853
+ const interval = setInterval(update, 1000);
2854
+ return () => clearInterval(interval);
2855
+ }, [startTime]);
2856
+ const mins = Math.floor(elapsed / 60);
2857
+ const secs = elapsed % 60;
2858
+ return (
2859
+ <span className="font-mono tabular-nums text-[10px] text-muted-foreground/60">
2860
+ {mins}:{secs.toString().padStart(2, '0')}
2861
+ </span>
2862
+ );
2863
+ }
2864
+
2865
+ function EstimateMetric({ label, value }: { label: string; value: string }) {
2866
+ return (
2867
+ <div className="min-w-0">
2868
+ <p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70">
2869
+ {label}
2870
+ </p>
2871
+ <p className="mt-0.5 truncate text-[11px] text-foreground" title={value}>
2872
+ {value}
2873
+ </p>
2874
+ </div>
2875
+ );
2876
+ }
2877
+
2878
+ function formatDuration(seconds: number): string {
2879
+ const hours = Math.floor(seconds / 3600);
2880
+ const minutes = Math.floor((seconds % 3600) / 60);
2881
+ const secs = Math.floor(seconds % 60);
2882
+ return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}:${String(secs).padStart(2, '0')}`;
2883
+ }