larkup 0.1.27 → 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.
Files changed (218) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +5 -5
  3. package/.next/diagnostics/route-bundle-stats.json +26 -26
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/next-minimal-server.js.nft.json +1 -1
  6. package/.next/next-server.js.nft.json +1 -1
  7. package/.next/prerender-manifest.json +3 -3
  8. package/.next/required-server-files.js +4 -4
  9. package/.next/required-server-files.json +4 -4
  10. package/.next/server/app/_global-error/page/build-manifest.json +2 -2
  11. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  12. package/.next/server/app/_global-error.html +1 -1
  13. package/.next/server/app/_global-error.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  18. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/.next/server/app/_not-found/page/build-manifest.json +2 -2
  20. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/_not-found.html +1 -1
  23. package/.next/server/app/_not-found.rsc +16 -16
  24. package/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
  25. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  26. package/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  27. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  28. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  29. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  30. package/.next/server/app/analytics/page/build-manifest.json +2 -2
  31. package/.next/server/app/analytics/page.js.nft.json +1 -1
  32. package/.next/server/app/analytics/page_client-reference-manifest.js +1 -1
  33. package/.next/server/app/analytics.html +1 -1
  34. package/.next/server/app/analytics.rsc +17 -17
  35. package/.next/server/app/analytics.segments/_full.segment.rsc +17 -17
  36. package/.next/server/app/analytics.segments/_head.segment.rsc +4 -4
  37. package/.next/server/app/analytics.segments/_index.segment.rsc +11 -11
  38. package/.next/server/app/analytics.segments/_tree.segment.rsc +1 -1
  39. package/.next/server/app/analytics.segments/analytics/__PAGE__.segment.rsc +3 -3
  40. package/.next/server/app/analytics.segments/analytics.segment.rsc +3 -3
  41. package/.next/server/app/api/analytics/route.js.nft.json +1 -1
  42. package/.next/server/app/api/chat/route.js +9 -10
  43. package/.next/server/app/api/chat/route.js.nft.json +1 -1
  44. package/.next/server/app/api/chat/status/route.js.nft.json +1 -1
  45. package/.next/server/app/api/chat/suggestions/route.js.nft.json +1 -1
  46. package/.next/server/app/api/config/route.js.nft.json +1 -1
  47. package/.next/server/app/api/config/test/route.js.nft.json +1 -1
  48. package/.next/server/app/api/config/test-embedding/route.js.nft.json +1 -1
  49. package/.next/server/app/api/config/test-llm/route.js.nft.json +1 -1
  50. package/.next/server/app/api/config/test-provider/route.js.nft.json +1 -1
  51. package/.next/server/app/api/config/verify/route.js.nft.json +1 -1
  52. package/.next/server/app/api/connections/route.js.nft.json +1 -1
  53. package/.next/server/app/api/connections/webhook/route.js.nft.json +1 -1
  54. package/.next/server/app/api/demo/route.js.nft.json +1 -1
  55. package/.next/server/app/api/deploy/cloud-project/route.js.nft.json +1 -1
  56. package/.next/server/app/api/deploy/ssh/route.js.nft.json +1 -1
  57. package/.next/server/app/api/deployment/blob-setup/route.js.nft.json +1 -1
  58. package/.next/server/app/api/deployment/config/route.js.nft.json +1 -1
  59. package/.next/server/app/api/describe-image/route.js.nft.json +1 -1
  60. package/.next/server/app/api/doc-editor/edit/route.js.nft.json +1 -1
  61. package/.next/server/app/api/doc-editor/export/route.js.nft.json +1 -1
  62. package/.next/server/app/api/doc-editor/extract-text/route.js.nft.json +1 -1
  63. package/.next/server/app/api/doc-editor/parse/route.js.nft.json +1 -1
  64. package/.next/server/app/api/doc-editor/restore/route.js.nft.json +1 -1
  65. package/.next/server/app/api/document/sign/route.js.nft.json +1 -1
  66. package/.next/server/app/api/documents/route.js.nft.json +1 -1
  67. package/.next/server/app/api/firecrawl/local/route.js.nft.json +1 -1
  68. package/.next/server/app/api/index/route.js.nft.json +1 -1
  69. package/.next/server/app/api/integrations/notion/callback/route.js.nft.json +1 -1
  70. package/.next/server/app/api/integrations/notion/route.js.nft.json +1 -1
  71. package/.next/server/app/api/jobs/[id]/route.js.nft.json +1 -1
  72. package/.next/server/app/api/jobs/route.js.nft.json +1 -1
  73. package/.next/server/app/api/marketplace/[toolId]/route.js +1 -1
  74. package/.next/server/app/api/marketplace/[toolId]/route.js.nft.json +1 -1
  75. package/.next/server/app/api/marketplace/route.js +1 -1
  76. package/.next/server/app/api/marketplace/route.js.nft.json +1 -1
  77. package/.next/server/app/api/media/[id]/route.js.nft.json +1 -1
  78. package/.next/server/app/api/media/process/route.js +4 -4
  79. package/.next/server/app/api/media/process/route.js.nft.json +1 -1
  80. package/.next/server/app/api/media/route.js +1 -1
  81. package/.next/server/app/api/media/route.js.nft.json +1 -1
  82. package/.next/server/app/api/media/stream/route.js.nft.json +1 -1
  83. package/.next/server/app/api/models/route.js.nft.json +1 -1
  84. package/.next/server/app/api/parse-file/route.js.nft.json +1 -1
  85. package/.next/server/app/api/proxy/verify/route.js.nft.json +1 -1
  86. package/.next/server/app/api/sandbox/route.js.nft.json +1 -1
  87. package/.next/server/app/api/search/bing/route.js.nft.json +1 -1
  88. package/.next/server/app/api/search/brave/route.js.nft.json +1 -1
  89. package/.next/server/app/api/search/exa/route.js.nft.json +1 -1
  90. package/.next/server/app/api/search/google/route.js.nft.json +1 -1
  91. package/.next/server/app/api/search/route.js.nft.json +1 -1
  92. package/.next/server/app/api/search/tavily/route.js.nft.json +1 -1
  93. package/.next/server/app/api/search/verify/route.js.nft.json +1 -1
  94. package/.next/server/app/api/server/agent/route.js.nft.json +1 -1
  95. package/.next/server/app/api/server/download/route.js.nft.json +1 -1
  96. package/.next/server/app/api/server/generate/route.js.nft.json +1 -1
  97. package/.next/server/app/api/server/local/route.js.nft.json +1 -1
  98. package/.next/server/app/api/servers/route.js.nft.json +1 -1
  99. package/.next/server/app/api/tabular/query/route.js.nft.json +1 -1
  100. package/.next/server/app/api/tabular/route.js.nft.json +1 -1
  101. package/.next/server/app/api/upload-file/route.js.nft.json +1 -1
  102. package/.next/server/app/api/uploads/[...slug]/route.js.nft.json +1 -1
  103. package/.next/server/app/api/vector-stores/install/route.js.nft.json +1 -1
  104. package/.next/server/app/api/vector-stores/status/route.js.nft.json +1 -1
  105. package/.next/server/app/chat/page/build-manifest.json +2 -2
  106. package/.next/server/app/chat/page.js.nft.json +1 -1
  107. package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  108. package/.next/server/app/chat.html +1 -1
  109. package/.next/server/app/chat.rsc +17 -17
  110. package/.next/server/app/chat.segments/_full.segment.rsc +17 -17
  111. package/.next/server/app/chat.segments/_head.segment.rsc +4 -4
  112. package/.next/server/app/chat.segments/_index.segment.rsc +11 -11
  113. package/.next/server/app/chat.segments/_tree.segment.rsc +1 -1
  114. package/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +3 -3
  115. package/.next/server/app/chat.segments/chat.segment.rsc +3 -3
  116. package/.next/server/app/data/page/build-manifest.json +2 -2
  117. package/.next/server/app/data/page.js.nft.json +1 -1
  118. package/.next/server/app/data/page_client-reference-manifest.js +1 -1
  119. package/.next/server/app/data.html +1 -1
  120. package/.next/server/app/data.rsc +17 -17
  121. package/.next/server/app/data.segments/_full.segment.rsc +17 -17
  122. package/.next/server/app/data.segments/_head.segment.rsc +4 -4
  123. package/.next/server/app/data.segments/_index.segment.rsc +11 -11
  124. package/.next/server/app/data.segments/_tree.segment.rsc +1 -1
  125. package/.next/server/app/data.segments/data/__PAGE__.segment.rsc +3 -3
  126. package/.next/server/app/data.segments/data.segment.rsc +3 -3
  127. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  128. package/.next/server/app/index.html +1 -1
  129. package/.next/server/app/index.rsc +16 -16
  130. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  131. package/.next/server/app/index.segments/_full.segment.rsc +16 -16
  132. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  133. package/.next/server/app/index.segments/_index.segment.rsc +11 -11
  134. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  135. package/.next/server/app/page/build-manifest.json +2 -2
  136. package/.next/server/app/page.js.nft.json +1 -1
  137. package/.next/server/app/page_client-reference-manifest.js +1 -1
  138. package/.next/server/app/settings/page/build-manifest.json +2 -2
  139. package/.next/server/app/settings/page/server-reference-manifest.json +3 -3
  140. package/.next/server/app/settings/page.js.nft.json +1 -1
  141. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  142. package/.next/server/app/settings.html +1 -1
  143. package/.next/server/app/settings.rsc +18 -18
  144. package/.next/server/app/settings.segments/_full.segment.rsc +18 -18
  145. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  146. package/.next/server/app/settings.segments/_index.segment.rsc +11 -11
  147. package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  148. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  149. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  150. package/.next/server/chunks/[root-of-the-server]__0c80bj0._.js +3 -0
  151. package/.next/server/chunks/{[root-of-the-server]__1m7m2x7._.js → [root-of-the-server]__0drkoru._.js} +2 -2
  152. package/.next/server/chunks/{[root-of-the-server]__0vnm6_8._.js → [root-of-the-server]__0kmh740._.js} +6 -6
  153. package/.next/server/chunks/{[root-of-the-server]__0ou70-v._.js → [root-of-the-server]__0nfenje._.js} +1 -1
  154. package/.next/server/chunks/[root-of-the-server]__0omd-x0._.js +3 -0
  155. package/.next/server/chunks/{[root-of-the-server]__1s1zui-._.js → [root-of-the-server]__10v5qsi._.js} +1 -1
  156. package/.next/server/chunks/[root-of-the-server]__1idgvjh._.js +172 -0
  157. package/.next/server/chunks/[root-of-the-server]__1ip05r1._.js +1 -1
  158. package/.next/server/chunks/[root-of-the-server]__1jc54o2._.js +3 -0
  159. package/.next/server/chunks/{[root-of-the-server]__0748x7h._.js → [root-of-the-server]__1lhls88._.js} +1 -1
  160. package/.next/server/chunks/{[root-of-the-server]__0g_q-44._.js → [root-of-the-server]__1wy6wes._.js} +2 -2
  161. package/.next/server/chunks/{packages_1f4dx4h._.js → packages_1ggqion._.js} +2 -2
  162. package/.next/server/chunks/packages_marketplace_src_125_g2p._.js +1 -1
  163. package/.next/server/chunks/{packages_vector-stores_src_064-9ay._.js → packages_vector-stores_src_1fib7u-._.js} +2 -2
  164. package/.next/server/chunks/ssr/_169pdlf._.js +2 -2
  165. package/.next/server/chunks/ssr/_1hetwwu._.js +3 -3
  166. package/.next/server/chunks/ssr/_1iff9ia._.js +1 -1
  167. package/.next/server/chunks/ssr/_1yqlx1m._.js +1 -1
  168. package/.next/server/chunks/ssr/apps_web_components_analytics_analytics-dashboard_tsx_1056-z3._.js +1 -1
  169. package/.next/server/chunks/ssr/apps_web_lib_pdf-images_ts_13-l5wa._.js +1 -1
  170. package/.next/server/middleware-build-manifest.js +5 -5
  171. package/.next/server/pages/404.html +1 -1
  172. package/.next/server/pages/500.html +1 -1
  173. package/.next/server/server-reference-manifest.js +1 -1
  174. package/.next/server/server-reference-manifest.json +4 -4
  175. package/.next/static/chunks/{3m_e34hfypn-x.js → 01qmngac69n9j.js} +1 -1
  176. package/.next/static/chunks/{3bpr59xr-_rv7.js → 0ge3nirnmx_np.js} +1 -1
  177. package/.next/static/chunks/{0-5iuj-s4wl9k.js → 102b8vinedggz.js} +6 -6
  178. package/.next/static/chunks/{2jqjx4-rh414_.js → 13kyiy7q3of41.js} +1 -1
  179. package/.next/static/chunks/{1fz_0-qb5r5wd.js → 1dykihoo_1ae5.js} +1 -1
  180. package/.next/static/chunks/{0rgt7-bqxonef.js → 1sh7d5tt49x8b.js} +2 -2
  181. package/.next/static/chunks/{1-afr-snks55m.js → 1z4nte07k3dt6.js} +1 -1
  182. package/.next/static/chunks/2ib3e0xrrdait.js +1 -0
  183. package/.next/static/chunks/{1w2jeq6n8-qz9.js → 3g_wl1fri6jb-.js} +1 -1
  184. package/.next/static/chunks/{turbopack-2gu-fmc6o5wkn.js → turbopack-2qt7vn_ocj4ao.js} +1 -1
  185. package/CHANGELOG.md +20 -0
  186. package/app/api/describe-image/route.ts +12 -4
  187. package/components/analytics/analytics-dashboard.tsx +7 -7
  188. package/components/data/media-panel.tsx +34 -2
  189. package/components/data/upload-panel.tsx +2 -1
  190. package/components/settings/marketplace-section.tsx +14 -2
  191. package/components/settings/server-section.tsx +9 -9
  192. package/lib/pdf-images.ts +20 -5
  193. package/package.json +5 -5
  194. package/.next/server/chunks/[root-of-the-server]__00r1kgp._.js +0 -143
  195. package/.next/server/chunks/[root-of-the-server]__064gb96._.js +0 -3
  196. package/.next/server/chunks/_0ce9vcg._.js +0 -32
  197. package/.next/static/chunks/00z1f-adeq1lg.js +0 -1
  198. package/archive/redesign-v2/app/configure/page.tsx +0 -53
  199. package/archive/redesign-v2/app/demo/page.tsx +0 -15
  200. package/archive/redesign-v2/app/server/page.tsx +0 -22
  201. package/archive/redesign-v2/app/simple/chat/page.tsx +0 -17
  202. package/archive/redesign-v2/app/simple/docs/page.tsx +0 -21
  203. package/archive/redesign-v2/app/simple/settings/page.tsx +0 -1104
  204. package/archive/redesign-v2/app-routes/configure/page.tsx +0 -53
  205. package/archive/redesign-v2/app-routes/demo/page.tsx +0 -15
  206. package/archive/redesign-v2/app-routes/server/page.tsx +0 -22
  207. package/archive/redesign-v2/app-routes/simple/chat/page.tsx +0 -17
  208. package/archive/redesign-v2/app-routes/simple/docs/page.tsx +0 -21
  209. package/archive/redesign-v2/app-routes/simple/layout.tsx +0 -9
  210. package/archive/redesign-v2/app-routes/simple/settings/page.tsx +0 -1104
  211. package/archive/redesign-v2/components/app-sidebar.tsx +0 -188
  212. package/archive/redesign-v2/components/app-topnav.tsx +0 -195
  213. package/archive/redesign-v2/components/global-settings.tsx +0 -288
  214. package/archive/redesign-v2/components/simple/simple-sidebar.tsx +0 -171
  215. package/archive/redesign-v2/components/simple/simple-topnav.tsx +0 -175
  216. /package/.next/static/{Ms6fqY45868GqHAvux2Qj → W6QdudzTPo_OVPpNt29f7}/_buildManifest.js +0 -0
  217. /package/.next/static/{Ms6fqY45868GqHAvux2Qj → W6QdudzTPo_OVPpNt29f7}/_clientMiddlewareManifest.js +0 -0
  218. /package/.next/static/{Ms6fqY45868GqHAvux2Qj → 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
- }