Self-hosted dashboard and admin auth (#4837)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
co-authored by
kartik-mem0
parent
15feaa8ac4
commit
db8ac61713
@@ -0,0 +1,42 @@
|
||||
import "@/styles/globals.css";
|
||||
import { Inter, Fustat, Roboto, DMMono, InterDisplay } from "../(root)/fonts";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { AuthProvider } from "@/lib/auth";
|
||||
|
||||
export const metadata = {
|
||||
title: "Setup | Mem0",
|
||||
description: "Set up your Mem0 instance",
|
||||
};
|
||||
|
||||
export default function SetupLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={cn(
|
||||
Fustat.variable,
|
||||
InterDisplay.variable,
|
||||
Inter.variable,
|
||||
Roboto.variable,
|
||||
DMMono.variable,
|
||||
)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body className="font-fustat" suppressHydrationWarning>
|
||||
<AuthProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { getErrorMessage } from "@/lib/error-message";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
API_KEY_ENDPOINTS,
|
||||
AUTH_ENDPOINTS,
|
||||
MEMORY_ENDPOINTS,
|
||||
} from "@/utils/api-endpoints";
|
||||
import {
|
||||
buildProviderConfig,
|
||||
getEffectiveConfig,
|
||||
} from "@/utils/self-hosted-config";
|
||||
import { isValidEmail } from "@/lib/validators";
|
||||
|
||||
type BundledProviders = {
|
||||
llm: string[];
|
||||
embedder: string[];
|
||||
};
|
||||
|
||||
const STEPS = [
|
||||
"Admin Account",
|
||||
"Providers",
|
||||
"API Key",
|
||||
"Use Case",
|
||||
"Quick Test",
|
||||
];
|
||||
const STEP_TITLES = [
|
||||
"Create your admin account",
|
||||
"Review provider configuration",
|
||||
"Your API key",
|
||||
"Tell us your use case",
|
||||
"Test your setup",
|
||||
];
|
||||
const SUPPORTED_PROVIDERS_URL =
|
||||
"https://docs.mem0.ai/open-source/setup#supported-providers";
|
||||
|
||||
const USE_CASE_PRESETS = [
|
||||
"Personal assistant",
|
||||
"Coding agent",
|
||||
"Customer support",
|
||||
"Research",
|
||||
"Therapy / journaling",
|
||||
];
|
||||
|
||||
const DEFAULT_TEST_MESSAGE = "I like to hike on weekends.";
|
||||
|
||||
export default function SetupPage() {
|
||||
const router = useRouter();
|
||||
const { register } = useAuth();
|
||||
const [step, setStep] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isPrefillingConfig, setIsPrefillingConfig] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
|
||||
const [llmProvider, setLlmProvider] = useState("");
|
||||
const [llmModel, setLlmModel] = useState("");
|
||||
const [llmApiKey, setLlmApiKey] = useState("");
|
||||
const [embedderProvider, setEmbedderProvider] = useState("");
|
||||
const [embedderModel, setEmbedderModel] = useState("");
|
||||
const [serverHasLlmKey, setServerHasLlmKey] = useState(false);
|
||||
const [providers, setProviders] = useState<BundledProviders | null>(null);
|
||||
const [initialLlmProvider, setInitialLlmProvider] = useState("");
|
||||
const [initialLlmModel, setInitialLlmModel] = useState("");
|
||||
const [initialEmbedderProvider, setInitialEmbedderProvider] = useState("");
|
||||
const [initialEmbedderModel, setInitialEmbedderModel] = useState("");
|
||||
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [keyLabel, setKeyLabel] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [testSuccess, setTestSuccess] = useState(false);
|
||||
|
||||
const [useCase, setUseCase] = useState("");
|
||||
const [customInstructions, setCustomInstructions] = useState("");
|
||||
const [testMessage, setTestMessage] = useState(DEFAULT_TEST_MESSAGE);
|
||||
const [isGeneratingInstructions, setIsGeneratingInstructions] =
|
||||
useState(false);
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "";
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 1) {
|
||||
setIsPrefillingConfig(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
setIsPrefillingConfig(true);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const [configRes, providersRes] = await Promise.all([
|
||||
api.get(MEMORY_ENDPOINTS.CONFIGURE),
|
||||
api.get<BundledProviders>(MEMORY_ENDPOINTS.CONFIGURE_PROVIDERS),
|
||||
]);
|
||||
const config = getEffectiveConfig(configRes.data);
|
||||
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const llmProv = config?.llm?.provider || "";
|
||||
const llmMod = config?.llm?.config?.model || "";
|
||||
const embProv = config?.embedder?.provider || "";
|
||||
const embMod = config?.embedder?.config?.model || "";
|
||||
|
||||
setLlmProvider(llmProv);
|
||||
setLlmModel(llmMod);
|
||||
setEmbedderProvider(embProv);
|
||||
setEmbedderModel(embMod);
|
||||
setInitialLlmProvider(llmProv);
|
||||
setInitialLlmModel(llmMod);
|
||||
setInitialEmbedderProvider(embProv);
|
||||
setInitialEmbedderModel(embMod);
|
||||
setServerHasLlmKey(!!config?.llm?.config?.api_key);
|
||||
setProviders(providersRes.data);
|
||||
} catch (err) {
|
||||
if (active) {
|
||||
setError(getErrorMessage(err, "Could not read server configuration"));
|
||||
}
|
||||
} finally {
|
||||
if (active) {
|
||||
setIsPrefillingConfig(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadConfig();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [step]);
|
||||
|
||||
const handleStep1 = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
setError("Enter a valid email address.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords don't match");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setError("Password must be at least 8 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await register(name, email, password);
|
||||
setStep(1);
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err, "Registration failed"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStep2 = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
const dirty =
|
||||
!!llmApiKey ||
|
||||
llmProvider !== initialLlmProvider ||
|
||||
llmModel !== initialLlmModel ||
|
||||
embedderProvider !== initialEmbedderProvider ||
|
||||
embedderModel !== initialEmbedderModel;
|
||||
|
||||
if (!dirty) {
|
||||
setStep(2);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const llm = buildProviderConfig({
|
||||
provider: llmProvider,
|
||||
model: llmModel,
|
||||
apiKey: llmApiKey,
|
||||
});
|
||||
const embedder = buildProviderConfig({
|
||||
provider: embedderProvider,
|
||||
model: embedderModel,
|
||||
apiKey:
|
||||
llmApiKey && embedderProvider === llmProvider ? llmApiKey : undefined,
|
||||
});
|
||||
|
||||
const payload: Record<string, unknown> = { version: "v1.1" };
|
||||
if (llm) payload.llm = llm;
|
||||
if (embedder) payload.embedder = embedder;
|
||||
|
||||
await api.post(MEMORY_ENDPOINTS.CONFIGURE, payload);
|
||||
if (llmApiKey) setServerHasLlmKey(true);
|
||||
setLlmApiKey("");
|
||||
setInitialLlmProvider(llmProvider);
|
||||
setInitialLlmModel(llmModel);
|
||||
setInitialEmbedderProvider(embedderProvider);
|
||||
setInitialEmbedderModel(embedderModel);
|
||||
setStep(2);
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err, "Failed to save configuration"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStep3 = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await api.post(API_KEY_ENDPOINTS.BASE, {
|
||||
label: keyLabel.trim() || "My First Key",
|
||||
});
|
||||
setApiKey(res.data.key);
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err, "Failed to create API key"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContinueToUseCase = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStep(3);
|
||||
};
|
||||
|
||||
const handleContinueToQuickTest = () => {
|
||||
setError("");
|
||||
setStep(4);
|
||||
};
|
||||
|
||||
const handleGoToDashboard = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
router.push("/dashboard/requests");
|
||||
};
|
||||
|
||||
const handleTest = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${apiUrl}/memories`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
|
||||
body: JSON.stringify({
|
||||
messages: [{ role: "user", content: testMessage }],
|
||||
user_id: "setup-test",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
let detail = body;
|
||||
try {
|
||||
detail = JSON.parse(body).detail ?? body;
|
||||
} catch {}
|
||||
throw new Error(
|
||||
`Test failed (${res.status}): ${detail || res.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
setTestSuccess(true);
|
||||
void api
|
||||
.post(AUTH_ENDPOINTS.ONBOARDING_COMPLETE, { use_case: useCase })
|
||||
.catch(() => {});
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err, "Test failed"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-surface-default-primary p-4">
|
||||
<div className="w-full max-w-[560px] space-y-6">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{STEPS.map((label, i) => (
|
||||
<div key={label} className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"size-7 rounded-full flex items-center justify-center text-xs font-medium",
|
||||
i <= step
|
||||
? "bg-memPurple-500 text-white"
|
||||
: "bg-memNeutral-200 text-onSurface-default-tertiary",
|
||||
)}
|
||||
>
|
||||
{i < step ? <Check className="size-3.5" /> : i + 1}
|
||||
</div>
|
||||
{i < STEPS.length - 1 && (
|
||||
<div
|
||||
className={cn(
|
||||
"w-8 h-[2px]",
|
||||
i < step ? "bg-memPurple-500" : "bg-memNeutral-200",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-center text-sm text-onSurface-default-tertiary">
|
||||
{STEPS[step]}
|
||||
</p>
|
||||
|
||||
<Card className="border-memBorder-primary">
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-base font-semibold font-fustat">
|
||||
{STEP_TITLES[step]}
|
||||
</h2>
|
||||
{isPrefillingConfig && step === 1 && (
|
||||
<p className="text-xs text-onSurface-default-tertiary">
|
||||
Checking server configuration...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-sm text-onSurface-danger-primary">{error}</p>
|
||||
)}
|
||||
|
||||
{step === 0 && (
|
||||
<form onSubmit={handleStep1} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-name">Name</Label>
|
||||
<Input
|
||||
id="setup-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-email">Email</Label>
|
||||
<Input
|
||||
id="setup-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="admin@company.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-password">Password</Label>
|
||||
<Input
|
||||
id="setup-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Min 8 characters"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-confirm-password">
|
||||
Confirm Password
|
||||
</Label>
|
||||
<Input
|
||||
id="setup-confirm-password"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading || !name || !email || !password}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? "Creating..." : "Create Admin Account"}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<form onSubmit={handleStep2} className="space-y-4">
|
||||
{!serverHasLlmKey && (
|
||||
<div className="rounded-md border border-memBorder-primary bg-surface-default-secondary p-3">
|
||||
<p className="text-xs text-onSurface-default-tertiary">
|
||||
No LLM provider API key is configured on the server. Paste
|
||||
one below to continue — it will be saved to the server and
|
||||
used for all memory operations.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-llm-provider">LLM Provider</Label>
|
||||
<Select
|
||||
value={llmProvider}
|
||||
onValueChange={setLlmProvider}
|
||||
disabled={!providers}
|
||||
>
|
||||
<SelectTrigger id="setup-llm-provider">
|
||||
<SelectValue placeholder="Select provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{providers?.llm.map((name) => (
|
||||
<SelectItem key={name} value={name}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-llm-model">Model</Label>
|
||||
<Input
|
||||
id="setup-llm-model"
|
||||
value={llmModel}
|
||||
onChange={(e) => setLlmModel(e.target.value)}
|
||||
placeholder="gpt-4.1-nano-2025-04-14"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-llm-api-key">LLM API Key</Label>
|
||||
<Input
|
||||
id="setup-llm-api-key"
|
||||
type="password"
|
||||
value={llmApiKey}
|
||||
onChange={(e) => setLlmApiKey(e.target.value)}
|
||||
placeholder={
|
||||
serverHasLlmKey
|
||||
? "Leave blank to keep existing key"
|
||||
: "sk-..."
|
||||
}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-onSurface-default-tertiary">
|
||||
Also used for the embedder when it shares the same provider.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-embedder-provider">
|
||||
Embedder Provider
|
||||
</Label>
|
||||
<Select
|
||||
value={embedderProvider}
|
||||
onValueChange={setEmbedderProvider}
|
||||
disabled={!providers}
|
||||
>
|
||||
<SelectTrigger id="setup-embedder-provider">
|
||||
<SelectValue placeholder="Select provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{providers?.embedder.map((name) => (
|
||||
<SelectItem key={name} value={name}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-embedder-model">Model</Label>
|
||||
<Input
|
||||
id="setup-embedder-model"
|
||||
value={embedderModel}
|
||||
onChange={(e) => setEmbedderModel(e.target.value)}
|
||||
placeholder="text-embedding-3-small"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-onSurface-default-tertiary">
|
||||
Need another provider? Install its Python package and rebuild
|
||||
the image. See{" "}
|
||||
<a
|
||||
href={SUPPORTED_PROVIDERS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-4 hover:text-onSurface-default-primary"
|
||||
>
|
||||
supported providers
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={
|
||||
isLoading ||
|
||||
!llmProvider ||
|
||||
!embedderProvider ||
|
||||
(!serverHasLlmKey && !llmApiKey)
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? "Saving..." : "Save & Continue"}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{step === 2 && !apiKey && (
|
||||
<form onSubmit={handleStep3} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-key-label">Label for this key</Label>
|
||||
<Input
|
||||
id="setup-key-label"
|
||||
value={keyLabel}
|
||||
onChange={(e) => setKeyLabel(e.target.value)}
|
||||
placeholder="My First Key"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={isLoading} className="w-full">
|
||||
{isLoading ? "Generating..." : "Generate API Key"}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{step === 2 && apiKey && (
|
||||
<form onSubmit={handleContinueToUseCase} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-api-key">Your API Key</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="setup-api-key"
|
||||
value={apiKey}
|
||||
readOnly
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<CopyToClipboard
|
||||
text={apiKey}
|
||||
onCopy={() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}}
|
||||
>
|
||||
<Button variant="outline" size="icon">
|
||||
{copied ? (
|
||||
<Check className="size-4" />
|
||||
) : (
|
||||
<Copy className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
<p className="text-xs text-onSurface-danger-primary">
|
||||
Save this key. You will not see it again.
|
||||
</p>
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Continue
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-use-case">Describe your use case</Label>
|
||||
<textarea
|
||||
id="setup-use-case"
|
||||
value={useCase}
|
||||
onChange={(e) => setUseCase(e.target.value)}
|
||||
placeholder="e.g. A personal assistant that remembers my preferences"
|
||||
className="flex w-full rounded-md border border-memBorder-primary bg-surface-default-primary px-3 py-2 text-sm placeholder:text-onSurface-default-tertiary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-memPurple-500 min-h-[80px] resize-y"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{USE_CASE_PRESETS.map((preset) => (
|
||||
<Button
|
||||
key={preset}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setUseCase(preset)}
|
||||
className={cn(
|
||||
useCase === preset &&
|
||||
"border-memPurple-500 text-memPurple-500",
|
||||
)}
|
||||
>
|
||||
{preset}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-onSurface-default-tertiary">
|
||||
We'll generate custom instructions that tell the memory
|
||||
extractor which facts to prioritize for your use case.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleContinueToQuickTest}
|
||||
className="flex-1"
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!useCase || isGeneratingInstructions}
|
||||
className="flex-1"
|
||||
onClick={async () => {
|
||||
setError("");
|
||||
setIsGeneratingInstructions(true);
|
||||
try {
|
||||
const res = await api.post(
|
||||
MEMORY_ENDPOINTS.GENERATE_INSTRUCTIONS,
|
||||
{
|
||||
use_case: useCase,
|
||||
},
|
||||
);
|
||||
setCustomInstructions(res.data.custom_instructions);
|
||||
if (res.data.test_message) {
|
||||
setTestMessage(res.data.test_message);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(
|
||||
getErrorMessage(
|
||||
err,
|
||||
"Failed to generate instructions",
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setIsGeneratingInstructions(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isGeneratingInstructions
|
||||
? "Generating instructions..."
|
||||
: "Generate instructions"}
|
||||
</Button>
|
||||
</div>
|
||||
{customInstructions && (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="setup-instructions">
|
||||
Generated instructions
|
||||
</Label>
|
||||
<textarea
|
||||
id="setup-instructions"
|
||||
value={customInstructions}
|
||||
onChange={(e) => setCustomInstructions(e.target.value)}
|
||||
className="flex w-full rounded-md border border-memBorder-primary bg-surface-default-primary px-3 py-2 text-sm placeholder:text-onSurface-default-tertiary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-memPurple-500 min-h-[120px] resize-y"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await api.post(MEMORY_ENDPOINTS.CONFIGURE, {
|
||||
custom_instructions: customInstructions,
|
||||
});
|
||||
handleContinueToQuickTest();
|
||||
} catch (err) {
|
||||
setError(
|
||||
getErrorMessage(err, "Failed to save instructions"),
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Saving..." : "Save & Continue"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 4 && (
|
||||
<form
|
||||
onSubmit={testSuccess ? handleGoToDashboard : handleTest}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Label>Test your setup</Label>
|
||||
{!apiUrl && (
|
||||
<p className="text-xs text-onSurface-danger-primary">
|
||||
NEXT_PUBLIC_API_URL is not set. Set it in .env and restart
|
||||
before running this test.
|
||||
</p>
|
||||
)}
|
||||
<pre className="text-xs bg-surface-default-secondary p-3 rounded font-mono overflow-x-auto">{`curl -X POST ${apiUrl}/memories \\
|
||||
-H "X-API-Key: ${apiKey}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"messages": [{"role": "user", "content": "${testMessage}"}], "user_id": "setup-test"}'`}</pre>
|
||||
</div>
|
||||
{!testSuccess ? (
|
||||
<>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? "Testing..." : "Run Test"}
|
||||
</Button>
|
||||
{error && (
|
||||
<p className="text-xs text-onSurface-default-tertiary">
|
||||
Provider credentials or model wrong? Fix them in{" "}
|
||||
<a
|
||||
href="/dashboard/configuration"
|
||||
className="underline underline-offset-4 hover:text-onSurface-default-primary"
|
||||
>
|
||||
Configuration
|
||||
</a>{" "}
|
||||
and run the test again.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-sm text-onSurface-positive-primary">
|
||||
<Check className="size-4" /> Memory created successfully
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user