Add provider endpoint preset dropdown to admin AI provider forms
User request: implement selection of default endpoints for known AI providers in the "New AI provider" dropdown, including chat completion endpoints. Changes (artifacts/skillguard/src/pages/admin.tsx): - Added PROVIDER_PRESETS constant with 9 OpenAI-compatible presets (OpenAI, Groq, OpenRouter, Mistral AI, DeepSeek, Together AI, Perplexity AI, Ollama lokal, LM Studio lokal) and 1 Anthropic preset. - Added addPreset / editPreset state + addSelectedPreset / editSelectedPreset derived values to ProviderTab. - Both Add and Edit dialogs now show an "Anbieter-Voreinstellung" dropdown (hidden for apiType=custom) between the API-type and Base-URL fields. Selecting a preset auto-fills the Base URL and resets discovery state. - On selection, a mono info panel shows both the Base URL and the full Chat Completions endpoint for the chosen preset. - apiType change clears the preset and (in Add form) also clears the baseUrl so no stale URL from a different provider type persists. - Removed the old static hint text (baseUrlHintOpenai / baseUrlHintAnthropic) since the dropdown replaces it. i18n (locales/de/en/es/admin.ts): - Added endpointPreset and endpointPresetPlaceholder keys to all three language files. Also fixed in this session: ran orval codegen after the i18n task merge left the generated types stale (language/lang fields missing from SkillScanInput, ScanDetail, useListRules); typecheck now fully green.
This commit is contained in:
parent
50d2adb674
commit
2695883f0d
5 changed files with 89 additions and 5 deletions
|
|
@ -32,6 +32,8 @@ export default {
|
|||
fields: {
|
||||
name: "Name",
|
||||
apiType: "API-Typ",
|
||||
endpointPreset: "Anbieter-Voreinstellung",
|
||||
endpointPresetPlaceholder: "Anbieter wählen …",
|
||||
baseUrl: "API-Endpunkt (Base URL)",
|
||||
baseUrlPlaceholder: "z.B. https://api.openai.com/v1",
|
||||
baseUrlHintOpenai: "OpenAI-kompatibel: https://api.openai.com/v1",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export default {
|
|||
fields: {
|
||||
name: "Name",
|
||||
apiType: "API type",
|
||||
endpointPreset: "Provider preset",
|
||||
endpointPresetPlaceholder: "Choose provider …",
|
||||
baseUrl: "API endpoint (base URL)",
|
||||
baseUrlPlaceholder: "e.g. https://api.openai.com/v1",
|
||||
baseUrlHintOpenai: "OpenAI-compatible: https://api.openai.com/v1",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export default {
|
|||
fields: {
|
||||
name: "Nombre",
|
||||
apiType: "Tipo de API",
|
||||
endpointPreset: "Configuración de proveedor",
|
||||
endpointPresetPlaceholder: "Elegir proveedor …",
|
||||
baseUrl: "Endpoint de API (URL base)",
|
||||
baseUrlPlaceholder: "p. ej. https://api.openai.com/v1",
|
||||
baseUrlHintOpenai: "Compatible con OpenAI: https://api.openai.com/v1",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,25 @@ import { useToast } from "@/hooks/use-toast";
|
|||
import { Loader2, Plus, Trash2, CheckCircle2, XCircle, BrainCircuit, ShieldAlert, KeyRound, Server, Activity } from "lucide-react";
|
||||
import { AxisBadge, SeverityBadge } from "@/components/ui-helpers";
|
||||
|
||||
type ProviderPreset = { label: string; baseUrl: string; chatCompletion: string };
|
||||
|
||||
const PROVIDER_PRESETS: Partial<Record<AiProviderApiType, ProviderPreset[]>> = {
|
||||
[AiProviderApiType.openai]: [
|
||||
{ label: "OpenAI", baseUrl: "https://api.openai.com/v1", chatCompletion: "https://api.openai.com/v1/chat/completions" },
|
||||
{ label: "Groq", baseUrl: "https://api.groq.com/openai/v1", chatCompletion: "https://api.groq.com/openai/v1/chat/completions" },
|
||||
{ label: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1", chatCompletion: "https://openrouter.ai/api/v1/chat/completions" },
|
||||
{ label: "Mistral AI", baseUrl: "https://api.mistral.ai/v1", chatCompletion: "https://api.mistral.ai/v1/chat/completions" },
|
||||
{ label: "DeepSeek", baseUrl: "https://api.deepseek.com/v1", chatCompletion: "https://api.deepseek.com/v1/chat/completions" },
|
||||
{ label: "Together AI", baseUrl: "https://api.together.xyz/v1", chatCompletion: "https://api.together.xyz/v1/chat/completions" },
|
||||
{ label: "Perplexity AI", baseUrl: "https://api.perplexity.ai", chatCompletion: "https://api.perplexity.ai/chat/completions" },
|
||||
{ label: "Ollama (lokal)", baseUrl: "http://localhost:11434/v1", chatCompletion: "http://localhost:11434/v1/chat/completions" },
|
||||
{ label: "LM Studio (lokal)", baseUrl: "http://localhost:1234/v1", chatCompletion: "http://localhost:1234/v1/chat/completions" },
|
||||
],
|
||||
[AiProviderApiType.anthropic]: [
|
||||
{ label: "Anthropic", baseUrl: "https://api.anthropic.com", chatCompletion: "https://api.anthropic.com/v1/messages" },
|
||||
],
|
||||
};
|
||||
|
||||
function ModelField({ models, loading, tried, value, onChange }: {
|
||||
models: string[];
|
||||
loading: boolean;
|
||||
|
|
@ -98,6 +117,11 @@ function ProviderTab() {
|
|||
const [editModelsTried, setEditModelsTried] = useState(false);
|
||||
|
||||
const [testingId, setTestingId] = useState<number | null>(null);
|
||||
const [addPreset, setAddPreset] = useState("");
|
||||
const [editPreset, setEditPreset] = useState("");
|
||||
|
||||
const addSelectedPreset = PROVIDER_PRESETS[addForm.apiType]?.find(p => p.label === addPreset) ?? null;
|
||||
const editSelectedPreset = PROVIDER_PRESETS[editForm.apiType]?.find(p => p.label === editPreset) ?? null;
|
||||
|
||||
const resetAddDiscovery = () => {
|
||||
setAddTestResult(null);
|
||||
|
|
@ -124,6 +148,7 @@ function ProviderTab() {
|
|||
toast({ title: t("admin.providers.toasts.added") });
|
||||
setIsAddOpen(false);
|
||||
setAddForm({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true });
|
||||
setAddPreset("");
|
||||
resetAddDiscovery();
|
||||
invalidate();
|
||||
},
|
||||
|
|
@ -262,6 +287,7 @@ function ProviderTab() {
|
|||
apiToken: "",
|
||||
enabled: provider.enabled
|
||||
});
|
||||
setEditPreset("");
|
||||
resetEditDiscovery();
|
||||
setEditingId(provider.id);
|
||||
};
|
||||
|
|
@ -294,19 +320,45 @@ function ProviderTab() {
|
|||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("admin.providers.fields.apiType")}</Label>
|
||||
<Select value={addForm.apiType} onValueChange={(v: AiProviderApiType) => setAddForm({...addForm, apiType: v})}>
|
||||
<Select value={addForm.apiType} onValueChange={(v: AiProviderApiType) => {
|
||||
setAddForm({ ...addForm, apiType: v, baseUrl: "" });
|
||||
setAddPreset("");
|
||||
resetAddDiscovery();
|
||||
}}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="openai">OpenAI</SelectItem>
|
||||
<SelectItem value="openai">OpenAI-kompatibel</SelectItem>
|
||||
<SelectItem value="anthropic">Anthropic</SelectItem>
|
||||
<SelectItem value="custom">Custom</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{(PROVIDER_PRESETS[addForm.apiType]?.length ?? 0) > 0 && (
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("admin.providers.fields.endpointPreset")}</Label>
|
||||
<Select value={addPreset} onValueChange={(label) => {
|
||||
const preset = PROVIDER_PRESETS[addForm.apiType]?.find(p => p.label === label);
|
||||
setAddPreset(label);
|
||||
if (preset) { setAddForm(f => ({ ...f, baseUrl: preset.baseUrl })); resetAddDiscovery(); }
|
||||
}}>
|
||||
<SelectTrigger><SelectValue placeholder={t("admin.providers.fields.endpointPresetPlaceholder")} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROVIDER_PRESETS[addForm.apiType]!.map(p => (
|
||||
<SelectItem key={p.label} value={p.label}>{p.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{addSelectedPreset && (
|
||||
<div className="rounded-md bg-muted/60 px-3 py-2 text-xs font-mono space-y-1">
|
||||
<div><span className="text-muted-foreground">Base URL: </span>{addSelectedPreset.baseUrl}</div>
|
||||
<div><span className="text-muted-foreground">Chat Completions: </span>{addSelectedPreset.chatCompletion}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("admin.providers.fields.baseUrl")}</Label>
|
||||
<Input value={addForm.baseUrl} onChange={e => { setAddForm({...addForm, baseUrl: e.target.value}); resetAddDiscovery(); }} required placeholder={t("admin.providers.fields.baseUrlPlaceholder")} />
|
||||
<p className="text-xs text-muted-foreground">{t("admin.providers.fields.baseUrlHintOpenai")} <br/> {t("admin.providers.fields.baseUrlHintAnthropic")}</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("admin.providers.fields.apiToken")}</Label>
|
||||
|
|
@ -399,15 +451,41 @@ function ProviderTab() {
|
|||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("admin.providers.fields.apiType")}</Label>
|
||||
<Select value={editForm.apiType} onValueChange={(v: AiProviderApiType) => setEditForm({...editForm, apiType: v})}>
|
||||
<Select value={editForm.apiType} onValueChange={(v: AiProviderApiType) => {
|
||||
setEditForm({ ...editForm, apiType: v });
|
||||
setEditPreset("");
|
||||
}}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="openai">OpenAI</SelectItem>
|
||||
<SelectItem value="openai">OpenAI-kompatibel</SelectItem>
|
||||
<SelectItem value="anthropic">Anthropic</SelectItem>
|
||||
<SelectItem value="custom">Custom</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{(PROVIDER_PRESETS[editForm.apiType]?.length ?? 0) > 0 && (
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("admin.providers.fields.endpointPreset")}</Label>
|
||||
<Select value={editPreset} onValueChange={(label) => {
|
||||
const preset = PROVIDER_PRESETS[editForm.apiType]?.find(p => p.label === label);
|
||||
setEditPreset(label);
|
||||
if (preset) { setEditForm(f => ({ ...f, baseUrl: preset.baseUrl })); resetEditDiscovery(); }
|
||||
}}>
|
||||
<SelectTrigger><SelectValue placeholder={t("admin.providers.fields.endpointPresetPlaceholder")} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROVIDER_PRESETS[editForm.apiType]!.map(p => (
|
||||
<SelectItem key={p.label} value={p.label}>{p.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{editSelectedPreset && (
|
||||
<div className="rounded-md bg-muted/60 px-3 py-2 text-xs font-mono space-y-1">
|
||||
<div><span className="text-muted-foreground">Base URL: </span>{editSelectedPreset.baseUrl}</div>
|
||||
<div><span className="text-muted-foreground">Chat Completions: </span>{editSelectedPreset.chatCompletion}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("admin.providers.fields.baseUrl")}</Label>
|
||||
<Input value={editForm.baseUrl} onChange={e => { setEditForm({...editForm, baseUrl: e.target.value}); resetEditDiscovery(); }} required />
|
||||
|
|
|
|||
BIN
attached_assets/image_1781631809853.png
Normal file
BIN
attached_assets/image_1781631809853.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
Loading…
Add table
Reference in a new issue