larkup 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/.env +2 -0
  2. package/app/actions/vercel.ts +417 -0
  3. package/app/api/chat/route.ts +192 -0
  4. package/app/api/chat/status/route.ts +55 -0
  5. package/app/api/chat/suggestions/route.ts +115 -0
  6. package/app/api/config/route.ts +73 -0
  7. package/app/api/config/test/route.ts +67 -0
  8. package/app/api/config/test-embedding/route.ts +40 -0
  9. package/app/api/config/test-llm/route.ts +47 -0
  10. package/app/api/config/test-provider/route.ts +36 -0
  11. package/app/api/demo/route.ts +176 -0
  12. package/app/api/deploy/ssh/route.ts +340 -0
  13. package/app/api/documents/route.ts +90 -0
  14. package/app/api/firecrawl/local/route.ts +46 -0
  15. package/app/api/index/route.ts +113 -0
  16. package/app/api/jobs/[id]/route.ts +44 -0
  17. package/app/api/jobs/route.ts +116 -0
  18. package/app/api/parse-file/route.ts +39 -0
  19. package/app/api/search/google/route.ts +162 -0
  20. package/app/api/search/route.ts +29 -0
  21. package/app/api/server/download/route.ts +112 -0
  22. package/app/api/server/generate/route.ts +46 -0
  23. package/app/api/server/local/route.ts +42 -0
  24. package/app/api/servers/route.ts +137 -0
  25. package/app/api/vector-stores/install/route.ts +65 -0
  26. package/app/api/vector-stores/status/route.ts +26 -0
  27. package/app/chat/page.tsx +15 -0
  28. package/app/configure/page.tsx +53 -0
  29. package/app/data/page.tsx +15 -0
  30. package/app/demo/page.tsx +15 -0
  31. package/app/favicon.ico +0 -0
  32. package/app/globals.css +190 -0
  33. package/app/index-data/page.tsx +15 -0
  34. package/app/layout.tsx +51 -0
  35. package/app/page.tsx +34 -0
  36. package/app/server/page.tsx +22 -0
  37. package/app/simple/chat/page.tsx +17 -0
  38. package/app/simple/docs/page.tsx +21 -0
  39. package/app/simple/layout.tsx +9 -0
  40. package/app/simple/settings/page.tsx +627 -0
  41. package/app/template.tsx +16 -0
  42. package/app/themes.css +733 -0
  43. package/components/alerts/generic-alert.tsx +74 -0
  44. package/components/app-sidebar.tsx +187 -0
  45. package/components/app-topnav.tsx +195 -0
  46. package/components/chat/chat-settings-modal.tsx +209 -0
  47. package/components/chat/chat-workspace.tsx +348 -0
  48. package/components/chat/knowledge-base-result.tsx +97 -0
  49. package/components/chat/message-item.tsx +103 -0
  50. package/components/client-layout-wrapper.tsx +182 -0
  51. package/components/configure/configure-form.tsx +1683 -0
  52. package/components/configure/store-fields.tsx +152 -0
  53. package/components/data/corpus-panel.tsx +499 -0
  54. package/components/data/data-workspace.tsx +272 -0
  55. package/components/data/firecrawl-notice.tsx +236 -0
  56. package/components/data/jobs-panel.tsx +485 -0
  57. package/components/data/paste-panel.tsx +79 -0
  58. package/components/data/scrape-panel.tsx +964 -0
  59. package/components/data/upload-panel.tsx +714 -0
  60. package/components/demo/demo-workspace.tsx +419 -0
  61. package/components/global-settings.tsx +286 -0
  62. package/components/index/index-workspace.tsx +482 -0
  63. package/components/onboarding/simple-setup.tsx +702 -0
  64. package/components/onboarding/tech-setup.tsx +121 -0
  65. package/components/onboarding/welcome-screen.tsx +183 -0
  66. package/components/page-header.tsx +44 -0
  67. package/components/server/code-viewer.tsx +87 -0
  68. package/components/server/deploy-button.tsx +1390 -0
  69. package/components/server/server-workspace.tsx +392 -0
  70. package/components/simple/sdk-connect-dialog.tsx +201 -0
  71. package/components/simple/simple-index-button.tsx +83 -0
  72. package/components/simple/simple-sidebar.tsx +161 -0
  73. package/components/theme-customizer-provider.tsx +171 -0
  74. package/components/theme-switcher.tsx +445 -0
  75. package/components/ui/alert-dialog.tsx +187 -0
  76. package/components/ui/alert.tsx +76 -0
  77. package/components/ui/badge.tsx +52 -0
  78. package/components/ui/button.tsx +59 -0
  79. package/components/ui/card.tsx +103 -0
  80. package/components/ui/checkbox.tsx +28 -0
  81. package/components/ui/collapsible.tsx +21 -0
  82. package/components/ui/dialog.tsx +159 -0
  83. package/components/ui/dropdown-menu.tsx +281 -0
  84. package/components/ui/input.tsx +22 -0
  85. package/components/ui/label.tsx +20 -0
  86. package/components/ui/popover.tsx +90 -0
  87. package/components/ui/progress.tsx +83 -0
  88. package/components/ui/provider-icon.tsx +94 -0
  89. package/components/ui/scroll-area.tsx +55 -0
  90. package/components/ui/select.tsx +248 -0
  91. package/components/ui/separator.tsx +25 -0
  92. package/components/ui/sheet.tsx +138 -0
  93. package/components/ui/skeleton.tsx +13 -0
  94. package/components/ui/sonner.tsx +63 -0
  95. package/components/ui/switch.tsx +32 -0
  96. package/components/ui/table.tsx +116 -0
  97. package/components/ui/tabs.tsx +84 -0
  98. package/components/ui/textarea.tsx +18 -0
  99. package/components/ui/tooltip.tsx +66 -0
  100. package/components/workspace/delete-server-dialog.tsx +71 -0
  101. package/components/workspace/server-form-dialog.tsx +103 -0
  102. package/components/workspace/server-switcher.tsx +163 -0
  103. package/components/workspace/workspace-provider.tsx +177 -0
  104. package/components/workspace/workspace-top-bar.tsx +23 -0
  105. package/components.json +21 -0
  106. package/lib/error-formatter.ts +15 -0
  107. package/lib/utils.ts +6 -0
  108. package/next-env.d.ts +6 -0
  109. package/next.config.mjs +18 -0
  110. package/package.json +65 -0
  111. package/postcss.config.mjs +8 -0
  112. package/public/docker.png +0 -0
  113. package/public/favicon.ico +0 -0
  114. package/public/github.svg +3 -0
  115. package/public/hetzner.svg +1 -0
  116. package/public/icons/aws.svg +38 -0
  117. package/public/icons/azure.svg +1 -0
  118. package/public/icons/chroma.png +0 -0
  119. package/public/icons/cohere.svg +30 -0
  120. package/public/icons/digital-ocean.webp +0 -0
  121. package/public/icons/firecrawl2.png +0 -0
  122. package/public/icons/gcp.svg +1 -0
  123. package/public/icons/gemini.svg +1 -0
  124. package/public/icons/jina.svg +1 -0
  125. package/public/icons/lancedb2.png +0 -0
  126. package/public/icons/mistral.svg +469 -0
  127. package/public/icons/nomic.png +0 -0
  128. package/public/icons/openai.svg +1 -0
  129. package/public/icons/pgvector2.png +0 -0
  130. package/public/icons/pinecone.png +0 -0
  131. package/public/icons/qdrant.svg +35 -0
  132. package/public/icons/supabase.png +0 -0
  133. package/public/icons/vercel copy.svg +1 -0
  134. package/public/icons/vercel.svg +1 -0
  135. package/public/icons/voyage-light.png +0 -0
  136. package/public/icons/weaviate.webp +0 -0
  137. package/public/logo-light.png +0 -0
  138. package/public/logo.png +0 -0
  139. package/public/logo9.png +0 -0
  140. package/public/vercel.svg +1 -0
  141. package/tsconfig.json +33 -0
