"use client"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { toast } from "sonner"; type Provider = "openai" | "anthropic" | "openrouter" | "ollama" | ""; type UseCase = { key: "text" | "chat" | "vision" | "mealPlan"; providerSetting: "DEFAULT_TEXT_PROVIDER" | "DEFAULT_CHAT_PROVIDER" | "DEFAULT_VISION_PROVIDER" | "DEFAULT_MEAL_PLAN_PROVIDER"; modelSetting: "DEFAULT_TEXT_MODEL" | "DEFAULT_CHAT_MODEL" | "DEFAULT_VISION_MODEL" | "DEFAULT_MEAL_PLAN_MODEL"; label: string; description: string; }; const USE_CASES: UseCase[] = [ { key: "text", providerSetting: "DEFAULT_TEXT_PROVIDER", modelSetting: "DEFAULT_TEXT_MODEL", label: "Text generation", description: "Recipe generation, substitutions, pairings, and other text-based AI features.", }, { key: "chat", providerSetting: "DEFAULT_CHAT_PROVIDER", modelSetting: "DEFAULT_CHAT_MODEL", label: "Chatbot", description: "The general cooking assistant and per-recipe Q&A chat.", }, { key: "vision", providerSetting: "DEFAULT_VISION_PROVIDER", modelSetting: "DEFAULT_VISION_MODEL", label: "Vision (photo import/scan)", description: "Importing a recipe from a photo and pantry barcode/photo scanning.", }, { key: "mealPlan", providerSetting: "DEFAULT_MEAL_PLAN_PROVIDER", modelSetting: "DEFAULT_MEAL_PLAN_MODEL", label: "Meal plan generation", description: "Generating a full week's meal plan.", }, ]; const PRESET_MODELS: Record = { openai: { label: "OpenAI", models: [ { value: "gpt-4o", label: "GPT-4o" }, { value: "gpt-4o-mini", label: "GPT-4o mini (faster)" }, { value: "o3-mini", label: "o3-mini (reasoning)" }, { value: "gpt-4-turbo", label: "GPT-4 Turbo" }, ], }, anthropic: { label: "Anthropic", models: [ { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" }, { value: "claude-opus-4-8", label: "Claude Opus 4.8 (most capable)" }, { value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 (fastest)" }, ], }, }; type Values = Partial>; export function AdminDefaultModelForm({ initialValues, initialToolCallingEnabled, }: { initialValues: Values; initialToolCallingEnabled: boolean; }) { const [values, setValues] = useState(initialValues); const [toolCallingEnabled, setToolCallingEnabled] = useState(initialToolCallingEnabled); const [saving, setSaving] = useState(false); function setField(key: UseCase["providerSetting"] | UseCase["modelSetting"], value: string | null) { setValues((prev) => ({ ...prev, [key]: value || null })); } async function handleSave() { setSaving(true); try { const res = await fetch("/api/v1/admin/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...values, AI_TOOL_CALLING_ENABLED: toolCallingEnabled ? null : "false" }), }); if (!res.ok) throw new Error("Save failed"); toast.success("Default AI providers saved"); } catch { toast.error("Failed to save defaults"); } finally { setSaving(false); } } return (

Default Models

What to use for a given feature when a user hasn't set their own model preference and has no personal API key. Leave a provider unset to fall back to whichever provider key is configured above.

{USE_CASES.map(({ key, providerSetting, modelSetting, label, description }) => { const provider = (values[providerSetting] ?? "") as Provider; const model = (values[modelSetting] ?? "") as string; const presets = provider && PRESET_MODELS[provider]?.models; return (

{label}

{description}

{presets ? ( ) : ( setField(modelSetting, e.target.value)} disabled={!provider} /> )}
{key === "chat" && (

Lets the chatbot draft a recipe or shopping list inline. Turn off for a model that can't reliably call tools (e.g. some local/Ollama models) — it'll fall back to plain text answers.

)}
); })}
); }