larkup 0.1.28 → 0.1.30

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 (163) 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 +22 -22
  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.js.nft.json +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +1 -1
  17. package/.next/server/app/_not-found.rsc +16 -16
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
  19. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  20. package/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  23. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  24. package/.next/server/app/analytics/page.js.nft.json +1 -1
  25. package/.next/server/app/analytics/page_client-reference-manifest.js +1 -1
  26. package/.next/server/app/analytics.html +1 -1
  27. package/.next/server/app/analytics.rsc +17 -17
  28. package/.next/server/app/analytics.segments/_full.segment.rsc +17 -17
  29. package/.next/server/app/analytics.segments/_head.segment.rsc +4 -4
  30. package/.next/server/app/analytics.segments/_index.segment.rsc +11 -11
  31. package/.next/server/app/analytics.segments/_tree.segment.rsc +1 -1
  32. package/.next/server/app/analytics.segments/analytics/__PAGE__.segment.rsc +3 -3
  33. package/.next/server/app/analytics.segments/analytics.segment.rsc +3 -3
  34. package/.next/server/app/api/chat/route.js +8 -9
  35. package/.next/server/app/api/chat/route.js.nft.json +1 -1
  36. package/.next/server/app/api/config/test-provider/route.js +4 -4
  37. package/.next/server/app/api/config/test-provider/route.js.nft.json +1 -1
  38. package/.next/server/app/api/config/verify/route.js +3 -3
  39. package/.next/server/app/api/config/verify/route.js.nft.json +1 -1
  40. package/.next/server/app/api/marketplace/[toolId]/route.js +1 -1
  41. package/.next/server/app/api/marketplace/[toolId]/route.js.nft.json +1 -1
  42. package/.next/server/app/api/marketplace/route.js +1 -1
  43. package/.next/server/app/api/marketplace/route.js.nft.json +1 -1
  44. package/.next/server/app/api/media/process/route.js +2 -2
  45. package/.next/server/app/api/media/process/route.js.nft.json +1 -1
  46. package/.next/server/app/api/media/route.js +1 -1
  47. package/.next/server/app/api/media/route.js.nft.json +1 -1
  48. package/.next/server/app/chat/page.js.nft.json +1 -1
  49. package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  50. package/.next/server/app/chat.html +1 -1
  51. package/.next/server/app/chat.rsc +17 -17
  52. package/.next/server/app/chat.segments/_full.segment.rsc +17 -17
  53. package/.next/server/app/chat.segments/_head.segment.rsc +4 -4
  54. package/.next/server/app/chat.segments/_index.segment.rsc +11 -11
  55. package/.next/server/app/chat.segments/_tree.segment.rsc +1 -1
  56. package/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +3 -3
  57. package/.next/server/app/chat.segments/chat.segment.rsc +3 -3
  58. package/.next/server/app/data/page.js.nft.json +1 -1
  59. package/.next/server/app/data/page_client-reference-manifest.js +1 -1
  60. package/.next/server/app/data.html +1 -1
  61. package/.next/server/app/data.rsc +17 -17
  62. package/.next/server/app/data.segments/_full.segment.rsc +17 -17
  63. package/.next/server/app/data.segments/_head.segment.rsc +4 -4
  64. package/.next/server/app/data.segments/_index.segment.rsc +11 -11
  65. package/.next/server/app/data.segments/_tree.segment.rsc +1 -1
  66. package/.next/server/app/data.segments/data/__PAGE__.segment.rsc +3 -3
  67. package/.next/server/app/data.segments/data.segment.rsc +3 -3
  68. package/.next/server/app/index.html +1 -1
  69. package/.next/server/app/index.rsc +16 -16
  70. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  71. package/.next/server/app/index.segments/_full.segment.rsc +16 -16
  72. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  73. package/.next/server/app/index.segments/_index.segment.rsc +11 -11
  74. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  75. package/.next/server/app/page.js.nft.json +1 -1
  76. package/.next/server/app/page_client-reference-manifest.js +1 -1
  77. package/.next/server/app/settings/page.js.nft.json +1 -1
  78. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  79. package/.next/server/app/settings.html +1 -1
  80. package/.next/server/app/settings.rsc +18 -18
  81. package/.next/server/app/settings.segments/_full.segment.rsc +18 -18
  82. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  83. package/.next/server/app/settings.segments/_index.segment.rsc +11 -11
  84. package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  85. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  86. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  87. package/.next/server/chunks/0598_next_dist_esm_build_templates_app-route_0kbcuk-.js +4 -3
  88. package/.next/server/chunks/0598_next_dist_esm_build_templates_app-route_1f4h7rr.js +1 -1
  89. package/.next/server/chunks/[root-of-the-server]__067hiof._.js +18 -6
  90. package/.next/server/chunks/[root-of-the-server]__0a_0ywc._.js +18 -6
  91. package/.next/server/chunks/[root-of-the-server]__0c80bj0._.js +3 -0
  92. package/.next/server/chunks/{[root-of-the-server]__1m7m2x7._.js → [root-of-the-server]__0drkoru._.js} +2 -2
  93. package/.next/server/chunks/{[root-of-the-server]__0vnm6_8._.js → [root-of-the-server]__0kmh740._.js} +6 -6
  94. package/.next/server/chunks/{[root-of-the-server]__0ou70-v._.js → [root-of-the-server]__0nfenje._.js} +1 -1
  95. package/.next/server/chunks/[root-of-the-server]__0omd-x0._.js +3 -0
  96. package/.next/server/chunks/[root-of-the-server]__0u17zp8._.js +18 -6
  97. package/.next/server/chunks/{[root-of-the-server]__1s1zui-._.js → [root-of-the-server]__10v5qsi._.js} +1 -1
  98. package/.next/server/chunks/{[root-of-the-server]__1xuhh_i._.js → [root-of-the-server]__16jk_se._.js} +2 -2
  99. package/.next/server/chunks/[root-of-the-server]__1ahdfq9._.js +19 -7
  100. package/.next/server/chunks/[root-of-the-server]__1idgvjh._.js +172 -0
  101. package/.next/server/chunks/[root-of-the-server]__1ip05r1._.js +1 -1
  102. package/.next/server/chunks/[root-of-the-server]__1jc54o2._.js +3 -0
  103. package/.next/server/chunks/{[root-of-the-server]__0748x7h._.js → [root-of-the-server]__1lhls88._.js} +1 -1
  104. package/.next/server/chunks/[root-of-the-server]__1o3rcbz._.js +2 -1
  105. package/.next/server/chunks/{[root-of-the-server]__10gcb7d._.js → [root-of-the-server]__1qh2h5v._.js} +2 -2
  106. package/.next/server/chunks/[root-of-the-server]__1s9is1o._.js +2 -1
  107. package/.next/server/chunks/[root-of-the-server]__1vsvck0._.js +18 -6
  108. package/.next/server/chunks/{[root-of-the-server]__0g_q-44._.js → [root-of-the-server]__1wy6wes._.js} +2 -2
  109. package/.next/server/chunks/[root-of-the-server]__1zptg4z._.js +2 -1
  110. package/.next/server/chunks/{packages_1f4dx4h._.js → packages_1ggqion._.js} +2 -2
  111. package/.next/server/chunks/packages_core_src_generator_server-runtime_ts_1_68l7t._.js +4 -4
  112. package/.next/server/chunks/packages_marketplace_src_125_g2p._.js +1 -1
  113. package/.next/server/chunks/{packages_vector-stores_src_064-9ay._.js → packages_vector-stores_src_1fib7u-._.js} +2 -2
  114. package/.next/server/chunks/ssr/{[root-of-the-server]__0wdatpx._.js → [root-of-the-server]__0m21fli._.js} +1 -1
  115. package/.next/server/chunks/ssr/{[root-of-the-server]__1r9ap67._.js → [root-of-the-server]__0t8ud_u._.js} +1 -1
  116. package/.next/server/chunks/ssr/{[root-of-the-server]__046ldu_._.js → [root-of-the-server]__0xyo3ni._.js} +1 -1
  117. package/.next/server/chunks/ssr/{_0k81--x._.js → _0ckxy33._.js} +2 -2
  118. package/.next/server/chunks/ssr/_169pdlf._.js +6 -6
  119. package/.next/server/chunks/ssr/_1hetwwu._.js +4 -3
  120. package/.next/server/chunks/ssr/_1yqlx1m._.js +1 -1
  121. package/.next/server/chunks/ssr/apps_web_components_analytics_analytics-dashboard_tsx_1056-z3._.js +1 -1
  122. package/.next/server/chunks/ssr/apps_web_lib_pdf-images_ts_13-l5wa._.js +1 -1
  123. package/.next/server/functions-config-manifest.json +3 -1
  124. package/.next/server/middleware-build-manifest.js +3 -3
  125. package/.next/server/pages/404.html +1 -1
  126. package/.next/server/pages/500.html +1 -1
  127. package/.next/static/chunks/{3bpr59xr-_rv7.js → 0ge3nirnmx_np.js} +1 -1
  128. package/.next/static/chunks/{0rgt7-bqxonef.js → 1uzf7x2okeuab.js} +6 -6
  129. package/.next/static/chunks/{3g_wl1fri6jb-.js → 21_6-9hjj2i68.js} +1 -1
  130. package/.next/static/chunks/2ib3e0xrrdait.js +1 -0
  131. package/.next/static/chunks/2jrpgzxyrmuqp.js +121 -0
  132. package/.next/static/chunks/390ds7l_plpmd.js +1 -0
  133. package/.next/static/chunks/{3een1tiktxr6a.js → 3zw6tgf7mxo39.js} +1 -1
  134. package/CHANGELOG.md +22 -0
  135. package/app/api/chat/route.ts +1 -0
  136. package/app/api/chat/tools.ts +5 -1
  137. package/app/api/config/test-provider/route.ts +16 -18
  138. package/app/api/config/verify/route.ts +5 -7
  139. package/app/api/describe-image/route.ts +12 -4
  140. package/app/api/firecrawl/local/route.ts +1 -1
  141. package/app/api/jobs/route.ts +1 -2
  142. package/app/api/marketplace/[toolId]/route.ts +1 -0
  143. package/components/analytics/analytics-dashboard.tsx +1 -1
  144. package/components/data/firecrawl-notice.tsx +49 -98
  145. package/components/data/media-panel.tsx +34 -5
  146. package/components/data/scrape-panel.tsx +17 -1
  147. package/components/data/upload-panel.tsx +2 -1
  148. package/components/onboarding/welcome-screen.tsx +45 -15
  149. package/components/settings/general-section.tsx +15 -10
  150. package/components/settings/marketplace-section.tsx +14 -2
  151. package/components/settings/server-section.tsx +3 -3
  152. package/lib/error-formatter.ts +19 -4
  153. package/lib/pdf-images.ts +20 -5
  154. package/package.json +7 -7
  155. package/.next/server/chunks/[root-of-the-server]__00r1kgp._.js +0 -143
  156. package/.next/server/chunks/[root-of-the-server]__064gb96._.js +0 -3
  157. package/.next/server/chunks/_0ce9vcg._.js +0 -32
  158. package/.next/static/chunks/00z1f-adeq1lg.js +0 -1
  159. package/.next/static/chunks/0k405nomf6s4e.js +0 -120
  160. package/.next/static/chunks/13hmtkhrbm4t3.js +0 -1
  161. /package/.next/static/{bDwMIFYSTVBKUJkJJx77o → Laf7YhpONHOZXSCw6POiL}/_buildManifest.js +0 -0
  162. /package/.next/static/{bDwMIFYSTVBKUJkJJx77o → Laf7YhpONHOZXSCw6POiL}/_clientMiddlewareManifest.js +0 -0
  163. /package/.next/static/{bDwMIFYSTVBKUJkJJx77o → Laf7YhpONHOZXSCw6POiL}/_ssgManifest.js +0 -0
