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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +12 -12
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +18 -18
- package/.next/server/app/_not-found.segments/_full.segment.rsc +18 -18
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +12 -12
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/analytics/page_client-reference-manifest.js +1 -1
- package/.next/server/app/analytics.html +1 -1
- package/.next/server/app/analytics.rsc +19 -19
- package/.next/server/app/analytics.segments/_full.segment.rsc +19 -19
- package/.next/server/app/analytics.segments/_head.segment.rsc +4 -4
- package/.next/server/app/analytics.segments/_index.segment.rsc +12 -12
- package/.next/server/app/analytics.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/analytics.segments/analytics/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/analytics.segments/analytics.segment.rsc +3 -3
- package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
- package/.next/server/app/chat.html +1 -1
- package/.next/server/app/chat.rsc +19 -19
- package/.next/server/app/chat.segments/_full.segment.rsc +19 -19
- package/.next/server/app/chat.segments/_head.segment.rsc +4 -4
- package/.next/server/app/chat.segments/_index.segment.rsc +12 -12
- package/.next/server/app/chat.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/chat.segments/chat.segment.rsc +3 -3
- package/.next/server/app/data/page_client-reference-manifest.js +1 -1
- package/.next/server/app/data.html +1 -1
- package/.next/server/app/data.rsc +19 -19
- package/.next/server/app/data.segments/_full.segment.rsc +19 -19
- package/.next/server/app/data.segments/_head.segment.rsc +4 -4
- package/.next/server/app/data.segments/_index.segment.rsc +12 -12
- package/.next/server/app/data.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/data.segments/data/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/data.segments/data.segment.rsc +3 -3
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +17 -17
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +17 -17
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +12 -12
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings.html +1 -1
- package/.next/server/app/settings.rsc +19 -19
- package/.next/server/app/settings.segments/_full.segment.rsc +19 -19
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +12 -12
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/server/chunks/0598_next_dist_esm_build_templates_app-route_0kbcuk-.js +24 -28
- package/.next/server/chunks/[root-of-the-server]__1jc54o2._.js +1 -1
- package/.next/server/chunks/_1twj5f0._.js +3 -3
- package/.next/server/chunks/ssr/_169pdlf._.js +1 -1
- package/.next/server/chunks/ssr/_18jagqc._.js +1 -1
- package/.next/server/chunks/ssr/_1hetwwu._.js +1 -1
- package/.next/server/chunks/ssr/_1yqlx1m._.js +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
- package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/required-server-files.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +18 -18
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +18 -18
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +12 -12
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/analytics/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/analytics.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/analytics.rsc +19 -19
- package/.next/standalone/apps/web/.next/server/app/analytics.segments/_full.segment.rsc +19 -19
- package/.next/standalone/apps/web/.next/server/app/analytics.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/analytics.segments/_index.segment.rsc +12 -12
- package/.next/standalone/apps/web/.next/server/app/analytics.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/analytics.segments/analytics/__PAGE__.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/analytics.segments/analytics.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/chat/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/chat.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/chat.rsc +19 -19
- package/.next/standalone/apps/web/.next/server/app/chat.segments/_full.segment.rsc +19 -19
- package/.next/standalone/apps/web/.next/server/app/chat.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/chat.segments/_index.segment.rsc +12 -12
- package/.next/standalone/apps/web/.next/server/app/chat.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/chat.segments/chat.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/data/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/data.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/data.rsc +19 -19
- package/.next/standalone/apps/web/.next/server/app/data.segments/_full.segment.rsc +19 -19
- package/.next/standalone/apps/web/.next/server/app/data.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/data.segments/_index.segment.rsc +12 -12
- package/.next/standalone/apps/web/.next/server/app/data.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/data.segments/data/__PAGE__.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/data.segments/data.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.rsc +17 -17
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +17 -17
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +12 -12
- package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.rsc +19 -19
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +19 -19
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +12 -12
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/chunks/0598_next_dist_esm_build_templates_app-route_0kbcuk-.js +24 -28
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__1jc54o2._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/_1twj5f0._.js +3 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_169pdlf._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_18jagqc._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_1hetwwu._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_1yqlx1m._.js +1 -1
- package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
- package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
- package/.next/{static/chunks/40jjwg-270y6t.js → standalone/apps/web/.next/static/chunks/03ju_837pa9ul.js} +3 -3
- package/.next/standalone/apps/web/.next/static/chunks/{3y2ki5tkccfxh.js → 19krxv-ou9c8c.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/1_nk3imchwn8a.css +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0xvo_v5tiy4hc.js → 1ov69q-i9ybqi.js} +1 -1
- package/.next/{static/chunks/195c9wbq4gya9.js → standalone/apps/web/.next/static/chunks/2ljwki2xxnblc.js} +1 -1
- package/.next/standalone/apps/web/CHANGELOG.md +12 -0
- package/.next/standalone/apps/web/app/api/chat/route.ts +45 -34
- package/.next/standalone/apps/web/app/api/media/route.ts +3 -2
- package/.next/standalone/apps/web/app/api/media/stream/route.ts +19 -3
- package/.next/standalone/apps/web/app/settings/page.tsx +8 -16
- package/.next/standalone/apps/web/components/chat/message-item.tsx +18 -6
- package/.next/standalone/apps/web/components/chat/tools/knowledge-base-result.tsx +3 -4
- package/.next/standalone/apps/web/components/data/data-workspace.tsx +14 -10
- package/.next/standalone/apps/web/components/data/media-panel.tsx +112 -86
- package/.next/standalone/apps/web/components/settings/marketplace-section.tsx +5 -0
- package/.next/standalone/apps/web/package.json +1 -1
- package/.next/standalone/apps/web/server.js +1 -1
- package/.next/{standalone/apps/web/.next/static/chunks/40jjwg-270y6t.js → static/chunks/03ju_837pa9ul.js} +3 -3
- package/.next/static/chunks/{3y2ki5tkccfxh.js → 19krxv-ou9c8c.js} +1 -1
- package/.next/static/chunks/1_nk3imchwn8a.css +1 -0
- package/.next/static/chunks/{0xvo_v5tiy4hc.js → 1ov69q-i9ybqi.js} +1 -1
- package/.next/{standalone/apps/web/.next/static/chunks/195c9wbq4gya9.js → static/chunks/2ljwki2xxnblc.js} +1 -1
- package/CHANGELOG.md +12 -0
- package/app/api/chat/route.ts +45 -34
- package/app/api/media/route.ts +3 -2
- package/app/api/media/stream/route.ts +19 -3
- package/app/settings/page.tsx +8 -16
- package/components/chat/message-item.tsx +18 -6
- package/components/chat/tools/knowledge-base-result.tsx +3 -4
- package/components/data/data-workspace.tsx +14 -10
- package/components/data/media-panel.tsx +112 -86
- package/components/settings/marketplace-section.tsx +5 -0
- package/package.json +4 -4
- package/.next/standalone/apps/web/.next/static/chunks/427kg1r2nc6x0.css +0 -1
- package/.next/static/chunks/427kg1r2nc6x0.css +0 -1
- /package/.next/standalone/apps/web/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_ssgManifest.js +0 -0
- /package/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_buildManifest.js +0 -0
- /package/.next/static/{j-tshQTNg-SioxN4TXZNS → iCALZzxFfElF4z3wYZwsC}/_clientMiddlewareManifest.js +0 -0
- /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
|
package/app/api/chat/route.ts
CHANGED
|
@@ -64,28 +64,24 @@ function createChatModel(
|
|
|
64
64
|
|
|
65
65
|
const SMART_RETRIEVAL_POLICY = `
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- Search
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
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  — 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
|
-
|
|
106
|
-
|
|
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|
|
|
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
|
-
? '\
|
|
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
|
-
//
|
|
405
|
-
|
|
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
|
-
//
|
|
417
|
-
|
|
418
|
-
if (forceKnowledgeBaseSearch)
|
|
419
|
-
|
|
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
|
},
|
package/app/api/media/route.ts
CHANGED
|
@@ -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
|
-
|
|
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(() =>
|
|
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
|
},
|
package/app/settings/page.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
96
|
-
<Loader2 className="size-3
|
|
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('
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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-
|
|
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" />
|