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.
- package/.env +2 -0
- package/app/actions/vercel.ts +417 -0
- package/app/api/chat/route.ts +192 -0
- package/app/api/chat/status/route.ts +55 -0
- package/app/api/chat/suggestions/route.ts +115 -0
- package/app/api/config/route.ts +73 -0
- package/app/api/config/test/route.ts +67 -0
- package/app/api/config/test-embedding/route.ts +40 -0
- package/app/api/config/test-llm/route.ts +47 -0
- package/app/api/config/test-provider/route.ts +36 -0
- package/app/api/demo/route.ts +176 -0
- package/app/api/deploy/ssh/route.ts +340 -0
- package/app/api/documents/route.ts +90 -0
- package/app/api/firecrawl/local/route.ts +46 -0
- package/app/api/index/route.ts +113 -0
- package/app/api/jobs/[id]/route.ts +44 -0
- package/app/api/jobs/route.ts +116 -0
- package/app/api/parse-file/route.ts +39 -0
- package/app/api/search/google/route.ts +162 -0
- package/app/api/search/route.ts +29 -0
- package/app/api/server/download/route.ts +112 -0
- package/app/api/server/generate/route.ts +46 -0
- package/app/api/server/local/route.ts +42 -0
- package/app/api/servers/route.ts +137 -0
- package/app/api/vector-stores/install/route.ts +65 -0
- package/app/api/vector-stores/status/route.ts +26 -0
- package/app/chat/page.tsx +15 -0
- package/app/configure/page.tsx +53 -0
- package/app/data/page.tsx +15 -0
- package/app/demo/page.tsx +15 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +190 -0
- package/app/index-data/page.tsx +15 -0
- package/app/layout.tsx +51 -0
- package/app/page.tsx +34 -0
- package/app/server/page.tsx +22 -0
- package/app/simple/chat/page.tsx +17 -0
- package/app/simple/docs/page.tsx +21 -0
- package/app/simple/layout.tsx +9 -0
- package/app/simple/settings/page.tsx +627 -0
- package/app/template.tsx +16 -0
- package/app/themes.css +733 -0
- package/components/alerts/generic-alert.tsx +74 -0
- package/components/app-sidebar.tsx +187 -0
- package/components/app-topnav.tsx +195 -0
- package/components/chat/chat-settings-modal.tsx +209 -0
- package/components/chat/chat-workspace.tsx +348 -0
- package/components/chat/knowledge-base-result.tsx +97 -0
- package/components/chat/message-item.tsx +103 -0
- package/components/client-layout-wrapper.tsx +182 -0
- package/components/configure/configure-form.tsx +1683 -0
- package/components/configure/store-fields.tsx +152 -0
- package/components/data/corpus-panel.tsx +499 -0
- package/components/data/data-workspace.tsx +272 -0
- package/components/data/firecrawl-notice.tsx +236 -0
- package/components/data/jobs-panel.tsx +485 -0
- package/components/data/paste-panel.tsx +79 -0
- package/components/data/scrape-panel.tsx +964 -0
- package/components/data/upload-panel.tsx +714 -0
- package/components/demo/demo-workspace.tsx +419 -0
- package/components/global-settings.tsx +286 -0
- package/components/index/index-workspace.tsx +482 -0
- package/components/onboarding/simple-setup.tsx +702 -0
- package/components/onboarding/tech-setup.tsx +121 -0
- package/components/onboarding/welcome-screen.tsx +183 -0
- package/components/page-header.tsx +44 -0
- package/components/server/code-viewer.tsx +87 -0
- package/components/server/deploy-button.tsx +1390 -0
- package/components/server/server-workspace.tsx +392 -0
- package/components/simple/sdk-connect-dialog.tsx +201 -0
- package/components/simple/simple-index-button.tsx +83 -0
- package/components/simple/simple-sidebar.tsx +161 -0
- package/components/theme-customizer-provider.tsx +171 -0
- package/components/theme-switcher.tsx +445 -0
- package/components/ui/alert-dialog.tsx +187 -0
- package/components/ui/alert.tsx +76 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +59 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/checkbox.tsx +28 -0
- package/components/ui/collapsible.tsx +21 -0
- package/components/ui/dialog.tsx +159 -0
- package/components/ui/dropdown-menu.tsx +281 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +20 -0
- package/components/ui/popover.tsx +90 -0
- package/components/ui/progress.tsx +83 -0
- package/components/ui/provider-icon.tsx +94 -0
- package/components/ui/scroll-area.tsx +55 -0
- package/components/ui/select.tsx +248 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/sonner.tsx +63 -0
- package/components/ui/switch.tsx +32 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +84 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/workspace/delete-server-dialog.tsx +71 -0
- package/components/workspace/server-form-dialog.tsx +103 -0
- package/components/workspace/server-switcher.tsx +163 -0
- package/components/workspace/workspace-provider.tsx +177 -0
- package/components/workspace/workspace-top-bar.tsx +23 -0
- package/components.json +21 -0
- package/lib/error-formatter.ts +15 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +18 -0
- package/package.json +65 -0
- package/postcss.config.mjs +8 -0
- package/public/docker.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/github.svg +3 -0
- package/public/hetzner.svg +1 -0
- package/public/icons/aws.svg +38 -0
- package/public/icons/azure.svg +1 -0
- package/public/icons/chroma.png +0 -0
- package/public/icons/cohere.svg +30 -0
- package/public/icons/digital-ocean.webp +0 -0
- package/public/icons/firecrawl2.png +0 -0
- package/public/icons/gcp.svg +1 -0
- package/public/icons/gemini.svg +1 -0
- package/public/icons/jina.svg +1 -0
- package/public/icons/lancedb2.png +0 -0
- package/public/icons/mistral.svg +469 -0
- package/public/icons/nomic.png +0 -0
- package/public/icons/openai.svg +1 -0
- package/public/icons/pgvector2.png +0 -0
- package/public/icons/pinecone.png +0 -0
- package/public/icons/qdrant.svg +35 -0
- package/public/icons/supabase.png +0 -0
- package/public/icons/vercel copy.svg +1 -0
- package/public/icons/vercel.svg +1 -0
- package/public/icons/voyage-light.png +0 -0
- package/public/icons/weaviate.webp +0 -0
- package/public/logo-light.png +0 -0
- package/public/logo.png +0 -0
- package/public/logo9.png +0 -0
- package/public/vercel.svg +1 -0
- 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
|
+
}
|