larkup 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +2 -0
- package/app/actions/vercel.ts +417 -0
- package/app/api/chat/route.ts +192 -0
- package/app/api/chat/status/route.ts +55 -0
- package/app/api/chat/suggestions/route.ts +115 -0
- package/app/api/config/route.ts +73 -0
- package/app/api/config/test/route.ts +67 -0
- package/app/api/config/test-embedding/route.ts +40 -0
- package/app/api/config/test-llm/route.ts +47 -0
- package/app/api/config/test-provider/route.ts +36 -0
- package/app/api/demo/route.ts +176 -0
- package/app/api/deploy/ssh/route.ts +340 -0
- package/app/api/documents/route.ts +90 -0
- package/app/api/firecrawl/local/route.ts +46 -0
- package/app/api/index/route.ts +113 -0
- package/app/api/jobs/[id]/route.ts +44 -0
- package/app/api/jobs/route.ts +116 -0
- package/app/api/parse-file/route.ts +39 -0
- package/app/api/search/google/route.ts +162 -0
- package/app/api/search/route.ts +29 -0
- package/app/api/server/download/route.ts +112 -0
- package/app/api/server/generate/route.ts +46 -0
- package/app/api/server/local/route.ts +42 -0
- package/app/api/servers/route.ts +137 -0
- package/app/api/vector-stores/install/route.ts +65 -0
- package/app/api/vector-stores/status/route.ts +26 -0
- package/app/chat/page.tsx +15 -0
- package/app/configure/page.tsx +53 -0
- package/app/data/page.tsx +15 -0
- package/app/demo/page.tsx +15 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +190 -0
- package/app/index-data/page.tsx +15 -0
- package/app/layout.tsx +51 -0
- package/app/page.tsx +34 -0
- package/app/server/page.tsx +22 -0
- package/app/simple/chat/page.tsx +17 -0
- package/app/simple/docs/page.tsx +21 -0
- package/app/simple/layout.tsx +9 -0
- package/app/simple/settings/page.tsx +627 -0
- package/app/template.tsx +16 -0
- package/app/themes.css +733 -0
- package/components/alerts/generic-alert.tsx +74 -0
- package/components/app-sidebar.tsx +187 -0
- package/components/app-topnav.tsx +195 -0
- package/components/chat/chat-settings-modal.tsx +209 -0
- package/components/chat/chat-workspace.tsx +348 -0
- package/components/chat/knowledge-base-result.tsx +97 -0
- package/components/chat/message-item.tsx +103 -0
- package/components/client-layout-wrapper.tsx +182 -0
- package/components/configure/configure-form.tsx +1683 -0
- package/components/configure/store-fields.tsx +152 -0
- package/components/data/corpus-panel.tsx +499 -0
- package/components/data/data-workspace.tsx +272 -0
- package/components/data/firecrawl-notice.tsx +236 -0
- package/components/data/jobs-panel.tsx +485 -0
- package/components/data/paste-panel.tsx +79 -0
- package/components/data/scrape-panel.tsx +964 -0
- package/components/data/upload-panel.tsx +714 -0
- package/components/demo/demo-workspace.tsx +419 -0
- package/components/global-settings.tsx +286 -0
- package/components/index/index-workspace.tsx +482 -0
- package/components/onboarding/simple-setup.tsx +702 -0
- package/components/onboarding/tech-setup.tsx +121 -0
- package/components/onboarding/welcome-screen.tsx +183 -0
- package/components/page-header.tsx +44 -0
- package/components/server/code-viewer.tsx +87 -0
- package/components/server/deploy-button.tsx +1390 -0
- package/components/server/server-workspace.tsx +392 -0
- package/components/simple/sdk-connect-dialog.tsx +201 -0
- package/components/simple/simple-index-button.tsx +83 -0
- package/components/simple/simple-sidebar.tsx +161 -0
- package/components/theme-customizer-provider.tsx +171 -0
- package/components/theme-switcher.tsx +445 -0
- package/components/ui/alert-dialog.tsx +187 -0
- package/components/ui/alert.tsx +76 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +59 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/checkbox.tsx +28 -0
- package/components/ui/collapsible.tsx +21 -0
- package/components/ui/dialog.tsx +159 -0
- package/components/ui/dropdown-menu.tsx +281 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +20 -0
- package/components/ui/popover.tsx +90 -0
- package/components/ui/progress.tsx +83 -0
- package/components/ui/provider-icon.tsx +94 -0
- package/components/ui/scroll-area.tsx +55 -0
- package/components/ui/select.tsx +248 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/sonner.tsx +63 -0
- package/components/ui/switch.tsx +32 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +84 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/workspace/delete-server-dialog.tsx +71 -0
- package/components/workspace/server-form-dialog.tsx +103 -0
- package/components/workspace/server-switcher.tsx +163 -0
- package/components/workspace/workspace-provider.tsx +177 -0
- package/components/workspace/workspace-top-bar.tsx +23 -0
- package/components.json +21 -0
- package/lib/error-formatter.ts +15 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +18 -0
- package/package.json +65 -0
- package/postcss.config.mjs +8 -0
- package/public/docker.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/github.svg +3 -0
- package/public/hetzner.svg +1 -0
- package/public/icons/aws.svg +38 -0
- package/public/icons/azure.svg +1 -0
- package/public/icons/chroma.png +0 -0
- package/public/icons/cohere.svg +30 -0
- package/public/icons/digital-ocean.webp +0 -0
- package/public/icons/firecrawl2.png +0 -0
- package/public/icons/gcp.svg +1 -0
- package/public/icons/gemini.svg +1 -0
- package/public/icons/jina.svg +1 -0
- package/public/icons/lancedb2.png +0 -0
- package/public/icons/mistral.svg +469 -0
- package/public/icons/nomic.png +0 -0
- package/public/icons/openai.svg +1 -0
- package/public/icons/pgvector2.png +0 -0
- package/public/icons/pinecone.png +0 -0
- package/public/icons/qdrant.svg +35 -0
- package/public/icons/supabase.png +0 -0
- package/public/icons/vercel copy.svg +1 -0
- package/public/icons/vercel.svg +1 -0
- package/public/icons/voyage-light.png +0 -0
- package/public/icons/weaviate.webp +0 -0
- package/public/logo-light.png +0 -0
- package/public/logo.png +0 -0
- package/public/logo9.png +0 -0
- package/public/vercel.svg +1 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import {
|
|
6
|
+
ArrowLeft,
|
|
7
|
+
ArrowRight,
|
|
8
|
+
Cloud,
|
|
9
|
+
Eye,
|
|
10
|
+
EyeOff,
|
|
11
|
+
Loader2,
|
|
12
|
+
Monitor,
|
|
13
|
+
CheckCircle2,
|
|
14
|
+
ShieldCheck,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import { Input } from "@/components/ui/input";
|
|
18
|
+
import { Label } from "@/components/ui/label";
|
|
19
|
+
import { useWorkspace } from "@/components/workspace/workspace-provider";
|
|
20
|
+
import { PROVIDER_META, ProviderIcon } from "@/components/ui/provider-icon";
|
|
21
|
+
import type { EmbeddingProvider } from "@larkup/core/types";
|
|
22
|
+
import { cn } from "@/lib/utils";
|
|
23
|
+
|
|
24
|
+
interface SimpleSetupProps {
|
|
25
|
+
onBack: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type ConnectionMode = "cloud" | "custom";
|
|
29
|
+
|
|
30
|
+
/** Ordered provider list — only Vercel AI Gateway is recommended. */
|
|
31
|
+
const CLOUD_PROVIDERS: { key: EmbeddingProvider; recommended?: boolean }[] = [
|
|
32
|
+
{ key: "vercel_ai_gateway", recommended: true },
|
|
33
|
+
{ key: "openai" },
|
|
34
|
+
{ key: "google" },
|
|
35
|
+
{ key: "mistral" },
|
|
36
|
+
{ key: "voyage" },
|
|
37
|
+
{ key: "jina" },
|
|
38
|
+
{ key: "nomic" },
|
|
39
|
+
{ key: "deepseek" },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default embedding model IDs for each provider.
|
|
44
|
+
* OpenAI and Vercel AI Gateway get the large model; others get the first in the registry.
|
|
45
|
+
*/
|
|
46
|
+
const DEFAULT_EMBEDDING_MODEL: Partial<Record<EmbeddingProvider, string>> = {
|
|
47
|
+
openai: "openai/text-embedding-3-large",
|
|
48
|
+
vercel_ai_gateway: "openai/text-embedding-3-large",
|
|
49
|
+
google: "google/text-embedding-004",
|
|
50
|
+
mistral: "mistral/mistral-embed",
|
|
51
|
+
voyage: "voyage/voyage-3",
|
|
52
|
+
jina: "jina/jina-embeddings-v3",
|
|
53
|
+
nomic: "nomic/nomic-embed-text-v1.5",
|
|
54
|
+
deepseek: "openai/text-embedding-3-large",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function SimpleSetup({ onBack }: SimpleSetupProps) {
|
|
58
|
+
const { createServer, setMode } = useWorkspace();
|
|
59
|
+
|
|
60
|
+
// Connection mode
|
|
61
|
+
const [connectionMode, setConnectionMode] = useState<ConnectionMode>("cloud");
|
|
62
|
+
|
|
63
|
+
// Cloud mode state
|
|
64
|
+
const [provider, setProvider] = useState<EmbeddingProvider>("openai");
|
|
65
|
+
const [apiKey, setApiKey] = useState("");
|
|
66
|
+
const [showApiKey, setShowApiKey] = useState(false);
|
|
67
|
+
const [cloudVerified, setCloudVerified] = useState(false);
|
|
68
|
+
const [testingCloud, setTestingCloud] = useState(false);
|
|
69
|
+
|
|
70
|
+
// Custom model state
|
|
71
|
+
const [embeddingBaseUrl, setEmbeddingBaseUrl] = useState(
|
|
72
|
+
"http://localhost:11434/v1",
|
|
73
|
+
);
|
|
74
|
+
const [embeddingModelName, setEmbeddingModelName] = useState("");
|
|
75
|
+
const [embeddingApiKey, setEmbeddingApiKey] = useState("");
|
|
76
|
+
const [showEmbeddingApiKey, setShowEmbeddingApiKey] = useState(false);
|
|
77
|
+
const [llmBaseUrl, setLlmBaseUrl] = useState("http://localhost:11434/v1");
|
|
78
|
+
const [llmModelName, setLlmModelName] = useState("");
|
|
79
|
+
const [llmApiKey, setLlmApiKey] = useState("");
|
|
80
|
+
const [showLlmApiKey, setShowLlmApiKey] = useState(false);
|
|
81
|
+
|
|
82
|
+
const [busy, setBusy] = useState(false);
|
|
83
|
+
const [testingEmbedding, setTestingEmbedding] = useState(false);
|
|
84
|
+
const [embeddingVerified, setEmbeddingVerified] = useState(false);
|
|
85
|
+
const [detectedDims, setDetectedDims] = useState<number | null>(null);
|
|
86
|
+
|
|
87
|
+
const [testingLlm, setTestingLlm] = useState(false);
|
|
88
|
+
const [llmVerified, setLlmVerified] = useState(false);
|
|
89
|
+
|
|
90
|
+
// Reset cloud verification when provider or key changes
|
|
91
|
+
function onProviderChange(p: EmbeddingProvider) {
|
|
92
|
+
setProvider(p);
|
|
93
|
+
setCloudVerified(false);
|
|
94
|
+
}
|
|
95
|
+
function onApiKeyChange(v: string) {
|
|
96
|
+
setApiKey(v);
|
|
97
|
+
setCloudVerified(false);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function testCloudProvider() {
|
|
101
|
+
if (!apiKey.trim()) {
|
|
102
|
+
toast.error("Please enter your API key first.");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
setTestingCloud(true);
|
|
106
|
+
try {
|
|
107
|
+
const embeddingModelId =
|
|
108
|
+
DEFAULT_EMBEDDING_MODEL[provider] ?? "openai/text-embedding-3-large";
|
|
109
|
+
const res = await fetch("/api/config/test-provider", {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: { "Content-Type": "application/json" },
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
embeddingProvider: provider,
|
|
114
|
+
embeddingApiKey: apiKey,
|
|
115
|
+
embeddingModelId,
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
const data = await res.json();
|
|
119
|
+
if (!res.ok) throw new Error(data.error || "Connection failed");
|
|
120
|
+
setCloudVerified(true);
|
|
121
|
+
toast.success("API key verified successfully!");
|
|
122
|
+
} catch (err) {
|
|
123
|
+
toast.error(
|
|
124
|
+
err instanceof Error
|
|
125
|
+
? err.message
|
|
126
|
+
: "Invalid API key. Please check and try again.",
|
|
127
|
+
);
|
|
128
|
+
setCloudVerified(false);
|
|
129
|
+
} finally {
|
|
130
|
+
setTestingCloud(false);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function testCustomEmbedding() {
|
|
135
|
+
if (!embeddingBaseUrl || !embeddingModelName) {
|
|
136
|
+
toast.error("Base URL and model name are required.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
setTestingEmbedding(true);
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch("/api/config/test-embedding", {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
baseUrl: embeddingBaseUrl,
|
|
146
|
+
apiKey: embeddingApiKey || undefined,
|
|
147
|
+
modelName: embeddingModelName,
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
const data = await res.json();
|
|
151
|
+
if (!res.ok) throw new Error(data.error || "Connection failed");
|
|
152
|
+
setDetectedDims(data.dimensions);
|
|
153
|
+
setEmbeddingVerified(true);
|
|
154
|
+
toast.success(`Connected! Detected ${data.dimensions} dimensions.`);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
toast.error(err instanceof Error ? err.message : "Test failed");
|
|
157
|
+
setEmbeddingVerified(false);
|
|
158
|
+
setDetectedDims(null);
|
|
159
|
+
} finally {
|
|
160
|
+
setTestingEmbedding(false);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function testCustomLlm() {
|
|
165
|
+
if (!llmBaseUrl || !llmModelName) {
|
|
166
|
+
toast.error("Base URL and model name are required.");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setTestingLlm(true);
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetch("/api/config/test-llm", {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "Content-Type": "application/json" },
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
baseUrl: llmBaseUrl,
|
|
176
|
+
apiKey: llmApiKey || undefined,
|
|
177
|
+
modelName: llmModelName,
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
const data = await res.json();
|
|
181
|
+
if (!res.ok) throw new Error(data.error || "Connection failed");
|
|
182
|
+
setLlmVerified(true);
|
|
183
|
+
toast.success("LLM connected successfully!");
|
|
184
|
+
} catch (err) {
|
|
185
|
+
toast.error(err instanceof Error ? err.message : "Test failed");
|
|
186
|
+
setLlmVerified(false);
|
|
187
|
+
} finally {
|
|
188
|
+
setTestingLlm(false);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function handleFinish() {
|
|
193
|
+
// Validate cloud mode
|
|
194
|
+
if (connectionMode === "cloud") {
|
|
195
|
+
if (!apiKey.trim()) {
|
|
196
|
+
toast.error("Please enter your API key.");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!cloudVerified) {
|
|
200
|
+
toast.error("Please verify your API key first.");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Validate custom mode
|
|
205
|
+
if (connectionMode === "custom") {
|
|
206
|
+
if (!embeddingVerified) {
|
|
207
|
+
toast.error("Please test your embedding connection first.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!llmVerified) {
|
|
211
|
+
toast.error("Please test your LLM connection first.");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
setBusy(true);
|
|
217
|
+
try {
|
|
218
|
+
// Create a default server
|
|
219
|
+
const server = await createServer("my-rag");
|
|
220
|
+
|
|
221
|
+
// Build config
|
|
222
|
+
const isCloud = connectionMode === "cloud";
|
|
223
|
+
const embeddingModelId = isCloud
|
|
224
|
+
? (DEFAULT_EMBEDDING_MODEL[provider] ?? "openai/text-embedding-3-large")
|
|
225
|
+
: `custom:${embeddingModelName}`;
|
|
226
|
+
|
|
227
|
+
const configBody: Record<string, unknown> = {
|
|
228
|
+
projectName: "my-rag",
|
|
229
|
+
embeddingProvider: isCloud ? provider : "custom",
|
|
230
|
+
embeddingApiKey: isCloud ? apiKey : "",
|
|
231
|
+
embeddingModelId,
|
|
232
|
+
indexType: "hybrid",
|
|
233
|
+
chunking: { chunkSize: 512, chunkOverlap: 64, strategy: "recursive" },
|
|
234
|
+
vectorStore: "lancedb",
|
|
235
|
+
storeConfig: {
|
|
236
|
+
mode: "local",
|
|
237
|
+
dbPath: `./.larkup/servers/${server?.id}/lancedb`,
|
|
238
|
+
tableName: "documents",
|
|
239
|
+
},
|
|
240
|
+
topK: 5,
|
|
241
|
+
// Chat configuration
|
|
242
|
+
chatProvider: isCloud ? provider : "custom",
|
|
243
|
+
chatApiKey: isCloud ? apiKey : "",
|
|
244
|
+
chatModelId: isCloud ? "" : undefined, // empty string lets the chat router pick default
|
|
245
|
+
updatedAt: new Date().toISOString(),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// For custom mode, add custom embedding config and LLM config
|
|
249
|
+
if (!isCloud) {
|
|
250
|
+
configBody.customEmbeddings = [
|
|
251
|
+
{
|
|
252
|
+
baseUrl: embeddingBaseUrl,
|
|
253
|
+
apiKey: embeddingApiKey || undefined,
|
|
254
|
+
modelName: embeddingModelName,
|
|
255
|
+
dimensions: detectedDims ?? 1536,
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
configBody.chatProvider = "custom";
|
|
259
|
+
configBody.chatApiKey = llmApiKey || "";
|
|
260
|
+
configBody.chatModelId = llmModelName;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Save config via API
|
|
264
|
+
if (server?.id) {
|
|
265
|
+
const configRes = await fetch(`/api/config?serverId=${encodeURIComponent(server.id)}`, {
|
|
266
|
+
method: "PUT",
|
|
267
|
+
headers: { "Content-Type": "application/json" },
|
|
268
|
+
body: JSON.stringify(configBody),
|
|
269
|
+
});
|
|
270
|
+
if (!configRes.ok) {
|
|
271
|
+
const err = await configRes.json().catch(() => ({}));
|
|
272
|
+
throw new Error(err.error || "Failed to save configuration");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await setMode("simple");
|
|
277
|
+
|
|
278
|
+
// Fire-and-forget: generate + launch RAG server so it's ready for chat
|
|
279
|
+
(async () => {
|
|
280
|
+
try {
|
|
281
|
+
// Generate server code
|
|
282
|
+
await fetch("/api/server/generate");
|
|
283
|
+
// Launch it
|
|
284
|
+
await fetch("/api/server/local", {
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: { "Content-Type": "application/json" },
|
|
287
|
+
body: JSON.stringify({ action: "start" }),
|
|
288
|
+
});
|
|
289
|
+
} catch {
|
|
290
|
+
// Non-blocking — server can be launched manually from settings
|
|
291
|
+
}
|
|
292
|
+
})();
|
|
293
|
+
|
|
294
|
+
toast.success("All set! Let's get started.");
|
|
295
|
+
} catch (err) {
|
|
296
|
+
toast.error(err instanceof Error ? err.message : "Setup failed. Please try again.");
|
|
297
|
+
setBusy(false);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const canProceed =
|
|
302
|
+
connectionMode === "cloud"
|
|
303
|
+
? cloudVerified
|
|
304
|
+
: embeddingVerified && llmVerified;
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div
|
|
308
|
+
className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
|
309
|
+
style={{ backgroundColor: "#FCFCFB" }}
|
|
310
|
+
>
|
|
311
|
+
{/* Background */}
|
|
312
|
+
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
|
313
|
+
<div className="absolute -left-20 -top-20 h-[500px] w-[500px] rounded-full bg-stone-200/40 blur-3xl animate-pulse" />
|
|
314
|
+
<div className="absolute -bottom-32 -right-32 h-[600px] w-[600px] rounded-full bg-amber-100/30 blur-3xl animate-pulse [animation-delay:1s]" />
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div className="relative z-10 flex w-full max-w-lg flex-col items-center gap-6 px-6 py-10">
|
|
318
|
+
{/* Back */}
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
onClick={onBack}
|
|
322
|
+
className="self-start flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
323
|
+
>
|
|
324
|
+
<ArrowLeft className="size-4" />
|
|
325
|
+
Back
|
|
326
|
+
</button>
|
|
327
|
+
|
|
328
|
+
{/* Header */}
|
|
329
|
+
<div className="flex flex-col items-center gap-3 text-center">
|
|
330
|
+
<div className="flex size-14 items-center justify-center rounded-2xl bg-primary text-primary-foreground">
|
|
331
|
+
<img src="/logo9.png" className="size-8" alt="Larkup" />
|
|
332
|
+
</div>
|
|
333
|
+
<div>
|
|
334
|
+
<h1 className="text-2xl font-bold tracking-tight text-foreground">
|
|
335
|
+
Connect your AI
|
|
336
|
+
</h1>
|
|
337
|
+
<p className="mt-1.5 text-sm text-muted-foreground text-pretty max-w-sm">
|
|
338
|
+
Choose how to connect to an AI provider. We'll handle
|
|
339
|
+
everything else automatically.
|
|
340
|
+
</p>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* Connection Mode Tabs */}
|
|
345
|
+
<div className="w-full rounded-xl border bg-white p-1 flex gap-1">
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
onClick={() => setConnectionMode("cloud")}
|
|
349
|
+
className={cn(
|
|
350
|
+
"flex-1 flex items-center justify-center gap-2 rounded-lg py-2.5 text-sm font-medium transition-all",
|
|
351
|
+
connectionMode === "cloud"
|
|
352
|
+
? "bg-primary text-primary-foreground"
|
|
353
|
+
: "text-muted-foreground hover:text-foreground hover:bg-muted/40",
|
|
354
|
+
)}
|
|
355
|
+
>
|
|
356
|
+
<Cloud className="size-4" />
|
|
357
|
+
Cloud Provider
|
|
358
|
+
</button>
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
onClick={() => setConnectionMode("custom")}
|
|
362
|
+
className={cn(
|
|
363
|
+
"flex-1 flex items-center justify-center gap-2 rounded-lg py-2.5 text-sm font-medium transition-all",
|
|
364
|
+
connectionMode === "custom"
|
|
365
|
+
? "bg-primary text-primary-foreground"
|
|
366
|
+
: "text-muted-foreground hover:text-foreground hover:bg-muted/40",
|
|
367
|
+
)}
|
|
368
|
+
>
|
|
369
|
+
<Monitor className="size-4" />
|
|
370
|
+
Custom Model
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{/* Form */}
|
|
375
|
+
<div className="w-full rounded-2xl border bg-transparent p-6 space-y-5">
|
|
376
|
+
{connectionMode === "cloud" ? (
|
|
377
|
+
<>
|
|
378
|
+
{/* Provider Selection */}
|
|
379
|
+
<div className="space-y-2.5">
|
|
380
|
+
<Label className="text-sm font-medium">AI Provider</Label>
|
|
381
|
+
<div className="grid grid-cols-2 gap-2">
|
|
382
|
+
{CLOUD_PROVIDERS.map(({ key, recommended }) => {
|
|
383
|
+
const meta = PROVIDER_META[key];
|
|
384
|
+
if (!meta) return null;
|
|
385
|
+
const selected = provider === key;
|
|
386
|
+
return (
|
|
387
|
+
<button
|
|
388
|
+
key={key}
|
|
389
|
+
type="button"
|
|
390
|
+
onClick={() => onProviderChange(key)}
|
|
391
|
+
className={cn(
|
|
392
|
+
"relative flex items-center gap-2.5 rounded-xl border-2 px-3 py-2.5 text-left transition-all duration-200",
|
|
393
|
+
selected
|
|
394
|
+
? "border-primary bg-primary/5"
|
|
395
|
+
: "border-border hover:border-primary/30 hover:bg-muted/30",
|
|
396
|
+
)}
|
|
397
|
+
>
|
|
398
|
+
<ProviderIcon
|
|
399
|
+
src={meta.iconSrc}
|
|
400
|
+
alt={meta.label}
|
|
401
|
+
pillBg={meta.pillBg}
|
|
402
|
+
size={22}
|
|
403
|
+
/>
|
|
404
|
+
<div className="flex flex-col min-w-0">
|
|
405
|
+
<span className="text-sm font-medium text-foreground truncate">
|
|
406
|
+
{meta.label}
|
|
407
|
+
</span>
|
|
408
|
+
{recommended && (
|
|
409
|
+
<span className="text-[10px] font-medium text-primary">
|
|
410
|
+
Recommended
|
|
411
|
+
</span>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
{selected && (
|
|
415
|
+
<CheckCircle2 className="absolute right-2.5 top-2.5 size-4 text-primary" />
|
|
416
|
+
)}
|
|
417
|
+
</button>
|
|
418
|
+
);
|
|
419
|
+
})}
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{/* API Key */}
|
|
424
|
+
<div className="space-y-2">
|
|
425
|
+
<Label htmlFor="simple-apikey">API Key</Label>
|
|
426
|
+
<div className="relative">
|
|
427
|
+
<Input
|
|
428
|
+
id="simple-apikey"
|
|
429
|
+
type={showApiKey ? "text" : "password"}
|
|
430
|
+
placeholder="sk-..."
|
|
431
|
+
value={apiKey}
|
|
432
|
+
onChange={(e) => onApiKeyChange(e.target.value)}
|
|
433
|
+
className="pr-10"
|
|
434
|
+
/>
|
|
435
|
+
<button
|
|
436
|
+
type="button"
|
|
437
|
+
onClick={() => setShowApiKey(!showApiKey)}
|
|
438
|
+
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground"
|
|
439
|
+
>
|
|
440
|
+
{showApiKey ? (
|
|
441
|
+
<EyeOff className="size-4" />
|
|
442
|
+
) : (
|
|
443
|
+
<Eye className="size-4" />
|
|
444
|
+
)}
|
|
445
|
+
</button>
|
|
446
|
+
</div>
|
|
447
|
+
<p className="text-xs text-muted-foreground">
|
|
448
|
+
Your key is stored locally and never sent to our servers.
|
|
449
|
+
</p>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
{/* Test Connection */}
|
|
453
|
+
{cloudVerified ? (
|
|
454
|
+
<div className="flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-3 py-2.5 text-sm font-medium text-green-700">
|
|
455
|
+
<CheckCircle2 className="size-4" />
|
|
456
|
+
API key verified. Ready to go!
|
|
457
|
+
</div>
|
|
458
|
+
) : (
|
|
459
|
+
<Button
|
|
460
|
+
variant="outline"
|
|
461
|
+
className="w-full gap-2"
|
|
462
|
+
onClick={testCloudProvider}
|
|
463
|
+
disabled={testingCloud || !apiKey.trim()}
|
|
464
|
+
>
|
|
465
|
+
{testingCloud ? (
|
|
466
|
+
<Loader2 className="size-4 animate-spin" />
|
|
467
|
+
) : (
|
|
468
|
+
<ShieldCheck className="size-4" />
|
|
469
|
+
)}
|
|
470
|
+
Verify API Key
|
|
471
|
+
</Button>
|
|
472
|
+
)}
|
|
473
|
+
</>
|
|
474
|
+
) : (
|
|
475
|
+
<>
|
|
476
|
+
{/* Embedding Connection */}
|
|
477
|
+
<div className="space-y-3">
|
|
478
|
+
<div className="flex items-center gap-2">
|
|
479
|
+
<span className="flex size-6 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-bold">
|
|
480
|
+
1
|
|
481
|
+
</span>
|
|
482
|
+
<Label className="text-sm font-medium">Embedding Model</Label>
|
|
483
|
+
</div>
|
|
484
|
+
<div className="grid gap-2.5 pl-8">
|
|
485
|
+
<div className="space-y-1.5">
|
|
486
|
+
<Label
|
|
487
|
+
htmlFor="emb-url"
|
|
488
|
+
className="text-xs text-muted-foreground"
|
|
489
|
+
>
|
|
490
|
+
Base URL
|
|
491
|
+
</Label>
|
|
492
|
+
<Input
|
|
493
|
+
id="emb-url"
|
|
494
|
+
placeholder="http://localhost:11434/v1"
|
|
495
|
+
value={embeddingBaseUrl}
|
|
496
|
+
onChange={(e) => {
|
|
497
|
+
setEmbeddingBaseUrl(e.target.value);
|
|
498
|
+
setEmbeddingVerified(false);
|
|
499
|
+
}}
|
|
500
|
+
/>
|
|
501
|
+
</div>
|
|
502
|
+
<div className="space-y-1.5">
|
|
503
|
+
<Label
|
|
504
|
+
htmlFor="emb-model"
|
|
505
|
+
className="text-xs text-muted-foreground"
|
|
506
|
+
>
|
|
507
|
+
Model Name
|
|
508
|
+
</Label>
|
|
509
|
+
<Input
|
|
510
|
+
id="emb-model"
|
|
511
|
+
placeholder="nomic-embed-text"
|
|
512
|
+
value={embeddingModelName}
|
|
513
|
+
onChange={(e) => {
|
|
514
|
+
setEmbeddingModelName(e.target.value);
|
|
515
|
+
setEmbeddingVerified(false);
|
|
516
|
+
}}
|
|
517
|
+
/>
|
|
518
|
+
</div>
|
|
519
|
+
<div className="space-y-1.5">
|
|
520
|
+
<Label
|
|
521
|
+
htmlFor="emb-apikey"
|
|
522
|
+
className="text-xs text-muted-foreground"
|
|
523
|
+
>
|
|
524
|
+
API Key{" "}
|
|
525
|
+
<span className="text-muted-foreground/60">
|
|
526
|
+
(optional)
|
|
527
|
+
</span>
|
|
528
|
+
</Label>
|
|
529
|
+
<div className="relative">
|
|
530
|
+
<Input
|
|
531
|
+
id="emb-apikey"
|
|
532
|
+
type={showEmbeddingApiKey ? "text" : "password"}
|
|
533
|
+
placeholder="sk-..."
|
|
534
|
+
value={embeddingApiKey}
|
|
535
|
+
onChange={(e) => {
|
|
536
|
+
setEmbeddingApiKey(e.target.value);
|
|
537
|
+
setEmbeddingVerified(false);
|
|
538
|
+
}}
|
|
539
|
+
className="pr-10"
|
|
540
|
+
/>
|
|
541
|
+
<button
|
|
542
|
+
type="button"
|
|
543
|
+
onClick={() =>
|
|
544
|
+
setShowEmbeddingApiKey(!showEmbeddingApiKey)
|
|
545
|
+
}
|
|
546
|
+
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground"
|
|
547
|
+
>
|
|
548
|
+
{showEmbeddingApiKey ? (
|
|
549
|
+
<EyeOff className="size-4" />
|
|
550
|
+
) : (
|
|
551
|
+
<Eye className="size-4" />
|
|
552
|
+
)}
|
|
553
|
+
</button>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
{embeddingVerified ? (
|
|
557
|
+
<div className="flex items-center gap-1.5 rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-sm font-medium text-green-700">
|
|
558
|
+
<CheckCircle2 className="size-4" />
|
|
559
|
+
Connected — {detectedDims} dimensions
|
|
560
|
+
</div>
|
|
561
|
+
) : (
|
|
562
|
+
<Button
|
|
563
|
+
variant="outline"
|
|
564
|
+
size="sm"
|
|
565
|
+
onClick={testCustomEmbedding}
|
|
566
|
+
disabled={testingEmbedding}
|
|
567
|
+
>
|
|
568
|
+
{testingEmbedding ? (
|
|
569
|
+
<Loader2 className="size-3.5 animate-spin mr-1.5" />
|
|
570
|
+
) : (
|
|
571
|
+
<ShieldCheck className="size-3.5 mr-1.5" />
|
|
572
|
+
)}
|
|
573
|
+
Test Connection
|
|
574
|
+
</Button>
|
|
575
|
+
)}
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
{/* LLM Connection */}
|
|
580
|
+
<div className="space-y-3 border-t pt-4">
|
|
581
|
+
<div className="flex items-center gap-2">
|
|
582
|
+
<span className="flex size-6 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-bold">
|
|
583
|
+
2
|
|
584
|
+
</span>
|
|
585
|
+
<Label className="text-sm font-medium">
|
|
586
|
+
Chat / LLM Model
|
|
587
|
+
</Label>
|
|
588
|
+
</div>
|
|
589
|
+
<div className="grid gap-2.5 pl-8">
|
|
590
|
+
<div className="space-y-1.5">
|
|
591
|
+
<Label
|
|
592
|
+
htmlFor="llm-url"
|
|
593
|
+
className="text-xs text-muted-foreground"
|
|
594
|
+
>
|
|
595
|
+
Base URL
|
|
596
|
+
</Label>
|
|
597
|
+
<Input
|
|
598
|
+
id="llm-url"
|
|
599
|
+
placeholder="http://localhost:11434/v1"
|
|
600
|
+
value={llmBaseUrl}
|
|
601
|
+
onChange={(e) => setLlmBaseUrl(e.target.value)}
|
|
602
|
+
/>
|
|
603
|
+
</div>
|
|
604
|
+
<div className="space-y-1.5">
|
|
605
|
+
<Label
|
|
606
|
+
htmlFor="llm-model"
|
|
607
|
+
className="text-xs text-muted-foreground"
|
|
608
|
+
>
|
|
609
|
+
Model Name
|
|
610
|
+
</Label>
|
|
611
|
+
<Input
|
|
612
|
+
id="llm-model"
|
|
613
|
+
placeholder="llama3.2"
|
|
614
|
+
value={llmModelName}
|
|
615
|
+
onChange={(e) => {
|
|
616
|
+
setLlmModelName(e.target.value);
|
|
617
|
+
setLlmVerified(false);
|
|
618
|
+
}}
|
|
619
|
+
/>
|
|
620
|
+
</div>
|
|
621
|
+
<div className="space-y-1.5">
|
|
622
|
+
<Label
|
|
623
|
+
htmlFor="llm-apikey"
|
|
624
|
+
className="text-xs text-muted-foreground"
|
|
625
|
+
>
|
|
626
|
+
API Key{" "}
|
|
627
|
+
<span className="text-muted-foreground/60">
|
|
628
|
+
(optional)
|
|
629
|
+
</span>
|
|
630
|
+
</Label>
|
|
631
|
+
<div className="relative">
|
|
632
|
+
<Input
|
|
633
|
+
id="llm-apikey"
|
|
634
|
+
type={showLlmApiKey ? "text" : "password"}
|
|
635
|
+
placeholder="sk-..."
|
|
636
|
+
value={llmApiKey}
|
|
637
|
+
onChange={(e) => {
|
|
638
|
+
setLlmApiKey(e.target.value);
|
|
639
|
+
setLlmVerified(false);
|
|
640
|
+
}}
|
|
641
|
+
className="pr-10"
|
|
642
|
+
/>
|
|
643
|
+
<button
|
|
644
|
+
type="button"
|
|
645
|
+
onClick={() => setShowLlmApiKey(!showLlmApiKey)}
|
|
646
|
+
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground"
|
|
647
|
+
>
|
|
648
|
+
{showLlmApiKey ? (
|
|
649
|
+
<EyeOff className="size-4" />
|
|
650
|
+
) : (
|
|
651
|
+
<Eye className="size-4" />
|
|
652
|
+
)}
|
|
653
|
+
</button>
|
|
654
|
+
</div>
|
|
655
|
+
</div>
|
|
656
|
+
{llmVerified ? (
|
|
657
|
+
<div className="flex items-center gap-1.5 rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-sm font-medium text-green-700">
|
|
658
|
+
<CheckCircle2 className="size-4" />
|
|
659
|
+
LLM connected successfully
|
|
660
|
+
</div>
|
|
661
|
+
) : (
|
|
662
|
+
<Button
|
|
663
|
+
variant="outline"
|
|
664
|
+
size="sm"
|
|
665
|
+
onClick={testCustomLlm}
|
|
666
|
+
disabled={testingLlm}
|
|
667
|
+
>
|
|
668
|
+
{testingLlm ? (
|
|
669
|
+
<Loader2 className="size-3.5 animate-spin mr-1.5" />
|
|
670
|
+
) : (
|
|
671
|
+
<ShieldCheck className="size-3.5 mr-1.5" />
|
|
672
|
+
)}
|
|
673
|
+
Test LLM Connection
|
|
674
|
+
</Button>
|
|
675
|
+
)}
|
|
676
|
+
<p className="text-xs text-muted-foreground">
|
|
677
|
+
E.g. Ollama, LM Studio, or any OpenAI-compatible endpoint.
|
|
678
|
+
</p>
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
</>
|
|
682
|
+
)}
|
|
683
|
+
</div>
|
|
684
|
+
|
|
685
|
+
{/* Submit */}
|
|
686
|
+
<Button
|
|
687
|
+
className="w-full"
|
|
688
|
+
size="lg"
|
|
689
|
+
onClick={() => void handleFinish()}
|
|
690
|
+
disabled={busy || !canProceed}
|
|
691
|
+
>
|
|
692
|
+
{busy ? (
|
|
693
|
+
<Loader2 className="size-4 animate-spin" />
|
|
694
|
+
) : (
|
|
695
|
+
<ArrowRight className="size-4" />
|
|
696
|
+
)}
|
|
697
|
+
Get Started
|
|
698
|
+
</Button>
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
);
|
|
702
|
+
}
|