@@ -1,21 +1,12 @@
1
- "use client";
1
+ 'use client';
2
2
 
3
- import { useState, useEffect } from "react";
4
- import useSWR from "swr";
5
- import {
6
- CheckCircle2,
7
- KeyRound,
8
- Loader2,
9
- Play,
10
- Server,
11
- Square,
12
- TriangleAlert,
13
- } from "lucide-react";
14
- import { toast } from "sonner";
15
- import { formatErrorMessage } from "@/lib/error-formatter";
16
- import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
17
- import { Button } from "@/components/ui/button";
18
- import { Badge } from "@/components/ui/badge";
3
+ import { useState, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import useSWR from 'swr';
6
+ import { Loader2, Play, Square } from 'lucide-react';
7
+ import { toast } from 'sonner';
8
+ import { formatErrorMessage } from '@/lib/error-formatter';
9
+ import { Button } from '@/components/ui/button';
19
10
 
20
11
  interface LocalState {
21
12
  running: boolean;
@@ -33,7 +24,7 @@ interface DockerState {
33
24
  interface LocalResponse {
34
25
  state: LocalState;
35
26
  docker: DockerState;
36
- runtimeEnv?: "web" | "desktop" | "docker";
27
+ runtimeEnv?: 'web' | 'desktop' | 'docker';
37
28
  }
38
29
 
39
30
  const fetcher = (url: string) => fetch(url).then((r) => r.json());
@@ -57,19 +48,15 @@ export function FirecrawlNotice({
57
48
  onChange?: () => void;
58
49
  onErrorChange?: (hasError: boolean) => void;
59
50
  }) {
60
- const { data, mutate, isLoading } = useSWR<LocalResponse>(
61
- "/api/firecrawl/local",
62
- fetcher,
63
- {
64
- refreshInterval: (d) =>
65
- d?.state.running || d?.state.startedAt ? 8000 : 0,
66
- },
67
- );
68
- const [busy, setBusy] = useState<"start" | "stop" | null>(null);
51
+ const router = useRouter();
52
+ const { data, mutate, isLoading } = useSWR<LocalResponse>('/api/firecrawl/local', fetcher, {
53
+ refreshInterval: (d) => (d?.state.running || d?.state.startedAt ? 8000 : 0),
54
+ });
55
+ const [busy, setBusy] = useState<'start' | 'stop' | null>(null);
69
56
 
70
57
  const state = data?.state;
71
58
  const docker = data?.docker;
72
- const runtimeEnv = data?.runtimeEnv ?? "web";
59
+ const runtimeEnv = data?.runtimeEnv ?? 'web';
73
60
  const running = state?.running ?? false;
74
61
  const dockerReady = docker?.compose ?? false;
75
62
  const hasError = !cloudConfigured && (!dockerReady || !!state?.lastError);
@@ -80,49 +67,45 @@ export function FirecrawlNotice({
80
67
  }
81
68
  }, [hasError, isLoading, onErrorChange]);
82
69
 
83
- async function control(action: "start" | "stop") {
70
+ async function control(action: 'start' | 'stop') {
84
71
  setBusy(action);
85
- if (action === "start") {
72
+ if (action === 'start') {
86
73
  toast.info(
87
- runtimeEnv === "docker"
88
- ? "Connecting to crawler service…"
89
- : "Launching web crawler…",
74
+ runtimeEnv === 'docker' ? 'Connecting to crawler service…' : 'Launching web crawler…',
90
75
  {
91
76
  description:
92
- runtimeEnv === "docker"
93
- ? "Checking if the crawler service is available on the network."
94
- : "Pulling images and starting containers. This can take a minute on first run.",
77
+ runtimeEnv === 'docker'
78
+ ? 'Checking if the crawler service is available on the network.'
79
+ : 'Pulling images and starting containers. This can take a minute on first run.',
95
80
  },
96
81
  );
97
82
  }
98
83
  try {
99
- const res = await fetch("/api/firecrawl/local", {
100
- method: "POST",
101
- headers: { "Content-Type": "application/json" },
84
+ const res = await fetch('/api/firecrawl/local', {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
102
87
  body: JSON.stringify({ action }),
103
88
  });
104
89
  const json = (await res.json()) as { state?: LocalState; error?: string };
105
- if (!res.ok) throw new Error(json.error || "Request failed");
90
+ if (!res.ok) throw new Error(json.error || 'Request failed');
106
91
 
107
- if (action === "start") {
92
+ if (action === 'start') {
108
93
  if (json.state?.running) {
109
- toast.success("Web crawler is running", {
110
- description: "Web scraping is now active.",
94
+ toast.success('Web crawler is running', {
95
+ description: 'Web scraping is now active.',
111
96
  });
112
97
  } else {
113
- toast.warning("Crawler started", {
114
- description:
115
- json.state?.lastError ??
116
- "Waiting for the service to become healthy.",
98
+ toast.warning('Web crawler is starting', {
99
+ description: 'It may take a moment to become available. Try again shortly.',
117
100
  });
118
101
  }
119
102
  } else {
120
- toast.success("Web crawler stopped");
103
+ toast.success('Web crawler stopped');
121
104
  }
122
105
  await mutate();
123
106
  onChange?.();
124
107
  } catch (err) {
125
- toast.error("Could not control web crawler", {
108
+ toast.error('Could not connect to the web crawler', {
126
109
  description: formatErrorMessage(err),
127
110
  });
128
111
  } finally {
@@ -130,31 +113,16 @@ export function FirecrawlNotice({
130
113
  }
131
114
  }
132
115
 
133
- // Both cloud and local options are kept available.
134
-
135
- // Description text based on environment
136
- const idleDescription = (() => {
137
- if (runtimeEnv === "docker") {
138
- return dockerReady
139
- ? "A crawler service was found on the network. Click to connect."
140
- : "Start your deployment with the crawler profile to enable web scraping: docker compose --profile crawler up";
141
- }
142
- if (cloudConfigured) {
143
- return "You have a cloud key set. You can also run the crawler locally via Docker.";
144
- }
145
- return "Launch a self-hosted web crawler via Docker. A key is generated automatically — no signup required. Pasting and uploading work without it.";
146
- })();
147
-
148
116
  return (
149
117
  <div className="flex items-center gap-2">
150
118
  {running ? (
151
119
  <Button
152
120
  variant="outline"
153
121
  className="h-10 gap-1.5 rounded-md bg-red-500 hover:bg-red-600 text-white hover:text-white hover:border-red-200 text-sm px-4 shadow-none"
154
- onClick={() => control("stop")}
122
+ onClick={() => control('stop')}
155
123
  disabled={busy !== null}
156
124
  >
157
- {busy === "stop" ? (
125
+ {busy === 'stop' ? (
158
126
  <Loader2 className="size-3 animate-spin" />
159
127
  ) : (
160
128
  <Square className="size-3" />
@@ -164,45 +132,28 @@ export function FirecrawlNotice({
164
132
  ) : (
165
133
  <Button
166
134
  variant="outline"
167
- onClick={() => control("start")}
168
- disabled={
169
- busy !== null ||
170
- isLoading ||
171
- (!dockerReady && runtimeEnv !== "docker")
172
- }
135
+ onClick={() => {
136
+ if (runtimeEnv === 'docker' && !dockerReady) {
137
+ router.push('/settings?section=general#web-crawler');
138
+ return;
139
+ }
140
+ control('start');
141
+ }}
142
+ disabled={busy !== null || isLoading}
173
143
  className="h-10 gap-1.5 rounded-md text-sm px-4 shadow-none bg-orange-500 text-white hover:bg-orange-600 hover:text-white"
174
144
  >
175
- {busy === "start" ? (
145
+ {busy === 'start' ? (
176
146
  <Loader2 className="size-3 animate-spin text-muted-foreground" />
177
147
  ) : (
178
- <img
179
- src="/icons/firecrawl2.png"
180
- alt=""
181
- className="size-3 object-contain"
182
- />
148
+ <img src="/icons/firecrawl2.png" alt="" className="size-3 object-contain" />
183
149
  )}
184
- {runtimeEnv === "docker" ? "Connect Crawler" : "Launch Crawler"}
150
+ {runtimeEnv === 'docker' && !dockerReady
151
+ ? 'Crawler settings'
152
+ : runtimeEnv === 'docker'
153
+ ? 'Connect Crawler'
154
+ : 'Launch Crawler'}
185
155
  </Button>
186
156
  )}
187
-
188
- {state?.lastError && !running && (
189
- <span
190
- className="text-[10px] text-destructive flex items-center gap-1 max-w-[200px] truncate"
191
- title={state.lastError}
192
- >
193
- <TriangleAlert className="size-3 shrink-0" />
194
- <span className="truncate">Error starting crawler</span>
195
- </span>
196
- )}
197
- {!isLoading && !dockerReady && !running && docker?.message && (
198
- <span
199
- className="text-[10px] text-destructive flex items-center gap-1 max-w-[200px] truncate"
200
- title={docker.message}
201
- >
202
- <TriangleAlert className="size-3 shrink-0" />
203
- <span className="truncate">Docker issue</span>
204
- </span>
205
- )}
206
157
  </div>
207
158
  );
208
159
  }
@@ -304,6 +304,12 @@ export function MediaPanel({
304
304
  const videoAudioTool = toolsData?.tools?.find((t: any) => t.id === 'video-audio');
305
305
  const isToolInstalled = videoAudioTool?.status === 'installed';
306
306
  const enabledTools = configData?.config?.enabledTools;
307
+ const videoAudioConfig = configData?.config?.toolConfigs?.['video-audio'] ?? {};
308
+ const audioProvider =
309
+ typeof videoAudioConfig.audioProvider === 'string' ? videoAudioConfig.audioProvider : '';
310
+ const audioApiKey =
311
+ typeof videoAudioConfig.audioApiKey === 'string' ? videoAudioConfig.audioApiKey : '';
312
+ const audioConfigured = Boolean(audioProvider && (audioProvider === 'local' || audioApiKey));
307
313
  const isToolEnabled = enabledTools
308
314
  ? enabledTools.length === 0 || enabledTools.includes('video-audio')
309
315
  : true;
@@ -391,6 +397,8 @@ export function MediaPanel({
391
397
  mutate();
392
398
  onAdded();
393
399
  }}
400
+ audioConfigured={audioConfigured}
401
+ onConfigureAudio={() => router.push('/settings?section=tool-settings')}
394
402
  />
395
403
  )}
396
404
  </div>
@@ -428,7 +436,13 @@ function InstallPrompt({
428
436
  const err = await res.json();
429
437
  throw new Error(err.error || 'Install failed');
430
438
  }
431
- toast.success('Tool installed successfully');
439
+ toast.success('Video & Audio installed', {
440
+ description: 'Choose an audio provider before indexing video or audio.',
441
+ action: {
442
+ label: 'Set up audio',
443
+ onClick: () => window.location.assign('/settings?section=tool-settings'),
444
+ },
445
+ });
432
446
  onInstallComplete();
433
447
  } catch (err) {
434
448
  toast.error(err instanceof Error ? err.message : 'Failed to install tool');
@@ -509,6 +523,8 @@ function MediaContent({
509
523
  serverId,
510
524
  onMutate,
511
525
  onUploadComplete,
526
+ audioConfigured,
527
+ onConfigureAudio,
512
528
  }: {
513
529
  mediaType: 'image' | 'video' | 'audio';
514
530
  tab: MediaSubTab;
@@ -518,6 +534,8 @@ function MediaContent({
518
534
  serverId?: string;
519
535
  onMutate: () => void | Promise<unknown>;
520
536
  onUploadComplete: () => void;
537
+ audioConfigured: boolean;
538
+ onConfigureAudio: () => void;
521
539
  }) {
522
540
  const inputRef = useRef<HTMLInputElement>(null);
523
541
  const router = useRouter();
@@ -587,7 +605,8 @@ function MediaContent({
587
605
  if (
588
606
  msg.toLowerCase().includes('api key') ||
589
607
  msg.toLowerCase().includes('configuration') ||
590
- msg.toLowerCase().includes('missing provider')
608
+ msg.toLowerCase().includes('missing provider') ||
609
+ msg.toLowerCase().includes('choose an audio provider')
591
610
  ) {
592
611
  toast.error(msg, {
593
612
  duration: 10000,
@@ -601,6 +620,16 @@ function MediaContent({
601
620
  }
602
621
  }
603
622
 
623
+ function ensureAudioConfiguration(): boolean {
624
+ if (mediaType === 'image' || audioConfigured) return true;
625
+ toast.error('Set up an audio provider to index video or audio.', {
626
+ description: 'Select a provider and add its API key in Installed Tools.',
627
+ duration: 10_000,
628
+ action: { label: 'Set up audio', onClick: onConfigureAudio },
629
+ });
630
+ return false;
631
+ }
632
+
604
633
  async function processAssets(assetIds: string[]) {
605
634
  const PROCESS_BATCH_SIZE = 4;
606
635
  for (let i = 0; i < assetIds.length; i += PROCESS_BATCH_SIZE) {
@@ -635,6 +664,7 @@ function MediaContent({
635
664
  async function reviewRemoteEstimate() {
636
665
  const urls = parsedUrls();
637
666
  if (urls.length === 0) return;
667
+ if (!ensureAudioConfiguration()) return;
638
668
  setCheckingUrls(true);
639
669
  try {
640
670
  const res = await fetch(mediaApiUrl, {
@@ -655,6 +685,7 @@ function MediaContent({
655
685
  async function importRemoteUrls() {
656
686
  const urls = parsedUrls();
657
687
  if (urls.length === 0 || !remoteEstimates) return;
688
+ if (!ensureAudioConfiguration()) return;
658
689
  setImportingUrls(true);
659
690
  try {
660
691
  const res = await fetch(mediaApiUrl, {
@@ -681,6 +712,7 @@ function MediaContent({
681
712
 
682
713
  async function uploadAll() {
683
714
  if (staged.length === 0) return;
715
+ if (!ensureAudioConfiguration()) return;
684
716
  setUploading(true);
685
717
 
686
718
  const BATCH_SIZE = 5;
@@ -1602,9 +1634,6 @@ function ActiveIndexingList({
1602
1634
  {asset.processingMessage || 'Preparing media indexing...'}
1603
1635
  </p>
1604
1636
  </div>
1605
- {asset.processingSteps?.length ? (
1606
- <PipelineSteps steps={asset.processingSteps} />
1607
- ) : null}
1608
1637
  </div>
1609
1638
  );
1610
1639
  })}
@@ -36,6 +36,7 @@ import { GenericAlert } from '@/components/alerts/generic-alert';
36
36
  import { cn } from '@/lib/utils';
37
37
  import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
38
38
  import useSWR from 'swr';
39
+ import { useRouter } from 'next/navigation';
39
40
  import type { RagConfig } from '@larkup/core/types';
40
41
 
41
42
  const fetcher = (url: string) => fetch(url).then((r) => r.json() as Promise<{ config: RagConfig }>);
@@ -85,6 +86,7 @@ export function ScrapePanel({
85
86
  onStarted: () => void;
86
87
  crawlerControl?: React.ReactNode;
87
88
  }) {
89
+ const router = useRouter();
88
90
  const {
89
91
  query,
90
92
  setQuery,
@@ -124,6 +126,15 @@ export function ScrapePanel({
124
126
  const inputRef = useRef<HTMLInputElement>(null);
125
127
  const dropdownRef = useRef<HTMLDivElement>(null);
126
128
 
129
+ function showCrawlerSettings() {
130
+ toast.error('Web crawler is not available. Choose a crawler provider in Settings.', {
131
+ action: {
132
+ label: 'Open settings',
133
+ onClick: () => router.push('/settings?section=general#web-crawler'),
134
+ },
135
+ });
136
+ }
137
+
127
138
  useEffect(() => {
128
139
  function handleClickOutside(event: MouseEvent) {
129
140
  if (
@@ -222,7 +233,12 @@ export function ScrapePanel({
222
233
  fetchProviderTotalCount(q, activeProvider);
223
234
  }
224
235
  } catch (err) {
225
- toast.error(formatErrorMessage(err));
236
+ const message = formatErrorMessage(err);
237
+ if (/crawler|firecrawl|not running|api key/i.test(message)) {
238
+ showCrawlerSettings();
239
+ } else {
240
+ toast.error(message);
241
+ }
226
242
  }
227
243
  }
228
244
 
@@ -314,7 +314,7 @@ export function UploadPanel({ onAdded }: { onAdded: () => void }) {
314
314
 
315
315
  const res = await fetch(img.base64);
316
316
  const blob = await res.blob();
317
- const file = new File([blob], `image-${img.index}.png`, { type: 'image/png' });
317
+ const file = new File([blob], `image-${img.index}.jpg`, { type: 'image/jpeg' });
318
318
 
319
319
  const formData = new FormData();
320
320
  formData.append('file', file);
@@ -386,6 +386,7 @@ export function UploadPanel({ onAdded }: { onAdded: () => void }) {
386
386
  imageUrl: img.imageUrl,
387
387
  pageNumber: img.pageNumber,
388
388
  index: img.index,
389
+ description: img.description,
389
390
  })),
390
391
  }
391
392
  : undefined,
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
- import { ArrowRight, Loader2, ChevronDown, ChevronUp, Pencil } from 'lucide-react';
4
+ import { ArrowRight, Loader2, ChevronDown, ChevronUp, Pencil, Eye, EyeOff } from 'lucide-react';
5
5
  import { Input } from '@/components/ui/input';
6
6
  import { Button } from '@/components/ui/button';
7
7
  import { useWorkspace } from '@/components/workspace/workspace-provider';
@@ -42,6 +42,8 @@ export function WelcomeScreen() {
42
42
  // UI state
43
43
  const [saving, setSaving] = useState(false);
44
44
  const [showSeparateChat, setShowSeparateChat] = useState(false);
45
+ const [showEmbeddingApiKey, setShowEmbeddingApiKey] = useState(false);
46
+ const [showChatApiKey, setShowChatApiKey] = useState(false);
45
47
  const [customModalOpen, setCustomModalOpen] = useState<'embedding' | 'chat' | null>(null);
46
48
 
47
49
  const isCustomEmbedding = embeddingProvider === 'custom';
@@ -326,13 +328,27 @@ export function WelcomeScreen() {
326
328
  </span>
327
329
  )}
328
330
  </label>
329
- <Input
330
- type="password"
331
- value={embeddingApiKey}
332
- onChange={(e) => setEmbeddingApiKey(e.target.value)}
333
- placeholder="sk-..."
334
- className="bg-background/50"
335
- />
331
+ <div className="relative">
332
+ <Input
333
+ type={showEmbeddingApiKey ? 'text' : 'password'}
334
+ value={embeddingApiKey}
335
+ onChange={(e) => setEmbeddingApiKey(e.target.value)}
336
+ placeholder="sk-..."
337
+ className="bg-background/50 pr-10"
338
+ />
339
+ <button
340
+ type="button"
341
+ aria-label={showEmbeddingApiKey ? 'Hide API key' : 'Show API key'}
342
+ onClick={() => setShowEmbeddingApiKey((visible) => !visible)}
343
+ className="absolute inset-y-0 right-0 flex items-center px-3 text-muted-foreground hover:text-foreground"
344
+ >
345
+ {showEmbeddingApiKey ? (
346
+ <EyeOff className="size-4" />
347
+ ) : (
348
+ <Eye className="size-4" />
349
+ )}
350
+ </button>
351
+ </div>
336
352
  </div>
337
353
  )}
338
354
 
@@ -368,13 +384,27 @@ export function WelcomeScreen() {
368
384
  {!isCustomChat && (
369
385
  <div className="space-y-1.5">
370
386
  <label className="text-xs font-medium text-foreground">Chat API Key</label>
371
- <Input
372
- type="password"
373
- value={chatApiKey}
374
- onChange={(e) => setChatApiKey(e.target.value)}
375
- placeholder="sk-..."
376
- className="bg-background/50"
377
- />
387
+ <div className="relative">
388
+ <Input
389
+ type={showChatApiKey ? 'text' : 'password'}
390
+ value={chatApiKey}
391
+ onChange={(e) => setChatApiKey(e.target.value)}
392
+ placeholder="sk-..."
393
+ className="bg-background/50 pr-10"
394
+ />
395
+ <button
396
+ type="button"
397
+ aria-label={showChatApiKey ? 'Hide chat API key' : 'Show chat API key'}
398
+ onClick={() => setShowChatApiKey((visible) => !visible)}
399
+ className="absolute inset-y-0 right-0 flex items-center px-3 text-muted-foreground hover:text-foreground"
400
+ >
401
+ {showChatApiKey ? (
402
+ <EyeOff className="size-4" />
403
+ ) : (
404
+ <Eye className="size-4" />
405
+ )}
406
+ </button>
407
+ </div>
378
408
  </div>
379
409
  )}
380
410
  </div>
@@ -201,7 +201,7 @@ export function GeneralSection() {
201
201
  </div>
202
202
 
203
203
  {/* Username */}
204
- <Card className="">
204
+ <Card id="web-crawler" className="">
205
205
  <CardHeader className="pb-3">
206
206
  <CardTitle className="text-sm">Your Name</CardTitle>
207
207
  <CardDescription className="text-xs">Displayed in the workspace header.</CardDescription>
@@ -427,7 +427,7 @@ export function GeneralSection() {
427
427
  <CardHeader className="pb-3">
428
428
  <CardTitle className="text-sm">Web Scraper</CardTitle>
429
429
  <CardDescription className="text-xs">
430
- Select your web crawler provider (Local or Cloud).
430
+ Choose the web crawler used to search and import websites.
431
431
  </CardDescription>
432
432
  </CardHeader>
433
433
  <CardContent className="space-y-4">
@@ -442,30 +442,35 @@ export function GeneralSection() {
442
442
  >
443
443
  <SelectTrigger className="w-full">
444
444
  <div className="flex items-center gap-2">
445
- <Image src="/icons/firecrawl.png" alt="Firecrawl" width={16} height={16} />
445
+ <Image src="/icons/firecrawl.png" alt="Web crawler" width={16} height={16} />
446
446
  <span>
447
447
  {form.webCrawlerProvider === 'cloud'
448
- ? 'Firecrawl (Cloud)'
449
- : 'Local (Firecrawl)'}
448
+ ? 'Cloud web crawler'
449
+ : 'Local web crawler'}
450
450
  </span>
451
451
  </div>
452
452
  </SelectTrigger>
453
453
  <SelectContent>
454
454
  <SelectItem value="local">
455
455
  <div className="flex items-center gap-2">
456
- <Image src="/icons/firecrawl.png" alt="Local Crawler" width={16} height={16} />
457
- <span>Local (Firecrawl)</span>
456
+ <Image
457
+ src="/icons/firecrawl.png"
458
+ alt="Local web crawler"
459
+ width={16}
460
+ height={16}
461
+ />
462
+ <span>Local web crawler</span>
458
463
  </div>
459
464
  </SelectItem>
460
465
  <SelectItem value="cloud">
461
466
  <div className="flex items-center gap-2">
462
467
  <Image
463
468
  src="/icons/firecrawl.png"
464
- alt="Firecrawl Cloud"
469
+ alt="Cloud web crawler"
465
470
  width={16}
466
471
  height={16}
467
472
  />
468
- <span>Firecrawl (Cloud)</span>
473
+ <span>Cloud web crawler</span>
469
474
  </div>
470
475
  </SelectItem>
471
476
  </SelectContent>
@@ -475,7 +480,7 @@ export function GeneralSection() {
475
480
  {form.webCrawlerProvider === 'cloud' && (
476
481
  <div className="space-y-1.5 pt-2">
477
482
  <div className="flex items-center justify-between">
478
- <Label className="text-xs">Firecrawl API Key (Cloud)</Label>
483
+ <Label className="text-xs">Web crawler API key</Label>
479
484
  {verifyCrawlerStatus.status === 'success' && (
480
485
  <span className="text-[10px] text-green-500 font-medium">✓ Verified</span>
481
486
  )}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useCallback, useMemo } from 'react';
4
4
  import useSWR from 'swr';
5
+ import { useRouter } from 'next/navigation';
5
6
  import {
6
7
  Download,
7
8
  Trash2,
@@ -88,6 +89,7 @@ const fetcher = (url: string) =>
88
89
  fetch(url).then((r) => r.json() as Promise<{ tools: MarketplaceTool[] }>);
89
90
 
90
91
  export function MarketplaceSection() {
92
+ const router = useRouter();
91
93
  const { data, isLoading, mutate } = useSWR('/api/marketplace', fetcher);
92
94
  const [installing, setInstalling] = useState<string | null>(null);
93
95
  const [searchQuery, setSearchQuery] = useState('');
@@ -104,7 +106,17 @@ export function MarketplaceSection() {
104
106
  const err = await res.json();
105
107
  throw new Error(err.error || 'Install failed');
106
108
  }
107
- toast.success('Tool installed successfully');
109
+ if (toolId === 'video-audio') {
110
+ toast.success('Video & Audio installed', {
111
+ description: 'Choose an audio provider before you index media.',
112
+ action: {
113
+ label: 'Set up audio',
114
+ onClick: () => router.push('/settings?section=tool-settings'),
115
+ },
116
+ });
117
+ } else {
118
+ toast.success('Tool installed successfully');
119
+ }
108
120
  mutate();
109
121
  } catch (err) {
110
122
  toast.error(err instanceof Error ? err.message : 'Failed to install tool');
@@ -112,7 +124,7 @@ export function MarketplaceSection() {
112
124
  setInstalling(null);
113
125
  }
114
126
  },
115
- [mutate],
127
+ [mutate, router],
116
128
  );
117
129
 
118
130
  const handleUninstall = useCallback(
@@ -758,9 +758,9 @@ export function ServerSection() {
758
758
  ))}
759
759
  </div>
760
760
  ) : (
761
- <div className="flex flex-col border border-border rounded-md items-center px-4 py-6 justify-center gap-2 mt-5">
762
- <div className="bg-white p-1 border border-border rounded-md">
763
- <ServerIcon className="size-5" />
761
+ <div className="flex flex-col rounded-md items-center px-4 py-6 justify-center gap-2 mt-5">
762
+ <div className="bg-white p-1.5 border border-border rounded-md">
763
+ <ServerIcon className="size-4.5" />
764
764
  </div>
765
765
  <p className=" rounded-lg text-sm text-muted-foreground text-center mx-auto">
766
766
  No cloud deployments yet. Deploy to a provider above to add one here.
@@ -1,12 +1,27 @@
1
1
  export function formatErrorMessage(error: unknown): string {
2
2
  const message = error instanceof Error ? error.message : String(error);
3
+ const normalized = message.toLowerCase();
3
4
 
4
- if (message.includes("Is the docker daemon running?")) {
5
- return "Docker is not running. Please make sure the Docker daemon is started.";
5
+ // Provider SDKs sometimes echo the submitted key or a URL-encoded request
6
+ // URL in authentication failures. Besides being unreadable, that can expose
7
+ // credential material in a toast. Keep the actionable part instead.
8
+ if (
9
+ normalized.includes('authentication failed') ||
10
+ normalized.includes('invalid api key') ||
11
+ normalized.includes('invalid api_key') ||
12
+ normalized.includes('invalid token') ||
13
+ normalized.includes('unauthorized') ||
14
+ normalized.includes('status code 401')
15
+ ) {
16
+ return 'Authentication failed. Check the API key for the selected provider.';
6
17
  }
7
18
 
8
- if (message.includes("fetch failed") || message.includes("Failed to fetch")) {
9
- return "Network error. Please check your connection and try again.";
19
+ if (message.includes('Is the docker daemon running?')) {
20
+ return 'Docker is not running. Please make sure the Docker daemon is started.';
21
+ }
22
+
23
+ if (message.includes('fetch failed') || message.includes('Failed to fetch')) {
24
+ return 'Network error. Please check your connection and try again.';
10
25
  }
11
26
 
12
27
  // Add more common error mappings here as needed