@@ -0,0 +1,272 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef } from "react";
4
+ import useSWR, { useSWRConfig } from "swr";
5
+ import { ClipboardPaste, FileUp, Globe, Layers, Plus } from "lucide-react";
6
+ import { toast } from "sonner";
7
+ import type { CrawlJob, SourceDocument } from "@larkup/core/types";
8
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
9
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
10
+ import { Skeleton } from "@/components/ui/skeleton";
11
+ import { Button } from "@/components/ui/button";
12
+
13
+ import { ScrapePanel } from "@/components/data/scrape-panel";
14
+ import { PastePanel } from "@/components/data/paste-panel";
15
+ import { UploadPanel } from "@/components/data/upload-panel";
16
+ import { JobsPanel } from "@/components/data/jobs-panel";
17
+ import { CorpusPanel } from "@/components/data/corpus-panel";
18
+ import { FirecrawlNotice } from "@/components/data/firecrawl-notice";
19
+
20
+ const fetcher = (url: string) => fetch(url).then((r) => r.json());
21
+
22
+ interface DocsResponse {
23
+ documents: SourceDocument[];
24
+ stats: {
25
+ docCount: number;
26
+ charCount: number;
27
+ bySource: Record<string, number>;
28
+ };
29
+ }
30
+
31
+ async function fetchJobsWithSync(url: string): Promise<{
32
+ jobs: CrawlJob[];
33
+ configured: boolean;
34
+ }> {
35
+ const { jobs, configured } = (await fetcher(url)) as {
36
+ jobs: CrawlJob[];
37
+ configured: boolean;
38
+ };
39
+ const active = jobs.filter(
40
+ (j) => j.status === "running" || j.status === "queued",
41
+ );
42
+ if (active.length === 0) return { jobs, configured };
43
+
44
+ const advanced = await Promise.all(
45
+ active.map((j) =>
46
+ fetch(`/api/jobs/${j.id}`)
47
+ .then((r) => r.json())
48
+ .then((d) => d.job as CrawlJob)
49
+ .catch(() => j),
50
+ ),
51
+ );
52
+ const map = new Map(advanced.map((j) => [j.id, j]));
53
+ return { jobs: jobs.map((j) => map.get(j.id) ?? j), configured };
54
+ }
55
+
56
+ export function DataWorkspace() {
57
+ const [activeTab, setActiveTab] = useState<"add" | "corpus" | "etl">("add");
58
+ const [scrapeHasError, setScrapeHasError] = useState(false);
59
+ const prevJobsRef = useRef<CrawlJob[]>([]);
60
+
61
+ const jobsQuery = useSWR("/api/jobs", fetchJobsWithSync, {
62
+ refreshInterval: (data) =>
63
+ data?.jobs.some((j) => j.status === "running" || j.status === "queued")
64
+ ? 4000
65
+ : 0,
66
+ });
67
+ const jobs = jobsQuery.data?.jobs ?? [];
68
+ const configured = jobsQuery.data?.configured ?? true;
69
+ const hasActive = jobs.some(
70
+ (j) => j.status === "running" || j.status === "queued",
71
+ );
72
+
73
+ useEffect(() => {
74
+ const prevJobs = prevJobsRef.current;
75
+ if (prevJobs.length > 0 && jobs.length > 0) {
76
+ const justCompleted = jobs.filter(
77
+ (j) =>
78
+ j.status === "completed" &&
79
+ prevJobs.some(
80
+ (pj) =>
81
+ pj.id === j.id &&
82
+ (pj.status === "running" || pj.status === "queued"),
83
+ ),
84
+ );
85
+
86
+ if (justCompleted.length > 0) {
87
+ toast.success(
88
+ "Indexing these corpus just added successfully completed.",
89
+ );
90
+ }
91
+ }
92
+ prevJobsRef.current = jobs;
93
+ }, [jobs]);
94
+
95
+ const docsQuery = useSWR<DocsResponse>("/api/documents", fetcher, {
96
+ refreshInterval: hasActive ? 5000 : 0,
97
+ });
98
+ const documents = docsQuery.data?.documents ?? [];
99
+ const stats = docsQuery.data?.stats;
100
+
101
+ const { mutate: mutateGlobal } = useSWRConfig();
102
+
103
+ const refreshAll = () => {
104
+ jobsQuery.mutate();
105
+ docsQuery.mutate();
106
+ mutateGlobal("/api/index");
107
+ };
108
+
109
+ const handleDataAdded = () => {
110
+ refreshAll();
111
+ setActiveTab("etl");
112
+ };
113
+
114
+ return (
115
+ <div className="px-6 md:px-8">
116
+ <Tabs
117
+ value={activeTab}
118
+ onValueChange={(v) => setActiveTab(v as "add" | "corpus" | "etl")}
119
+ className="w-full"
120
+ >
121
+ <TabsList className="mb-3 flex w-full h-10 justify-start rounded-none border-b border-border bg-transparent p-0! ">
122
+ <TabsTrigger
123
+ value="add"
124
+ className="h-full flex-none rounded-none border-b-2 border-transparent bg-transparent! px-4 font-medium text-muted-foreground shadow-none! transition-none data-active:border-transparent data-[state=active]:border-transparent data-active:border-b-foreground data-[state=active]:border-b-foreground data-active:text-foreground data-[state=active]:text-foreground hover:text-foreground"
125
+ >
126
+ {/* <Plus className="size-4 mr-2" /> */}
127
+ Add Data
128
+ </TabsTrigger>
129
+ <TabsTrigger
130
+ value="corpus"
131
+ className="h-full flex-none rounded-none border-b-2 border-transparent bg-transparent! px-4 font-medium text-muted-foreground shadow-none! transition-none data-active:border-transparent data-[state=active]:border-transparent data-active:border-b-foreground data-[state=active]:border-b-foreground data-active:text-foreground data-[state=active]:text-foreground hover:text-foreground"
132
+ >
133
+ Corpus
134
+ </TabsTrigger>
135
+ <TabsTrigger
136
+ value="etl"
137
+ className="h-full flex-none rounded-none border-b-2 border-transparent bg-transparent! px-4 font-medium text-muted-foreground shadow-none! transition-none data-active:border-transparent data-[state=active]:border-transparent data-active:border-b-foreground data-[state=active]:border-b-foreground data-active:text-foreground data-[state=active]:text-foreground hover:text-foreground"
138
+ >
139
+ ETL Jobs
140
+ {hasActive && (
141
+ <span className="ml-2 flex items-center gap-1.5 text-xs text-green-600">
142
+ <span className="relative flex size-2">
143
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-600 opacity-75" />
144
+ <span className="relative inline-flex size-2 rounded-full bg-green-600" />
145
+ </span>
146
+ </span>
147
+ )}
148
+ </TabsTrigger>
149
+ </TabsList>
150
+
151
+ <TabsContent
152
+ value="add"
153
+ className="m-0 focus-visible:outline-none focus-visible:ring-0 p-0"
154
+ >
155
+ <Card className="bg-transparent border-none! ring-0 p-0 m-0! pl-0! shadow-none">
156
+ <CardContent className="border-none shadow-none m-0! p-0.5">
157
+ <Tabs defaultValue="scrape" className="flex flex-col w-full">
158
+ <div className="shrink-0 mb-6">
159
+ <TabsList className="inline-flex h-10! items-center justify-center rounded-lg bg-muted/30! border p-1 text-muted-foreground">
160
+ <TabsTrigger
161
+ value="scrape"
162
+ className="inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all focus-visible:outline-none data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm hover:text-foreground"
163
+ >
164
+ <Globe className="size-4 mr-2" />
165
+ Web scrape
166
+ </TabsTrigger>
167
+ <TabsTrigger
168
+ value="paste"
169
+ className="inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all focus-visible:outline-none data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm hover:text-foreground"
170
+ >
171
+ <ClipboardPaste className="size-4 mr-2" />
172
+ Paste text
173
+ </TabsTrigger>
174
+ <TabsTrigger
175
+ value="upload"
176
+ className="inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all focus-visible:outline-none data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm hover:text-foreground"
177
+ >
178
+ <FileUp className="size-4 mr-2" />
179
+ Upload files
180
+ </TabsTrigger>
181
+ </TabsList>
182
+ </div>
183
+
184
+ <div className="flex-1 w-full">
185
+ <TabsContent
186
+ value="scrape"
187
+ className="m-0 h-full focus-visible:outline-none focus-visible:ring-0"
188
+ >
189
+ <div className="mb-4">
190
+ <FirecrawlNotice
191
+ cloudConfigured={configured}
192
+ onChange={refreshAll}
193
+ onErrorChange={setScrapeHasError}
194
+ />
195
+ </div>
196
+ {!scrapeHasError && (
197
+ <ScrapePanel
198
+ disabled={!configured}
199
+ onStarted={handleDataAdded}
200
+ />
201
+ )}
202
+ </TabsContent>
203
+ <TabsContent
204
+ value="paste"
205
+ className="m-0 h-full focus-visible:outline-none focus-visible:ring-0"
206
+ >
207
+ <PastePanel onAdded={handleDataAdded} />
208
+ </TabsContent>
209
+ <TabsContent
210
+ value="upload"
211
+ className="m-0 h-full focus-visible:outline-none focus-visible:ring-0"
212
+ >
213
+ <UploadPanel onAdded={handleDataAdded} />
214
+ </TabsContent>
215
+ </div>
216
+ </Tabs>
217
+ </CardContent>
218
+ </Card>
219
+ </TabsContent>
220
+
221
+ <TabsContent
222
+ value="corpus"
223
+ className="m-0 focus-visible:outline-none focus-visible:ring-0"
224
+ >
225
+ <Card>
226
+ <CardHeader className="flex-row flex-wrap items-center justify-between gap-3">
227
+ <CardTitle className="flex items-center gap-2 text-base">
228
+ <Layers className="size-4 text-primary" />
229
+ Corpus
230
+ </CardTitle>
231
+ {stats && (
232
+ <div className="flex items-center gap-4 text-xs text-muted-foreground tabular-nums">
233
+ <span>{stats.docCount.toLocaleString()} docs</span>
234
+ <span>{stats.charCount.toLocaleString()} chars</span>
235
+ </div>
236
+ )}
237
+ </CardHeader>
238
+ <CardContent>
239
+ {docsQuery.isLoading ? (
240
+ <Skeleton className="h-40 w-full" />
241
+ ) : (
242
+ <CorpusPanel documents={documents} onChanged={refreshAll} />
243
+ )}
244
+ </CardContent>
245
+ </Card>
246
+ </TabsContent>
247
+
248
+ <TabsContent
249
+ value="etl"
250
+ className="m-0 focus-visible:outline-none focus-visible:ring-0"
251
+ >
252
+ <Card>
253
+ <CardHeader className="flex-row items-center justify-between shrink-0">
254
+ <CardTitle className="text-base">ETL jobs</CardTitle>
255
+ </CardHeader>
256
+ <CardContent>
257
+ {jobsQuery.isLoading ? (
258
+ <div className="space-y-3">
259
+ <Skeleton className="h-8 w-full" />
260
+ <Skeleton className="h-8 w-full" />
261
+ <Skeleton className="h-8 w-full" />
262
+ </div>
263
+ ) : (
264
+ <JobsPanel jobs={jobs} onChanged={refreshAll} />
265
+ )}
266
+ </CardContent>
267
+ </Card>
268
+ </TabsContent>
269
+ </Tabs>
270
+ </div>
271
+ );
272
+ }
@@ -0,0 +1,236 @@
1
+ "use client";
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";
19
+
20
+ interface LocalState {
21
+ running: boolean;
22
+ endpoint: string;
23
+ port: number;
24
+ hasKey: boolean;
25
+ startedAt?: string;
26
+ lastError?: string;
27
+ }
28
+ interface DockerState {
29
+ docker: boolean;
30
+ compose: boolean;
31
+ message: string;
32
+ }
33
+ interface LocalResponse {
34
+ state: LocalState;
35
+ docker: DockerState;
36
+ }
37
+
38
+ const fetcher = (url: string) => fetch(url).then((r) => r.json());
39
+
40
+ /**
41
+ * Local Firecrawl launcher.
42
+ *
43
+ * Lets the user spin up a self-hosted Firecrawl via Docker straight from the
44
+ * UI — no manual API key needed. We generate a bearer token automatically and
45
+ * wire the local endpoint into the scraper, so web search + crawling work
46
+ * against localhost. If the cloud key is already set, this stays collapsed.
47
+ */
48
+ export function FirecrawlNotice({
49
+ cloudConfigured = false,
50
+ onChange,
51
+ onErrorChange,
52
+ }: {
53
+ cloudConfigured?: boolean;
54
+ onChange?: () => void;
55
+ onErrorChange?: (hasError: boolean) => void;
56
+ }) {
57
+ const { data, mutate, isLoading } = useSWR<LocalResponse>(
58
+ "/api/firecrawl/local",
59
+ fetcher,
60
+ {
61
+ refreshInterval: (d) =>
62
+ d?.state.running || d?.state.startedAt ? 8000 : 0,
63
+ },
64
+ );
65
+ const [busy, setBusy] = useState<"start" | "stop" | null>(null);
66
+
67
+ const state = data?.state;
68
+ const docker = data?.docker;
69
+ const running = state?.running ?? false;
70
+ const dockerReady = docker?.compose ?? false;
71
+ const hasError = !cloudConfigured && (!dockerReady || !!state?.lastError);
72
+
73
+ useEffect(() => {
74
+ if (!isLoading) {
75
+ onErrorChange?.(hasError);
76
+ }
77
+ }, [hasError, isLoading, onErrorChange]);
78
+
79
+ async function control(action: "start" | "stop") {
80
+ setBusy(action);
81
+ if (action === "start") {
82
+ toast.info("Launching local Firecrawl…", {
83
+ description:
84
+ "Pulling images and starting containers. This can take a minute on first run.",
85
+ });
86
+ }
87
+ try {
88
+ const res = await fetch("/api/firecrawl/local", {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" },
91
+ body: JSON.stringify({ action }),
92
+ });
93
+ const json = (await res.json()) as { state?: LocalState; error?: string };
94
+ if (!res.ok) throw new Error(json.error || "Request failed");
95
+
96
+ if (action === "start") {
97
+ if (json.state?.running) {
98
+ toast.success("Local Firecrawl is running", {
99
+ description: "Web scraping is now wired to your local instance.",
100
+ });
101
+ } else {
102
+ toast.warning("Containers started", {
103
+ description:
104
+ json.state?.lastError ?? "Waiting for the API to become healthy.",
105
+ });
106
+ }
107
+ } else {
108
+ toast.success("Local Firecrawl stopped");
109
+ }
110
+ await mutate();
111
+ onChange?.();
112
+ } catch (err) {
113
+ toast.error("Could not control local Firecrawl", {
114
+ description: formatErrorMessage(err),
115
+ });
116
+ } finally {
117
+ setBusy(null);
118
+ }
119
+ }
120
+
121
+ // Cloud key present and no local instance running → nothing to show.
122
+ if (cloudConfigured && !running && !state?.startedAt) return null;
123
+
124
+ return (
125
+ <Alert>
126
+ {running ? (
127
+ <Server className="size-4 text-primary" />
128
+ ) : (
129
+ <KeyRound className="size-4" />
130
+ )}
131
+ <AlertTitle className="flex items-center gap-2">
132
+ {running ? "Web Crawler is running" : "Launch Web Crawler"}
133
+ {running && (
134
+ <Badge
135
+ variant="secondary"
136
+ className="gap-1 font-normal text-green-600"
137
+ >
138
+ <span className="relative flex size-1.5">
139
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75" />
140
+ <span className="relative inline-flex size-1.5 rounded-full bg-green-500" />
141
+ </span>
142
+ live
143
+ </Badge>
144
+ )}
145
+ </AlertTitle>
146
+ <AlertDescription className="flex flex-col gap-0">
147
+ {running ? (
148
+ <p className="text-sm">
149
+ Connected to{" "}
150
+ <code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">
151
+ {state?.endpoint}
152
+ </code>
153
+ . A bearer token was generated automatically — web search and
154
+ crawling use it without any setup.
155
+ </p>
156
+ ) : (
157
+ <p className="text-sm">
158
+ {cloudConfigured
159
+ ? "You have a cloud key set. You can also run Firecrawl locally in Docker."
160
+ : "Launch a self-hosted Firecrawl in Docker. A key is generated for you automatically, no signup required. Pasting and uploading work without it."}
161
+ </p>
162
+ )}
163
+
164
+ {/* Docker readiness hint */}
165
+ {!isLoading && !dockerReady && !running && docker?.message && (
166
+ <div className="flex items-start gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
167
+ <TriangleAlert className="mt-0.5 size-3.5 shrink-0 text-destructive" />
168
+ <span>{formatErrorMessage(docker.message)}</span>
169
+ </div>
170
+ )}
171
+
172
+ {state?.lastError && !running && (
173
+ <div className="flex items-start mb-3 gap-2 max-h-[200px]! overflow-auto rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
174
+ <TriangleAlert className="mt-0.5 size-3.5 shrink-0" />
175
+ <span>{formatErrorMessage(state.lastError)}</span>
176
+ </div>
177
+ )}
178
+
179
+ <div className="flex items-center gap-2 pb-2">
180
+ {running ? (
181
+ <Button
182
+ size="sm"
183
+ variant="destructive"
184
+ className={"bg-red-500 hover:bg-red-500/80 text-white "}
185
+ onClick={() => control("stop")}
186
+ disabled={busy !== null}
187
+ >
188
+ {busy === "stop" ? (
189
+ <Loader2 className="size-4 animate-spin text-white" />
190
+ ) : (
191
+ <Square className="size-4 text-white!" />
192
+ )}
193
+ Stop
194
+ </Button>
195
+ ) : (
196
+ <Button
197
+ size="sm"
198
+ onClick={() => control("start")}
199
+ disabled={busy !== null || isLoading || !dockerReady}
200
+ className="
201
+ inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium
202
+ bg-[#F97316] hover:bg-[#EA6C0A]
203
+ text-white border-none
204
+ rounded-md transition-all duration-200
205
+ disabled:opacity-70 disabled:cursor-not-allowed
206
+ "
207
+ >
208
+ {busy === "start" ? (
209
+ <Loader2 className="size-4 animate-spin text-white" />
210
+ ) : (
211
+ <img
212
+ src="/icons/firecrawl2.png"
213
+ alt="Firecrawl"
214
+ className="size-4 object-contain"
215
+ />
216
+ )}
217
+ Launch Web Crawler
218
+ </Button>
219
+ )}
220
+
221
+ {/* {dockerReady && !running && (
222
+ <span className="flex items-center gap-1 text-xs text-cyan-700 bg-white px-3 p-1 rounded-md">
223
+ <img
224
+ src={"/docker.png"}
225
+ alt="docker"
226
+ width={20}
227
+ height={20}
228
+ />
229
+ Docker ready
230
+ </span>
231
+ )} */}
232
+ </div>
233
+ </AlertDescription>
234
+ </Alert>
235
+ );
236
+ }