refine AI provider model layout
This commit is contained in:
1 parent
6cdd5b7a12
commit
1160db7b65
2 files changed
+66
-72
No files matched your search
@@ -168,21 +168,22 @@ export const AiModelCard = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{settings?.providers.map((item, index) => (
|
||||
<AiProviderCard
|
||||
key={item.id}
|
||||
provider={item}
|
||||
defaultDisplayName={getDefaultProviderName(index)}
|
||||
defaultModelId={settings.defaultModelId}
|
||||
readOnly={readOnly}
|
||||
onChanged={refresh}
|
||||
/>
|
||||
))}
|
||||
{!settings?.providers.length ? (
|
||||
<p className="rounded-lg border border-dashed p-5 text-center text-sm text-slate-500">{t("aiModel.noProviders")}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{settings?.providers.length ? (
|
||||
<div className="divide-y border-y">
|
||||
{settings.providers.map((item, index) => (
|
||||
<AiProviderCard
|
||||
key={item.id}
|
||||
provider={item}
|
||||
defaultDisplayName={getDefaultProviderName(index)}
|
||||
defaultModelId={settings.defaultModelId}
|
||||
readOnly={readOnly}
|
||||
onChanged={refresh}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded-lg border border-dashed p-5 text-center text-sm text-slate-500">{t("aiModel.noProviders")}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<Dialog open={showAdd} onOpenChange={handleAddDialogChange}>
|
||||
|
||||
@@ -157,15 +157,40 @@ export const AiProviderCard = ({ provider: saved, defaultDisplayName, defaultMod
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden rounded-lg border bg-white">
|
||||
<div className="flex items-start justify-between gap-3 p-4">
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate text-sm font-semibold text-slate-900">{effectiveDisplayName}</h3>
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500">
|
||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 font-medium text-slate-600">{providerLabel}</span>
|
||||
<span className="max-w-full truncate">{formatBaseUrl(saved.baseUrl)}</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{t("aiModel.modelCount", { count: saved.models.length })}</span>
|
||||
<section>
|
||||
<div className="flex items-start justify-between gap-3 px-4 py-2.5">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<h3 className="truncate text-sm font-semibold text-slate-900">{effectiveDisplayName}</h3>
|
||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-xs font-medium text-slate-600">{providerLabel}</span>
|
||||
<span className="max-w-full truncate text-xs text-slate-500">{formatBaseUrl(saved.baseUrl)}</span>
|
||||
</div>
|
||||
<div className="mt-1.5 flex min-w-0 flex-wrap items-center gap-1.5">
|
||||
{saved.models.length ? saved.models.map((model) => (
|
||||
<span key={model.id} className="inline-flex h-7 max-w-full min-w-0 items-center gap-1 rounded-md bg-slate-100/80 pl-2 pr-1 text-xs text-slate-700">
|
||||
<span className="min-w-0 truncate font-medium">{model.displayName}</span>
|
||||
{model.modelId !== model.displayName ? (
|
||||
<span className="min-w-0 truncate text-slate-500">{model.modelId}</span>
|
||||
) : null}
|
||||
{model.id === defaultModelId ? (
|
||||
<span className="shrink-0 text-[11px] font-medium text-emerald-700">{t("aiModel.defaultBadge")}</span>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 shrink-0 text-slate-400 hover:bg-white hover:text-rose-600"
|
||||
disabled={readOnly || deleteModelMutation.isPending}
|
||||
onClick={() => deleteModel(model)}
|
||||
aria-label={`${t("aiModel.removeModel")}: ${model.displayName}`}
|
||||
title={t("aiModel.removeModel")}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</span>
|
||||
)) : (
|
||||
<span className="text-xs text-slate-400">{t("aiModel.noModels")}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
@@ -178,6 +203,17 @@ export const AiProviderCard = ({ provider: saved, defaultDisplayName, defaultMod
|
||||
aria-label={t("aiModel.serviceEnabled")}
|
||||
onCheckedChange={(checked) => toggleMutation.mutate(checked)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={readOnly}
|
||||
onClick={() => setShowAddModel(true)}
|
||||
aria-label={t("aiModel.addModel")}
|
||||
title={t("aiModel.addModel")}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" variant="ghost" size="icon" disabled={cardBusy}>
|
||||
@@ -198,54 +234,11 @@ export const AiProviderCard = ({ provider: saved, defaultDisplayName, defaultMod
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 border-t bg-slate-50/40 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h4 className="text-sm font-semibold text-slate-900">{t("aiModel.modelsTitle")}</h4>
|
||||
<Button type="button" variant="outline" size="sm" disabled={readOnly} onClick={() => setShowAddModel(true)}>
|
||||
<Plus className="h-4 w-4" />{t("aiModel.addModel")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{saved.models.length ? (
|
||||
<div className="divide-y overflow-hidden rounded-md border bg-white">
|
||||
{saved.models.map((model) => (
|
||||
<div key={model.id} className="group flex min-w-0 items-center justify-between gap-3 px-3 py-2.5">
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<p className="truncate text-sm text-slate-800">{model.displayName}</p>
|
||||
{model.id === defaultModelId ? (
|
||||
<span className="shrink-0 rounded-full bg-emerald-50 px-2 py-0.5 text-[11px] font-medium text-emerald-700">
|
||||
{t("aiModel.defaultBadge")}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{model.modelId !== model.displayName ? <p className="mt-0.5 truncate text-xs text-slate-500">{model.modelId}</p> : null}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 text-slate-400 hover:text-rose-600"
|
||||
disabled={readOnly || deleteModelMutation.isPending}
|
||||
onClick={() => deleteModel(model)}
|
||||
aria-label={t("aiModel.removeModel")}
|
||||
title={t("aiModel.removeModel")}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded-md border border-dashed bg-white p-4 text-center text-xs text-slate-500">{t("aiModel.noModels")}</p>
|
||||
)}
|
||||
|
||||
{cardError ? (
|
||||
<p className="text-xs font-medium text-rose-600" role="alert">
|
||||
{aiErrorMessage(cardError, t("aiModel.failed"), t("aiModel.encryptionKeyMissing"))}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{cardError ? (
|
||||
<p className="border-t px-4 py-3 text-xs font-medium text-rose-600" role="alert">
|
||||
{aiErrorMessage(cardError, t("aiModel.failed"), t("aiModel.encryptionKeyMissing"))}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<Dialog open={showConnection} onOpenChange={handleConnectionChange}>
|
||||
<DialogContent className="max-h-[calc(100vh-2rem)] overflow-y-auto sm:max-w-2xl">
|
||||
|
||||
Reference in new issue
Block a user