|
|
@@ -255,10 +255,10 @@ export function ModelsPage() {
|
|
|
if (error) return <ApiErrorState message={error} onRetry={() => void load()} />;
|
|
|
|
|
|
return (
|
|
|
- <div className="space-y-6">
|
|
|
+ <div className="space-y-5">
|
|
|
<PageHeader
|
|
|
title={t("models.title")}
|
|
|
- description={t("models.credentialsFirstDescription", "Configure a provider once, sync its catalog, and use the generated models everywhere.")}
|
|
|
+ description={t("models.credentialsFirstDescription", "Connect a provider, sync its catalog, and manage usable model entries from one workspace.")}
|
|
|
actions={
|
|
|
<>
|
|
|
<Button variant="outline" onClick={() => void load()}>
|
|
|
@@ -275,27 +275,29 @@ export function ModelsPage() {
|
|
|
syncedCatalogCount={modelProviders.reduce((total, provider) => total + provider.models.length, 0)}
|
|
|
/>
|
|
|
|
|
|
- <ProviderCredentialPanel
|
|
|
- existingModels={models}
|
|
|
- providers={modelProviders}
|
|
|
- onProviderSaved={upsertProvider}
|
|
|
- onProviderModelsDiscovered={updateProviderModels}
|
|
|
- onModelsSynced={() => load()}
|
|
|
- />
|
|
|
+ <div className="grid items-start gap-5 2xl:grid-cols-[420px_minmax(0,1fr)]">
|
|
|
+ <ProviderCredentialPanel
|
|
|
+ existingModels={models}
|
|
|
+ providers={modelProviders}
|
|
|
+ onProviderSaved={upsertProvider}
|
|
|
+ onProviderModelsDiscovered={updateProviderModels}
|
|
|
+ onModelsSynced={() => load()}
|
|
|
+ />
|
|
|
|
|
|
- <EnabledModelsPanel
|
|
|
- models={filtered}
|
|
|
- totalCount={models.length}
|
|
|
- providers={providers}
|
|
|
- providerFilter={providerFilter}
|
|
|
- search={search}
|
|
|
- onSearchChange={setSearch}
|
|
|
- onProviderFilterChange={setProviderFilter}
|
|
|
- onManualCreate={() => setCreateOpen(true)}
|
|
|
- onEdit={openEdit}
|
|
|
- onTest={openTest}
|
|
|
- onDelete={(model) => void removeModel(model)}
|
|
|
- />
|
|
|
+ <EnabledModelsPanel
|
|
|
+ models={filtered}
|
|
|
+ totalCount={models.length}
|
|
|
+ providers={providers}
|
|
|
+ providerFilter={providerFilter}
|
|
|
+ search={search}
|
|
|
+ onSearchChange={setSearch}
|
|
|
+ onProviderFilterChange={setProviderFilter}
|
|
|
+ onManualCreate={() => setCreateOpen(true)}
|
|
|
+ onEdit={openEdit}
|
|
|
+ onTest={openTest}
|
|
|
+ onDelete={(model) => void removeModel(model)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
|
|
|
<CreateModelDialog
|
|
|
open={createOpen}
|
|
|
@@ -373,12 +375,14 @@ function ModelAccessOverview({
|
|
|
];
|
|
|
|
|
|
return (
|
|
|
- <div className="grid gap-3 md:grid-cols-4">
|
|
|
+ <div className="grid gap-0 overflow-hidden rounded-md border border-border bg-surface-elevated md:grid-cols-4">
|
|
|
{items.map((item) => (
|
|
|
- <div key={item.label} className="rounded-md border border-border bg-surface-elevated px-4 py-3">
|
|
|
- <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{item.label}</p>
|
|
|
- <p className="mt-2 text-2xl font-semibold">{item.value}</p>
|
|
|
- <p className="mt-1 text-xs text-muted-foreground">{item.hint}</p>
|
|
|
+ <div key={item.label} className="border-border px-4 py-3 md:border-r md:last:border-r-0">
|
|
|
+ <div className="flex items-baseline justify-between gap-3">
|
|
|
+ <p className="truncate text-xs font-medium uppercase tracking-wide text-muted-foreground">{item.label}</p>
|
|
|
+ <p className="shrink-0 text-xl font-semibold tabular-nums">{item.value}</p>
|
|
|
+ </div>
|
|
|
+ <p className="mt-1 truncate text-xs text-muted-foreground">{item.hint}</p>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
@@ -413,9 +417,9 @@ function EnabledModelsPanel({
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
return (
|
|
|
- <Card>
|
|
|
- <CardHeader>
|
|
|
- <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
|
+ <Card className="min-w-0">
|
|
|
+ <CardHeader className="pb-4">
|
|
|
+ <div className="flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
|
|
|
<div>
|
|
|
<CardTitle>{t("models.enabledModels", "Enabled models")}</CardTitle>
|
|
|
<CardDescription>
|
|
|
@@ -426,26 +430,25 @@ function EnabledModelsPanel({
|
|
|
})}
|
|
|
</CardDescription>
|
|
|
</div>
|
|
|
- <Button type="button" variant="outline" onClick={onManualCreate}>
|
|
|
- <Plus className="h-4 w-4" /> {t("models.manualModel", "Manual model")}
|
|
|
- </Button>
|
|
|
+ <div className="grid gap-2 sm:grid-cols-[minmax(220px,1fr)_180px_auto] xl:min-w-[640px]">
|
|
|
+ <SearchInput value={search} onChange={onSearchChange} placeholder={t("models.searchPlaceholder")} />
|
|
|
+ <Select
|
|
|
+ value={providerFilter}
|
|
|
+ onChange={(event) => onProviderFilterChange(event.target.value)}
|
|
|
+ options={[
|
|
|
+ { value: "all", label: t("models.allProviders") },
|
|
|
+ ...providers.map((provider) => ({ value: provider, label: providerLabel(provider, t) })),
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ <Button type="button" variant="outline" onClick={onManualCreate}>
|
|
|
+ <Plus className="h-4 w-4" /> {t("models.manualModel", "Manual model")}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</CardHeader>
|
|
|
- <CardContent className="space-y-4">
|
|
|
- <div className="grid gap-3 lg:grid-cols-[1fr_220px]">
|
|
|
- <SearchInput value={search} onChange={onSearchChange} placeholder={t("models.searchPlaceholder")} />
|
|
|
- <Select
|
|
|
- value={providerFilter}
|
|
|
- onChange={(event) => onProviderFilterChange(event.target.value)}
|
|
|
- options={[
|
|
|
- { value: "all", label: t("models.allProviders") },
|
|
|
- ...providers.map((provider) => ({ value: provider, label: providerLabel(provider, t) })),
|
|
|
- ]}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
+ <CardContent>
|
|
|
{models.length ? (
|
|
|
- <div className="grid gap-3 xl:grid-cols-2">
|
|
|
+ <div className="overflow-hidden rounded-md border border-border">
|
|
|
{models.map((model) => (
|
|
|
<ModelCard
|
|
|
key={model.id}
|
|
|
@@ -482,51 +485,51 @@ function ModelCard({
|
|
|
onDelete: () => void;
|
|
|
}) {
|
|
|
const { t } = useTranslation();
|
|
|
+ const shownCapabilities = model.capabilities_json.slice(0, 3);
|
|
|
|
|
|
return (
|
|
|
- <div className="rounded-md border border-border bg-surface-elevated p-4 transition hover:border-primary/35">
|
|
|
- <div className="flex items-start justify-between gap-3">
|
|
|
- <div className="min-w-0">
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <BrainCircuit className="h-4 w-4 shrink-0 text-primary" />
|
|
|
- <h3 className="truncate text-sm font-semibold">{demoText(model.name, t)}</h3>
|
|
|
- </div>
|
|
|
- <p className="mt-1 truncate font-mono text-xs text-muted-foreground">{model.model_name}</p>
|
|
|
- </div>
|
|
|
- <div className="flex shrink-0 items-center gap-1">
|
|
|
- <Button size="icon" variant="ghost" onClick={onTest} aria-label={t("models.testNamed", { name: demoText(model.name, t) })}>
|
|
|
- <FlaskConical className="h-4 w-4" />
|
|
|
- </Button>
|
|
|
- <Button size="icon" variant="ghost" onClick={onEdit} aria-label={t("models.editNamed", { name: demoText(model.name, t) })}>
|
|
|
- <Pencil className="h-4 w-4" />
|
|
|
- </Button>
|
|
|
- <Button size="icon" variant="ghost" className="text-destructive hover:text-destructive" onClick={onDelete} aria-label={t("models.deleteNamed", { name: demoText(model.name, t) })}>
|
|
|
- <Trash2 className="h-4 w-4" />
|
|
|
- </Button>
|
|
|
+ <div className="grid gap-3 border-b border-border bg-surface-elevated p-4 last:border-b-0 transition hover:bg-muted/20 xl:grid-cols-[minmax(220px,1.15fr)_minmax(180px,0.8fr)_minmax(180px,0.8fr)_126px] xl:items-center">
|
|
|
+ <div className="min-w-0">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <BrainCircuit className="h-4 w-4 shrink-0 text-primary" />
|
|
|
+ <h3 className="truncate text-sm font-semibold">{demoText(model.name, t)}</h3>
|
|
|
</div>
|
|
|
+ <p className="mt-1 truncate font-mono text-xs text-muted-foreground">{model.model_name}</p>
|
|
|
</div>
|
|
|
|
|
|
- <div className="mt-4 grid gap-3 sm:grid-cols-2">
|
|
|
- <div className="min-w-0 rounded-md bg-muted/25 px-3 py-2">
|
|
|
- <p className="text-xs font-medium text-muted-foreground">{t("models.provider")}</p>
|
|
|
- <p className="mt-1 truncate text-sm">{providerLabel(model.provider_type, t)}</p>
|
|
|
- <p className="mt-1 truncate text-xs text-muted-foreground">{model.provider_base_url}</p>
|
|
|
- </div>
|
|
|
- <div className="rounded-md bg-muted/25 px-3 py-2">
|
|
|
- <p className="text-xs font-medium text-muted-foreground">{t("common.updated")}</p>
|
|
|
- <p className="mt-1 text-sm">{formatDateTime(model.updated_time)}</p>
|
|
|
- <p className="mt-1 text-xs text-muted-foreground">
|
|
|
- {model.has_provider_api_key ? t("models.usingSavedCredential", "Saved credential") : t("models.noCredential", "No credential")}
|
|
|
- </p>
|
|
|
+ <div className="min-w-0 text-sm">
|
|
|
+ <p className="truncate">{providerLabel(model.provider_type, t)}</p>
|
|
|
+ <p className="mt-1 truncate text-xs text-muted-foreground">{model.provider_base_url}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="min-w-0">
|
|
|
+ <div className="flex flex-wrap gap-1.5">
|
|
|
+ {shownCapabilities.map((capability) => (
|
|
|
+ <Badge key={capability} className="border-primary/20 bg-primary/10 text-primary">
|
|
|
+ {capabilityLabel(capability, t)}
|
|
|
+ </Badge>
|
|
|
+ ))}
|
|
|
+ {model.capabilities_json.length > shownCapabilities.length ? (
|
|
|
+ <Badge className="border-border bg-muted text-muted-foreground">+{model.capabilities_json.length - shownCapabilities.length}</Badge>
|
|
|
+ ) : null}
|
|
|
</div>
|
|
|
+ <p className="mt-2 text-xs text-muted-foreground">
|
|
|
+ {model.has_provider_api_key ? t("models.usingSavedCredential", "Saved credential") : t("models.noCredential", "No credential")}
|
|
|
+ <span className="mx-2 text-border">/</span>
|
|
|
+ {formatDateTime(model.updated_time)}
|
|
|
+ </p>
|
|
|
</div>
|
|
|
|
|
|
- <div className="mt-3 flex flex-wrap gap-1.5">
|
|
|
- {model.capabilities_json.map((capability) => (
|
|
|
- <Badge key={capability} className="border-primary/20 bg-primary/10 text-primary">
|
|
|
- {capabilityLabel(capability, t)}
|
|
|
- </Badge>
|
|
|
- ))}
|
|
|
+ <div className="flex items-center gap-1 xl:justify-end">
|
|
|
+ <Button size="icon" variant="ghost" onClick={onTest} aria-label={t("models.testNamed", { name: demoText(model.name, t) })}>
|
|
|
+ <FlaskConical className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ <Button size="icon" variant="ghost" onClick={onEdit} aria-label={t("models.editNamed", { name: demoText(model.name, t) })}>
|
|
|
+ <Pencil className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ <Button size="icon" variant="ghost" className="text-destructive hover:text-destructive" onClick={onDelete} aria-label={t("models.deleteNamed", { name: demoText(model.name, t) })}>
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
@@ -758,67 +761,67 @@ function ProviderCredentialPanel({
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
- <Card className="overflow-hidden">
|
|
|
- <CardHeader>
|
|
|
- <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
|
+ <Card className="h-fit overflow-hidden 2xl:sticky 2xl:top-5">
|
|
|
+ <CardHeader className="pb-4">
|
|
|
+ <div className="flex items-start justify-between gap-3">
|
|
|
<div>
|
|
|
<CardTitle>{t("models.credentialsTitle", "Provider credentials")}</CardTitle>
|
|
|
- <CardDescription>{t("models.credentialsDescription", "Save one credential, then sync and auto-create every model returned by that provider.")}</CardDescription>
|
|
|
- </div>
|
|
|
- <div className="flex flex-wrap gap-2">
|
|
|
- <Badge className="w-fit border-border bg-muted/50 text-muted-foreground">
|
|
|
- {providers.length} {t("modelProviders.providers", "providers")}
|
|
|
- </Badge>
|
|
|
- <Badge className="w-fit border-primary/20 bg-primary/10 text-primary">
|
|
|
- {providers.reduce((total, provider) => total + provider.models.length, 0)} {t("modelProviders.models", "models")}
|
|
|
- </Badge>
|
|
|
+ <CardDescription>{t("models.credentialsDescription", "Save credentials once, then sync model entries automatically.")}</CardDescription>
|
|
|
</div>
|
|
|
+ <Badge className={providerModelCount ? "shrink-0 border-primary/20 bg-primary/10 text-primary" : "shrink-0 border-border bg-muted text-muted-foreground"}>
|
|
|
+ {providerModelCount} {t("modelProviders.models", "models")}
|
|
|
+ </Badge>
|
|
|
</div>
|
|
|
</CardHeader>
|
|
|
- <CardContent>
|
|
|
- <div className="grid gap-0 overflow-hidden rounded-md border border-border xl:grid-cols-[340px_1fr]">
|
|
|
- <div className="border-border bg-muted/15 p-3 xl:border-r">
|
|
|
- <div className="mb-3">
|
|
|
- <p className="text-sm font-semibold">{t("models.connections", "Connections")}</p>
|
|
|
- <p className="text-xs text-muted-foreground">{t("models.connectionListHint", "Choose a saved credential or add a new one.")}</p>
|
|
|
- </div>
|
|
|
+ <CardContent className="space-y-5">
|
|
|
+ <section>
|
|
|
+ <div className="mb-2 flex items-center justify-between gap-3">
|
|
|
+ <p className="text-sm font-semibold">{t("models.connections", "Connections")}</p>
|
|
|
+ <span className="text-xs text-muted-foreground">
|
|
|
+ {providers.length} {t("modelProviders.providers", "providers")}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
<button
|
|
|
type="button"
|
|
|
- className={`flex min-h-16 w-full items-center gap-3 rounded-md border px-3 py-3 text-left transition ${
|
|
|
+ className={`flex min-h-12 w-full items-center gap-3 rounded-md border px-3 py-2 text-left transition ${
|
|
|
!form.provider_id ? "border-primary bg-primary/10" : "border-border bg-muted/20 hover:bg-muted/35"
|
|
|
}`}
|
|
|
onClick={() => selectProvider("")}
|
|
|
>
|
|
|
- <div className="grid h-10 w-10 shrink-0 place-items-center rounded-md bg-surface-elevated text-primary">
|
|
|
+ <span className="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-surface-elevated text-primary">
|
|
|
<Plus className="h-4 w-4" />
|
|
|
- </div>
|
|
|
- <div className="min-w-0">
|
|
|
- <p className="text-sm font-medium">{t("models.newProviderConnection", "New connection")}</p>
|
|
|
- <p className="mt-0.5 text-xs text-muted-foreground">{t("models.newProviderConnectionHint", "Credential first, models next")}</p>
|
|
|
- </div>
|
|
|
+ </span>
|
|
|
+ <span className="min-w-0">
|
|
|
+ <span className="block truncate text-sm font-medium">{t("models.newProviderConnection", "New connection")}</span>
|
|
|
+ <span className="block truncate text-xs text-muted-foreground">{t("models.newProviderConnectionHint", "Credential first, models next")}</span>
|
|
|
+ </span>
|
|
|
</button>
|
|
|
|
|
|
{providers.length ? (
|
|
|
- <div className="mt-3 space-y-2">
|
|
|
+ <div className="max-h-56 space-y-2 overflow-auto pr-1">
|
|
|
{providers.map((provider) => (
|
|
|
<button
|
|
|
key={provider.id}
|
|
|
type="button"
|
|
|
- className={`flex min-h-16 w-full items-center gap-3 rounded-md border px-3 py-3 text-left transition ${
|
|
|
+ className={`flex min-h-12 w-full items-center gap-3 rounded-md border px-3 py-2 text-left transition ${
|
|
|
form.provider_id === provider.id ? "border-primary bg-primary/10" : "border-border hover:bg-muted/35"
|
|
|
}`}
|
|
|
onClick={() => selectProvider(provider.id)}
|
|
|
>
|
|
|
- <div className="grid h-10 w-10 shrink-0 place-items-center rounded-md bg-muted/50 text-muted-foreground">
|
|
|
+ <span className="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-muted/50 text-muted-foreground">
|
|
|
<Server className="h-4 w-4" />
|
|
|
- </div>
|
|
|
- <div className="min-w-0 flex-1">
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <p className="truncate text-sm font-medium">{provider.name}</p>
|
|
|
+ </span>
|
|
|
+ <span className="min-w-0 flex-1">
|
|
|
+ <span className="flex items-center gap-2">
|
|
|
+ <span className="truncate text-sm font-medium">{provider.name}</span>
|
|
|
{provider.api_key_ref ? <ShieldCheck className="h-3.5 w-3.5 shrink-0 text-primary" /> : null}
|
|
|
- </div>
|
|
|
- <p className="mt-0.5 truncate text-xs text-muted-foreground">{providerLabel(provider.provider_type, t)} - {provider.models.length} {t("modelProviders.models", "models")}</p>
|
|
|
- </div>
|
|
|
+ </span>
|
|
|
+ <span className="block truncate text-xs text-muted-foreground">
|
|
|
+ {providerLabel(provider.provider_type, t)} / {provider.models.length} {t("modelProviders.models", "models")}
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
</button>
|
|
|
))}
|
|
|
</div>
|
|
|
@@ -828,88 +831,82 @@ function ProviderCredentialPanel({
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|
|
|
+ </section>
|
|
|
|
|
|
- <div className="bg-surface-elevated p-4">
|
|
|
- <div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
|
- <div>
|
|
|
- <h3 className="text-base font-semibold">
|
|
|
- {form.provider_id ? t("models.editProviderConnection", "Connection details") : t("models.createProviderConnection", "Create connection")}
|
|
|
- </h3>
|
|
|
- <p className="mt-1 text-sm text-muted-foreground">
|
|
|
- {form.provider_id
|
|
|
- ? t("models.credentialReuseHint", "Update credentials here; synced models keep reusing this connection.")
|
|
|
- : t("models.saveCredentialFirstHint", "DeepSeek and OpenAI-compatible providers can be synced immediately after saving.")}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- <Badge className={providerModelCount ? "w-fit border-primary/20 bg-primary/10 text-primary" : "w-fit border-border bg-muted text-muted-foreground"}>
|
|
|
- {providerModelCount} {t("modelProviders.models", "models")}
|
|
|
- </Badge>
|
|
|
- </div>
|
|
|
+ <section className="space-y-4 border-t border-border pt-4">
|
|
|
+ <div>
|
|
|
+ <h3 className="text-sm font-semibold">
|
|
|
+ {form.provider_id ? t("models.editProviderConnection", "Connection details") : t("models.createProviderConnection", "Create connection")}
|
|
|
+ </h3>
|
|
|
+ <p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
|
+ {form.provider_id
|
|
|
+ ? t("models.credentialReuseHint", "Update credentials here; synced models keep reusing this connection.")
|
|
|
+ : t("models.saveCredentialFirstHint", "Create the credential, then sync the supported models.")}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
|
|
|
- <div className="grid gap-4 lg:grid-cols-[1fr_220px]">
|
|
|
- <Field label={t("modelProviders.providerName")}>
|
|
|
- <Input
|
|
|
- value={form.name}
|
|
|
- onChange={(event) => setField("name", event.target.value)}
|
|
|
- placeholder={providerLabel(form.provider_type, t)}
|
|
|
- />
|
|
|
- </Field>
|
|
|
- <Field label={t("models.provider")}>
|
|
|
- <Select
|
|
|
- value={form.provider_type}
|
|
|
- onChange={(event) => setProviderType(event.target.value)}
|
|
|
- disabled={Boolean(form.provider_id)}
|
|
|
- options={providerPresets.map((preset) => ({
|
|
|
- value: preset.value,
|
|
|
- label: providerLabel(preset.value, t),
|
|
|
- }))}
|
|
|
- />
|
|
|
- </Field>
|
|
|
- </div>
|
|
|
+ <Field label={t("modelProviders.providerName")}>
|
|
|
+ <Input
|
|
|
+ value={form.name}
|
|
|
+ onChange={(event) => setField("name", event.target.value)}
|
|
|
+ placeholder={providerLabel(form.provider_type, t)}
|
|
|
+ />
|
|
|
+ </Field>
|
|
|
|
|
|
- <div className="mt-4 grid gap-4 lg:grid-cols-[1fr_300px]">
|
|
|
- <Field label={t("models.baseUrl")}>
|
|
|
- <Input value={form.base_url} onChange={(event) => setField("base_url", event.target.value)} />
|
|
|
- </Field>
|
|
|
- <Field label={t("models.apiKey")}>
|
|
|
- <Input
|
|
|
- type="password"
|
|
|
- value={form.api_key}
|
|
|
- onChange={(event) => setField("api_key", event.target.value)}
|
|
|
- placeholder={form.provider_id ? t("models.keepSecretPlaceholder") : "sk-..."}
|
|
|
- />
|
|
|
- </Field>
|
|
|
- </div>
|
|
|
+ <div className="grid gap-4 sm:grid-cols-2 2xl:grid-cols-1">
|
|
|
+ <Field label={t("models.provider")}>
|
|
|
+ <Select
|
|
|
+ value={form.provider_type}
|
|
|
+ onChange={(event) => setProviderType(event.target.value)}
|
|
|
+ disabled={Boolean(form.provider_id)}
|
|
|
+ options={providerPresets.map((preset) => ({
|
|
|
+ value: preset.value,
|
|
|
+ label: providerLabel(preset.value, t),
|
|
|
+ }))}
|
|
|
+ />
|
|
|
+ </Field>
|
|
|
+ <Field label={t("models.apiKey")}>
|
|
|
+ <Input
|
|
|
+ type="password"
|
|
|
+ value={form.api_key}
|
|
|
+ onChange={(event) => setField("api_key", event.target.value)}
|
|
|
+ placeholder={form.provider_id ? t("models.keepSecretPlaceholder") : "sk-..."}
|
|
|
+ />
|
|
|
+ </Field>
|
|
|
+ </div>
|
|
|
|
|
|
- {selectedProvider?.models.length ? (
|
|
|
- <div className="mt-5 rounded-md border border-border bg-muted/15 p-3">
|
|
|
- <div className="mb-2 flex items-center justify-between gap-3">
|
|
|
- <p className="text-sm font-medium">{t("models.discoveredModels", "Discovered models")}</p>
|
|
|
- <span className="text-xs text-muted-foreground">{selectedProvider.models.length}</span>
|
|
|
- </div>
|
|
|
- <div className="flex max-h-24 flex-wrap gap-1.5 overflow-auto">
|
|
|
- {selectedProvider.models.slice(0, 12).map((model) => (
|
|
|
- <Badge key={model.model_id} className="border-border bg-surface-elevated text-muted-foreground">
|
|
|
- {model.display_name || model.model_id}
|
|
|
- </Badge>
|
|
|
- ))}
|
|
|
- {selectedProvider.models.length > 12 ? (
|
|
|
- <Badge className="border-border bg-muted text-muted-foreground">+{selectedProvider.models.length - 12}</Badge>
|
|
|
- ) : null}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ) : null}
|
|
|
+ <Field label={t("models.baseUrl")}>
|
|
|
+ <Input value={form.base_url} onChange={(event) => setField("base_url", event.target.value)} />
|
|
|
+ </Field>
|
|
|
|
|
|
- <div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
|
- <Button type="button" variant="outline" onClick={() => void discoverProviderModels()} disabled={discovering || saving || !canSave}>
|
|
|
- <RefreshCw className={`h-4 w-4 ${discovering ? "animate-spin" : ""}`} /> {discovering ? t("modelProviders.discovering") : t("modelProviders.discoverModels")}
|
|
|
- </Button>
|
|
|
- <Button type="button" onClick={() => void saveProvider({ discover: true })} disabled={saving || discovering || !canSave}>
|
|
|
- <Save className="h-4 w-4" /> {saving ? t("common.saving") : t("models.saveCredential", "Save connection")}
|
|
|
- </Button>
|
|
|
+ {selectedProvider?.models.length ? (
|
|
|
+ <div className="rounded-md border border-border bg-muted/15 p-3">
|
|
|
+ <div className="mb-2 flex items-center justify-between gap-3">
|
|
|
+ <p className="text-sm font-medium">{t("models.discoveredModels", "Discovered models")}</p>
|
|
|
+ <span className="text-xs text-muted-foreground">{selectedProvider.models.length}</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex max-h-24 flex-wrap gap-1.5 overflow-auto">
|
|
|
+ {selectedProvider.models.slice(0, 10).map((model) => (
|
|
|
+ <Badge key={model.model_id} className="border-border bg-surface-elevated text-muted-foreground">
|
|
|
+ {model.display_name || model.model_id}
|
|
|
+ </Badge>
|
|
|
+ ))}
|
|
|
+ {selectedProvider.models.length > 10 ? (
|
|
|
+ <Badge className="border-border bg-muted text-muted-foreground">+{selectedProvider.models.length - 10}</Badge>
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ ) : null}
|
|
|
+
|
|
|
+ <div className="grid gap-2 sm:grid-cols-2 2xl:grid-cols-1">
|
|
|
+ <Button type="button" onClick={() => void saveProvider({ discover: true })} disabled={saving || discovering || !canSave}>
|
|
|
+ <Save className="h-4 w-4" /> {saving ? t("common.saving") : t("models.saveCredential", "Save connection")}
|
|
|
+ </Button>
|
|
|
+ <Button type="button" variant="outline" onClick={() => void discoverProviderModels()} disabled={discovering || saving || !canSave}>
|
|
|
+ <RefreshCw className={`h-4 w-4 ${discovering ? "animate-spin" : ""}`} /> {discovering ? t("modelProviders.discovering") : t("modelProviders.discoverModels")}
|
|
|
+ </Button>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </section>
|
|
|
</CardContent>
|
|
|
</Card>
|
|
|
);
|