larkup 0.1.37 → 0.1.39

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 (190) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +12 -12
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/required-server-files.js +1 -1
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_global-error.html +1 -1
  8. package/.next/server/app/_global-error.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/_not-found.html +1 -1
  16. package/.next/server/app/_not-found.rsc +18 -18
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +18 -18
  18. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  19. package/.next/server/app/_not-found.segments/_index.segment.rsc +12 -12
  20. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/server/app/analytics/page_client-reference-manifest.js +1 -1
  24. package/.next/server/app/analytics.html +1 -1
  25. package/.next/server/app/analytics.rsc +19 -19
  26. package/.next/server/app/analytics.segments/_full.segment.rsc +19 -19
  27. package/.next/server/app/analytics.segments/_head.segment.rsc +4 -4
  28. package/.next/server/app/analytics.segments/_index.segment.rsc +12 -12
  29. package/.next/server/app/analytics.segments/_tree.segment.rsc +2 -2
  30. package/.next/server/app/analytics.segments/analytics/__PAGE__.segment.rsc +3 -3
  31. package/.next/server/app/analytics.segments/analytics.segment.rsc +3 -3
  32. package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  33. package/.next/server/app/chat.html +1 -1
  34. package/.next/server/app/chat.rsc +19 -19
  35. package/.next/server/app/chat.segments/_full.segment.rsc +19 -19
  36. package/.next/server/app/chat.segments/_head.segment.rsc +4 -4
  37. package/.next/server/app/chat.segments/_index.segment.rsc +12 -12
  38. package/.next/server/app/chat.segments/_tree.segment.rsc +2 -2
  39. package/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +3 -3
  40. package/.next/server/app/chat.segments/chat.segment.rsc +3 -3
  41. package/.next/server/app/data/page_client-reference-manifest.js +1 -1
  42. package/.next/server/app/data.html +1 -1
  43. package/.next/server/app/data.rsc +19 -19
  44. package/.next/server/app/data.segments/_full.segment.rsc +19 -19
  45. package/.next/server/app/data.segments/_head.segment.rsc +4 -4
  46. package/.next/server/app/data.segments/_index.segment.rsc +12 -12
  47. package/.next/server/app/data.segments/_tree.segment.rsc +2 -2
  48. package/.next/server/app/data.segments/data/__PAGE__.segment.rsc +3 -3
  49. package/.next/server/app/data.segments/data.segment.rsc +3 -3
  50. package/.next/server/app/index.html +1 -1
  51. package/.next/server/app/index.rsc +17 -17
  52. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  53. package/.next/server/app/index.segments/_full.segment.rsc +17 -17
  54. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  55. package/.next/server/app/index.segments/_index.segment.rsc +12 -12
  56. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  57. package/.next/server/app/page_client-reference-manifest.js +1 -1
  58. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  59. package/.next/server/app/settings.html +1 -1
  60. package/.next/server/app/settings.rsc +19 -19
  61. package/.next/server/app/settings.segments/_full.segment.rsc +19 -19
  62. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  63. package/.next/server/app/settings.segments/_index.segment.rsc +12 -12
  64. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  65. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  66. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  67. package/.next/server/chunks/0598_next_dist_esm_build_templates_app-route_0kbcuk-.js +24 -28
  68. package/.next/server/chunks/[root-of-the-server]__1jc54o2._.js +1 -1
  69. package/.next/server/chunks/_1twj5f0._.js +3 -3
  70. package/.next/server/chunks/ssr/_169pdlf._.js +1 -1
  71. package/.next/server/chunks/ssr/_18jagqc._.js +1 -1
  72. package/.next/server/chunks/ssr/_1hetwwu._.js +1 -1
  73. package/.next/server/chunks/ssr/_1yqlx1m._.js +1 -1
  74. package/.next/server/middleware-build-manifest.js +3 -3
  75. package/.next/server/pages/404.html +1 -1
  76. package/.next/server/pages/500.html +1 -1
  77. package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
  78. package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
  79. package/.next/standalone/apps/web/.next/required-server-files.json +1 -1
  80. package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
  81. package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
  82. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  83. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  84. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  85. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  86. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  87. package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  88. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
  89. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +18 -18
  90. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +18 -18
  91. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  92. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +12 -12
  93. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  94. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  95. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  96. package/.next/standalone/apps/web/.next/server/app/analytics/page_client-reference-manifest.js +1 -1
  97. package/.next/standalone/apps/web/.next/server/app/analytics.html +1 -1
  98. package/.next/standalone/apps/web/.next/server/app/analytics.rsc +19 -19
  99. package/.next/standalone/apps/web/.next/server/app/analytics.segments/_full.segment.rsc +19 -19
  100. package/.next/standalone/apps/web/.next/server/app/analytics.segments/_head.segment.rsc +4 -4
  101. package/.next/standalone/apps/web/.next/server/app/analytics.segments/_index.segment.rsc +12 -12
  102. package/.next/standalone/apps/web/.next/server/app/analytics.segments/_tree.segment.rsc +2 -2
  103. package/.next/standalone/apps/web/.next/server/app/analytics.segments/analytics/__PAGE__.segment.rsc +3 -3
  104. package/.next/standalone/apps/web/.next/server/app/analytics.segments/analytics.segment.rsc +3 -3
  105. package/.next/standalone/apps/web/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/apps/web/.next/server/app/chat.html +1 -1
  107. package/.next/standalone/apps/web/.next/server/app/chat.rsc +19 -19
  108. package/.next/standalone/apps/web/.next/server/app/chat.segments/_full.segment.rsc +19 -19
  109. package/.next/standalone/apps/web/.next/server/app/chat.segments/_head.segment.rsc +4 -4
  110. package/.next/standalone/apps/web/.next/server/app/chat.segments/_index.segment.rsc +12 -12
  111. package/.next/standalone/apps/web/.next/server/app/chat.segments/_tree.segment.rsc +2 -2
  112. package/.next/standalone/apps/web/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +3 -3
  113. package/.next/standalone/apps/web/.next/server/app/chat.segments/chat.segment.rsc +3 -3
  114. package/.next/standalone/apps/web/.next/server/app/data/page_client-reference-manifest.js +1 -1
  115. package/.next/standalone/apps/web/.next/server/app/data.html +1 -1
  116. package/.next/standalone/apps/web/.next/server/app/data.rsc +19 -19
  117. package/.next/standalone/apps/web/.next/server/app/data.segments/_full.segment.rsc +19 -19
  118. package/.next/standalone/apps/web/.next/server/app/data.segments/_head.segment.rsc +4 -4
  119. package/.next/standalone/apps/web/.next/server/app/data.segments/_index.segment.rsc +12 -12
  120. package/.next/standalone/apps/web/.next/server/app/data.segments/_tree.segment.rsc +2 -2
  121. package/.next/standalone/apps/web/.next/server/app/data.segments/data/__PAGE__.segment.rsc +3 -3
  122. package/.next/standalone/apps/web/.next/server/app/data.segments/data.segment.rsc +3 -3
  123. package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
  124. package/.next/standalone/apps/web/.next/server/app/index.rsc +17 -17
  125. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  126. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +17 -17
  127. package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
  128. package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +12 -12
  129. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  130. package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  131. package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  132. package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
  133. package/.next/standalone/apps/web/.next/server/app/settings.rsc +19 -19
  134. package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +19 -19
  135. package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  136. package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +12 -12
  137. package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  138. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  139. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  140. package/.next/standalone/apps/web/.next/server/chunks/0598_next_dist_esm_build_templates_app-route_0kbcuk-.js +24 -28
  141. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__1jc54o2._.js +1 -1
  142. package/.next/standalone/apps/web/.next/server/chunks/_1twj5f0._.js +3 -3
  143. package/.next/standalone/apps/web/.next/server/chunks/ssr/_169pdlf._.js +1 -1
  144. package/.next/standalone/apps/web/.next/server/chunks/ssr/_18jagqc._.js +1 -1
  145. package/.next/standalone/apps/web/.next/server/chunks/ssr/_1hetwwu._.js +1 -1
  146. package/.next/standalone/apps/web/.next/server/chunks/ssr/_1yqlx1m._.js +1 -1
  147. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  148. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  149. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  150. package/.next/{static/chunks/40jjwg-270y6t.js → standalone/apps/web/.next/static/chunks/03ju_837pa9ul.js} +3 -3
  151. package/.next/standalone/apps/web/.next/static/chunks/{3y2ki5tkccfxh.js → 19krxv-ou9c8c.js} +1 -1
  152. package/.next/standalone/apps/web/.next/static/chunks/1_nk3imchwn8a.css +1 -0
  153. package/.next/standalone/apps/web/.next/static/chunks/{0xvo_v5tiy4hc.js → 1ov69q-i9ybqi.js} +1 -1
  154. package/.next/{static/chunks/195c9wbq4gya9.js → standalone/apps/web/.next/static/chunks/2ljwki2xxnblc.js} +1 -1
  155. package/.next/standalone/apps/web/CHANGELOG.md +12 -0
  156. package/.next/standalone/apps/web/app/api/chat/route.ts +45 -34
  157. package/.next/standalone/apps/web/app/api/media/route.ts +3 -2
  158. package/.next/standalone/apps/web/app/api/media/stream/route.ts +19 -3
  159. package/.next/standalone/apps/web/app/settings/page.tsx +8 -16
  160. package/.next/standalone/apps/web/components/chat/message-item.tsx +18 -6
  161. package/.next/standalone/apps/web/components/chat/tools/knowledge-base-result.tsx +3 -4
  162. package/.next/standalone/apps/web/components/data/data-workspace.tsx +14 -10
  163. package/.next/standalone/apps/web/components/data/media-panel.tsx +112 -86
  164. package/.next/standalone/apps/web/components/settings/marketplace-section.tsx +5 -0
  165. package/.next/standalone/apps/web/package.json +1 -1
  166. package/.next/standalone/apps/web/server.js +1 -1
  167. package/.next/{standalone/apps/web/.next/static/chunks/40jjwg-270y6t.js → static/chunks/03ju_837pa9ul.js} +3 -3
  168. package/.next/static/chunks/{3y2ki5tkccfxh.js → 19krxv-ou9c8c.js} +1 -1
  169. package/.next/static/chunks/1_nk3imchwn8a.css +1 -0
  170. package/.next/static/chunks/{0xvo_v5tiy4hc.js → 1ov69q-i9ybqi.js} +1 -1
  171. package/.next/{standalone/apps/web/.next/static/chunks/195c9wbq4gya9.js → static/chunks/2ljwki2xxnblc.js} +1 -1
  172. package/CHANGELOG.md +12 -0
  173. package/app/api/chat/route.ts +45 -34
  174. package/app/api/media/route.ts +3 -2
  175. package/app/api/media/stream/route.ts +19 -3
  176. package/app/settings/page.tsx +8 -16
  177. package/components/chat/message-item.tsx +18 -6
  178. package/components/chat/tools/knowledge-base-result.tsx +3 -4
  179. package/components/data/data-workspace.tsx +14 -10
  180. package/components/data/media-panel.tsx +112 -86
  181. package/components/settings/marketplace-section.tsx +5 -0
  182. package/package.json +4 -4
  183. package/.next/standalone/apps/web/.next/static/chunks/427kg1r2nc6x0.css +0 -1
  184. package/.next/static/chunks/427kg1r2nc6x0.css +0 -1
  185. /package/.next/standalone/apps/web/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_buildManifest.js +0 -0
  186. /package/.next/standalone/apps/web/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_clientMiddlewareManifest.js +0 -0
  187. /package/.next/standalone/apps/web/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_ssgManifest.js +0 -0
  188. /package/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_buildManifest.js +0 -0
  189. /package/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_clientMiddlewareManifest.js +0 -0
  190. /package/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # larkup
