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,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
|
+
}
|
package/app/template.tsx
ADDED
|
@@ -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
|
+
}
|