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,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
+ }