2
2
 
3
+ ## 0.1.39
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix TS2448 ReferenceError in data-workspace.tsx caused by docsQuery declaration order
8
+
9
+ ## 0.1.38
10
+
11
+ ### Patch Changes
12
+
13
+ - Fix 10 production issues: SSE controller-closed error, cleaner system prompt, smarter KB search, one-shot web search, corpus refresh after scrape, minimal KB loading UI, settings sidebar navigation, force-delete processing media, playlist confirmation, image preview in chat, marketplace install progress toast.
14
+
3
15
  ## 0.1.37
4
16
 
5
17
  ### Patch Changes
@@ -64,28 +64,24 @@ function createChatModel(
64
64
 
65
65
  const SMART_RETRIEVAL_POLICY = `
66
66
 
67
- CONVERSATION, RETRIEVAL, AND MEDIA POLICY:
68
-
69
- TOOL PRIORITY (strict order):
70
- 1. searchKnowledgeBase ALWAYS try this FIRST when the question may relate to the user's indexed data. This includes any question mentioning "I", "my", "our", "the document", "the file", "the diagram", "the image", "show me", or referencing specific topics the user has likely indexed.
71
- 2. Web search (if enabled) Use ONLY if searchKnowledgeBase returned zero relevant hits AND the question is factual and benefits from web results.
72
- 3. Other tools (tabular queries, analysis, etc.) — Use when the question specifically requires them.
73
- 4. General knowledge — Answer from your training data ONLY for general knowledge questions clearly unrelated to the user's data.
74
- 5. If no tool yields relevant results AND you cannot answer reliably from general knowledge, respond honestly: "I could not find relevant information in your knowledge base or available sources."
75
-
76
- RETRIEVAL RULES:
77
- - Default to a short, direct answer. One to three sentences is usually enough; use structure only when the task is genuinely complex.
78
- - Do not search for greetings, casual conversation, writing help, brainstorming, explanations you can answer reliably from general knowledge, or questions unrelated to the user's indexed data.
79
- - Use searchKnowledgeBase only when the answer depends on the user's indexed/private content, an exact source-specific fact, or a topic that plausibly refers to something the user indexed.
80
- - Treat questions about the user or their materials as knowledge-base questions: phrases such as "I", "my", "our", "you have", "the database", "the document", "the file", or "the diagram" require searchKnowledgeBase before answering whenever indexed content may contain the answer.
81
- - Search at most once per user request. Never repeat a search when a prior tool result in this conversation already contains enough evidence. If the results do not answer the question, say so plainly instead of making more searches or guessing.
82
-
83
- IMAGE AND MEDIA RULES:
84
- - NEVER output raw markdown image syntax like ![alt](url). The chat UI blocks these and shows "[Image unavailable]". Instead, always use the presentMedia tool to display images, videos, or audio from indexed content.
85
- - When search results contain images[].imageUrl (an image extracted from a PDF), use presentMedia with that exact imageUrl. When they contain a mediaAssetId, use presentMedia with the exact assetId. This is the ONLY way indexed images can appear in the conversation.
86
- - For a follow-up such as "show me that part", "play the clip", "show the image", or "let me hear it", reuse the mediaAssetId and timestamps already present in the earlier search result and call presentMedia directly. Do not search again just to rediscover the same media.
87
- - Use presentMedia to embed exactly one best video, image, or audio citation when the user asks to see/hear it or when one compact preview materially supports the answer. Do not present every search result.
88
- - Keep the written answer minimal around a media citation. Do not print internal media reference markers.
67
+ TOOL RULES:
68
+ 1. searchKnowledgeBase → Use FIRST when the question is about the user's indexed data (documents, files, images, videos, diagrams, anything they uploaded).
69
+ 2. webSearch Use ONLY after searchKnowledgeBase returned nothing relevant AND the question needs current/public facts.
70
+ 3. Other tools Use when the question specifically requires them.
71
+ 4. General knowledge Answer directly for common knowledge unrelated to the user's data.
72
+ 5. If nothing helps, say so honestly.
73
+
74
+ DO:
75
+ - Keep answers short and direct. 1–3 sentences unless the task is complex.
76
+ - Search at most ONCE per tool per turn. Never repeat the same search.
77
+ - Use presentMedia to show images/videos/audio from indexed content.
78
+ - For follow-ups like "show me that", reuse the mediaAssetId from earlier results do not search again.
79
+
80
+ DO NOT:
81
+ - Search for greetings, casual chat, writing help, or general knowledge you already know.
82
+ - Output markdown images like ![alt](url) — the UI blocks them. Always use presentMedia instead.
83
+ - Present every search result. Pick the single best one.
84
+ - Guess or fabricate when results are insufficient.
89
85
  `;
90
86
 
91
87
  function latestUserText(messages: UIMessage[]): string {
@@ -101,16 +97,29 @@ function latestUserText(messages: UIMessage[]): string {
101
97
  return '';
102
98
  }
103
99
 
100
+ /** Only force KB search when the user explicitly refers to their own data.
101
+ * Matches "my document", "our files", "the uploaded image" etc. but NOT
102
+ * generic uses like "I want to know", "can you tell me", "I think". */
104
103
  function requiresKnowledgeBaseSearch(text: string): boolean {
105
- return /\b(my|our|we|i|database|db|document|file|diagram|image|picture|upload(?:ed)?|knowledge base|corpus)\b|\bwhat is my\b/i.test(
106
- text,
104
+ // Skip generic conversational patterns
105
+ if (/^(hi|hello|hey|thanks|thank you|ok|sure|yes|no|please|help)\b/i.test(text.trim())) {
106
+ return false;
107
+ }
108
+ // Match possessive references to user data
109
+ return (
110
+ /\b(my|our)\s+(document|file|data|image|picture|diagram|video|audio|upload|corpus|knowledge|database|db|pdf|report|spreadsheet|presentation)/i.test(
111
+ text,
112
+ ) ||
113
+ /\b(the|this|that)\s+(document|file|diagram|image|picture|upload|pdf|report)/i.test(text) ||
114
+ /\b(uploaded?|indexed|scraped|knowledge base|corpus)\b/i.test(text) ||
115
+ /\bshow\s+me\b/i.test(text)
107
116
  );
108
117
  }
109
118
 
110
119
  /** Questions whose answer is inherently time-sensitive should not rely on the
111
120
  * model's training snapshot when Web Search is available. */
112
121
  function requiresCurrentWebSearch(text: string): boolean {
113
- return /\b(who won|winner|score|match|game|latest|current|today|yesterday|news|weather|stock|share price|exchange rate|election|president|ceo|schedule)\b/i.test(
122
+ return /\b(who won|winner|final score|match result|latest|current|today|yesterday|breaking news|weather|stock price|exchange rate|election result|schedule)\b/i.test(
114
123
  text,
115
124
  );
116
125
  }
@@ -378,7 +387,7 @@ ${fieldLines}`;
378
387
  : '') +
379
388
  SMART_RETRIEVAL_POLICY +
380
389
  (config.webSearchEnabled
381
- ? '\nWEB SEARCH IS ENABLED: For a public, current, or external factual question, you MUST call webSearch after any required knowledge-base search. Do not tell the user to check the web yourself when this tool is available.\n'
390
+ ? '\nWeb search is available. Use it for current events or public facts not in the knowledge base.\n'
382
391
  : '') +
383
392
  tabularContext +
384
393
  docContext;
@@ -401,22 +410,24 @@ ${fieldLines}`;
401
410
  system: systemPrompt,
402
411
  messages: await convertToModelMessages(safeMessages, { tools }),
403
412
  maxOutputTokens: 4096,
404
- // Five steps covers the only useful multi-step case (retrieve, optionally
405
- // present one media item, then answer) without allowing a tool loop to
406
- // burn the context window and end with no response.
407
- stopWhen: stepCountIs(5),
413
+ // Three steps: retrieve optionally present media answer.
414
+ stopWhen: stepCountIs(3),
408
415
  toolChoice: 'auto',
409
416
  prepareStep: ({ stepNumber }) => {
417
+ // Step 0: force KB search if needed
410
418
  if (forceKnowledgeBaseSearch && stepNumber === 0) {
411
419
  return { toolChoice: { type: 'tool', toolName: 'searchKnowledgeBase' } };
412
420
  }
421
+ // Step 1 (or 0 if no KB): force web search if needed
413
422
  if (forceWebSearch && (!forceKnowledgeBaseSearch || stepNumber === 1)) {
414
423
  return { toolChoice: { type: 'tool', toolName: 'webSearch' } };
415
424
  }
416
- // A retrieval is deliberately one-shot per turn. This is applied at the
417
- // runtime level rather than relying only on the model prompt.
418
- if (forceKnowledgeBaseSearch) {
419
- return { activeTools: Object.keys(tools).filter((name) => name !== 'searchKnowledgeBase') };
425
+ // After forced tools, remove them so the model cannot loop on them.
426
+ const usedTools: string[] = [];
427
+ if (forceKnowledgeBaseSearch) usedTools.push('searchKnowledgeBase');
428
+ if (forceWebSearch) usedTools.push('webSearch');
429
+ if (usedTools.length > 0) {
430
+ return { activeTools: Object.keys(tools).filter((name) => !usedTools.includes(name)) };
420
431
  }
421
432
  return undefined;
422
433
  },
@@ -177,6 +177,7 @@ async function removeMedia(req: Request) {
177
177
  const url = new URL(req.url);
178
178
  const id = url.searchParams.get('id');
179
179
  const ids = url.searchParams.get('ids');
180
+ const force = url.searchParams.get('force') === 'true';
180
181
 
181
182
  const storage = createStorageProvider();
182
183
 
@@ -184,7 +185,7 @@ async function removeMedia(req: Request) {
184
185
  const { getMediaAsset } = await import('@larkup/core/media-store');
185
186
  const asset = await getMediaAsset(id);
186
187
  if (asset) {
187
- if (isMediaProcessing(asset)) {
188
+ if (!force && isMediaProcessing(asset)) {
188
189
  return NextResponse.json(
189
190
  { error: 'Wait for the media indexing job to finish before deleting it.' },
190
191
  { status: 409 },
@@ -201,7 +202,7 @@ async function removeMedia(req: Request) {
201
202
  const idList = ids.split(',');
202
203
  const assets = await readMediaAssets();
203
204
  const selectedAssets = assets.filter((asset) => idList.includes(asset.id));
204
- if (selectedAssets.some(isMediaProcessing)) {
205
+ if (!force && selectedAssets.some(isMediaProcessing)) {
205
206
  return NextResponse.json(
206
207
  { error: 'Wait for all selected media indexing jobs to finish before deleting them.' },
207
208
  { status: 409 },
@@ -50,14 +50,29 @@ export async function GET(req: Request) {
50
50
 
51
51
  let interval: ReturnType<typeof setInterval> | undefined;
52
52
  let keepAlive: ReturnType<typeof setInterval> | undefined;
53
+ let closed = false;
54
+
53
55
  const stream = new ReadableStream<Uint8Array>({
54
56
  async start(controller) {
55
57
  let lastPayload = '';
56
58
  let checking = false;
57
59
  let storageSnapshot = { usedBytes: 0, fileCount: 0 };
58
60
  let storageCheckedAt = 0;
61
+
62
+ const safeEnqueue = (data: Uint8Array) => {
63
+ if (closed) return;
64
+ try {
65
+ controller.enqueue(data);
66
+ } catch {
67
+ // Controller already closed — client disconnected.
68
+ closed = true;
69
+ if (interval) clearInterval(interval);
70
+ if (keepAlive) clearInterval(keepAlive);
71
+ }
72
+ };
73
+
59
74
  const publish = async () => {
60
- if (checking) return;
75
+ if (checking || closed) return;
61
76
  checking = true;
62
77
  try {
63
78
  if (Date.now() - storageCheckedAt > 30_000) {
@@ -81,7 +96,7 @@ export async function GET(req: Request) {
81
96
  0,
82
97
  ...nextSnapshot.assets.map((asset) => asset.processingRevision ?? 0),
83
98
  );
84
- controller.enqueue(
99
+ safeEnqueue(
85
100
  encoder.encode(`id: ${revision}\nevent: media-update\ndata: ${payload}\n\n`),
86
101
  );
87
102
  }
@@ -94,9 +109,10 @@ export async function GET(req: Request) {
94
109
 
95
110
  await publish();
96
111
  interval = setInterval(() => void publish(), 1_000);
97
- keepAlive = setInterval(() => controller.enqueue(encoder.encode(': keep-alive\n\n')), 15_000);
112
+ keepAlive = setInterval(() => safeEnqueue(encoder.encode(': keep-alive\n\n')), 15_000);
98
113
  },
99
114
  cancel() {
115
+ closed = true;
100
116
  if (interval) clearInterval(interval);
101
117
  if (keepAlive) clearInterval(keepAlive);
102
118
  },
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, Suspense } from 'react';
3
+ import { useEffect, Suspense } from 'react';
4
4
  import { useSearchParams, useRouter, usePathname } from 'next/navigation';
5
5
  import { SettingsLayout, type SettingsSection } from '@/components/settings/settings-layout';
6
6
  import { GeneralSection } from '@/components/settings/general-section';
@@ -20,36 +20,28 @@ function SettingsContent() {
20
20
  const router = useRouter();
21
21
  const pathname = usePathname();
22
22
 
23
- const getInitialSection = (): SettingsSection => {
23
+ const resolveSection = (): SettingsSection => {
24
24
  if (searchParams.has('ai-models')) return 'models';
25
25
  const section = searchParams.get('section') as SettingsSection;
26
26
  if (section) return section;
27
27
  return 'general';
28
28
  };
29
29
 
30
- const [activeSection, setActiveSection] = useState<SettingsSection>(getInitialSection());
30
+ // Always derive the active section from the URL — this ensures navigating
31
+ // away and back never leaves the sidebar stuck on a stale section.
32
+ const activeSection = resolveSection();
31
33
 
32
34
  useEffect(() => {
33
- const currentSection = searchParams.get('section');
34
- const hasAiModels = searchParams.has('ai-models');
35
-
36
- if (hasAiModels) {
35
+ // Normalize legacy `?ai-models` param to `?section=models`
36
+ if (searchParams.has('ai-models')) {
37
37
  const params = new URLSearchParams(searchParams.toString());
38
38
  params.delete('ai-models');
39
39
  params.set('section', 'models');
40
40
  router.replace(`${pathname}?${params.toString()}`);
41
- if (activeSection !== 'models') {
42
- setActiveSection('models');
43
- }
44
- } else if (currentSection && currentSection !== activeSection) {
45
- setActiveSection(currentSection as SettingsSection);
46
41
  }
47
- }, [searchParams, pathname, router, activeSection]);
42
+ }, [searchParams, pathname, router]);
48
43
 
49
44
  const handleSectionChange = (newSection: SettingsSection) => {
50
- setActiveSection(newSection);
51
- // A settings item is a destination, not a filter. Rebuild the query so a
52
- // stale deep-link such as `section=tool-settings` cannot linger in the URL.
53
45
  router.replace(`${pathname}?section=${newSection}`, { scroll: false });
54
46
  };
55
47
 
@@ -382,7 +382,7 @@ function renderToolPart(
382
382
  <div key={index} className="mb-2 w-full">
383
383
  <div className="inline-flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground bg-muted/30 rounded-md border border-border/50">
384
384
  <Globe className="h-3.5 w-3.5 shrink-0" />
385
- <span className="truncate max-w-[250px] sm:max-w-[400px]">
385
+ <span className="truncate max-w-62.5 sm:max-w-100">
386
386
  Searched web for "{input?.query}"
387
387
  </span>
388
388
  <span className="shrink-0 text-[10px] bg-secondary text-foreground px-1.5 py-0.5 rounded-full font-medium ml-1">
@@ -395,12 +395,24 @@ function renderToolPart(
395
395
 
396
396
  case 'analyzeImageDeeply': {
397
397
  if (output.error) return null;
398
+ const imageUrl = input?.imageUrl as string | undefined;
398
399
  return (
399
400
  <div key={index} className="mb-2 w-full">
400
401
  <div className="inline-flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground bg-muted/30 rounded-md border border-border/50">
401
402
  <Sparkles className="h-3.5 w-3.5 shrink-0" />
402
- <span className="truncate max-w-[250px] sm:max-w-[400px]">Analyzed image</span>
403
+ <span className="truncate max-w-62.5 sm:max-w-100">Analyzed image</span>
403
404
  </div>
405
+ {imageUrl ? (
406
+ <div className="mt-1.5 max-w-xs">
407
+ {/* eslint-disable-next-line @next/next/no-img-element */}
408
+ <img
409
+ src={imageUrl}
410
+ alt="Analyzed image"
411
+ className="rounded-lg border border-border/50 object-cover max-h-48 w-auto"
412
+ loading="lazy"
413
+ />
414
+ </div>
415
+ ) : null}
404
416
  </div>
405
417
  );
406
418
  }
@@ -433,7 +445,7 @@ function renderToolPart(
433
445
  <div key={index} className="mb-2 w-full">
434
446
  <div className="inline-flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground bg-muted/30 rounded-md border border-border/50">
435
447
  <CheckCircle2 className="h-3.5 w-3.5 shrink-0" />
436
- <span className="truncate max-w-[250px] sm:max-w-[400px]">Used {toolName}</span>
448
+ <span className="truncate max-w-62.5 sm:max-w-100">Used {toolName}</span>
437
449
  </div>
438
450
  </div>
439
451
  );
@@ -714,7 +726,7 @@ export function MessageItem({
714
726
  <div className="size-7 shrink-0 rounded-md overflow-hidden bg-background border border-border/90 flex items-center justify-center p-1">
715
727
  <img src={iconPath} alt={nameStr} className="w-full h-full object-contain" />
716
728
  </div>
717
- <div className="flex flex-col justify-center max-w-[150px]">
729
+ <div className="flex flex-col justify-center max-w-37.5">
718
730
  <span className="text-xs font-medium truncate text-foreground">
719
731
  {nameStr}
720
732
  </span>
@@ -932,10 +944,10 @@ function renderMarkdown(text: string): string {
932
944
  html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, label, url) => {
933
945
  const lowerUrl = url.toLowerCase();
934
946
  if (lowerUrl.match(/\.(mp4|webm|ogg)$/) || lowerUrl.includes('video')) {
935
- return `<video src="${url}" controls preload="metadata" class="max-w-[360px] w-full rounded-lg my-2 border border-border bg-card"></video>`;
947
+ return `<video src="${url}" controls preload="metadata" class="max-w-90 w-full rounded-lg my-2 border border-border bg-card"></video>`;
936
948
  }
937
949
  if (lowerUrl.match(/\.(mp3|wav|m4a|oga)$/) || lowerUrl.includes('audio')) {
938
- return `<audio src="${url}" controls preload="metadata" class="max-w-[320px] w-full my-2"></audio>`;
950
+ return `<audio src="${url}" controls preload="metadata" class="max-w-80 w-full my-2"></audio>`;
939
951
  }
940
952
  return `<a href="${url}" target="_blank" rel="noreferrer" class="msg-link">${label}</a>`;
941
953
  });
@@ -73,7 +73,7 @@ export function KnowledgeBaseResult({
73
73
  ) : (
74
74
  <Database className="h-3.5 w-3.5 shrink-0" />
75
75
  )}
76
- <span className="truncate max-w-[250px] sm:max-w-[400px]">
76
+ <span className="truncate max-w-62.5 sm:max-w-100">
77
77
  {isRunning ? 'Checking knowledge base…' : 'Checked knowledge base'}
78
78
  </span>
79
79
  {!isRunning && hits.length > 0 ? (
@@ -92,9 +92,8 @@ export function KnowledgeBaseResult({
92
92
  {open && (isRunning || hits.length > 0) ? (
93
93
  <div className="mt-2 flex flex-col gap-px border border-border rounded-xl bg-transparent overflow-hidden">
94
94
  {isRunning && hits.length === 0 ? (
95
- <div className="flex items-center gap-2 px-3.5 py-3 text-xs text-muted-foreground">
96
- <Loader2 className="size-3.5 animate-spin" />
97
- Looking through your Knowledge Base…
95
+ <div className="flex items-center gap-2 px-3.5 py-2.5">
96
+ <Loader2 className="size-3 animate-spin text-muted-foreground" />
98
97
  </div>
99
98
  ) : null}
100
99
  {hits.slice(0, 3).map((h, i) => (
@@ -160,6 +160,14 @@ export function DataWorkspace() {
160
160
  const configured = jobsQuery.data?.configured ?? true;
161
161
  const hasActive = jobs.some((j) => j.status === 'running' || j.status === 'queued');
162
162
 
163
+ const docsQuery = useSWR<DocsResponse>('/api/documents', fetcher, {
164
+ refreshInterval: hasActive ? 5000 : 0,
165
+ });
166
+ const { mutate: mutateDocuments } = docsQuery;
167
+ const documents = docsQuery.data?.documents ?? [];
168
+ const stats = docsQuery.data?.stats;
169
+ const docCount = stats?.docCount ?? 0;
170
+
163
171
  useEffect(() => {
164
172
  const prevJobs = prevJobsRef.current;
165
173
  if (prevJobs.length > 0 && jobs.length > 0) {
@@ -172,19 +180,15 @@ export function DataWorkspace() {
172
180
  );
173
181
 
174
182
  if (justCompleted.length > 0) {
175
- toast.success('Indexing completed.');
183
+ toast.success('Scraping completed.');
184
+ // Refresh the corpus immediately so new documents appear.
185
+ mutateDocuments();
186
+ // Documents may still be flushing — re-fetch after a short delay.
187
+ setTimeout(() => mutateDocuments(), 2500);
176
188
  }
177
189
  }
178
190
  prevJobsRef.current = jobs;
179
- }, [jobs]);
180
-
181
- const docsQuery = useSWR<DocsResponse>('/api/documents', fetcher, {
182
- refreshInterval: hasActive ? 5000 : 0,
183
- });
184
- const { mutate: mutateDocuments } = docsQuery;
185
- const documents = docsQuery.data?.documents ?? [];
186
- const stats = docsQuery.data?.stats;
187
- const docCount = stats?.docCount ?? 0;
191
+ }, [jobs, mutateDocuments]);
188
192
 
189
193
  const indexQuery = useSWR<{
190
194
  unindexedCount: number;
@@ -428,6 +428,9 @@ function InstallPrompt({
428
428
 
429
429
  const handleInstall = async () => {
430
430
  setInstalling(true);
431
+ const toastId = toast.loading('Downloading and installing tool…', {
432
+ description: 'This may take a moment on first install.',
433
+ });
431
434
  try {
432
435
  const res = await fetch(`/api/marketplace/${toolId}`, {
433
436
  method: 'POST',
@@ -436,6 +439,7 @@ function InstallPrompt({
436
439
  const err = await res.json();
437
440
  throw new Error(err.error || 'Install failed');
438
441
  }
442
+ toast.dismiss(toastId);
439
443
  toast.success('Video & Audio installed', {
440
444
  description: 'Choose an audio provider before indexing video or audio.',
441
445
  action: {
@@ -445,6 +449,7 @@ function InstallPrompt({
445
449
  });
446
450
  onInstallComplete();
447
451
  } catch (err) {
452
+ toast.dismiss(toastId);
448
453
  toast.error(err instanceof Error ? err.message : 'Failed to install tool');
449
454
  } finally {
450
455
  setInstalling(false);
@@ -772,7 +777,7 @@ function MediaContent({
772
777
 
773
778
  async function handleDelete(assetId: string) {
774
779
  try {
775
- const query = new URLSearchParams({ id: assetId });
780
+ const query = new URLSearchParams({ id: assetId, force: 'true' });
776
781
  if (serverId) query.set('serverId', serverId);
777
782
  const response = await fetch(`/api/media?${query.toString()}`, { method: 'DELETE' });
778
783
  if (!response.ok) {
@@ -912,93 +917,114 @@ function MediaContent({
912
917
  {mediaType === 'video' ? ', YouTube videos, or playlists' : ''}. Add up to 10 URLs
913
918
  separated by commas.
914
919
  </p>
915
- {remoteEstimates ? (
916
- <div className="mt-4 flex flex-col gap-4 border-t border-border/70 pt-4">
917
- <div className="grid gap-2 sm:grid-cols-3">
918
- <EstimateMetric
919
- label="Media"
920
- value={`${remoteEstimates.reduce(
921
- (sum: any, item: any) => sum + (item.entryCount ?? 1),
922
- 0,
923
- )} item(s) · ${
924
- remoteDuration ? formatDuration(remoteDuration) : 'duration unavailable'
925
- }`}
926
- />
927
- <EstimateMetric
928
- label="Expected time"
929
- value={
930
- remoteDuration
931
- ? `~${remoteEstimate.processingMinutes} min`
932
- : 'Calculated during import'
933
- }
934
- />
935
- <EstimateMetric
936
- label="Estimated API cost"
937
- value={
938
- remoteDuration
939
- ? `~$${remoteEstimate.transcriptionCost.toFixed(2)} +`
940
- : 'Model-dependent'
941
- }
942
- />
943
- </div>
944
-
945
- {(mediaType === 'audio'
946
- ? remoteEstimates.slice(0, 1)
947
- : remoteEstimates.filter((est: any) => est.mediaType === 'video').slice(0, 1)
948
- ).map((est: any, i: number) => {
949
- if (mediaType === 'audio') {
950
- return (
951
- <div key={i} className="rounded-xl border border-border/70 bg-muted/20 p-3">
952
- <p className="mb-2 truncate text-[11px] font-medium text-foreground">
953
- Preview before importing
954
- </p>
955
- <audio
956
- controls
957
- preload="metadata"
958
- src={est.originalUrl}
959
- className="h-9 w-full"
920
+ {remoteEstimates
921
+ ? (() => {
922
+ const totalEntries = remoteEstimates.reduce(
923
+ (sum: number, item: any) => sum + (item.entryCount ?? 1),
924
+ 0,
925
+ );
926
+ const isPlaylist = totalEntries > 1;
927
+ return (
928
+ <div className="mt-4 flex flex-col gap-4 border-t border-border/70 pt-4">
929
+ <div className="grid gap-2 sm:grid-cols-3">
930
+ <EstimateMetric
931
+ label="Media"
932
+ value={`${totalEntries} item(s) · ${
933
+ remoteDuration ? formatDuration(remoteDuration) : 'duration unavailable'
934
+ }`}
960
935
  />
961
- </div>
962
- );
963
- }
964
- let embedUrl = est.originalUrl;
965
- if (est.isYouTube) {
966
- try {
967
- const u = new URL(est.originalUrl);
968
- const v = u.hostname.includes('youtu.be')
969
- ? u.pathname.slice(1)
970
- : u.searchParams.get('v');
971
- embedUrl = `https://www.youtube.com/embed/${v}`;
972
- } catch (e) {
973
- // ignore
974
- }
975
- return (
976
- <div
977
- key={i}
978
- className="relative w-full overflow-hidden rounded-xl aspect-video shadow-md border border-border/50 ring-1 ring-border/20 bg-black"
979
- >
980
- <iframe
981
- src={embedUrl}
982
- className="absolute inset-0 w-full h-full"
983
- frameBorder="0"
984
- allowFullScreen
985
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
936
+ <EstimateMetric
937
+ label="Expected time"
938
+ value={
939
+ remoteDuration
940
+ ? `~${remoteEstimate.processingMinutes} min`
941
+ : 'Calculated during import'
942
+ }
943
+ />
944
+ <EstimateMetric
945
+ label="Estimated API cost"
946
+ value={
947
+ remoteDuration
948
+ ? `~$${remoteEstimate.transcriptionCost.toFixed(2)} +`
949
+ : 'Model-dependent'
950
+ }
986
951
  />
987
952
  </div>
988
- );
989
- } else {
990
- return (
991
- <video
992
- key={i}
993
- src={embedUrl}
994
- controls
995
- className="w-full max-w-sm rounded-md aspect-video bg-muted object-cover shadow-sm border border-border"
996
- />
997
- );
998
- }
999
- })}
1000
- </div>
1001
- ) : null}
953
+
954
+ {isPlaylist ? (
955
+ <div className="flex items-start gap-2 rounded-lg border border-amber-200 bg-amber-50/60 dark:border-amber-800/50 dark:bg-amber-950/30 px-3 py-2.5">
956
+ <AlertCircle className="size-3.5 shrink-0 mt-0.5 text-amber-600 dark:text-amber-400" />
957
+ <p className="text-[11px] text-amber-800 dark:text-amber-300 leading-relaxed">
958
+ <span className="font-medium">Playlist detected</span> — this will
959
+ download and index{' '}
960
+ <span className="font-semibold">{totalEntries} items</span> (max 10). Each
961
+ item is transcribed and analyzed separately.
962
+ </p>
963
+ </div>
964
+ ) : null}
965
+
966
+ {(mediaType === 'audio'
967
+ ? remoteEstimates.slice(0, 1)
968
+ : remoteEstimates.filter((est: any) => est.mediaType === 'video').slice(0, 1)
969
+ ).map((est: any, i: number) => {
970
+ if (mediaType === 'audio') {
971
+ return (
972
+ <div
973
+ key={i}
974
+ className="rounded-xl border border-border/70 bg-muted/20 p-3"
975
+ >
976
+ <p className="mb-2 truncate text-[11px] font-medium text-foreground">
977
+ Preview before importing
978
+ </p>
979
+ <audio
980
+ controls
981
+ preload="metadata"
982
+ src={est.originalUrl}
983
+ className="h-9 w-full"
984
+ />
985
+ </div>
986
+ );
987
+ }
988
+ let embedUrl = est.originalUrl;
989
+ if (est.isYouTube) {
990
+ try {
991
+ const u = new URL(est.originalUrl);
992
+ const v = u.hostname.includes('youtu.be')
993
+ ? u.pathname.slice(1)
994
+ : u.searchParams.get('v');
995
+ embedUrl = `https://www.youtube.com/embed/${v}`;
996
+ } catch (e) {
997
+ // ignore
998
+ }
999
+ return (
1000
+ <div
1001
+ key={i}
1002
+ className="relative w-full overflow-hidden rounded-xl aspect-video shadow-md border border-border/50 ring-1 ring-border/20 bg-black"
1003
+ >
1004
+ <iframe
1005
+ src={embedUrl}
1006
+ className="absolute inset-0 w-full h-full"
1007
+ frameBorder="0"
1008
+ allowFullScreen
1009
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
1010
+ />
1011
+ </div>
1012
+ );
1013
+ } else {
1014
+ return (
1015
+ <video
1016
+ key={i}
1017
+ src={embedUrl}
1018
+ controls
1019
+ className="w-full max-w-sm rounded-md aspect-video bg-muted object-cover shadow-sm border border-border"
1020
+ />
1021
+ );
1022
+ }
1023
+ })}
1024
+ </div>
1025
+ );
1026
+ })()
1027
+ : null}
1002
1028
  </div>
1003
1029
  ) : (
1004
1030
  <div
@@ -1582,7 +1608,7 @@ function ActiveIndexingList({
1582
1608
  <section
1583
1609
  aria-live="polite"
1584
1610
  aria-label="Active media indexing jobs"
1585
- className="space-y-3 rounded-xl border border-emerald-300 bg-emerald-50/20 p-3 dark:border-emerald-700/60 dark:bg-emerald-950/20"
1611
+ 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"
1586
1612
  >
1587
1613
  <div className="flex items-center gap-2">
1588
1614
  <Loader2 className="size-3.5 animate-spin text-emerald-600 dark:text-emerald-400" />