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,627 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import useSWR from "swr";
5
+ import { useRouter } from "next/navigation";
6
+ import {
7
+ Eye,
8
+ EyeOff,
9
+ Loader2,
10
+ Save,
11
+ Copy,
12
+ CheckCircle2,
13
+ RefreshCw,
14
+ Key,
15
+ Sparkles,
16
+ MessageCircle,
17
+ Settings2,
18
+ Database,
19
+ Clock,
20
+ AlertCircle,
21
+ } from "lucide-react";
22
+ import { Button } from "@/components/ui/button";
23
+ import { Input } from "@/components/ui/input";
24
+ import { Label } from "@/components/ui/label";
25
+ import {
26
+ Select,
27
+ SelectContent,
28
+ SelectItem,
29
+ SelectTrigger,
30
+ } from "@/components/ui/select";
31
+ import {
32
+ Card,
33
+ CardContent,
34
+ CardDescription,
35
+ CardHeader,
36
+ CardTitle,
37
+ } from "@/components/ui/card";
38
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
39
+ import { toast } from "sonner";
40
+ import { useWorkspace } from "@/components/workspace/workspace-provider";
41
+ import { PROVIDER_META, ProviderIcon } from "@/components/ui/provider-icon";
42
+ import { LaunchPanel } from "@/components/server/server-workspace";
43
+ import {
44
+ VECTOR_STORE_LIST,
45
+ getVectorStore,
46
+ } from "@larkup/vector-stores/registry";
47
+ import { StoreFields } from "@/components/configure/store-fields";
48
+ import type { RagConfig, VectorStoreId } from "@larkup/core/types";
49
+
50
+ const fetcher = (url: string) =>
51
+ fetch(url).then((r) => r.json() as Promise<{ config: RagConfig }>);
52
+
53
+ const PROVIDER_LIST = [
54
+ "vercel_ai_gateway",
55
+ "openai",
56
+ "google",
57
+ "deepseek",
58
+ "mistral",
59
+ "voyage",
60
+ "jina",
61
+ "nomic",
62
+ ] as const;
63
+
64
+ type StoreMeta = { iconSrc: string; pillBg: string };
65
+ const STORE_META: Record<string, StoreMeta> = {
66
+ lancedb: {
67
+ iconSrc: "/icons/lancedb2.png",
68
+ pillBg: "bg-yellow-50 dark:bg-yellow-950/40",
69
+ },
70
+ pinecone: {
71
+ iconSrc: "/icons/pinecone.png",
72
+ pillBg: "bg-green-50 dark:bg-green-950/40",
73
+ },
74
+ };
75
+
76
+ export default function SimpleSettingsPage() {
77
+ const router = useRouter();
78
+ const { activeServer, setMode } = useWorkspace();
79
+ const serverId = activeServer?.id;
80
+
81
+ const configUrl = serverId
82
+ ? `/api/config?serverId=${encodeURIComponent(serverId)}`
83
+ : "/api/config";
84
+ const { data, isLoading, mutate } = useSWR(configUrl, fetcher);
85
+
86
+ const [form, setForm] = useState<Partial<RagConfig>>({});
87
+ const [saving, setSaving] = useState(false);
88
+ const [showEmbeddingKey, setShowEmbeddingKey] = useState(false);
89
+ const [showChatKey, setShowChatKey] = useState(false);
90
+ const [serverApiKey, setServerApiKey] = useState("");
91
+ const [copiedKey, setCopiedKey] = useState(false);
92
+
93
+ useEffect(() => {
94
+ if (data?.config) {
95
+ setForm(data.config);
96
+ }
97
+ }, [data]);
98
+
99
+ useEffect(() => {
100
+ const saved = localStorage.getItem("rag_server_api_key");
101
+ if (saved) setServerApiKey(saved);
102
+ }, []);
103
+
104
+ const dirty = JSON.stringify(form) !== JSON.stringify(data?.config ?? {});
105
+ const needsReindex =
106
+ data?.config &&
107
+ (form.vectorStore !== data.config.vectorStore ||
108
+ form.embeddingModelId !== data.config.embeddingModelId);
109
+
110
+ async function handleSave() {
111
+ if (!data?.config) return;
112
+ setSaving(true);
113
+ try {
114
+ const merged = { ...data.config, ...form };
115
+ const res = await fetch(configUrl, {
116
+ method: "PUT",
117
+ headers: { "Content-Type": "application/json" },
118
+ body: JSON.stringify(merged),
119
+ });
120
+ const json = await res.json();
121
+ if (!res.ok)
122
+ throw new Error(json.error ?? "Failed to save configuration");
123
+ await mutate(json, { revalidate: false });
124
+ toast.success("Settings saved");
125
+ } catch (err) {
126
+ toast.error(err instanceof Error ? err.message : "Failed to save");
127
+ } finally {
128
+ setSaving(false);
129
+ }
130
+ }
131
+
132
+ const selectStore = (id: VectorStoreId) => {
133
+ setForm((f) => ({
134
+ ...f,
135
+ vectorStore: id,
136
+ storeConfig: {},
137
+ }));
138
+ };
139
+
140
+ const setStoreValue = (key: string, value: string) => {
141
+ setForm((f) => ({ ...f, storeConfig: { ...f.storeConfig, [key]: value } }));
142
+ };
143
+
144
+ function handleServerApiKeyChange(v: string) {
145
+ setServerApiKey(v);
146
+ localStorage.setItem("rag_server_api_key", v);
147
+ }
148
+
149
+ function generateApiKey() {
150
+ const key =
151
+ "sk-" +
152
+ Array.from(crypto.getRandomValues(new Uint8Array(24)))
153
+ .map((b) => b.toString(16).padStart(2, "0"))
154
+ .join("");
155
+ handleServerApiKeyChange(key);
156
+ toast.success("API key generated");
157
+ }
158
+
159
+ async function copyApiKey() {
160
+ await navigator.clipboard.writeText(serverApiKey);
161
+ setCopiedKey(true);
162
+ setTimeout(() => setCopiedKey(false), 1500);
163
+ }
164
+
165
+ if (isLoading) {
166
+ return (
167
+ <div className="flex h-40 items-center justify-center">
168
+ <Loader2 className="size-6 animate-spin text-muted-foreground" />
169
+ </div>
170
+ );
171
+ }
172
+
173
+ return (
174
+ <div className="flex min-h-full flex-col">
175
+ {/* Header */}
176
+ <div className="flex items-center justify-between px-6 pt-5 pb-4 md:px-8">
177
+ <div>
178
+ <h1 className="text-lg font-semibold tracking-tight text-foreground">
179
+ Settings
180
+ </h1>
181
+ <p className="text-sm text-muted-foreground">
182
+ Manage your AI providers, API keys, and server configuration.
183
+ </p>
184
+ </div>
185
+ <div className="flex items-center gap-3">
186
+ <Button
187
+ size="lg"
188
+ variant="outline"
189
+ onClick={async () => {
190
+ await setMode("tech");
191
+ router.push("/configure");
192
+ }}
193
+ className="px-3.5 h-9.5 rounded-md transition-all text-[13px]"
194
+ >
195
+ Developer Mode
196
+ </Button>
197
+ <Button
198
+ size="lg"
199
+ variant="default"
200
+ disabled={saving || !dirty}
201
+ onClick={handleSave}
202
+ className="min-w-[110px] px-3.5 h-9.5 rounded-md transition-all text-white/90 text-[13px]"
203
+ >
204
+ {saving ? (
205
+ <>
206
+ <Loader2 className="size-4 animate-spin" />
207
+ Saving…
208
+ </>
209
+ ) : (
210
+ <>
211
+ <Save className="size-4" />
212
+ Save Settings
213
+ </>
214
+ )}
215
+ </Button>
216
+ </div>
217
+ </div>
218
+
219
+ {/* Content */}
220
+ <div className="flex-1 overflow-y-auto px-6 pb-8 md:px-8">
221
+ <div className="mx-auto max-w-3xl space-y-6 pt-1">
222
+ {needsReindex && (
223
+ <Alert
224
+ variant="destructive"
225
+ className="bg-destructive/5 border-destructive/20 text-destructive mt-4"
226
+ >
227
+ <AlertCircle className="h-4 w-4" />
228
+ <AlertTitle>Warning: Re-indexing required</AlertTitle>
229
+ <AlertDescription>
230
+ Changing your vector store or embedding model will invalidate
231
+ your existing documents. When you click Save, the RAG server
232
+ will shut down and you will need to re-index your documents in
233
+ the Docs page.
234
+ </AlertDescription>
235
+ </Alert>
236
+ )}
237
+
238
+ {/* ── AI Provider & Keys ────────────────────────────────────── */}
239
+ <Card>
240
+ <CardHeader className="pb-3">
241
+ <CardTitle className="flex items-center gap-2 text-base">
242
+ <Sparkles className="size-4 text-primary" />
243
+ Embedding Provider
244
+ </CardTitle>
245
+ <CardDescription className="text-xs">
246
+ Used to generate vector embeddings for your documents.
247
+ </CardDescription>
248
+ </CardHeader>
249
+ <CardContent className="space-y-4">
250
+ <div className="space-y-1.5">
251
+ <Label className="text-xs">Provider</Label>
252
+ <Select
253
+ value={form.embeddingProvider ?? "openai"}
254
+ onValueChange={(v: any) =>
255
+ setForm({ ...form, embeddingProvider: v })
256
+ }
257
+ >
258
+ <SelectTrigger className="w-full">
259
+ <span className="flex items-center gap-2">
260
+ {PROVIDER_META[
261
+ (form.embeddingProvider ??
262
+ "openai") as keyof typeof PROVIDER_META
263
+ ] ? (
264
+ <>
265
+ <ProviderIcon
266
+ src={
267
+ PROVIDER_META[
268
+ form.embeddingProvider as keyof typeof PROVIDER_META
269
+ ]?.iconSrc ?? ""
270
+ }
271
+ alt={
272
+ PROVIDER_META[
273
+ form.embeddingProvider as keyof typeof PROVIDER_META
274
+ ]?.label ?? ""
275
+ }
276
+ pillBg={
277
+ PROVIDER_META[
278
+ form.embeddingProvider as keyof typeof PROVIDER_META
279
+ ]?.pillBg ?? undefined
280
+ }
281
+ size={16}
282
+ />
283
+ {PROVIDER_META[
284
+ form.embeddingProvider as keyof typeof PROVIDER_META
285
+ ]?.label ?? form.embeddingProvider}
286
+ </>
287
+ ) : (
288
+ form.embeddingProvider || "Select provider"
289
+ )}
290
+ </span>
291
+ </SelectTrigger>
292
+ <SelectContent>
293
+ {PROVIDER_LIST.map((key) => {
294
+ const meta =
295
+ PROVIDER_META[key as keyof typeof PROVIDER_META];
296
+ if (!meta) return null;
297
+ return (
298
+ <SelectItem key={key} value={key}>
299
+ <div className="flex items-center gap-2">
300
+ <ProviderIcon
301
+ src={meta.iconSrc}
302
+ alt={meta.label}
303
+ pillBg={meta.pillBg ?? undefined}
304
+ size={16}
305
+ />
306
+ <span>
307
+ {meta.label}
308
+ {key === "vercel_ai_gateway" && " (Recommended)"}
309
+ </span>
310
+ </div>
311
+ </SelectItem>
312
+ );
313
+ })}
314
+ </SelectContent>
315
+ </Select>
316
+ </div>
317
+ <div className="space-y-1.5">
318
+ <Label className="text-xs">API Key</Label>
319
+ <div className="relative">
320
+ <Input
321
+ type={showEmbeddingKey ? "text" : "password"}
322
+ value={form.embeddingApiKey || ""}
323
+ onChange={(e) =>
324
+ setForm({ ...form, embeddingApiKey: e.target.value })
325
+ }
326
+ placeholder="sk-..."
327
+ className="pr-10"
328
+ />
329
+ <button
330
+ type="button"
331
+ onClick={() => setShowEmbeddingKey(!showEmbeddingKey)}
332
+ className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground"
333
+ >
334
+ {showEmbeddingKey ? (
335
+ <EyeOff className="size-4" />
336
+ ) : (
337
+ <Eye className="size-4" />
338
+ )}
339
+ </button>
340
+ </div>
341
+ </div>
342
+ </CardContent>
343
+ </Card>
344
+
345
+ {/* ── Chat (LLM) Provider ───────────────────────────────────── */}
346
+ <Card>
347
+ <CardHeader className="pb-3">
348
+ <CardTitle className="flex items-center gap-2 text-base">
349
+ <MessageCircle className="size-4 text-primary" />
350
+ Chat (LLM) Provider
351
+ </CardTitle>
352
+ <CardDescription className="text-xs">
353
+ Used to answer questions based on retrieved documents. Leave
354
+ blank to use the same provider and key as embedding.
355
+ </CardDescription>
356
+ </CardHeader>
357
+ <CardContent className="space-y-4">
358
+ <div className="space-y-1.5">
359
+ <Label className="text-xs">Provider</Label>
360
+ <Select
361
+ value={
362
+ form.chatProvider || form.embeddingProvider || "openai"
363
+ }
364
+ onValueChange={(v: any) =>
365
+ setForm({ ...form, chatProvider: v })
366
+ }
367
+ >
368
+ <SelectTrigger className="w-full">
369
+ <span className="flex items-center gap-2">
370
+ {(() => {
371
+ const pKey =
372
+ form.chatProvider ||
373
+ form.embeddingProvider ||
374
+ "openai";
375
+ const meta =
376
+ PROVIDER_META[pKey as keyof typeof PROVIDER_META];
377
+ return meta ? (
378
+ <>
379
+ <ProviderIcon
380
+ src={meta.iconSrc}
381
+ alt={meta.label}
382
+ pillBg={meta.pillBg ?? undefined}
383
+ size={16}
384
+ />
385
+ {meta.label}
386
+ </>
387
+ ) : (
388
+ pKey
389
+ );
390
+ })()}
391
+ </span>
392
+ </SelectTrigger>
393
+ <SelectContent>
394
+ {PROVIDER_LIST.map((key) => {
395
+ const meta =
396
+ PROVIDER_META[key as keyof typeof PROVIDER_META];
397
+ if (!meta) return null;
398
+ return (
399
+ <SelectItem key={key} value={key}>
400
+ <div className="flex items-center gap-2">
401
+ <ProviderIcon
402
+ src={meta.iconSrc}
403
+ alt={meta.label}
404
+ pillBg={meta.pillBg}
405
+ size={16}
406
+ />
407
+ <span>{meta.label}</span>
408
+ </div>
409
+ </SelectItem>
410
+ );
411
+ })}
412
+ </SelectContent>
413
+ </Select>
414
+ </div>
415
+ <div className="space-y-1.5">
416
+ <Label className="text-xs">API Key</Label>
417
+ <div className="relative">
418
+ <Input
419
+ type={showChatKey ? "text" : "password"}
420
+ value={form.chatApiKey || ""}
421
+ onChange={(e) =>
422
+ setForm({ ...form, chatApiKey: e.target.value })
423
+ }
424
+ placeholder="Leave blank to use embedding key"
425
+ className="pr-10"
426
+ />
427
+ <button
428
+ type="button"
429
+ onClick={() => setShowChatKey(!showChatKey)}
430
+ className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground"
431
+ >
432
+ {showChatKey ? (
433
+ <EyeOff className="size-4" />
434
+ ) : (
435
+ <Eye className="size-4" />
436
+ )}
437
+ </button>
438
+ </div>
439
+ </div>
440
+ </CardContent>
441
+ </Card>
442
+
443
+ {/* ── System Prompt ─────────────────────────────────────────── */}
444
+ <Card>
445
+ <CardHeader className="pb-3">
446
+ <CardTitle className="flex items-center gap-2 text-base">
447
+ <Settings2 className="size-4 text-primary" />
448
+ System Prompt
449
+ </CardTitle>
450
+ <CardDescription className="text-xs">
451
+ Override the default instructions given to the chat agent. Leave
452
+ blank for the default RAG prompt.
453
+ </CardDescription>
454
+ </CardHeader>
455
+ <CardContent>
456
+ <textarea
457
+ value={form.systemPrompt || ""}
458
+ onChange={(e) =>
459
+ setForm({ ...form, systemPrompt: e.target.value })
460
+ }
461
+ placeholder="You are a helpful research assistant..."
462
+ rows={5}
463
+ className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground outline-none transition focus:border-primary focus:ring-1 focus:ring-primary resize-y min-h-[100px]"
464
+ />
465
+ </CardContent>
466
+ </Card>
467
+
468
+ {/* ── Vector Store ──────────────────────────────────────────── */}
469
+ <Card>
470
+ <CardHeader className="pb-3">
471
+ <CardTitle className="flex items-center gap-2 text-base">
472
+ <Database className="size-4 text-primary" />
473
+ Vector Store
474
+ </CardTitle>
475
+ <CardDescription className="text-xs">
476
+ Choose where your document vectors are stored.
477
+ </CardDescription>
478
+ </CardHeader>
479
+ <CardContent className="space-y-5">
480
+ <div className="space-y-2">
481
+ <Label className="text-xs">Vector Store</Label>
482
+ <Select
483
+ value={form.vectorStore || ""}
484
+ onValueChange={(v) => selectStore(v as VectorStoreId)}
485
+ >
486
+ <SelectTrigger className="w-full">
487
+ {form.vectorStore ? (
488
+ <span className="flex items-center gap-2.5">
489
+ <ProviderIcon
490
+ src={STORE_META[form.vectorStore]?.iconSrc ?? ""}
491
+ alt={
492
+ getVectorStore(form.vectorStore)?.label ??
493
+ form.vectorStore
494
+ }
495
+ pillBg={
496
+ STORE_META[form.vectorStore]?.pillBg ?? undefined
497
+ }
498
+ size={16}
499
+ />
500
+ <span className="font-medium text-sm">
501
+ {getVectorStore(form.vectorStore)?.label ??
502
+ form.vectorStore}
503
+ </span>
504
+ </span>
505
+ ) : (
506
+ <span className="text-muted-foreground">
507
+ Select a vector store…
508
+ </span>
509
+ )}
510
+ </SelectTrigger>
511
+ <SelectContent>
512
+ {VECTOR_STORE_LIST.map((s) => {
513
+ const meta = STORE_META[s.id];
514
+ const isComingSoon = s.installStatus === "coming-soon";
515
+ return (
516
+ <SelectItem
517
+ key={s.id}
518
+ value={s.id}
519
+ disabled={isComingSoon}
520
+ className={isComingSoon ? "opacity-50" : ""}
521
+ >
522
+ <span className="flex items-center gap-2.5 w-full">
523
+ {meta && (
524
+ <ProviderIcon
525
+ src={meta.iconSrc}
526
+ alt={s.label}
527
+ pillBg={meta.pillBg ?? undefined}
528
+ size={16}
529
+ />
530
+ )}
531
+ <span className="flex flex-col flex-1 min-w-0">
532
+ <span className="flex items-center gap-2">
533
+ <span className="font-medium text-sm">
534
+ {s.label}
535
+ </span>
536
+ {isComingSoon && (
537
+ <span className="h-3.5 px-1 text-[9px] font-medium shrink-0 text-muted-foreground border rounded-full flex items-center bg-transparent">
538
+ <Clock className="size-2.5 mr-0.5" />
539
+ Coming Soon
540
+ </span>
541
+ )}
542
+ </span>
543
+ </span>
544
+ </span>
545
+ </SelectItem>
546
+ );
547
+ })}
548
+ </SelectContent>
549
+ </Select>
550
+ </div>
551
+
552
+ {form.vectorStore && (
553
+ <div className="border-t pt-4">
554
+ <StoreFields
555
+ store={getVectorStore(form.vectorStore)}
556
+ values={form.storeConfig || {}}
557
+ errors={{}}
558
+ onChange={setStoreValue}
559
+ indexType={form.indexType || "hybrid"}
560
+ />
561
+ </div>
562
+ )}
563
+ </CardContent>
564
+ </Card>
565
+
566
+ {/* ── Server API Key ────────────────────────────────────────── */}
567
+ <Card>
568
+ <CardHeader className="pb-3">
569
+ <CardTitle className="flex items-center gap-2 text-base">
570
+ <Key className="size-4 text-primary" />
571
+ Server API Key
572
+ </CardTitle>
573
+ <CardDescription className="text-xs">
574
+ Protect your RAG server with an API key. Clients must send this
575
+ key in the{" "}
576
+ <code className="text-[11px] bg-muted px-1 py-0.5 rounded">
577
+ Authorization
578
+ </code>{" "}
579
+ header.
580
+ </CardDescription>
581
+ </CardHeader>
582
+ <CardContent className="space-y-3">
583
+ <div className="flex items-center gap-2">
584
+ <Input
585
+ type="text"
586
+ value={serverApiKey}
587
+ onChange={(e) => handleServerApiKeyChange(e.target.value)}
588
+ placeholder="sk-..."
589
+ className="flex-1 font-mono text-xs"
590
+ />
591
+ <Button
592
+ variant="outline"
593
+ size="icon"
594
+ className="shrink-0 h-9 w-9"
595
+ onClick={copyApiKey}
596
+ disabled={!serverApiKey}
597
+ >
598
+ {copiedKey ? (
599
+ <CheckCircle2 className="size-4 text-primary" />
600
+ ) : (
601
+ <Copy className="size-4" />
602
+ )}
603
+ </Button>
604
+ <Button
605
+ variant="outline"
606
+ size="sm"
607
+ className="shrink-0 gap-1.5"
608
+ onClick={generateApiKey}
609
+ >
610
+ <RefreshCw className="size-3.5" />
611
+ Generate
612
+ </Button>
613
+ </div>
614
+ <p className="text-xs text-muted-foreground">
615
+ Stored locally in your browser. Generate a new key or paste your
616
+ own.
617
+ </p>
618
+ </CardContent>
619
+ </Card>
620
+
621
+ {/* ── RAG Server Launch ─────────────────────────────────────── */}
622
+ <LaunchPanel serverId={serverId || "default"} />
623
+ </div>
624
+ </div>
625
+ </div>
626
+ );
627
+ }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+
5
+ export default function Template({ children }: { children: React.ReactNode }) {
6
+ return (
7
+ <motion.div
8
+ initial={{ opacity: 0, y: 15 }}
9
+ animate={{ opacity: 1, y: 0 }}
10
+ transition={{ duration: 0.3, ease: "easeOut" }}
11
+ className="h-full w-full"
12
+ >
13
+ {children}
14
+ </motion.div>
15
+ );
16
+ }