137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { testAiConnection, saveAiConfig } from "@/lib/actions/ai";
|
|
import type { AiSettingsView } from "@/lib/ai-settings";
|
|
|
|
/** Lets an admin point the site at an OpenAI-compatible endpoint (e.g.
|
|
* OpenWebUI): a URL, a key, and -- once a test confirms it works -- which
|
|
* of the provider's models to use for AI generation features. */
|
|
export function AiSettingsCard({ initial }: { initial: AiSettingsView }) {
|
|
const [apiUrl, setApiUrl] = useState(initial.apiUrl ?? "");
|
|
// Never prefilled with the real key -- the server never sends it back.
|
|
// A blank field on save means "keep the key that's already stored".
|
|
const [apiKey, setApiKey] = useState("");
|
|
const [model, setModel] = useState(initial.model ?? "");
|
|
// Seeded with just the saved model (if any) so the picker isn't empty
|
|
// on first load -- a fresh "Test connection" replaces this with the
|
|
// provider's real, current list.
|
|
const [models, setModels] = useState<string[]>(initial.model ? [initial.model] : []);
|
|
const [hasApiKey, setHasApiKey] = useState(initial.hasApiKey);
|
|
const [testing, setTesting] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
async function handleTest() {
|
|
setTesting(true);
|
|
const result = await testAiConnection(apiUrl, apiKey);
|
|
setTesting(false);
|
|
if (result.error) {
|
|
toast.error(result.error);
|
|
return;
|
|
}
|
|
const found = result.models ?? [];
|
|
setModels(found);
|
|
setModel((prev) => (found.includes(prev) ? prev : found[0] ?? ""));
|
|
toast.success(`Connected -- found ${found.length} model${found.length === 1 ? "" : "s"}.`);
|
|
}
|
|
|
|
async function handleSave() {
|
|
setSaving(true);
|
|
const result = await saveAiConfig({ apiUrl, apiKey, model });
|
|
setSaving(false);
|
|
if (result.error) {
|
|
toast.error(result.error);
|
|
return;
|
|
}
|
|
if (apiKey.trim()) setHasApiKey(true);
|
|
setApiKey(""); // saved -- a blank field means "unchanged" from here on
|
|
toast.success("AI settings saved.");
|
|
}
|
|
|
|
return (
|
|
<Card className="max-w-3xl">
|
|
<CardHeader>
|
|
<CardTitle>AI generation</CardTitle>
|
|
<CardDescription>
|
|
Connect an OpenAI-compatible endpoint (e.g. OpenWebUI) to power AI generation features
|
|
on the site.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="ai-api-url">API URL</Label>
|
|
<Input
|
|
id="ai-api-url"
|
|
value={apiUrl}
|
|
onChange={(e) => setApiUrl(e.target.value)}
|
|
placeholder="https://your-openwebui-host/api/v1"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="ai-api-key">API key</Label>
|
|
<Input
|
|
id="ai-api-key"
|
|
type="password"
|
|
value={apiKey}
|
|
onChange={(e) => setApiKey(e.target.value)}
|
|
placeholder={hasApiKey ? "Unchanged -- leave blank to keep the saved key" : "sk-…"}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<Button type="button" variant="outline" onClick={handleTest} disabled={testing || !apiUrl.trim()}>
|
|
{testing ? "Testing…" : "Test connection"}
|
|
</Button>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="ai-model">Model</Label>
|
|
<Select
|
|
value={model}
|
|
onValueChange={(value) => setModel(value ?? "")}
|
|
disabled={models.length === 0}
|
|
>
|
|
<SelectTrigger id="ai-model" className="w-full">
|
|
<SelectValue
|
|
placeholder={models.length === 0 ? "Test the connection to list models" : "Choose a model"}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{models.map((m) => (
|
|
<SelectItem key={m} value={m}>
|
|
{m}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button onClick={handleSave} disabled={saving || !apiUrl.trim() || !model}>
|
|
{saving ? "Saving…" : "Save"}
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
);
|
|
}
|