Organize/components/admin/ai-settings-card.tsx

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>
);
}