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,392 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import useSWR from "swr";
|
|
5
|
+
import {
|
|
6
|
+
CheckCircle2,
|
|
7
|
+
Copy,
|
|
8
|
+
ExternalLink,
|
|
9
|
+
Loader2,
|
|
10
|
+
Play,
|
|
11
|
+
Server,
|
|
12
|
+
Square,
|
|
13
|
+
Terminal,
|
|
14
|
+
Trash2,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import { SdkConnectDialog } from "@/components/simple/sdk-connect-dialog";
|
|
17
|
+
|
|
18
|
+
// Vercel triangle icon
|
|
19
|
+
function VercelIcon({ className }: { className?: string }) {
|
|
20
|
+
return (
|
|
21
|
+
<svg
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
fill="currentColor"
|
|
24
|
+
className={className}
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
>
|
|
27
|
+
<path d="M12 2L2 19.778h20L12 2z" />
|
|
28
|
+
</svg>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
import type { RagConfig } from "@larkup/core/types";
|
|
32
|
+
import type {
|
|
33
|
+
GeneratedFile,
|
|
34
|
+
GeneratedServer,
|
|
35
|
+
} from "@larkup/core/generator/generate-server";
|
|
36
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
37
|
+
import { Button, buttonVariants } from "@/components/ui/button";
|
|
38
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
39
|
+
import {
|
|
40
|
+
AlertDialog,
|
|
41
|
+
AlertDialogAction,
|
|
42
|
+
AlertDialogCancel,
|
|
43
|
+
AlertDialogContent,
|
|
44
|
+
AlertDialogDescription,
|
|
45
|
+
AlertDialogFooter,
|
|
46
|
+
AlertDialogHeader,
|
|
47
|
+
AlertDialogTitle,
|
|
48
|
+
AlertDialogTrigger,
|
|
49
|
+
} from "@/components/ui/alert-dialog";
|
|
50
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
51
|
+
import { cn } from "@/lib/utils";
|
|
52
|
+
import { toast } from "sonner";
|
|
53
|
+
|
|
54
|
+
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
|
55
|
+
|
|
56
|
+
interface GenerateResponse {
|
|
57
|
+
config: RagConfig;
|
|
58
|
+
server: GeneratedServer;
|
|
59
|
+
serverId: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface LocalServerState {
|
|
63
|
+
running: boolean;
|
|
64
|
+
pid?: number;
|
|
65
|
+
port: number;
|
|
66
|
+
endpoint: string;
|
|
67
|
+
startedAt?: string;
|
|
68
|
+
lastError?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ServerWorkspace({
|
|
72
|
+
onServerId,
|
|
73
|
+
}: {
|
|
74
|
+
onServerId?: (id: string) => void;
|
|
75
|
+
}) {
|
|
76
|
+
const { data, isLoading } = useSWR<GenerateResponse>(
|
|
77
|
+
"/api/server/generate",
|
|
78
|
+
fetcher,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Notify parent of the active serverId as soon as we have it
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (data?.serverId) onServerId?.(data.serverId);
|
|
84
|
+
}, [data?.serverId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
85
|
+
|
|
86
|
+
if (isLoading || !data) {
|
|
87
|
+
return (
|
|
88
|
+
<div className="px-6 py-6 md:px-8">
|
|
89
|
+
<Skeleton className="h-112 w-full" />
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="space-y-6 px-6 py-6 md:px-8">
|
|
96
|
+
<LaunchPanel serverId={data.serverId} />
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function LaunchPanel({
|
|
102
|
+
serverId,
|
|
103
|
+
}: {
|
|
104
|
+
serverId: string;
|
|
105
|
+
}) {
|
|
106
|
+
const [busy, setBusy] = useState<"start" | "stop" | null>(null);
|
|
107
|
+
const [remoteUrl, setRemoteUrl] = useState<string | null>(null);
|
|
108
|
+
const [remoteProvider, setRemoteProvider] = useState<string | null>(null);
|
|
109
|
+
const [apiKey, setApiKey] = useState("");
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
// Read deployed remote URL (set by DeployButton on success).
|
|
113
|
+
// Keys are scoped per-server to avoid stale URLs from old servers.
|
|
114
|
+
const url = localStorage.getItem(`vercel_deployed_url_${serverId}`);
|
|
115
|
+
const provider = localStorage.getItem(
|
|
116
|
+
`vercel_deployed_provider_${serverId}`,
|
|
117
|
+
);
|
|
118
|
+
const savedApiKey = localStorage.getItem("rag_server_api_key");
|
|
119
|
+
if (url) setRemoteUrl(url);
|
|
120
|
+
if (provider) setRemoteProvider(provider);
|
|
121
|
+
if (savedApiKey) setApiKey(savedApiKey);
|
|
122
|
+
}, [serverId]);
|
|
123
|
+
|
|
124
|
+
function removeRemoteServer() {
|
|
125
|
+
localStorage.removeItem(`vercel_deployed_url_${serverId}`);
|
|
126
|
+
localStorage.removeItem(`vercel_deployed_provider_${serverId}`);
|
|
127
|
+
setRemoteUrl(null);
|
|
128
|
+
setRemoteProvider(null);
|
|
129
|
+
toast.success("Remote server removed.");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { data, mutate } = useSWR<{ state: LocalServerState }>(
|
|
133
|
+
"/api/server/local",
|
|
134
|
+
fetcher,
|
|
135
|
+
{ refreshInterval: (d) => (d?.state.running ? 5000 : 0) },
|
|
136
|
+
);
|
|
137
|
+
const state = data?.state;
|
|
138
|
+
|
|
139
|
+
async function control(action: "start" | "stop") {
|
|
140
|
+
setBusy(action);
|
|
141
|
+
try {
|
|
142
|
+
const currentApiKey = localStorage.getItem("rag_server_api_key") || "";
|
|
143
|
+
const res = await fetch("/api/server/local", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "Content-Type": "application/json" },
|
|
146
|
+
body: JSON.stringify({ action, serverApiKey: currentApiKey }),
|
|
147
|
+
});
|
|
148
|
+
const body = await res.json();
|
|
149
|
+
if (action === "start") {
|
|
150
|
+
if (body.state?.running) toast.success("Local server is running.");
|
|
151
|
+
else toast.error(body.state?.lastError ?? "Server did not start.");
|
|
152
|
+
} else {
|
|
153
|
+
toast.success("Local server stopped.");
|
|
154
|
+
}
|
|
155
|
+
mutate();
|
|
156
|
+
} catch {
|
|
157
|
+
toast.error("Could not reach the local server controller.");
|
|
158
|
+
} finally {
|
|
159
|
+
setBusy(null);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<Card>
|
|
165
|
+
<CardHeader className="flex-row items-center justify-between">
|
|
166
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
167
|
+
<Server className="size-4 text-primary" />
|
|
168
|
+
Launch locally
|
|
169
|
+
</CardTitle>
|
|
170
|
+
{state?.running && (
|
|
171
|
+
<span className="flex items-center gap-1.5 text-xs text-green-700">
|
|
172
|
+
<span className="relative flex size-2">
|
|
173
|
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-600 opacity-75" />
|
|
174
|
+
<span className="relative inline-flex size-2 rounded-full bg-green-600" />
|
|
175
|
+
</span>
|
|
176
|
+
running on :{state.port}
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
179
|
+
</CardHeader>
|
|
180
|
+
<CardContent className="space-y-6">
|
|
181
|
+
<div className="space-y-4">
|
|
182
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
183
|
+
Spin up the generated server right here to test your RAG endpoints.
|
|
184
|
+
Settings and API keys configured in the Deploy menu will be
|
|
185
|
+
automatically applied.
|
|
186
|
+
</p>
|
|
187
|
+
|
|
188
|
+
<Alert>
|
|
189
|
+
<ExternalLink className="size-4" />
|
|
190
|
+
<AlertTitle>API Documentation</AlertTitle>
|
|
191
|
+
<AlertDescription>
|
|
192
|
+
{/* We use{" "}
|
|
193
|
+
<a
|
|
194
|
+
href="https://scalar.com/"
|
|
195
|
+
target="_blank"
|
|
196
|
+
rel="noreferrer"
|
|
197
|
+
className="text-primary hover:underline"
|
|
198
|
+
>
|
|
199
|
+
Scalar
|
|
200
|
+
</a>{" "}
|
|
201
|
+
for full API endpoint documentation. */}
|
|
202
|
+
{state?.running ? (
|
|
203
|
+
<div className="mt-0">
|
|
204
|
+
<a
|
|
205
|
+
href={`${state.endpoint}/reference`}
|
|
206
|
+
target="_blank"
|
|
207
|
+
rel="noreferrer"
|
|
208
|
+
className={buttonVariants({
|
|
209
|
+
variant: "outline",
|
|
210
|
+
size: "sm",
|
|
211
|
+
})}
|
|
212
|
+
>
|
|
213
|
+
Open API Reference
|
|
214
|
+
</a>
|
|
215
|
+
</div>
|
|
216
|
+
) : (
|
|
217
|
+
<p className="mt-2 text-muted-foreground text-xs">
|
|
218
|
+
Launch the local server to view the API reference.
|
|
219
|
+
</p>
|
|
220
|
+
)}
|
|
221
|
+
</AlertDescription>
|
|
222
|
+
</Alert>
|
|
223
|
+
|
|
224
|
+
{state?.lastError && !state.running && (
|
|
225
|
+
<Alert variant="destructive">
|
|
226
|
+
<AlertTitle>Launch failed</AlertTitle>
|
|
227
|
+
<AlertDescription className="wrap-break-word">
|
|
228
|
+
{state.lastError}
|
|
229
|
+
</AlertDescription>
|
|
230
|
+
</Alert>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
234
|
+
{state?.running ? (
|
|
235
|
+
<>
|
|
236
|
+
<Button
|
|
237
|
+
variant="destructive"
|
|
238
|
+
onClick={() => control("stop")}
|
|
239
|
+
disabled={busy !== null}
|
|
240
|
+
className={""}
|
|
241
|
+
>
|
|
242
|
+
{busy === "stop" ? (
|
|
243
|
+
<Loader2 className="size-4 animate-spin" />
|
|
244
|
+
) : (
|
|
245
|
+
<Square className="size-4 text-white!" />
|
|
246
|
+
)}
|
|
247
|
+
Stop server
|
|
248
|
+
</Button>
|
|
249
|
+
<a
|
|
250
|
+
href={`${state.endpoint}`}
|
|
251
|
+
target="_blank"
|
|
252
|
+
rel="noreferrer"
|
|
253
|
+
className={buttonVariants({ variant: "secondary" })}
|
|
254
|
+
>
|
|
255
|
+
<ExternalLink className="size-4" />
|
|
256
|
+
{state.endpoint}
|
|
257
|
+
</a>
|
|
258
|
+
</>
|
|
259
|
+
) : (
|
|
260
|
+
<>
|
|
261
|
+
<Button
|
|
262
|
+
onClick={() => control("start")}
|
|
263
|
+
disabled={busy !== null}
|
|
264
|
+
>
|
|
265
|
+
{busy === "start" ? (
|
|
266
|
+
<Loader2 className="size-4 animate-spin" />
|
|
267
|
+
) : (
|
|
268
|
+
<Play className="size-4" />
|
|
269
|
+
)}
|
|
270
|
+
{busy === "start" ? "Starting…" : "Launch local server"}
|
|
271
|
+
</Button>
|
|
272
|
+
</>
|
|
273
|
+
)}
|
|
274
|
+
<SdkConnectDialog serverUrl={state?.endpoint || "http://localhost:8080"} />
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{state?.running && (
|
|
279
|
+
<div className="rounded-lg border border-border bg-muted/40 p-3">
|
|
280
|
+
<div className="mb-1.5 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
|
281
|
+
<Terminal className="size-3.5" />
|
|
282
|
+
Try it
|
|
283
|
+
</div>
|
|
284
|
+
<CodeLine
|
|
285
|
+
text={`curl -X POST ${state.endpoint}/query -H "Content-Type: application/json"${apiKey ? ` -H "Authorization: Bearer ${apiKey}"` : ""} -d '{"query":"hello"}'`}
|
|
286
|
+
/>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{/* ── Remote server (deployed via Vercel / Hetzner) ── */}
|
|
291
|
+
{remoteUrl && (
|
|
292
|
+
<>
|
|
293
|
+
<div className="flex items-center gap-3 pt-2 border-t border-border">
|
|
294
|
+
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground min-w-[110px]">
|
|
295
|
+
{remoteProvider === "vercel" ? (
|
|
296
|
+
<VercelIcon className="size-4 shrink-0" />
|
|
297
|
+
) : (
|
|
298
|
+
<Server className="size-4 shrink-0" />
|
|
299
|
+
)}
|
|
300
|
+
Remote server
|
|
301
|
+
</div>
|
|
302
|
+
<a
|
|
303
|
+
href={remoteUrl}
|
|
304
|
+
target="_blank"
|
|
305
|
+
rel="noreferrer"
|
|
306
|
+
className={buttonVariants({ variant: "outline" })}
|
|
307
|
+
>
|
|
308
|
+
<ExternalLink className="size-4" />
|
|
309
|
+
{remoteUrl.replace(/^https?:\/\//, "")}
|
|
310
|
+
</a>
|
|
311
|
+
<AlertDialog>
|
|
312
|
+
<AlertDialogTrigger
|
|
313
|
+
render={
|
|
314
|
+
<Button
|
|
315
|
+
variant="ghost"
|
|
316
|
+
size="icon"
|
|
317
|
+
className="h-9 w-9 text-muted-foreground hover:text-destructive"
|
|
318
|
+
>
|
|
319
|
+
<Trash2 className="size-4" />
|
|
320
|
+
</Button>
|
|
321
|
+
}
|
|
322
|
+
/>
|
|
323
|
+
<AlertDialogContent>
|
|
324
|
+
<AlertDialogHeader>
|
|
325
|
+
<AlertDialogTitle>Remove remote server?</AlertDialogTitle>
|
|
326
|
+
<AlertDialogDescription>
|
|
327
|
+
This will remove the remote server connection from this
|
|
328
|
+
workspace. The deployed server itself will not be deleted
|
|
329
|
+
from your provider.
|
|
330
|
+
</AlertDialogDescription>
|
|
331
|
+
</AlertDialogHeader>
|
|
332
|
+
<AlertDialogFooter>
|
|
333
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
334
|
+
<AlertDialogAction
|
|
335
|
+
onClick={removeRemoteServer}
|
|
336
|
+
className="bg-destructive hover:bg-destructive/90"
|
|
337
|
+
>
|
|
338
|
+
Remove
|
|
339
|
+
</AlertDialogAction>
|
|
340
|
+
</AlertDialogFooter>
|
|
341
|
+
</AlertDialogContent>
|
|
342
|
+
</AlertDialog>
|
|
343
|
+
</div>
|
|
344
|
+
<div className="rounded-lg border border-border bg-muted/40 p-3">
|
|
345
|
+
<div className="mb-1.5 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
|
346
|
+
<Terminal className="size-3.5" />
|
|
347
|
+
Try remote
|
|
348
|
+
</div>
|
|
349
|
+
<CodeLine
|
|
350
|
+
text={`curl -X POST ${remoteUrl}/query -H "Content-Type: application/json"${apiKey ? ` -H "Authorization: Bearer ${apiKey}"` : ""} -d '{"query":"hello"}'`}
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
</>
|
|
354
|
+
)}
|
|
355
|
+
</CardContent>
|
|
356
|
+
</Card>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function CopyButton({ text }: { text: string }) {
|
|
361
|
+
const [copied, setCopied] = useState(false);
|
|
362
|
+
return (
|
|
363
|
+
<Button
|
|
364
|
+
variant="ghost"
|
|
365
|
+
size="sm"
|
|
366
|
+
className="h-7 gap-1.5 text-xs"
|
|
367
|
+
onClick={async () => {
|
|
368
|
+
await navigator.clipboard.writeText(text);
|
|
369
|
+
setCopied(true);
|
|
370
|
+
setTimeout(() => setCopied(false), 1500);
|
|
371
|
+
}}
|
|
372
|
+
>
|
|
373
|
+
{copied ? (
|
|
374
|
+
<CheckCircle2 className="size-3.5 text-primary" />
|
|
375
|
+
) : (
|
|
376
|
+
<Copy className="size-3.5" />
|
|
377
|
+
)}
|
|
378
|
+
{copied ? "Copied" : "Copy"}
|
|
379
|
+
</Button>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function CodeLine({ text }: { text: string }) {
|
|
384
|
+
return (
|
|
385
|
+
<div className="flex items-center gap-2">
|
|
386
|
+
<code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap font-mono text-xs text-foreground">
|
|
387
|
+
{text}
|
|
388
|
+
</code>
|
|
389
|
+
<CopyButton text={text} />
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Code2, Copy, Check, ExternalLink } from "lucide-react";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
} from "@/components/ui/dialog";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
import { CodeViewer } from "@/components/server/code-viewer";
|
|
16
|
+
|
|
17
|
+
const TABS = [
|
|
18
|
+
{ id: "python", label: "Python" },
|
|
19
|
+
{ id: "typescript", label: "TypeScript" },
|
|
20
|
+
{ id: "openai", label: "OpenAI Compatible" },
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
type TabId = (typeof TABS)[number]["id"];
|
|
24
|
+
|
|
25
|
+
function getSnippets(serverUrl: string): Record<TabId, string> {
|
|
26
|
+
return {
|
|
27
|
+
python: `# Install: pip install larkup
|
|
28
|
+
from larkup import LarkupClient, LarkupClientOptions
|
|
29
|
+
|
|
30
|
+
options = LarkupClientOptions(
|
|
31
|
+
base_url="${serverUrl}",
|
|
32
|
+
api_key="your-api-key"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
client = LarkupClient(options)
|
|
36
|
+
|
|
37
|
+
# Add a document
|
|
38
|
+
from larkup import Document
|
|
39
|
+
|
|
40
|
+
doc = Document(
|
|
41
|
+
id="doc-1",
|
|
42
|
+
text="Your document text here.",
|
|
43
|
+
title="Example Document"
|
|
44
|
+
)
|
|
45
|
+
client.add_document(doc)
|
|
46
|
+
|
|
47
|
+
# Query
|
|
48
|
+
results = client.query("What is this about?", top_k=5)
|
|
49
|
+
for hit in results.hits:
|
|
50
|
+
print(f"Score: {hit.score} | Text: {hit.text}")`,
|
|
51
|
+
|
|
52
|
+
typescript: `// Install: npm install @larkup/rag-sdk
|
|
53
|
+
import { LarkupClient } from "@larkup/rag-sdk";
|
|
54
|
+
|
|
55
|
+
const client = new LarkupClient({
|
|
56
|
+
baseUrl: "${serverUrl}",
|
|
57
|
+
apiKey: "your-api-key",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Add a document
|
|
61
|
+
await client.addDocument({
|
|
62
|
+
id: "doc-1",
|
|
63
|
+
text: "Your document text here.",
|
|
64
|
+
title: "Example Document",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Query
|
|
68
|
+
const results = await client.query("What is this about?", 5);
|
|
69
|
+
for (const hit of results.hits) {
|
|
70
|
+
console.log(\`Score: \${hit.score} | Text: \${hit.text}\`);
|
|
71
|
+
}`,
|
|
72
|
+
|
|
73
|
+
openai: `// Connect via OpenAI-compatible endpoint
|
|
74
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
75
|
+
import { generateText } from 'ai';
|
|
76
|
+
|
|
77
|
+
const larkup = createOpenAI({
|
|
78
|
+
baseURL: '${serverUrl}/v1',
|
|
79
|
+
apiKey: 'your-api-key',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const { text } = await generateText({
|
|
83
|
+
model: larkup('rag-model'),
|
|
84
|
+
prompt: 'What is this about?',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log(text);`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function SdkConnectDialog({ serverUrl }: { serverUrl: string }) {
|
|
92
|
+
const [activeTab, setActiveTab] = useState<TabId>("python");
|
|
93
|
+
const [copied, setCopied] = useState(false);
|
|
94
|
+
|
|
95
|
+
const snippets = getSnippets(serverUrl || "http://localhost:8080");
|
|
96
|
+
const currentSnippet = snippets[activeTab];
|
|
97
|
+
|
|
98
|
+
async function handleCopy() {
|
|
99
|
+
await navigator.clipboard.writeText(currentSnippet);
|
|
100
|
+
setCopied(true);
|
|
101
|
+
setTimeout(() => setCopied(false), 2000);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Dialog>
|
|
106
|
+
<DialogTrigger
|
|
107
|
+
render={
|
|
108
|
+
<Button variant="outline" className="gap-2">
|
|
109
|
+
<Code2 className="size-4" />
|
|
110
|
+
Connect to Server SDK
|
|
111
|
+
</Button>
|
|
112
|
+
}
|
|
113
|
+
/>
|
|
114
|
+
<DialogContent className="sm:max-w-2xl">
|
|
115
|
+
<DialogHeader>
|
|
116
|
+
<DialogTitle className="flex items-center gap-2">
|
|
117
|
+
<Code2 className="size-5 text-primary" />
|
|
118
|
+
Connect to your RAG Server
|
|
119
|
+
</DialogTitle>
|
|
120
|
+
<DialogDescription>
|
|
121
|
+
Use one of these SDKs to programmatically interact with your RAG
|
|
122
|
+
server.
|
|
123
|
+
</DialogDescription>
|
|
124
|
+
</DialogHeader>
|
|
125
|
+
|
|
126
|
+
{/* Tabs */}
|
|
127
|
+
<div className="flex gap-1 rounded-lg border bg-muted/40 p-1">
|
|
128
|
+
{TABS.map((tab) => (
|
|
129
|
+
<button
|
|
130
|
+
key={tab.id}
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={() => setActiveTab(tab.id)}
|
|
133
|
+
className={cn(
|
|
134
|
+
"flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-all",
|
|
135
|
+
activeTab === tab.id
|
|
136
|
+
? "bg-background text-foreground shadow-sm"
|
|
137
|
+
: "text-muted-foreground hover:text-foreground",
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
{tab.label}
|
|
141
|
+
</button>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Code block */}
|
|
146
|
+
<div className="relative rounded-lg border bg-slate-50 overflow-hidden">
|
|
147
|
+
<div className="flex items-center justify-between border-b border-slate-200 px-4 py-2 bg-slate-100/50">
|
|
148
|
+
<span className="text-xs text-slate-500 font-mono">
|
|
149
|
+
{activeTab === "python"
|
|
150
|
+
? "main.py"
|
|
151
|
+
: activeTab === "typescript"
|
|
152
|
+
? "index.ts"
|
|
153
|
+
: "connect.ts"}
|
|
154
|
+
</span>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
onClick={handleCopy}
|
|
158
|
+
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-slate-500 transition hover:bg-slate-200 hover:text-slate-900"
|
|
159
|
+
>
|
|
160
|
+
{copied ? (
|
|
161
|
+
<>
|
|
162
|
+
<Check className="size-3.5" />
|
|
163
|
+
Copied!
|
|
164
|
+
</>
|
|
165
|
+
) : (
|
|
166
|
+
<>
|
|
167
|
+
<Copy className="size-3.5" />
|
|
168
|
+
Copy
|
|
169
|
+
</>
|
|
170
|
+
)}
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
<CodeViewer
|
|
174
|
+
value={currentSnippet}
|
|
175
|
+
language={activeTab === "python" ? "python" : "javascript"}
|
|
176
|
+
height="auto"
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{/* Docs link */}
|
|
181
|
+
<div className="flex items-center justify-between text-sm">
|
|
182
|
+
<span className="text-muted-foreground">
|
|
183
|
+
Server URL:{" "}
|
|
184
|
+
<code className="rounded bg-muted px-1.5 py-0.5 text-xs font-mono">
|
|
185
|
+
{serverUrl || "Not running"}
|
|
186
|
+
</code>
|
|
187
|
+
</span>
|
|
188
|
+
<a
|
|
189
|
+
href="https://docs.larkup.dev/sdk/overview"
|
|
190
|
+
target="_blank"
|
|
191
|
+
rel="noopener noreferrer"
|
|
192
|
+
className="flex items-center gap-1 text-primary hover:underline"
|
|
193
|
+
>
|
|
194
|
+
Full docs
|
|
195
|
+
<ExternalLink className="size-3.5" />
|
|
196
|
+
</a>
|
|
197
|
+
</div>
|
|
198
|
+
</DialogContent>
|
|
199
|
+
</Dialog>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import useSWR from "swr";
|
|
6
|
+
import { Loader2, Play } from "lucide-react";
|
|
7
|
+
import { Button } from "@/components/ui/button";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
import type { IndexRun } from "@larkup/core/types";
|
|
10
|
+
|
|
11
|
+
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
|
12
|
+
|
|
13
|
+
const ACTIVE: IndexRun["status"][] = ["chunking", "embedding", "upserting"];
|
|
14
|
+
|
|
15
|
+
export function SimpleIndexButton() {
|
|
16
|
+
const [starting, setStarting] = useState(false);
|
|
17
|
+
const { data, isLoading, mutate } = useSWR("/api/index", fetcher, {
|
|
18
|
+
refreshInterval: (d) =>
|
|
19
|
+
d?.run && ACTIVE.includes(d.run.status) ? 1000 : 0,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const run = data?.run;
|
|
23
|
+
const running = Boolean(run && ACTIVE.includes(run.status));
|
|
24
|
+
const ready = data?.ready;
|
|
25
|
+
const unindexedCount = data?.unindexedCount || 0;
|
|
26
|
+
|
|
27
|
+
const prevRunId = React.useRef<string | null>(null);
|
|
28
|
+
const prevStatus = React.useRef<string | null>(null);
|
|
29
|
+
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (run && run.status === "failed" && (run.id !== prevRunId.current || prevStatus.current !== "failed")) {
|
|
32
|
+
toast.error(`Indexing failed: ${run.error}`);
|
|
33
|
+
}
|
|
34
|
+
prevRunId.current = run?.id ?? null;
|
|
35
|
+
prevStatus.current = run?.status ?? null;
|
|
36
|
+
}, [run]);
|
|
37
|
+
|
|
38
|
+
async function build() {
|
|
39
|
+
setStarting(true);
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch("/api/index", {
|
|
42
|
+
method: "POST",
|
|
43
|
+
body: JSON.stringify({ incremental: run?.status === "completed" }),
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
});
|
|
46
|
+
const body = await res.json();
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
toast.error(body.error ?? "Could not start indexing.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
toast.success("Indexing started.");
|
|
52
|
+
mutate();
|
|
53
|
+
} catch {
|
|
54
|
+
toast.error("Could not start indexing.");
|
|
55
|
+
} finally {
|
|
56
|
+
setStarting(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Button
|
|
62
|
+
onClick={build}
|
|
63
|
+
disabled={
|
|
64
|
+
!ready ||
|
|
65
|
+
running ||
|
|
66
|
+
starting ||
|
|
67
|
+
(run?.status === "completed" && unindexedCount === 0)
|
|
68
|
+
}
|
|
69
|
+
size="sm"
|
|
70
|
+
>
|
|
71
|
+
{running || starting ? (
|
|
72
|
+
<Loader2 className="size-4 animate-spin mr-2" />
|
|
73
|
+
) : (
|
|
74
|
+
<Play className="size-4 mr-2" />
|
|
75
|
+
)}
|
|
76
|
+
{running
|
|
77
|
+
? "Indexing…"
|
|
78
|
+
: run?.status === "completed"
|
|
79
|
+
? `Index new (${unindexedCount})`
|
|
80
|
+
: `Start indexing (${unindexedCount})`}
|
|
81
|
+
</Button>
|
|
82
|
+
);
|
|
83
|
+
}
|