larkup 0.1.28 → 0.1.29
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/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +5 -5
- package/.next/diagnostics/route-bundle-stats.json +25 -25
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +4 -4
- package/.next/required-server-files.json +4 -4
- package/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +16 -16
- package/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/analytics/page/build-manifest.json +2 -2
- package/.next/server/app/analytics/page.js.nft.json +1 -1
- package/.next/server/app/analytics/page_client-reference-manifest.js +1 -1
- package/.next/server/app/analytics.html +1 -1
- package/.next/server/app/analytics.rsc +17 -17
- package/.next/server/app/analytics.segments/_full.segment.rsc +17 -17
- package/.next/server/app/analytics.segments/_head.segment.rsc +4 -4
- package/.next/server/app/analytics.segments/_index.segment.rsc +11 -11
- package/.next/server/app/analytics.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/analytics.segments/analytics/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/analytics.segments/analytics.segment.rsc +3 -3
- package/.next/server/app/api/analytics/route.js.nft.json +1 -1
- package/.next/server/app/api/chat/route.js +8 -9
- package/.next/server/app/api/chat/route.js.nft.json +1 -1
- package/.next/server/app/api/chat/status/route.js.nft.json +1 -1
- package/.next/server/app/api/chat/suggestions/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/config/test/route.js.nft.json +1 -1
- package/.next/server/app/api/config/test-embedding/route.js.nft.json +1 -1
- package/.next/server/app/api/config/test-llm/route.js.nft.json +1 -1
- package/.next/server/app/api/config/test-provider/route.js.nft.json +1 -1
- package/.next/server/app/api/config/verify/route.js.nft.json +1 -1
- package/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/server/app/api/connections/webhook/route.js.nft.json +1 -1
- package/.next/server/app/api/demo/route.js.nft.json +1 -1
- package/.next/server/app/api/deploy/cloud-project/route.js.nft.json +1 -1
- package/.next/server/app/api/deploy/ssh/route.js.nft.json +1 -1
- package/.next/server/app/api/deployment/blob-setup/route.js.nft.json +1 -1
- package/.next/server/app/api/deployment/config/route.js.nft.json +1 -1
- package/.next/server/app/api/describe-image/route.js.nft.json +1 -1
- package/.next/server/app/api/doc-editor/edit/route.js.nft.json +1 -1
- package/.next/server/app/api/doc-editor/export/route.js.nft.json +1 -1
- package/.next/server/app/api/doc-editor/extract-text/route.js.nft.json +1 -1
- package/.next/server/app/api/doc-editor/parse/route.js.nft.json +1 -1
- package/.next/server/app/api/doc-editor/restore/route.js.nft.json +1 -1
- package/.next/server/app/api/document/sign/route.js.nft.json +1 -1
- package/.next/server/app/api/documents/route.js.nft.json +1 -1
- package/.next/server/app/api/firecrawl/local/route.js.nft.json +1 -1
- package/.next/server/app/api/index/route.js.nft.json +1 -1
- package/.next/server/app/api/integrations/notion/callback/route.js.nft.json +1 -1
- package/.next/server/app/api/integrations/notion/route.js.nft.json +1 -1
- package/.next/server/app/api/jobs/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/jobs/route.js.nft.json +1 -1
- package/.next/server/app/api/marketplace/[toolId]/route.js +1 -1
- package/.next/server/app/api/marketplace/[toolId]/route.js.nft.json +1 -1
- package/.next/server/app/api/marketplace/route.js +1 -1
- package/.next/server/app/api/marketplace/route.js.nft.json +1 -1
- package/.next/server/app/api/media/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/media/process/route.js +2 -2
- package/.next/server/app/api/media/process/route.js.nft.json +1 -1
- package/.next/server/app/api/media/route.js +1 -1
- package/.next/server/app/api/media/route.js.nft.json +1 -1
- package/.next/server/app/api/media/stream/route.js.nft.json +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/parse-file/route.js.nft.json +1 -1
- package/.next/server/app/api/proxy/verify/route.js.nft.json +1 -1
- package/.next/server/app/api/sandbox/route.js.nft.json +1 -1
- package/.next/server/app/api/search/bing/route.js.nft.json +1 -1
- package/.next/server/app/api/search/brave/route.js.nft.json +1 -1
- package/.next/server/app/api/search/exa/route.js.nft.json +1 -1
- package/.next/server/app/api/search/google/route.js.nft.json +1 -1
- package/.next/server/app/api/search/route.js.nft.json +1 -1
- package/.next/server/app/api/search/tavily/route.js.nft.json +1 -1
- package/.next/server/app/api/search/verify/route.js.nft.json +1 -1
- package/.next/server/app/api/server/agent/route.js.nft.json +1 -1
- package/.next/server/app/api/server/download/route.js.nft.json +1 -1
- package/.next/server/app/api/server/generate/route.js.nft.json +1 -1
- package/.next/server/app/api/server/local/route.js.nft.json +1 -1
- package/.next/server/app/api/servers/route.js.nft.json +1 -1
- package/.next/server/app/api/tabular/query/route.js.nft.json +1 -1
- package/.next/server/app/api/tabular/route.js.nft.json +1 -1
- package/.next/server/app/api/upload-file/route.js.nft.json +1 -1
- package/.next/server/app/api/uploads/[...slug]/route.js.nft.json +1 -1
- package/.next/server/app/api/vector-stores/install/route.js.nft.json +1 -1
- package/.next/server/app/api/vector-stores/status/route.js.nft.json +1 -1
- package/.next/server/app/chat/page/build-manifest.json +2 -2
- package/.next/server/app/chat/page.js.nft.json +1 -1
- package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
- package/.next/server/app/chat.html +1 -1
- package/.next/server/app/chat.rsc +17 -17
- package/.next/server/app/chat.segments/_full.segment.rsc +17 -17
- package/.next/server/app/chat.segments/_head.segment.rsc +4 -4
- package/.next/server/app/chat.segments/_index.segment.rsc +11 -11
- package/.next/server/app/chat.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/chat.segments/chat.segment.rsc +3 -3
- package/.next/server/app/data/page/build-manifest.json +2 -2
- package/.next/server/app/data/page.js.nft.json +1 -1
- package/.next/server/app/data/page_client-reference-manifest.js +1 -1
- package/.next/server/app/data.html +1 -1
- package/.next/server/app/data.rsc +17 -17
- package/.next/server/app/data.segments/_full.segment.rsc +17 -17
- package/.next/server/app/data.segments/_head.segment.rsc +4 -4
- package/.next/server/app/data.segments/_index.segment.rsc +11 -11
- package/.next/server/app/data.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/data.segments/data/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/data.segments/data.segment.rsc +3 -3
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +16 -16
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +11 -11
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/page/build-manifest.json +2 -2
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/page/build-manifest.json +2 -2
- package/.next/server/app/settings/page/server-reference-manifest.json +3 -3
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings.html +1 -1
- package/.next/server/app/settings.rsc +18 -18
- package/.next/server/app/settings.segments/_full.segment.rsc +18 -18
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +11 -11
- package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/server/chunks/[root-of-the-server]__0c80bj0._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__1m7m2x7._.js → [root-of-the-server]__0drkoru._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__0vnm6_8._.js → [root-of-the-server]__0kmh740._.js} +6 -6
- package/.next/server/chunks/{[root-of-the-server]__0ou70-v._.js → [root-of-the-server]__0nfenje._.js} +1 -1
- package/.next/server/chunks/[root-of-the-server]__0omd-x0._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__1s1zui-._.js → [root-of-the-server]__10v5qsi._.js} +1 -1
- package/.next/server/chunks/[root-of-the-server]__1idgvjh._.js +172 -0
- package/.next/server/chunks/[root-of-the-server]__1ip05r1._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1jc54o2._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__0748x7h._.js → [root-of-the-server]__1lhls88._.js} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__0g_q-44._.js → [root-of-the-server]__1wy6wes._.js} +2 -2
- package/.next/server/chunks/{packages_1f4dx4h._.js → packages_1ggqion._.js} +2 -2
- package/.next/server/chunks/packages_marketplace_src_125_g2p._.js +1 -1
- package/.next/server/chunks/{packages_vector-stores_src_064-9ay._.js → packages_vector-stores_src_1fib7u-._.js} +2 -2
- package/.next/server/chunks/ssr/_169pdlf._.js +2 -2
- package/.next/server/chunks/ssr/_1hetwwu._.js +2 -2
- package/.next/server/chunks/ssr/_1iff9ia._.js +1 -1
- package/.next/server/chunks/ssr/_1yqlx1m._.js +1 -1
- package/.next/server/chunks/ssr/apps_web_lib_pdf-images_ts_13-l5wa._.js +1 -1
- package/.next/server/middleware-build-manifest.js +5 -5
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +4 -4
- package/.next/static/chunks/{13hmtkhrbm4t3.js → 01qmngac69n9j.js} +1 -1
- package/.next/static/chunks/{3bpr59xr-_rv7.js → 0ge3nirnmx_np.js} +1 -1
- package/.next/static/chunks/{0k405nomf6s4e.js → 102b8vinedggz.js} +6 -6
- package/.next/static/chunks/{2jqjx4-rh414_.js → 13kyiy7q3of41.js} +1 -1
- package/.next/static/chunks/{1fz_0-qb5r5wd.js → 1dykihoo_1ae5.js} +1 -1
- package/.next/static/chunks/{0rgt7-bqxonef.js → 1sh7d5tt49x8b.js} +2 -2
- package/.next/static/chunks/{1-afr-snks55m.js → 1z4nte07k3dt6.js} +1 -1
- package/.next/static/chunks/2ib3e0xrrdait.js +1 -0
- package/.next/static/chunks/{turbopack-2gu-fmc6o5wkn.js → turbopack-2qt7vn_ocj4ao.js} +1 -1
- package/CHANGELOG.md +10 -0
- package/app/api/describe-image/route.ts +12 -4
- package/components/data/media-panel.tsx +34 -2
- package/components/data/upload-panel.tsx +2 -1
- package/components/settings/marketplace-section.tsx +14 -2
- package/lib/pdf-images.ts +20 -5
- package/package.json +5 -5
- package/.next/server/chunks/[root-of-the-server]__00r1kgp._.js +0 -143
- package/.next/server/chunks/[root-of-the-server]__064gb96._.js +0 -3
- package/.next/server/chunks/_0ce9vcg._.js +0 -32
- package/.next/static/chunks/00z1f-adeq1lg.js +0 -1
- package/archive/redesign-v2/app/configure/page.tsx +0 -53
- package/archive/redesign-v2/app/demo/page.tsx +0 -15
- package/archive/redesign-v2/app/server/page.tsx +0 -22
- package/archive/redesign-v2/app/simple/chat/page.tsx +0 -17
- package/archive/redesign-v2/app/simple/docs/page.tsx +0 -21
- package/archive/redesign-v2/app/simple/settings/page.tsx +0 -1104
- package/archive/redesign-v2/app-routes/configure/page.tsx +0 -53
- package/archive/redesign-v2/app-routes/demo/page.tsx +0 -15
- package/archive/redesign-v2/app-routes/server/page.tsx +0 -22
- package/archive/redesign-v2/app-routes/simple/chat/page.tsx +0 -17
- package/archive/redesign-v2/app-routes/simple/docs/page.tsx +0 -21
- package/archive/redesign-v2/app-routes/simple/layout.tsx +0 -9
- package/archive/redesign-v2/app-routes/simple/settings/page.tsx +0 -1104
- package/archive/redesign-v2/components/app-sidebar.tsx +0 -188
- package/archive/redesign-v2/components/app-topnav.tsx +0 -195
- package/archive/redesign-v2/components/global-settings.tsx +0 -288
- package/archive/redesign-v2/components/simple/simple-sidebar.tsx +0 -171
- package/archive/redesign-v2/components/simple/simple-topnav.tsx +0 -175
- /package/.next/static/{bDwMIFYSTVBKUJkJJx77o → W6QdudzTPo_OVPpNt29f7}/_buildManifest.js +0 -0
- /package/.next/static/{bDwMIFYSTVBKUJkJJx77o → W6QdudzTPo_OVPpNt29f7}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{bDwMIFYSTVBKUJkJJx77o → W6QdudzTPo_OVPpNt29f7}/_ssgManifest.js +0 -0
|
@@ -1,1104 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useMemo } 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
|
-
Plus,
|
|
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 { cn } from "@/lib/utils";
|
|
26
|
-
import {
|
|
27
|
-
Select,
|
|
28
|
-
SelectContent,
|
|
29
|
-
SelectItem,
|
|
30
|
-
SelectTrigger,
|
|
31
|
-
SelectGroup,
|
|
32
|
-
SelectLabel,
|
|
33
|
-
} from "@/components/ui/select";
|
|
34
|
-
import {
|
|
35
|
-
Card,
|
|
36
|
-
CardContent,
|
|
37
|
-
CardDescription,
|
|
38
|
-
CardHeader,
|
|
39
|
-
CardTitle,
|
|
40
|
-
} from "@/components/ui/card";
|
|
41
|
-
import { toast } from "sonner";
|
|
42
|
-
import { useWorkspace } from "@/components/workspace/workspace-provider";
|
|
43
|
-
import { PROVIDER_META, ProviderIcon } from "@/components/ui/provider-icon";
|
|
44
|
-
import { LaunchPanel } from "@/components/server/server-workspace";
|
|
45
|
-
import { ServerFormDialog } from "@/components/workspace/server-form-dialog";
|
|
46
|
-
import {
|
|
47
|
-
VECTOR_STORE_LIST,
|
|
48
|
-
getVectorStore,
|
|
49
|
-
validateStoreConfig,
|
|
50
|
-
} from "@larkup/vector-stores/registry";
|
|
51
|
-
import { StoreFields } from "@/components/configure/store-fields";
|
|
52
|
-
import { CustomModelModal } from "@/components/configure/custom-model-modal";
|
|
53
|
-
import type {
|
|
54
|
-
RagConfig,
|
|
55
|
-
VectorStoreId,
|
|
56
|
-
CustomModelConfig,
|
|
57
|
-
EmbeddingModelDescriptor,
|
|
58
|
-
} from "@larkup/core/types";
|
|
59
|
-
|
|
60
|
-
const fetcher = (url: string) =>
|
|
61
|
-
fetch(url).then((r) => r.json() as Promise<{ config: RagConfig }>);
|
|
62
|
-
|
|
63
|
-
const PROVIDER_LIST = [
|
|
64
|
-
"vercel_ai_gateway",
|
|
65
|
-
"openai",
|
|
66
|
-
"google",
|
|
67
|
-
"deepseek",
|
|
68
|
-
"mistral",
|
|
69
|
-
"voyage",
|
|
70
|
-
"jina",
|
|
71
|
-
"nomic",
|
|
72
|
-
] as const;
|
|
73
|
-
|
|
74
|
-
type StoreMeta = { iconSrc: string; pillBg: string };
|
|
75
|
-
const STORE_META: Record<string, StoreMeta> = {
|
|
76
|
-
lancedb: {
|
|
77
|
-
iconSrc: "/icons/lancedb2.png",
|
|
78
|
-
pillBg: "bg-yellow-50 dark:bg-yellow-950/40",
|
|
79
|
-
},
|
|
80
|
-
pinecone: {
|
|
81
|
-
iconSrc: "/icons/pinecone.png",
|
|
82
|
-
pillBg: "bg-green-50 dark:bg-green-950/40",
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export default function SimpleSettingsPage() {
|
|
87
|
-
const router = useRouter();
|
|
88
|
-
const { activeServer, setMode } = useWorkspace();
|
|
89
|
-
const serverId = activeServer?.id;
|
|
90
|
-
|
|
91
|
-
const configUrl = serverId
|
|
92
|
-
? `/api/config?serverId=${encodeURIComponent(serverId)}`
|
|
93
|
-
: "/api/config";
|
|
94
|
-
const { data, isLoading, mutate } = useSWR(configUrl, fetcher);
|
|
95
|
-
|
|
96
|
-
const [form, setForm] = useState<Partial<RagConfig>>({});
|
|
97
|
-
const [saving, setSaving] = useState(false);
|
|
98
|
-
const [showEmbeddingKey, setShowEmbeddingKey] = useState(false);
|
|
99
|
-
|
|
100
|
-
const currentChatProvider = form.chatProvider || form.embeddingProvider || "openai";
|
|
101
|
-
const statusKey = serverId
|
|
102
|
-
? `/api/chat/status?serverId=${encodeURIComponent(serverId)}&provider=${currentChatProvider}`
|
|
103
|
-
: `/api/chat/status?provider=${currentChatProvider}`;
|
|
104
|
-
const { data: chatStatus } = useSWR(statusKey, (url: string) => fetch(url).then(r => r.json()));
|
|
105
|
-
|
|
106
|
-
// Build embedding models grouped by provider from the dynamic API response
|
|
107
|
-
const embeddingModels: EmbeddingModelDescriptor[] = chatStatus?.availableEmbeddingModels ?? [];
|
|
108
|
-
const EMBEDDING_BY_PROVIDER = useMemo(() => {
|
|
109
|
-
return embeddingModels.reduce<Record<string, EmbeddingModelDescriptor[]>>((acc, m) => {
|
|
110
|
-
(acc[m.provider] ??= []).push(m);
|
|
111
|
-
return acc;
|
|
112
|
-
}, {});
|
|
113
|
-
}, [embeddingModels]);
|
|
114
|
-
const [showChatKey, setShowChatKey] = useState(false);
|
|
115
|
-
const [serverApiKey, setServerApiKey] = useState("");
|
|
116
|
-
const [copiedKey, setCopiedKey] = useState(false);
|
|
117
|
-
|
|
118
|
-
const { data: indexData } = useSWR(
|
|
119
|
-
"/api/index",
|
|
120
|
-
(url: string) => fetch(url).then((r) => r.json()),
|
|
121
|
-
{
|
|
122
|
-
refreshInterval: 0,
|
|
123
|
-
},
|
|
124
|
-
);
|
|
125
|
-
const indexedRun =
|
|
126
|
-
indexData?.run?.status === "completed" ? indexData.run : null;
|
|
127
|
-
|
|
128
|
-
const [newServerModalOpen, setNewServerModalOpen] = useState(false);
|
|
129
|
-
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
130
|
-
const [customEmbeddingModalOpen, setCustomEmbeddingModalOpen] =
|
|
131
|
-
useState(false);
|
|
132
|
-
const [customChatModalOpen, setCustomChatModalOpen] = useState(false);
|
|
133
|
-
|
|
134
|
-
const [isOtherEmbedding, setIsOtherEmbedding] = useState(false);
|
|
135
|
-
const [isOtherChat, setIsOtherChat] = useState(false);
|
|
136
|
-
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
if (data?.config) {
|
|
139
|
-
setForm({
|
|
140
|
-
...data.config,
|
|
141
|
-
chatProvider: data.config.chatProvider || data.config.embeddingProvider,
|
|
142
|
-
chatApiKey: data.config.chatApiKey || data.config.embeddingApiKey,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}, [data]);
|
|
146
|
-
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
const saved = localStorage.getItem("rag_server_api_key");
|
|
149
|
-
if (saved) setServerApiKey(saved);
|
|
150
|
-
}, []);
|
|
151
|
-
|
|
152
|
-
const dirty = JSON.stringify(form) !== JSON.stringify(data?.config ?? {});
|
|
153
|
-
const needsReindex =
|
|
154
|
-
data?.config &&
|
|
155
|
-
(form.vectorStore !== data.config.vectorStore ||
|
|
156
|
-
form.embeddingModelId !== data.config.embeddingModelId);
|
|
157
|
-
|
|
158
|
-
async function handleSave() {
|
|
159
|
-
if (!data?.config) return;
|
|
160
|
-
|
|
161
|
-
let hasError = false;
|
|
162
|
-
const newErrors: Record<string, string> = {};
|
|
163
|
-
|
|
164
|
-
if (!form.embeddingProvider) {
|
|
165
|
-
newErrors.embeddingProvider = "Required";
|
|
166
|
-
hasError = true;
|
|
167
|
-
}
|
|
168
|
-
if (!form.embeddingApiKey) {
|
|
169
|
-
newErrors.embeddingApiKey = "Required";
|
|
170
|
-
hasError = true;
|
|
171
|
-
}
|
|
172
|
-
if (!form.chatProvider) {
|
|
173
|
-
newErrors.chatProvider = "Required";
|
|
174
|
-
hasError = true;
|
|
175
|
-
}
|
|
176
|
-
if (!form.chatApiKey) {
|
|
177
|
-
newErrors.chatApiKey = "Required";
|
|
178
|
-
hasError = true;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (form.vectorStore) {
|
|
182
|
-
const store = getVectorStore(form.vectorStore);
|
|
183
|
-
if (store) {
|
|
184
|
-
const storeErrs = validateStoreConfig(
|
|
185
|
-
store,
|
|
186
|
-
form.storeConfig || {},
|
|
187
|
-
form.indexType || "hybrid",
|
|
188
|
-
);
|
|
189
|
-
if (Object.keys(storeErrs).length > 0) {
|
|
190
|
-
Object.assign(newErrors, storeErrs);
|
|
191
|
-
hasError = true;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
setErrors(newErrors);
|
|
197
|
-
|
|
198
|
-
if (hasError) {
|
|
199
|
-
toast.error("Please fill in all required fields", { duration: Number.POSITIVE_INFINITY });
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
setSaving(true);
|
|
204
|
-
try {
|
|
205
|
-
const verifyRes = await fetch("/api/config/verify", {
|
|
206
|
-
method: "POST",
|
|
207
|
-
headers: { "Content-Type": "application/json" },
|
|
208
|
-
body: JSON.stringify({
|
|
209
|
-
embeddingProvider: form.embeddingProvider,
|
|
210
|
-
embeddingApiKey: form.embeddingApiKey,
|
|
211
|
-
embeddingModelId: form.embeddingModelId,
|
|
212
|
-
customEmbeddings: form.customEmbeddings,
|
|
213
|
-
chatProvider: form.chatProvider,
|
|
214
|
-
chatApiKey: form.chatApiKey,
|
|
215
|
-
chatModelId: form.chatModelId,
|
|
216
|
-
customChatModels: form.customChatModels,
|
|
217
|
-
}),
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
if (!verifyRes.ok) {
|
|
221
|
-
const err = await verifyRes.json();
|
|
222
|
-
throw new Error(err.error || "Verification failed");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const merged = { ...data.config, ...form };
|
|
226
|
-
const res = await fetch(configUrl, {
|
|
227
|
-
method: "PUT",
|
|
228
|
-
headers: { "Content-Type": "application/json" },
|
|
229
|
-
body: JSON.stringify(merged),
|
|
230
|
-
});
|
|
231
|
-
const json = await res.json();
|
|
232
|
-
if (!res.ok)
|
|
233
|
-
throw new Error(json.error ?? "Failed to save configuration");
|
|
234
|
-
await mutate(json, { revalidate: false });
|
|
235
|
-
toast.success("Settings saved", { duration: Number.POSITIVE_INFINITY });
|
|
236
|
-
} catch (err) {
|
|
237
|
-
toast.error(err instanceof Error ? err.message : "Failed to save", { duration: Number.POSITIVE_INFINITY });
|
|
238
|
-
} finally {
|
|
239
|
-
setSaving(false);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const selectStore = (id: VectorStoreId) => {
|
|
244
|
-
setForm((f) => ({
|
|
245
|
-
...f,
|
|
246
|
-
vectorStore: id,
|
|
247
|
-
storeConfig: {},
|
|
248
|
-
}));
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const clearError = (key: string) => {
|
|
252
|
-
if (errors[key]) {
|
|
253
|
-
setErrors((prev) => {
|
|
254
|
-
const next = { ...prev };
|
|
255
|
-
delete next[key];
|
|
256
|
-
return next;
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const setStoreValue = (key: string, value: string) => {
|
|
262
|
-
setForm((f) => ({ ...f, storeConfig: { ...f.storeConfig, [key]: value } }));
|
|
263
|
-
clearError(key);
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
function handleServerApiKeyChange(v: string) {
|
|
267
|
-
setServerApiKey(v);
|
|
268
|
-
localStorage.setItem("rag_server_api_key", v);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function handleStructuralChangeBlock() {
|
|
272
|
-
toast("Cannot modify configuration", {
|
|
273
|
-
description:
|
|
274
|
-
"You cannot change this in the current project because it already has an index. You must initiate a new project.",
|
|
275
|
-
duration: Number.POSITIVE_INFINITY,
|
|
276
|
-
action: {
|
|
277
|
-
label: "New Project",
|
|
278
|
-
onClick: () => {
|
|
279
|
-
setNewServerModalOpen(true);
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function generateApiKey() {
|
|
286
|
-
const key =
|
|
287
|
-
"sk-" +
|
|
288
|
-
Array.from(crypto.getRandomValues(new Uint8Array(24)))
|
|
289
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
290
|
-
.join("");
|
|
291
|
-
handleServerApiKeyChange(key);
|
|
292
|
-
toast.success("API key generated", { duration: Number.POSITIVE_INFINITY });
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function copyApiKey() {
|
|
296
|
-
await navigator.clipboard.writeText(serverApiKey);
|
|
297
|
-
setCopiedKey(true);
|
|
298
|
-
setTimeout(() => setCopiedKey(false), 1500);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (isLoading) {
|
|
302
|
-
return (
|
|
303
|
-
<div className="flex h-40 items-center justify-center">
|
|
304
|
-
<Loader2 className="size-6 animate-spin text-muted-foreground" />
|
|
305
|
-
</div>
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return (
|
|
310
|
-
<div className="flex min-h-full flex-col">
|
|
311
|
-
{/* Header */}
|
|
312
|
-
<div className="flex items-center justify-between px-6 pt-5 pb-4 md:px-8">
|
|
313
|
-
<div>
|
|
314
|
-
<h1 className="text-lg font-semibold tracking-tight text-foreground">
|
|
315
|
-
Settings
|
|
316
|
-
</h1>
|
|
317
|
-
<p className="text-sm text-muted-foreground">
|
|
318
|
-
Manage your AI providers, API keys, and server configuration.
|
|
319
|
-
</p>
|
|
320
|
-
</div>
|
|
321
|
-
<div className="flex items-center gap-3">
|
|
322
|
-
<Button
|
|
323
|
-
size="lg"
|
|
324
|
-
variant="outline"
|
|
325
|
-
onClick={async () => {
|
|
326
|
-
await setMode("tech");
|
|
327
|
-
router.push("/configure");
|
|
328
|
-
}}
|
|
329
|
-
className="px-3.5 h-9.5 rounded-md transition-all text-[13px]"
|
|
330
|
-
>
|
|
331
|
-
Developer Mode
|
|
332
|
-
</Button>
|
|
333
|
-
<Button
|
|
334
|
-
size="lg"
|
|
335
|
-
variant="default"
|
|
336
|
-
disabled={saving || !dirty}
|
|
337
|
-
onClick={handleSave}
|
|
338
|
-
className="min-w-[110px] px-3.5 h-9.5 rounded-md transition-all text-white/90 text-[13px]"
|
|
339
|
-
>
|
|
340
|
-
{saving ? (
|
|
341
|
-
<>
|
|
342
|
-
<Loader2 className="size-4 animate-spin" />
|
|
343
|
-
Saving…
|
|
344
|
-
</>
|
|
345
|
-
) : (
|
|
346
|
-
<>
|
|
347
|
-
<Save className="size-4" />
|
|
348
|
-
Save Settings
|
|
349
|
-
</>
|
|
350
|
-
)}
|
|
351
|
-
</Button>
|
|
352
|
-
</div>
|
|
353
|
-
</div>
|
|
354
|
-
|
|
355
|
-
<ServerFormDialog
|
|
356
|
-
mode="create"
|
|
357
|
-
open={newServerModalOpen}
|
|
358
|
-
onOpenChange={setNewServerModalOpen}
|
|
359
|
-
/>
|
|
360
|
-
|
|
361
|
-
<CustomModelModal
|
|
362
|
-
type="embedding"
|
|
363
|
-
open={customEmbeddingModalOpen}
|
|
364
|
-
onOpenChange={setCustomEmbeddingModalOpen}
|
|
365
|
-
onSave={(cfg) => {
|
|
366
|
-
setForm({
|
|
367
|
-
...form,
|
|
368
|
-
embeddingProvider: "custom",
|
|
369
|
-
embeddingModelId: `custom:${cfg.modelName}`,
|
|
370
|
-
customEmbeddings: [cfg],
|
|
371
|
-
});
|
|
372
|
-
clearError("embeddingProvider");
|
|
373
|
-
}}
|
|
374
|
-
/>
|
|
375
|
-
<CustomModelModal
|
|
376
|
-
type="chat"
|
|
377
|
-
open={customChatModalOpen}
|
|
378
|
-
onOpenChange={setCustomChatModalOpen}
|
|
379
|
-
onSave={(cfg) => {
|
|
380
|
-
setForm({
|
|
381
|
-
...form,
|
|
382
|
-
chatProvider: "custom",
|
|
383
|
-
chatModelId: `custom:${cfg.modelName}`,
|
|
384
|
-
customChatModels: [cfg],
|
|
385
|
-
});
|
|
386
|
-
clearError("chatProvider");
|
|
387
|
-
}}
|
|
388
|
-
/>
|
|
389
|
-
|
|
390
|
-
{/* Content */}
|
|
391
|
-
<div className="flex-1 overflow-y-auto px-6 pb-8 md:px-8">
|
|
392
|
-
<div className="mx-auto max-w-3xl space-y-6 pt-1">
|
|
393
|
-
{/* ── AI Provider & Keys ────────────────────────────────────── */}
|
|
394
|
-
<Card>
|
|
395
|
-
<CardHeader className="pb-3">
|
|
396
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
397
|
-
<Sparkles className="size-4 text-primary" />
|
|
398
|
-
Embedding Provider
|
|
399
|
-
</CardTitle>
|
|
400
|
-
<CardDescription className="text-xs">
|
|
401
|
-
Used to generate vector embeddings for your documents.
|
|
402
|
-
</CardDescription>
|
|
403
|
-
</CardHeader>
|
|
404
|
-
<CardContent className="space-y-4">
|
|
405
|
-
<div className="space-y-1.5">
|
|
406
|
-
<Label className="text-xs">Provider</Label>
|
|
407
|
-
<div className="flex gap-2">
|
|
408
|
-
<Select
|
|
409
|
-
value={form.embeddingProvider ?? "openai"}
|
|
410
|
-
onValueChange={(v: any) => {
|
|
411
|
-
if (
|
|
412
|
-
indexedRun &&
|
|
413
|
-
data?.config &&
|
|
414
|
-
v !== data.config.embeddingProvider
|
|
415
|
-
) {
|
|
416
|
-
handleStructuralChangeBlock();
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
setForm({
|
|
420
|
-
...form,
|
|
421
|
-
embeddingProvider: v,
|
|
422
|
-
embeddingModelId: "", // clear so backend uses new provider's default
|
|
423
|
-
chatProvider:
|
|
424
|
-
!form.chatProvider ||
|
|
425
|
-
form.chatProvider === form.embeddingProvider
|
|
426
|
-
? v
|
|
427
|
-
: form.chatProvider,
|
|
428
|
-
chatModelId:
|
|
429
|
-
!form.chatProvider ||
|
|
430
|
-
form.chatProvider === form.embeddingProvider
|
|
431
|
-
? "" // clear chat model id too if we are updating chatProvider
|
|
432
|
-
: form.chatModelId,
|
|
433
|
-
});
|
|
434
|
-
clearError("embeddingProvider");
|
|
435
|
-
clearError("chatProvider");
|
|
436
|
-
}}
|
|
437
|
-
>
|
|
438
|
-
<SelectTrigger
|
|
439
|
-
className={cn(
|
|
440
|
-
"w-full",
|
|
441
|
-
errors.embeddingProvider && "border-destructive",
|
|
442
|
-
)}
|
|
443
|
-
>
|
|
444
|
-
<span className="flex items-center gap-2">
|
|
445
|
-
{PROVIDER_META[
|
|
446
|
-
(form.embeddingProvider ??
|
|
447
|
-
"openai") as keyof typeof PROVIDER_META
|
|
448
|
-
] ? (
|
|
449
|
-
<>
|
|
450
|
-
<ProviderIcon
|
|
451
|
-
src={
|
|
452
|
-
PROVIDER_META[
|
|
453
|
-
form.embeddingProvider as keyof typeof PROVIDER_META
|
|
454
|
-
]?.iconSrc ?? ""
|
|
455
|
-
}
|
|
456
|
-
alt={
|
|
457
|
-
PROVIDER_META[
|
|
458
|
-
form.embeddingProvider as keyof typeof PROVIDER_META
|
|
459
|
-
]?.label ?? ""
|
|
460
|
-
}
|
|
461
|
-
pillBg={
|
|
462
|
-
PROVIDER_META[
|
|
463
|
-
form.embeddingProvider as keyof typeof PROVIDER_META
|
|
464
|
-
]?.pillBg ?? undefined
|
|
465
|
-
}
|
|
466
|
-
size={16}
|
|
467
|
-
/>
|
|
468
|
-
{PROVIDER_META[
|
|
469
|
-
form.embeddingProvider as keyof typeof PROVIDER_META
|
|
470
|
-
]?.label ?? form.embeddingProvider}
|
|
471
|
-
</>
|
|
472
|
-
) : (
|
|
473
|
-
form.embeddingProvider || "Select provider"
|
|
474
|
-
)}
|
|
475
|
-
</span>
|
|
476
|
-
</SelectTrigger>
|
|
477
|
-
<SelectContent>
|
|
478
|
-
{PROVIDER_LIST.filter(
|
|
479
|
-
(key) => (EMBEDDING_BY_PROVIDER[key] && EMBEDDING_BY_PROVIDER[key].length > 0) || key === "vercel_ai_gateway"
|
|
480
|
-
).map((key) => {
|
|
481
|
-
const meta =
|
|
482
|
-
PROVIDER_META[key as keyof typeof PROVIDER_META];
|
|
483
|
-
if (!meta) return null;
|
|
484
|
-
return (
|
|
485
|
-
<SelectItem key={key} value={key}>
|
|
486
|
-
<div className="flex items-center gap-2">
|
|
487
|
-
<ProviderIcon
|
|
488
|
-
src={meta.iconSrc}
|
|
489
|
-
alt={meta.label}
|
|
490
|
-
pillBg={meta.pillBg ?? undefined}
|
|
491
|
-
size={16}
|
|
492
|
-
/>
|
|
493
|
-
<span>
|
|
494
|
-
{meta.label}
|
|
495
|
-
{key === "vercel_ai_gateway" &&
|
|
496
|
-
" (Recommended)"}
|
|
497
|
-
</span>
|
|
498
|
-
</div>
|
|
499
|
-
</SelectItem>
|
|
500
|
-
);
|
|
501
|
-
})}
|
|
502
|
-
{form.customEmbeddings?.map((cfg) => (
|
|
503
|
-
<SelectItem
|
|
504
|
-
key={`custom:${cfg.modelName}`}
|
|
505
|
-
value="custom"
|
|
506
|
-
>
|
|
507
|
-
<div className="flex items-center gap-2">
|
|
508
|
-
<ProviderIcon
|
|
509
|
-
src={PROVIDER_META.custom.iconSrc}
|
|
510
|
-
alt="Custom"
|
|
511
|
-
pillBg={PROVIDER_META.custom.pillBg}
|
|
512
|
-
size={16}
|
|
513
|
-
/>
|
|
514
|
-
<span>{cfg.modelName} (Custom)</span>
|
|
515
|
-
</div>
|
|
516
|
-
</SelectItem>
|
|
517
|
-
))}
|
|
518
|
-
</SelectContent>
|
|
519
|
-
</Select>
|
|
520
|
-
<Button
|
|
521
|
-
variant="outline"
|
|
522
|
-
size="icon"
|
|
523
|
-
type="button"
|
|
524
|
-
onClick={() => setCustomEmbeddingModalOpen(true)}
|
|
525
|
-
className="shrink-0 h-9 w-9"
|
|
526
|
-
>
|
|
527
|
-
<Plus className="size-4" />
|
|
528
|
-
</Button>
|
|
529
|
-
</div>
|
|
530
|
-
</div>
|
|
531
|
-
|
|
532
|
-
<div className="space-y-1.5 mt-4">
|
|
533
|
-
<Label className="text-xs">Model</Label>
|
|
534
|
-
<div className="flex flex-col gap-2">
|
|
535
|
-
<Select
|
|
536
|
-
value={isOtherEmbedding ? "other" : (form.embeddingModelId || "")}
|
|
537
|
-
onValueChange={(v: string | null) => {
|
|
538
|
-
if (!v) return;
|
|
539
|
-
if (v === "other") {
|
|
540
|
-
setIsOtherEmbedding(true);
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
setIsOtherEmbedding(false);
|
|
544
|
-
if (
|
|
545
|
-
indexedRun &&
|
|
546
|
-
data?.config &&
|
|
547
|
-
v !== data.config.embeddingModelId
|
|
548
|
-
) {
|
|
549
|
-
handleStructuralChangeBlock();
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
setForm({ ...form, embeddingModelId: v });
|
|
553
|
-
clearError("embeddingModelId");
|
|
554
|
-
}}
|
|
555
|
-
>
|
|
556
|
-
<SelectTrigger className="w-full">
|
|
557
|
-
<span>{form.embeddingModelId || "Default"}</span>
|
|
558
|
-
</SelectTrigger>
|
|
559
|
-
<SelectContent className="max-h-[320px]">
|
|
560
|
-
{Object.entries(EMBEDDING_BY_PROVIDER)
|
|
561
|
-
.filter(
|
|
562
|
-
([provider]) =>
|
|
563
|
-
form.embeddingProvider === "vercel_ai_gateway" ||
|
|
564
|
-
provider === form.embeddingProvider ||
|
|
565
|
-
provider === "deepseek",
|
|
566
|
-
)
|
|
567
|
-
.map(([provider, models]) => {
|
|
568
|
-
const meta =
|
|
569
|
-
PROVIDER_META[
|
|
570
|
-
provider as keyof typeof PROVIDER_META
|
|
571
|
-
];
|
|
572
|
-
return (
|
|
573
|
-
<SelectGroup key={provider}>
|
|
574
|
-
<SelectLabel className="flex items-center gap-2 py-1.5">
|
|
575
|
-
{meta && (
|
|
576
|
-
<ProviderIcon
|
|
577
|
-
src={meta.iconSrc}
|
|
578
|
-
alt={meta.label}
|
|
579
|
-
pillBg={meta.pillBg}
|
|
580
|
-
size={18}
|
|
581
|
-
/>
|
|
582
|
-
)}
|
|
583
|
-
<span className="font-medium">
|
|
584
|
-
{meta?.label ?? provider}
|
|
585
|
-
</span>
|
|
586
|
-
</SelectLabel>
|
|
587
|
-
{models.map((m) => (
|
|
588
|
-
<SelectItem
|
|
589
|
-
key={m.id}
|
|
590
|
-
value={m.id}
|
|
591
|
-
className="pl-8"
|
|
592
|
-
>
|
|
593
|
-
<span className="flex items-center gap-2">
|
|
594
|
-
<span>{m.label}</span>
|
|
595
|
-
<span className="text-[10px] text-muted-foreground font-mono">
|
|
596
|
-
{m.dimensions}d
|
|
597
|
-
</span>
|
|
598
|
-
</span>
|
|
599
|
-
</SelectItem>
|
|
600
|
-
))}
|
|
601
|
-
</SelectGroup>
|
|
602
|
-
);
|
|
603
|
-
})}
|
|
604
|
-
{/* Custom models */}
|
|
605
|
-
{(form.customEmbeddings ?? []).length > 0 && (
|
|
606
|
-
<SelectGroup>
|
|
607
|
-
<SelectLabel className="flex items-center gap-2 py-1.5">
|
|
608
|
-
<ProviderIcon
|
|
609
|
-
src={PROVIDER_META.custom.iconSrc}
|
|
610
|
-
alt="Custom"
|
|
611
|
-
pillBg={PROVIDER_META.custom.pillBg}
|
|
612
|
-
size={18}
|
|
613
|
-
/>
|
|
614
|
-
<span className="font-medium">Custom</span>
|
|
615
|
-
</SelectLabel>
|
|
616
|
-
{(form.customEmbeddings ?? []).map((m) => (
|
|
617
|
-
<SelectItem
|
|
618
|
-
key={`custom:${m.modelName}`}
|
|
619
|
-
value={`custom:${m.modelName}`}
|
|
620
|
-
className="pl-8"
|
|
621
|
-
>
|
|
622
|
-
<span className="flex items-center gap-2">
|
|
623
|
-
<span>{m.modelName}</span>
|
|
624
|
-
<span className="text-[10px] text-muted-foreground font-mono">
|
|
625
|
-
{m.dimensions}d
|
|
626
|
-
</span>
|
|
627
|
-
</span>
|
|
628
|
-
</SelectItem>
|
|
629
|
-
))}
|
|
630
|
-
</SelectGroup>
|
|
631
|
-
)}
|
|
632
|
-
<SelectGroup>
|
|
633
|
-
<SelectItem value="other" className="pl-8">
|
|
634
|
-
<span className="flex items-center gap-2">
|
|
635
|
-
<span>Other...</span>
|
|
636
|
-
</span>
|
|
637
|
-
</SelectItem>
|
|
638
|
-
</SelectGroup>
|
|
639
|
-
</SelectContent>
|
|
640
|
-
</Select>
|
|
641
|
-
{isOtherEmbedding && (
|
|
642
|
-
<div className="w-full mt-2">
|
|
643
|
-
<Input
|
|
644
|
-
placeholder="Enter custom model ID"
|
|
645
|
-
value={form.embeddingModelId || ""}
|
|
646
|
-
onChange={(e) => setForm({ ...form, embeddingModelId: e.target.value })}
|
|
647
|
-
/>
|
|
648
|
-
</div>
|
|
649
|
-
)}
|
|
650
|
-
</div>
|
|
651
|
-
</div>
|
|
652
|
-
|
|
653
|
-
<div className="space-y-1.5">
|
|
654
|
-
<Label className="text-xs">API Key</Label>
|
|
655
|
-
<div className="relative">
|
|
656
|
-
<Input
|
|
657
|
-
type={showEmbeddingKey ? "text" : "password"}
|
|
658
|
-
value={form.embeddingApiKey || ""}
|
|
659
|
-
onChange={(e) => {
|
|
660
|
-
setForm({
|
|
661
|
-
...form,
|
|
662
|
-
embeddingApiKey: e.target.value,
|
|
663
|
-
chatApiKey:
|
|
664
|
-
!form.chatApiKey ||
|
|
665
|
-
form.chatApiKey === form.embeddingApiKey
|
|
666
|
-
? e.target.value
|
|
667
|
-
: form.chatApiKey,
|
|
668
|
-
});
|
|
669
|
-
clearError("embeddingApiKey");
|
|
670
|
-
clearError("chatApiKey");
|
|
671
|
-
}}
|
|
672
|
-
placeholder="sk-..."
|
|
673
|
-
className={cn(
|
|
674
|
-
"pr-10",
|
|
675
|
-
errors.embeddingApiKey && "border-destructive",
|
|
676
|
-
)}
|
|
677
|
-
/>
|
|
678
|
-
<button
|
|
679
|
-
type="button"
|
|
680
|
-
onClick={() => setShowEmbeddingKey(!showEmbeddingKey)}
|
|
681
|
-
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground"
|
|
682
|
-
>
|
|
683
|
-
{showEmbeddingKey ? (
|
|
684
|
-
<EyeOff className="size-4" />
|
|
685
|
-
) : (
|
|
686
|
-
<Eye className="size-4" />
|
|
687
|
-
)}
|
|
688
|
-
</button>
|
|
689
|
-
</div>
|
|
690
|
-
</div>
|
|
691
|
-
</CardContent>
|
|
692
|
-
</Card>
|
|
693
|
-
|
|
694
|
-
{/* ── Vector Store ──────────────────────────────────────────── */}
|
|
695
|
-
<Card>
|
|
696
|
-
<CardHeader className="pb-3">
|
|
697
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
698
|
-
<Database className="size-4 text-primary" />
|
|
699
|
-
Vector Store
|
|
700
|
-
</CardTitle>
|
|
701
|
-
<CardDescription className="text-xs">
|
|
702
|
-
Choose where your document vectors are stored.
|
|
703
|
-
</CardDescription>
|
|
704
|
-
</CardHeader>
|
|
705
|
-
<CardContent className="space-y-5">
|
|
706
|
-
<div className="space-y-2">
|
|
707
|
-
<Label className="text-xs">Vector Store</Label>
|
|
708
|
-
<Select
|
|
709
|
-
value={form.vectorStore || ""}
|
|
710
|
-
onValueChange={(v) => {
|
|
711
|
-
if (
|
|
712
|
-
indexedRun &&
|
|
713
|
-
data?.config &&
|
|
714
|
-
v !== data.config.vectorStore
|
|
715
|
-
) {
|
|
716
|
-
handleStructuralChangeBlock();
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
selectStore(v as VectorStoreId);
|
|
720
|
-
}}
|
|
721
|
-
>
|
|
722
|
-
<SelectTrigger className="w-full">
|
|
723
|
-
{form.vectorStore ? (
|
|
724
|
-
<span className="flex items-center gap-2.5">
|
|
725
|
-
<ProviderIcon
|
|
726
|
-
src={STORE_META[form.vectorStore]?.iconSrc ?? ""}
|
|
727
|
-
alt={
|
|
728
|
-
getVectorStore(form.vectorStore)?.label ??
|
|
729
|
-
form.vectorStore
|
|
730
|
-
}
|
|
731
|
-
pillBg={
|
|
732
|
-
STORE_META[form.vectorStore]?.pillBg ?? undefined
|
|
733
|
-
}
|
|
734
|
-
size={16}
|
|
735
|
-
/>
|
|
736
|
-
<span className="font-medium text-sm">
|
|
737
|
-
{getVectorStore(form.vectorStore)?.label ??
|
|
738
|
-
form.vectorStore}
|
|
739
|
-
</span>
|
|
740
|
-
</span>
|
|
741
|
-
) : (
|
|
742
|
-
<span className="text-muted-foreground">
|
|
743
|
-
Select a vector store…
|
|
744
|
-
</span>
|
|
745
|
-
)}
|
|
746
|
-
</SelectTrigger>
|
|
747
|
-
<SelectContent>
|
|
748
|
-
{VECTOR_STORE_LIST.map((s) => {
|
|
749
|
-
const meta = STORE_META[s.id];
|
|
750
|
-
const isComingSoon = s.installStatus === "coming-soon";
|
|
751
|
-
return (
|
|
752
|
-
<SelectItem
|
|
753
|
-
key={s.id}
|
|
754
|
-
value={s.id}
|
|
755
|
-
disabled={isComingSoon}
|
|
756
|
-
className={isComingSoon ? "opacity-50" : ""}
|
|
757
|
-
>
|
|
758
|
-
<span className="flex items-center gap-2.5 w-full">
|
|
759
|
-
{meta && (
|
|
760
|
-
<ProviderIcon
|
|
761
|
-
src={meta.iconSrc}
|
|
762
|
-
alt={s.label}
|
|
763
|
-
pillBg={meta.pillBg ?? undefined}
|
|
764
|
-
size={16}
|
|
765
|
-
/>
|
|
766
|
-
)}
|
|
767
|
-
<span className="flex flex-col flex-1 min-w-0">
|
|
768
|
-
<span className="flex items-center gap-2">
|
|
769
|
-
<span className="font-medium text-sm">
|
|
770
|
-
{s.label}
|
|
771
|
-
</span>
|
|
772
|
-
{isComingSoon && (
|
|
773
|
-
<span className="h-3.5 px-1 text-[9px] font-medium shrink-0 text-muted-foreground border rounded-full flex items-center bg-transparent">
|
|
774
|
-
<Clock className="size-2.5 mr-0.5" />
|
|
775
|
-
Coming Soon
|
|
776
|
-
</span>
|
|
777
|
-
)}
|
|
778
|
-
</span>
|
|
779
|
-
</span>
|
|
780
|
-
</span>
|
|
781
|
-
</SelectItem>
|
|
782
|
-
);
|
|
783
|
-
})}
|
|
784
|
-
</SelectContent>
|
|
785
|
-
</Select>
|
|
786
|
-
</div>
|
|
787
|
-
|
|
788
|
-
{form.vectorStore && (
|
|
789
|
-
<div className="border-t pt-4">
|
|
790
|
-
<StoreFields
|
|
791
|
-
store={getVectorStore(form.vectorStore)}
|
|
792
|
-
values={form.storeConfig || {}}
|
|
793
|
-
errors={errors}
|
|
794
|
-
onChange={setStoreValue}
|
|
795
|
-
indexType={form.indexType || "hybrid"}
|
|
796
|
-
/>
|
|
797
|
-
</div>
|
|
798
|
-
)}
|
|
799
|
-
</CardContent>
|
|
800
|
-
</Card>
|
|
801
|
-
|
|
802
|
-
{/* ── Chat (LLM) Provider ───────────────────────────────────── */}
|
|
803
|
-
<Card>
|
|
804
|
-
<CardHeader className="pb-3">
|
|
805
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
806
|
-
<MessageCircle className="size-4 text-primary" />
|
|
807
|
-
Chat (LLM) Provider
|
|
808
|
-
</CardTitle>
|
|
809
|
-
<CardDescription className="text-xs">
|
|
810
|
-
Used to answer questions based on retrieved documents. Leave
|
|
811
|
-
blank to use the same provider and key as embedding.
|
|
812
|
-
</CardDescription>
|
|
813
|
-
</CardHeader>
|
|
814
|
-
<CardContent className="space-y-4">
|
|
815
|
-
<div className="space-y-1.5">
|
|
816
|
-
<Label className="text-xs">Provider</Label>
|
|
817
|
-
<div className="flex gap-2">
|
|
818
|
-
<Select
|
|
819
|
-
value={
|
|
820
|
-
form.chatProvider || form.embeddingProvider || "openai"
|
|
821
|
-
}
|
|
822
|
-
onValueChange={(v: any) => {
|
|
823
|
-
setForm({ ...form, chatProvider: v, chatModelId: "" });
|
|
824
|
-
clearError("chatProvider");
|
|
825
|
-
}}
|
|
826
|
-
>
|
|
827
|
-
<SelectTrigger
|
|
828
|
-
className={cn(
|
|
829
|
-
"w-full",
|
|
830
|
-
errors.chatProvider && "border-destructive",
|
|
831
|
-
)}
|
|
832
|
-
>
|
|
833
|
-
<span className="flex items-center gap-2">
|
|
834
|
-
{(() => {
|
|
835
|
-
const pKey =
|
|
836
|
-
form.chatProvider ||
|
|
837
|
-
form.embeddingProvider ||
|
|
838
|
-
"openai";
|
|
839
|
-
const meta =
|
|
840
|
-
PROVIDER_META[pKey as keyof typeof PROVIDER_META];
|
|
841
|
-
return meta ? (
|
|
842
|
-
<>
|
|
843
|
-
<ProviderIcon
|
|
844
|
-
src={meta.iconSrc}
|
|
845
|
-
alt={meta.label}
|
|
846
|
-
pillBg={meta.pillBg ?? undefined}
|
|
847
|
-
size={16}
|
|
848
|
-
/>
|
|
849
|
-
{meta.label}
|
|
850
|
-
</>
|
|
851
|
-
) : (
|
|
852
|
-
pKey
|
|
853
|
-
);
|
|
854
|
-
})()}
|
|
855
|
-
</span>
|
|
856
|
-
</SelectTrigger>
|
|
857
|
-
<SelectContent>
|
|
858
|
-
{PROVIDER_LIST.map((key) => {
|
|
859
|
-
const meta =
|
|
860
|
-
PROVIDER_META[key as keyof typeof PROVIDER_META];
|
|
861
|
-
if (!meta) return null;
|
|
862
|
-
return (
|
|
863
|
-
<SelectItem key={key} value={key}>
|
|
864
|
-
<div className="flex items-center gap-2">
|
|
865
|
-
<ProviderIcon
|
|
866
|
-
src={meta.iconSrc}
|
|
867
|
-
alt={meta.label}
|
|
868
|
-
pillBg={meta.pillBg}
|
|
869
|
-
size={16}
|
|
870
|
-
/>
|
|
871
|
-
<span>{meta.label}</span>
|
|
872
|
-
</div>
|
|
873
|
-
</SelectItem>
|
|
874
|
-
);
|
|
875
|
-
})}
|
|
876
|
-
{form.customChatModels?.map((cfg) => (
|
|
877
|
-
<SelectItem
|
|
878
|
-
key={`custom:${cfg.modelName}`}
|
|
879
|
-
value="custom"
|
|
880
|
-
>
|
|
881
|
-
<div className="flex items-center gap-2">
|
|
882
|
-
<ProviderIcon
|
|
883
|
-
src={PROVIDER_META.custom.iconSrc}
|
|
884
|
-
alt="Custom"
|
|
885
|
-
pillBg={PROVIDER_META.custom.pillBg}
|
|
886
|
-
size={16}
|
|
887
|
-
/>
|
|
888
|
-
<span>{cfg.modelName} (Custom)</span>
|
|
889
|
-
</div>
|
|
890
|
-
</SelectItem>
|
|
891
|
-
))}
|
|
892
|
-
</SelectContent>
|
|
893
|
-
</Select>
|
|
894
|
-
<Button
|
|
895
|
-
variant="outline"
|
|
896
|
-
size="icon"
|
|
897
|
-
type="button"
|
|
898
|
-
onClick={() => setCustomChatModalOpen(true)}
|
|
899
|
-
className="shrink-0 h-9 w-9"
|
|
900
|
-
>
|
|
901
|
-
<Plus className="size-4" />
|
|
902
|
-
</Button>
|
|
903
|
-
</div>
|
|
904
|
-
</div>
|
|
905
|
-
|
|
906
|
-
<div className="space-y-1.5 mt-4">
|
|
907
|
-
<Label className="text-xs">Model</Label>
|
|
908
|
-
<div className="flex flex-col gap-2">
|
|
909
|
-
<Select
|
|
910
|
-
value={isOtherChat ? "other" : (form.chatModelId || "")}
|
|
911
|
-
onValueChange={(v: string | null) => {
|
|
912
|
-
if (!v) return;
|
|
913
|
-
if (v === "other") {
|
|
914
|
-
setIsOtherChat(true);
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
setIsOtherChat(false);
|
|
918
|
-
setForm({ ...form, chatModelId: v });
|
|
919
|
-
}}
|
|
920
|
-
>
|
|
921
|
-
<SelectTrigger className="w-full">
|
|
922
|
-
<span className="truncate">{form.chatModelId || "Default"}</span>
|
|
923
|
-
</SelectTrigger>
|
|
924
|
-
<SelectContent className="max-h-[320px]">
|
|
925
|
-
{chatStatus?.availableModels ? (
|
|
926
|
-
<SelectGroup>
|
|
927
|
-
{chatStatus.availableModels.map((m: any) => (
|
|
928
|
-
<SelectItem
|
|
929
|
-
key={m.id}
|
|
930
|
-
value={m.id}
|
|
931
|
-
>
|
|
932
|
-
<span className="flex items-center gap-2">
|
|
933
|
-
<span>{m.label}</span>
|
|
934
|
-
</span>
|
|
935
|
-
</SelectItem>
|
|
936
|
-
))}
|
|
937
|
-
</SelectGroup>
|
|
938
|
-
) : (
|
|
939
|
-
<div className="p-2 text-sm text-muted-foreground">Loading models...</div>
|
|
940
|
-
)}
|
|
941
|
-
{/* Custom models */}
|
|
942
|
-
{(form.customChatModels ?? []).length > 0 && (
|
|
943
|
-
<SelectGroup>
|
|
944
|
-
<SelectLabel className="flex items-center gap-2 py-1.5">
|
|
945
|
-
<ProviderIcon
|
|
946
|
-
src={PROVIDER_META.custom.iconSrc}
|
|
947
|
-
alt="Custom"
|
|
948
|
-
pillBg={PROVIDER_META.custom.pillBg}
|
|
949
|
-
size={18}
|
|
950
|
-
/>
|
|
951
|
-
<span className="font-medium">Custom</span>
|
|
952
|
-
</SelectLabel>
|
|
953
|
-
{(form.customChatModels ?? []).map((m) => (
|
|
954
|
-
<SelectItem
|
|
955
|
-
key={`custom:${m.modelName}`}
|
|
956
|
-
value={`custom:${m.modelName}`}
|
|
957
|
-
className="pl-8"
|
|
958
|
-
>
|
|
959
|
-
<span className="flex items-center gap-2">
|
|
960
|
-
<span>{m.modelName}</span>
|
|
961
|
-
</span>
|
|
962
|
-
</SelectItem>
|
|
963
|
-
))}
|
|
964
|
-
</SelectGroup>
|
|
965
|
-
)}
|
|
966
|
-
<SelectGroup>
|
|
967
|
-
<SelectItem value="other" className="pl-8">
|
|
968
|
-
<span className="flex items-center gap-2">
|
|
969
|
-
<span>Other...</span>
|
|
970
|
-
</span>
|
|
971
|
-
</SelectItem>
|
|
972
|
-
</SelectGroup>
|
|
973
|
-
</SelectContent>
|
|
974
|
-
</Select>
|
|
975
|
-
{isOtherChat && (
|
|
976
|
-
<div className="w-full mt-2">
|
|
977
|
-
<Input
|
|
978
|
-
placeholder="Enter custom model ID"
|
|
979
|
-
value={form.chatModelId || ""}
|
|
980
|
-
onChange={(e) => setForm({ ...form, chatModelId: e.target.value })}
|
|
981
|
-
/>
|
|
982
|
-
</div>
|
|
983
|
-
)}
|
|
984
|
-
</div>
|
|
985
|
-
</div>
|
|
986
|
-
<div className="space-y-1.5">
|
|
987
|
-
<Label className="text-xs">API Key</Label>
|
|
988
|
-
<div className="relative">
|
|
989
|
-
<Input
|
|
990
|
-
type={showChatKey ? "text" : "password"}
|
|
991
|
-
value={form.chatApiKey || ""}
|
|
992
|
-
onChange={(e) => {
|
|
993
|
-
setForm({ ...form, chatApiKey: e.target.value });
|
|
994
|
-
clearError("chatApiKey");
|
|
995
|
-
}}
|
|
996
|
-
placeholder="sk-..."
|
|
997
|
-
className={cn(
|
|
998
|
-
"pr-10",
|
|
999
|
-
errors.chatApiKey && "border-destructive",
|
|
1000
|
-
)}
|
|
1001
|
-
/>
|
|
1002
|
-
<button
|
|
1003
|
-
type="button"
|
|
1004
|
-
onClick={() => setShowChatKey(!showChatKey)}
|
|
1005
|
-
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground"
|
|
1006
|
-
>
|
|
1007
|
-
{showChatKey ? (
|
|
1008
|
-
<EyeOff className="size-4" />
|
|
1009
|
-
) : (
|
|
1010
|
-
<Eye className="size-4" />
|
|
1011
|
-
)}
|
|
1012
|
-
</button>
|
|
1013
|
-
</div>
|
|
1014
|
-
</div>
|
|
1015
|
-
</CardContent>
|
|
1016
|
-
</Card>
|
|
1017
|
-
|
|
1018
|
-
{/* ── System Prompt ─────────────────────────────────────────── */}
|
|
1019
|
-
<Card>
|
|
1020
|
-
<CardHeader className="pb-3">
|
|
1021
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
1022
|
-
<Settings2 className="size-4 text-primary" />
|
|
1023
|
-
System Prompt
|
|
1024
|
-
</CardTitle>
|
|
1025
|
-
<CardDescription className="text-xs">
|
|
1026
|
-
Override the default instructions given to the chat agent. Leave
|
|
1027
|
-
blank for the default RAG prompt.
|
|
1028
|
-
</CardDescription>
|
|
1029
|
-
</CardHeader>
|
|
1030
|
-
<CardContent>
|
|
1031
|
-
<textarea
|
|
1032
|
-
value={form.systemPrompt || ""}
|
|
1033
|
-
onChange={(e) =>
|
|
1034
|
-
setForm({ ...form, systemPrompt: e.target.value })
|
|
1035
|
-
}
|
|
1036
|
-
placeholder="You are a helpful research assistant..."
|
|
1037
|
-
rows={5}
|
|
1038
|
-
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]"
|
|
1039
|
-
/>
|
|
1040
|
-
</CardContent>
|
|
1041
|
-
</Card>
|
|
1042
|
-
|
|
1043
|
-
{/* ── Server API Key ────────────────────────────────────────── */}
|
|
1044
|
-
<Card>
|
|
1045
|
-
<CardHeader className="pb-3">
|
|
1046
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
1047
|
-
<Key className="size-4 text-primary" />
|
|
1048
|
-
Server API Key
|
|
1049
|
-
</CardTitle>
|
|
1050
|
-
<CardDescription className="text-xs">
|
|
1051
|
-
Protect your RAG server with an API key. Clients must send this
|
|
1052
|
-
key in the{" "}
|
|
1053
|
-
<code className="text-[11px] bg-muted px-1 py-0.5 rounded">
|
|
1054
|
-
Authorization
|
|
1055
|
-
</code>{" "}
|
|
1056
|
-
header.
|
|
1057
|
-
</CardDescription>
|
|
1058
|
-
</CardHeader>
|
|
1059
|
-
<CardContent className="space-y-3">
|
|
1060
|
-
<div className="flex items-center gap-2">
|
|
1061
|
-
<Input
|
|
1062
|
-
type="text"
|
|
1063
|
-
value={serverApiKey}
|
|
1064
|
-
onChange={(e) => handleServerApiKeyChange(e.target.value)}
|
|
1065
|
-
placeholder="sk-..."
|
|
1066
|
-
className="flex-1 font-mono text-xs"
|
|
1067
|
-
/>
|
|
1068
|
-
<Button
|
|
1069
|
-
variant="outline"
|
|
1070
|
-
size="icon"
|
|
1071
|
-
className="shrink-0 h-9 w-9"
|
|
1072
|
-
onClick={copyApiKey}
|
|
1073
|
-
disabled={!serverApiKey}
|
|
1074
|
-
>
|
|
1075
|
-
{copiedKey ? (
|
|
1076
|
-
<CheckCircle2 className="size-4 text-primary" />
|
|
1077
|
-
) : (
|
|
1078
|
-
<Copy className="size-4" />
|
|
1079
|
-
)}
|
|
1080
|
-
</Button>
|
|
1081
|
-
<Button
|
|
1082
|
-
variant="outline"
|
|
1083
|
-
size="sm"
|
|
1084
|
-
className="shrink-0 gap-1.5"
|
|
1085
|
-
onClick={generateApiKey}
|
|
1086
|
-
>
|
|
1087
|
-
<RefreshCw className="size-3.5" />
|
|
1088
|
-
Generate
|
|
1089
|
-
</Button>
|
|
1090
|
-
</div>
|
|
1091
|
-
<p className="text-xs text-muted-foreground">
|
|
1092
|
-
Stored locally in your browser. Generate a new key or paste your
|
|
1093
|
-
own.
|
|
1094
|
-
</p>
|
|
1095
|
-
</CardContent>
|
|
1096
|
-
</Card>
|
|
1097
|
-
|
|
1098
|
-
{/* ── RAG Server Launch ─────────────────────────────────────── */}
|
|
1099
|
-
<LaunchPanel serverId={serverId || "default"} />
|
|
1100
|
-
</div>
|
|
1101
|
-
</div>
|
|
1102
|
-
</div>
|
|
1103
|
-
);
|
|
1104
|
-
}
|