"use client"; import { useEffect, useState } from "react"; import type { ProviderProtocol } from "@infiplot/types"; import { clearStoredModelConfig, readStoredModelConfig, writeStoredModelConfig, } from "@/lib/clientModelConfig"; const PROVIDER_OPTIONS: { value: ProviderProtocol | ""; label: string }[] = [ { value: "", label: "自动推断(推荐)" }, { value: "openai_compatible", label: "OpenAI Compatible" }, { value: "openai", label: "OpenAI (Native)" }, { value: "anthropic", label: "Anthropic" }, { value: "google", label: "Google Gemini" }, { value: "runware", label: "Runware" }, ]; type ModelGroup = { key: "text" | "image" | "vision"; label: string; icon: string; baseUrl: string; apiKey: string; model: string; provider: string; }; export function ModelSettingsModal({ onClose, onSaved, }: { onClose: () => void; onSaved: () => void; }) { const initial = readStoredModelConfig(); const [groups, setGroups] = useState([ { key: "text", label: "文本模型", icon: "fa-solid fa-pen-nib", baseUrl: initial?.textBaseUrl ?? "", apiKey: initial?.textApiKey ?? "", model: initial?.textModel ?? "", provider: initial?.textProvider ?? "", }, { key: "image", label: "绘图模型", icon: "fa-solid fa-palette", baseUrl: initial?.imageBaseUrl ?? "", apiKey: initial?.imageApiKey ?? "", model: initial?.imageModel ?? "", provider: initial?.imageProvider ?? "", }, { key: "vision", label: "识图模型", icon: "fa-solid fa-eye", baseUrl: initial?.visionBaseUrl ?? "", apiKey: initial?.visionApiKey ?? "", model: initial?.visionModel ?? "", provider: initial?.visionProvider ?? "", }, ]); const [showKeys, setShowKeys] = useState>({}); const [shown, setShown] = useState(false); useEffect(() => { const id = requestAnimationFrame(() => setShown(true)); return () => cancelAnimationFrame(id); }, []); useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Escape") close(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, []); const close = () => { setShown(false); setTimeout(onClose, 280); }; const updateGroup = ( key: string, field: keyof Omit, value: string, ) => { setGroups((prev) => prev.map((g) => (g.key === key ? { ...g, [field]: value } : g)), ); }; const save = () => { const [text, image, vision] = groups; writeStoredModelConfig({ textBaseUrl: text.baseUrl, textApiKey: text.apiKey, textModel: text.model, textProvider: (text.provider as ProviderProtocol) || undefined, imageBaseUrl: image.baseUrl, imageApiKey: image.apiKey, imageModel: image.model, imageProvider: (image.provider as ProviderProtocol) || undefined, visionBaseUrl: vision.baseUrl, visionApiKey: vision.apiKey, visionModel: vision.model, visionProvider: (vision.provider as ProviderProtocol) || undefined, }); onSaved(); close(); }; const clearAll = () => { clearStoredModelConfig(); setGroups((prev) = prev.map((g) => ({ ...g, baseUrl: "", apiKey: "", model: "", provider: "" })), ); onSaved(); close(); }; const hasAnySetting = groups.some( (g) => g.baseUrl.trim() && g.apiKey.trim() && g.model.trim(), ); return (
e.stopPropagation()} className={ "flex w-[600px] max-w-[96vw] max-h-[90vh] flex-col overflow-hidden rounded-sm border border-clay-900/15 bg-cream-50 shadow-2xl shadow-clay-900/25 transition-all duration-300 " + (shown ? "opacity-100 scale-100" : "opacity-0 scale-95") } > {/* Header */}
模型设置 API Key 仅保存在浏览器本地,不会发送到服务器
{groups.map((g, idx) => (
{idx > 0 && (
)}
{g.label}
B A S E · U R L updateGroup(g.key, "baseUrl", e.target.value)} type="text" autoComplete="off" spellCheck={false} placeholder="https://api.example.com/v1" className="h-10 w-full rounded-sm border border-clay-900/15 bg-cream-100 px-4 font-sans text-sm text-clay-900 outline-none transition-colors focus:border-ember-500 placeholder:text-clay-400" />
A P I · K e y
updateGroup(g.key, "apiKey", e.target.value)} type={showKeys[g.key] ? "text" : "password"} autoComplete="off" spellCheck={false} placeholder="sk-..." className="h-10 w-full rounded-sm border border-clay-900/15 bg-cream-100 pl-4 pr-11 font-sans text-sm text-clay-900 outline-none transition-colors focus:border-ember-500 placeholder:text-clay-400" />
M o d e l updateGroup(g.key, "model", e.target.value)} type="text" autoComplete="off" spellCheck={false} placeholder="gpt-4o / claude-3-5-sonnet / flux-1-dev ..." className="h-10 w-full rounded-sm border border-clay-900/15 bg-cream-100 px-4 font-sans text-sm text-clay-900 outline-none transition-colors focus:border-ember-500 placeholder:text-clay-400" />
P r o v i d e r(可选) 留空时系统会根据 Base URL 自动推断协议。
))}

请确保你的 API 端点支持浏览器跨域请求(CORS)。大多数主流提供商(OpenAI、Anthropic、Gemini、Runware 等)已默认支持。

{/* Footer */}
{hasAnySetting && ( )}
); }