mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-07 21:08:34 +03:00
refactor(webui): expand model preset editor inline
This commit is contained in:
@@ -3375,9 +3375,10 @@ function ModelsSettings({
|
|||||||
);
|
);
|
||||||
const callOrderBusy = orderSaving || saving;
|
const callOrderBusy = orderSaving || saving;
|
||||||
const selectPreset = (preset: SettingsPayload["model_presets"][number]) => {
|
const selectPreset = (preset: SettingsPayload["model_presets"][number]) => {
|
||||||
|
const toggleCurrentPreset = !creating && selectedPreset?.name === preset.name;
|
||||||
onSelectConfiguration();
|
onSelectConfiguration();
|
||||||
if (!creating && selectedPreset?.name === preset.name) {
|
if (toggleCurrentPreset) {
|
||||||
setEditorOpen(true);
|
setEditorOpen((open) => !open);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
@@ -3393,11 +3394,6 @@ function ModelsSettings({
|
|||||||
}));
|
}));
|
||||||
setEditorOpen(true);
|
setEditorOpen(true);
|
||||||
};
|
};
|
||||||
const closeEditor = () => {
|
|
||||||
if (creatingSaving) return;
|
|
||||||
if (creating) onCancelCreate();
|
|
||||||
setEditorOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveCallOrderItem = (index: number, offset: -1 | 1) => {
|
const moveCallOrderItem = (index: number, offset: -1 | 1) => {
|
||||||
if (callOrderBusy) return;
|
if (callOrderBusy) return;
|
||||||
@@ -3436,316 +3432,20 @@ function ModelsSettings({
|
|||||||
onChangeCallOrder(next);
|
onChangeCallOrder(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const renderPresetEditor = (key: string) => (
|
||||||
<div className="space-y-7">
|
|
||||||
<section>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
{tx("settings.models.presets", "Model presets")}
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsGroup>
|
|
||||||
{!settings.model_call_order_editable ? (
|
|
||||||
<div className="flex flex-col gap-4 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-5">
|
|
||||||
<div className="flex min-w-0 items-start gap-3">
|
|
||||||
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-[12px] bg-muted text-muted-foreground">
|
|
||||||
<ListOrdered className="h-4 w-4" aria-hidden />
|
|
||||||
</span>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-[14px] font-medium text-foreground">
|
|
||||||
{tx("settings.models.convertTitle", "Convert the current model setup")}
|
|
||||||
</p>
|
|
||||||
<p className="mt-0.5 max-w-[34rem] text-[12px] leading-5 text-muted-foreground">
|
|
||||||
{tx(
|
|
||||||
"settings.models.convertHelp",
|
|
||||||
"Turn the existing primary and fallback models into presets so their order can be managed here.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="shrink-0 rounded-full"
|
|
||||||
disabled={migrationSaving}
|
|
||||||
onClick={onMigrate}
|
|
||||||
>
|
|
||||||
{migrationSaving ? (
|
|
||||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
|
|
||||||
) : null}
|
|
||||||
{migrationSaving
|
|
||||||
? tx("settings.models.converting", "Converting...")
|
|
||||||
: tx("settings.models.convertAction", "Convert to presets")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="xl:grid xl:grid-cols-[minmax(0,5fr)_minmax(0,6fr)] xl:divide-x xl:divide-border/45">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"min-w-0 divide-y divide-border/45",
|
|
||||||
editorOpen && "hidden xl:block",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div role="list" className="divide-y divide-border/45">
|
|
||||||
{presetRows.map(({ key, name, orderIndex, preset }) => {
|
|
||||||
const ordered = orderIndex >= 0;
|
|
||||||
const provider = preset
|
|
||||||
? modelPresetProviderKey(preset, settings)
|
|
||||||
: settings.agent.resolved_provider ?? settings.agent.provider;
|
|
||||||
const presetConfigured = preset
|
|
||||||
? settingsProviderConfigured(
|
|
||||||
settings,
|
|
||||||
preset.provider,
|
|
||||||
preset.resolved_provider,
|
|
||||||
)
|
|
||||||
: true;
|
|
||||||
const isDropTarget =
|
|
||||||
ordered &&
|
|
||||||
dragOverCallOrderIndex === orderIndex &&
|
|
||||||
draggedCallOrderIndex !== orderIndex;
|
|
||||||
const dropAfterTarget =
|
|
||||||
isDropTarget &&
|
|
||||||
draggedCallOrderIndex !== null &&
|
|
||||||
draggedCallOrderIndex < orderIndex;
|
|
||||||
const isSelected =
|
|
||||||
editorOpen && !creating && selectedPreset?.name === name;
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
role="listitem"
|
id="model-preset-editor"
|
||||||
tabIndex={ordered ? 0 : -1}
|
data-testid="model-preset-editor"
|
||||||
draggable={ordered && !callOrderBusy}
|
className="divide-y divide-border/45 bg-muted/10 motion-reduce:animate-none animate-in fade-in-0 slide-in-from-top-1 duration-200"
|
||||||
aria-label={
|
|
||||||
ordered
|
|
||||||
? `${preset?.label ?? name}. ${tx(
|
|
||||||
"settings.models.dragToReorder",
|
|
||||||
"Drag to reorder",
|
|
||||||
)}`
|
|
||||||
: preset?.label ?? name
|
|
||||||
}
|
|
||||||
data-testid={`model-call-order-row-${name}`}
|
|
||||||
onDragStart={(event) => {
|
|
||||||
if (!ordered || callOrderBusy) {
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.dataTransfer.effectAllowed = "move";
|
|
||||||
event.dataTransfer.setData("text/plain", name);
|
|
||||||
setDraggedCallOrderIndex(orderIndex);
|
|
||||||
setDragOverCallOrderIndex(orderIndex);
|
|
||||||
}}
|
|
||||||
onDragEnd={() => {
|
|
||||||
setDraggedCallOrderIndex(null);
|
|
||||||
setDragOverCallOrderIndex(null);
|
|
||||||
}}
|
|
||||||
onDragEnter={(event) => {
|
|
||||||
if (ordered && draggedCallOrderIndex !== null) {
|
|
||||||
event.preventDefault();
|
|
||||||
setDragOverCallOrderIndex(orderIndex);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragOver={(event) => {
|
|
||||||
if (!ordered || draggedCallOrderIndex === null) return;
|
|
||||||
event.preventDefault();
|
|
||||||
event.dataTransfer.dropEffect = "move";
|
|
||||||
}}
|
|
||||||
onDrop={(event) => {
|
|
||||||
if (!ordered) return;
|
|
||||||
event.preventDefault();
|
|
||||||
dropCallOrderItem(orderIndex);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.currentTarget !== event.target) return;
|
|
||||||
if (ordered && event.key === "ArrowUp") {
|
|
||||||
event.preventDefault();
|
|
||||||
moveCallOrderItem(orderIndex, -1);
|
|
||||||
} else if (ordered && event.key === "ArrowDown") {
|
|
||||||
event.preventDefault();
|
|
||||||
moveCallOrderItem(orderIndex, 1);
|
|
||||||
} else if ((event.key === "Enter" || event.key === " ") && preset) {
|
|
||||||
event.preventDefault();
|
|
||||||
selectPreset(preset);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"group relative flex min-h-[76px] select-none items-center gap-3 px-4 py-3 outline-none transition-[background-color,opacity] duration-150 sm:px-5",
|
|
||||||
ordered &&
|
|
||||||
(callOrderBusy
|
|
||||||
? "cursor-wait"
|
|
||||||
: "cursor-grab active:cursor-grabbing"),
|
|
||||||
"hover:bg-muted/25",
|
|
||||||
isDropTarget &&
|
|
||||||
!dropAfterTarget &&
|
|
||||||
"before:absolute before:inset-x-4 before:top-0 before:z-10 before:h-0.5 before:rounded-full before:bg-foreground sm:before:inset-x-5",
|
|
||||||
isDropTarget &&
|
|
||||||
dropAfterTarget &&
|
|
||||||
"after:absolute after:inset-x-4 after:bottom-0 after:z-10 after:h-0.5 after:rounded-full after:bg-foreground sm:after:inset-x-5",
|
|
||||||
ordered && draggedCallOrderIndex === orderIndex && "opacity-35",
|
|
||||||
isSelected && "bg-muted/45 hover:bg-muted/45",
|
|
||||||
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{ordered ? (
|
{creating ? (
|
||||||
<GripVertical
|
<div className="flex min-h-[52px] items-center px-4 py-3 sm:px-5">
|
||||||
className="pointer-events-none h-4 w-4 shrink-0 text-muted-foreground/40 transition-colors group-hover:text-muted-foreground"
|
<span className="text-[13px] font-semibold text-foreground/85">
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="h-4 w-4 shrink-0" aria-hidden />
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-pressed={isSelected}
|
|
||||||
aria-controls="model-preset-detail"
|
|
||||||
disabled={!preset}
|
|
||||||
onClick={() => preset && selectPreset(preset)}
|
|
||||||
className="flex min-w-0 flex-1 items-center gap-3 rounded-[12px] text-left outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
>
|
|
||||||
{ordered ? (
|
|
||||||
<span className="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-muted font-mono text-[11px] font-semibold tabular-nums text-muted-foreground">
|
|
||||||
{orderIndex + 1}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="h-7 w-7 shrink-0" aria-hidden />
|
|
||||||
)}
|
|
||||||
<ProviderPickerIcon
|
|
||||||
provider={provider}
|
|
||||||
showBrandLogos={showBrandLogos}
|
|
||||||
unconfigured={!presetConfigured}
|
|
||||||
/>
|
|
||||||
<span className="min-w-0 flex-1">
|
|
||||||
<span className="flex min-w-0 flex-wrap items-center gap-2">
|
|
||||||
<span className="truncate text-[14px] font-medium text-foreground">
|
|
||||||
{preset?.label ?? name}
|
|
||||||
</span>
|
|
||||||
{orderIndex === 0 ? (
|
|
||||||
<StatusPill tone="success">
|
|
||||||
{tx("settings.models.primary", "Primary")}
|
|
||||||
</StatusPill>
|
|
||||||
) : !ordered ? (
|
|
||||||
<StatusPill tone="neutral">
|
|
||||||
{tx("settings.models.disabled", "Disabled")}
|
|
||||||
</StatusPill>
|
|
||||||
) : null}
|
|
||||||
{!presetConfigured ? (
|
|
||||||
<span className="text-[11px] font-medium text-amber-700 dark:text-amber-300">
|
|
||||||
{tx(
|
|
||||||
"settings.models.providerSetupRequired",
|
|
||||||
"Provider setup required",
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
<span className="mt-0.5 block truncate text-[12px] text-muted-foreground">
|
|
||||||
{preset?.model ?? name}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<ChevronRight
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-4 shrink-0 text-muted-foreground transition-[color,transform] group-hover:translate-x-0.5",
|
|
||||||
isSelected && "text-foreground",
|
|
||||||
)}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="switch"
|
|
||||||
aria-checked={ordered}
|
|
||||||
aria-label={
|
|
||||||
ordered
|
|
||||||
? tx("settings.models.removeFromOrder", "Disable preset")
|
|
||||||
: tx("settings.models.addToOrder", "Enable preset")
|
|
||||||
}
|
|
||||||
disabled={callOrderBusy || (ordered && callOrder.length <= 1)}
|
|
||||||
onClick={() => {
|
|
||||||
if (ordered) {
|
|
||||||
removeCallOrderItem(orderIndex);
|
|
||||||
} else if (preset) {
|
|
||||||
onChangeCallOrder([...callOrder, preset.name]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-40",
|
|
||||||
ordered ? "bg-foreground" : "bg-muted-foreground/25",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-4 rounded-full bg-background shadow-sm transition-transform",
|
|
||||||
ordered ? "translate-x-[18px]" : "translate-x-0.5",
|
|
||||||
)}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="flex min-h-[58px] flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5">
|
|
||||||
{!creating ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="rounded-full"
|
|
||||||
disabled={callOrderBusy}
|
|
||||||
onClick={() => {
|
|
||||||
setEditorOpen(true);
|
|
||||||
onBeginCreate();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
|
||||||
{tx("settings.models.newPreset", "New model preset")}
|
{tx("settings.models.newPreset", "New model preset")}
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<span />
|
|
||||||
)}
|
|
||||||
{orderSaving ? (
|
|
||||||
<SettingsStatusMessage>
|
|
||||||
<span className="inline-flex items-center gap-1.5">
|
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />
|
|
||||||
{tx("settings.actions.saving", "Saving...")}
|
|
||||||
</span>
|
</span>
|
||||||
</SettingsStatusMessage>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="model-preset-detail"
|
|
||||||
className={cn(
|
|
||||||
"min-w-0 divide-y divide-border/45",
|
|
||||||
!editorOpen && "hidden xl:block",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{editorOpen && (selectedPreset || creating) ? (
|
|
||||||
<>
|
|
||||||
<div className="flex min-h-[64px] items-center gap-2 bg-muted/15 px-4 py-3 sm:px-5">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeEditor}
|
|
||||||
disabled={creatingSaving}
|
|
||||||
aria-label={tx(
|
|
||||||
"settings.models.backToPresets",
|
|
||||||
"Back to model presets",
|
|
||||||
)}
|
|
||||||
className="-ml-2 grid h-8 w-8 shrink-0 place-items-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-40 xl:hidden"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4" aria-hidden />
|
|
||||||
</button>
|
|
||||||
<span className="min-w-0">
|
|
||||||
<span className="block truncate text-[14px] font-semibold text-foreground/90">
|
|
||||||
{creating
|
|
||||||
? tx("settings.models.newPreset", "New model preset")
|
|
||||||
: selectedPreset?.label}
|
|
||||||
</span>
|
|
||||||
<span className="mt-0.5 block truncate text-[12px] text-muted-foreground">
|
|
||||||
{creating
|
|
||||||
? tx(
|
|
||||||
"settings.models.newPresetHelp",
|
|
||||||
"Save a reusable model and its generation settings.",
|
|
||||||
)
|
|
||||||
: tx("settings.models.editPreset", "Edit preset")}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<SettingsRow title={tx("settings.models.presetName", "Preset name")}>
|
<SettingsRow title={tx("settings.models.presetName", "Preset name")}>
|
||||||
<Input
|
<Input
|
||||||
autoFocus={creating}
|
autoFocus={creating}
|
||||||
@@ -3853,7 +3553,10 @@ function ModelsSettings({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="self-start rounded-full text-muted-foreground"
|
className="self-start rounded-full text-muted-foreground"
|
||||||
disabled={creatingSaving}
|
disabled={creatingSaving}
|
||||||
onClick={closeEditor}
|
onClick={() => {
|
||||||
|
setEditorOpen(false);
|
||||||
|
onCancelCreate();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{tx("settings.actions.cancel", "Cancel")}
|
{tx("settings.actions.cancel", "Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -3897,27 +3600,278 @@ function ModelsSettings({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
) : (
|
);
|
||||||
<div className="flex min-h-[320px] items-center justify-center px-8 py-12 text-center">
|
|
||||||
<div className="max-w-[16rem]">
|
return (
|
||||||
<span className="mx-auto grid h-10 w-10 place-items-center rounded-[14px] bg-muted text-muted-foreground">
|
<div className="space-y-7">
|
||||||
<Pencil className="h-4 w-4" aria-hidden />
|
<section>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{tx("settings.models.presets", "Model presets")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsGroup>
|
||||||
|
{!settings.model_call_order_editable ? (
|
||||||
|
<div className="flex flex-col gap-4 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-5">
|
||||||
|
<div className="flex min-w-0 items-start gap-3">
|
||||||
|
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-[12px] bg-muted text-muted-foreground">
|
||||||
|
<ListOrdered className="h-4 w-4" aria-hidden />
|
||||||
</span>
|
</span>
|
||||||
<p className="mt-3 text-[14px] font-medium text-foreground">
|
<div className="min-w-0">
|
||||||
{tx("settings.models.selectPreset", "Select a preset")}
|
<p className="text-[14px] font-medium text-foreground">
|
||||||
|
{tx("settings.models.convertTitle", "Convert the current model setup")}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-[12px] leading-5 text-muted-foreground">
|
<p className="mt-0.5 max-w-[34rem] text-[12px] leading-5 text-muted-foreground">
|
||||||
{tx(
|
{tx(
|
||||||
"settings.models.selectPresetHelp",
|
"settings.models.convertHelp",
|
||||||
"Select a preset from the list to edit its model and generation settings.",
|
"Turn the existing primary and fallback models into presets so their order can be managed here.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="shrink-0 rounded-full"
|
||||||
|
disabled={migrationSaving}
|
||||||
|
onClick={onMigrate}
|
||||||
|
>
|
||||||
|
{migrationSaving ? (
|
||||||
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
|
||||||
|
) : null}
|
||||||
|
{migrationSaving
|
||||||
|
? tx("settings.models.converting", "Converting...")
|
||||||
|
: tx("settings.models.convertAction", "Convert to presets")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div role="list" className="divide-y divide-border/45">
|
||||||
|
{presetRows.map(({ key, name, orderIndex, preset }) => {
|
||||||
|
const ordered = orderIndex >= 0;
|
||||||
|
const provider = preset
|
||||||
|
? modelPresetProviderKey(preset, settings)
|
||||||
|
: settings.agent.resolved_provider ?? settings.agent.provider;
|
||||||
|
const presetConfigured = preset
|
||||||
|
? settingsProviderConfigured(
|
||||||
|
settings,
|
||||||
|
preset.provider,
|
||||||
|
preset.resolved_provider,
|
||||||
|
)
|
||||||
|
: true;
|
||||||
|
const isDropTarget =
|
||||||
|
ordered &&
|
||||||
|
dragOverCallOrderIndex === orderIndex &&
|
||||||
|
draggedCallOrderIndex !== orderIndex;
|
||||||
|
const dropAfterTarget =
|
||||||
|
isDropTarget &&
|
||||||
|
draggedCallOrderIndex !== null &&
|
||||||
|
draggedCallOrderIndex < orderIndex;
|
||||||
|
const isSelected =
|
||||||
|
editorOpen && !creating && selectedPreset?.name === name;
|
||||||
|
return [
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
role="listitem"
|
||||||
|
tabIndex={ordered ? 0 : -1}
|
||||||
|
draggable={ordered && !callOrderBusy}
|
||||||
|
aria-label={
|
||||||
|
ordered
|
||||||
|
? `${preset?.label ?? name}. ${tx(
|
||||||
|
"settings.models.dragToReorder",
|
||||||
|
"Drag to reorder",
|
||||||
|
)}`
|
||||||
|
: preset?.label ?? name
|
||||||
|
}
|
||||||
|
data-testid={`model-call-order-row-${name}`}
|
||||||
|
onDragStart={(event) => {
|
||||||
|
if (!ordered || callOrderBusy) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.dataTransfer.effectAllowed = "move";
|
||||||
|
event.dataTransfer.setData("text/plain", name);
|
||||||
|
setDraggedCallOrderIndex(orderIndex);
|
||||||
|
setDragOverCallOrderIndex(orderIndex);
|
||||||
|
}}
|
||||||
|
onDragEnd={() => {
|
||||||
|
setDraggedCallOrderIndex(null);
|
||||||
|
setDragOverCallOrderIndex(null);
|
||||||
|
}}
|
||||||
|
onDragEnter={(event) => {
|
||||||
|
if (ordered && draggedCallOrderIndex !== null) {
|
||||||
|
event.preventDefault();
|
||||||
|
setDragOverCallOrderIndex(orderIndex);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDragOver={(event) => {
|
||||||
|
if (!ordered || draggedCallOrderIndex === null) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = "move";
|
||||||
|
}}
|
||||||
|
onDrop={(event) => {
|
||||||
|
if (!ordered) return;
|
||||||
|
event.preventDefault();
|
||||||
|
dropCallOrderItem(orderIndex);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.currentTarget !== event.target) return;
|
||||||
|
if (ordered && event.key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
moveCallOrderItem(orderIndex, -1);
|
||||||
|
} else if (ordered && event.key === "ArrowDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
moveCallOrderItem(orderIndex, 1);
|
||||||
|
} else if ((event.key === "Enter" || event.key === " ") && preset) {
|
||||||
|
event.preventDefault();
|
||||||
|
selectPreset(preset);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"group relative flex min-h-[76px] select-none items-center gap-3 px-4 py-3 outline-none transition-[background-color,opacity] duration-150 sm:px-5",
|
||||||
|
ordered &&
|
||||||
|
(callOrderBusy
|
||||||
|
? "cursor-wait"
|
||||||
|
: "cursor-grab active:cursor-grabbing"),
|
||||||
|
"hover:bg-muted/25",
|
||||||
|
isDropTarget &&
|
||||||
|
!dropAfterTarget &&
|
||||||
|
"before:absolute before:inset-x-4 before:top-0 before:z-10 before:h-0.5 before:rounded-full before:bg-foreground sm:before:inset-x-5",
|
||||||
|
isDropTarget &&
|
||||||
|
dropAfterTarget &&
|
||||||
|
"after:absolute after:inset-x-4 after:bottom-0 after:z-10 after:h-0.5 after:rounded-full after:bg-foreground sm:after:inset-x-5",
|
||||||
|
ordered && draggedCallOrderIndex === orderIndex && "opacity-35",
|
||||||
|
isSelected && "bg-muted/45 hover:bg-muted/45",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
{ordered ? (
|
||||||
|
<GripVertical
|
||||||
|
className="pointer-events-none h-4 w-4 shrink-0 text-muted-foreground/40 transition-colors group-hover:text-muted-foreground"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="h-4 w-4 shrink-0" aria-hidden />
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-pressed={selectedPreset?.name === name}
|
||||||
|
aria-expanded={isSelected}
|
||||||
|
aria-controls={isSelected ? "model-preset-editor" : undefined}
|
||||||
|
disabled={!preset}
|
||||||
|
onClick={() => preset && selectPreset(preset)}
|
||||||
|
className="flex min-w-0 flex-1 items-center gap-3 rounded-[12px] text-left outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
>
|
||||||
|
{ordered ? (
|
||||||
|
<span className="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-muted font-mono text-[11px] font-semibold tabular-nums text-muted-foreground">
|
||||||
|
{orderIndex + 1}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="h-7 w-7 shrink-0" aria-hidden />
|
||||||
|
)}
|
||||||
|
<ProviderPickerIcon
|
||||||
|
provider={provider}
|
||||||
|
showBrandLogos={showBrandLogos}
|
||||||
|
unconfigured={!presetConfigured}
|
||||||
|
/>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
|
<span className="truncate text-[14px] font-medium text-foreground">
|
||||||
|
{preset?.label ?? name}
|
||||||
|
</span>
|
||||||
|
{orderIndex === 0 ? (
|
||||||
|
<StatusPill tone="success">
|
||||||
|
{tx("settings.models.primary", "Primary")}
|
||||||
|
</StatusPill>
|
||||||
|
) : !ordered ? (
|
||||||
|
<StatusPill tone="neutral">
|
||||||
|
{tx("settings.models.disabled", "Disabled")}
|
||||||
|
</StatusPill>
|
||||||
|
) : null}
|
||||||
|
{!presetConfigured ? (
|
||||||
|
<span className="text-[11px] font-medium text-amber-700 dark:text-amber-300">
|
||||||
|
{tx(
|
||||||
|
"settings.models.providerSetupRequired",
|
||||||
|
"Provider setup required",
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
<span className="mt-0.5 block truncate text-[12px] text-muted-foreground">
|
||||||
|
{preset?.model ?? name}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<ChevronRight
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 shrink-0 text-muted-foreground transition-transform",
|
||||||
|
isSelected && "rotate-90",
|
||||||
|
)}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={ordered}
|
||||||
|
aria-label={
|
||||||
|
ordered
|
||||||
|
? tx("settings.models.removeFromOrder", "Disable preset")
|
||||||
|
: tx("settings.models.addToOrder", "Enable preset")
|
||||||
|
}
|
||||||
|
disabled={callOrderBusy || (ordered && callOrder.length <= 1)}
|
||||||
|
onClick={() => {
|
||||||
|
if (ordered) {
|
||||||
|
removeCallOrderItem(orderIndex);
|
||||||
|
} else if (preset) {
|
||||||
|
onChangeCallOrder([...callOrder, preset.name]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-40",
|
||||||
|
ordered ? "bg-foreground" : "bg-muted-foreground/25",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 rounded-full bg-background shadow-sm transition-transform",
|
||||||
|
ordered ? "translate-x-[18px]" : "translate-x-0.5",
|
||||||
|
)}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>,
|
||||||
|
isSelected ? renderPresetEditor(`editor:${key}`) : null,
|
||||||
|
];
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex min-h-[58px] flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5">
|
||||||
|
{!creating ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="rounded-full"
|
||||||
|
disabled={callOrderBusy}
|
||||||
|
onClick={() => {
|
||||||
|
setEditorOpen(true);
|
||||||
|
onBeginCreate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
|
{tx("settings.models.newPreset", "New model preset")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{orderSaving ? (
|
||||||
|
<SettingsStatusMessage>
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden />
|
||||||
|
{tx("settings.actions.saving", "Saving...")}
|
||||||
|
</span>
|
||||||
|
</SettingsStatusMessage>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{creating && editorOpen ? renderPresetEditor("editor:new") : null}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -132,9 +132,6 @@
|
|||||||
"newPresetHelp": "Save a reusable model and its generation settings.",
|
"newPresetHelp": "Save a reusable model and its generation settings.",
|
||||||
"presets": "Model presets",
|
"presets": "Model presets",
|
||||||
"editPreset": "Edit preset",
|
"editPreset": "Edit preset",
|
||||||
"backToPresets": "Back to model presets",
|
|
||||||
"selectPreset": "Select a preset",
|
|
||||||
"selectPresetHelp": "Select a preset from the list to edit its model and generation settings.",
|
|
||||||
"presetName": "Preset name",
|
"presetName": "Preset name",
|
||||||
"presetNameHelp": "A short name used throughout model settings.",
|
"presetNameHelp": "A short name used throughout model settings.",
|
||||||
"presetNamePlaceholder": "Fast writing",
|
"presetNamePlaceholder": "Fast writing",
|
||||||
|
|||||||
@@ -408,9 +408,6 @@
|
|||||||
"newPresetHelp": "Guarda un modelo reutilizable y sus ajustes de generación.",
|
"newPresetHelp": "Guarda un modelo reutilizable y sus ajustes de generación.",
|
||||||
"presets": "Preajustes de modelo",
|
"presets": "Preajustes de modelo",
|
||||||
"editPreset": "Editar preajuste",
|
"editPreset": "Editar preajuste",
|
||||||
"backToPresets": "Volver a los preajustes de modelo",
|
|
||||||
"selectPreset": "Seleccionar un preajuste",
|
|
||||||
"selectPresetHelp": "Selecciona un preajuste de la lista para editar su modelo y los ajustes de generación.",
|
|
||||||
"presetName": "Nombre del preajuste",
|
"presetName": "Nombre del preajuste",
|
||||||
"presetNameHelp": "Un nombre corto para usar en los ajustes de modelos.",
|
"presetNameHelp": "Un nombre corto para usar en los ajustes de modelos.",
|
||||||
"presetNamePlaceholder": "Escritura rápida",
|
"presetNamePlaceholder": "Escritura rápida",
|
||||||
|
|||||||
@@ -408,9 +408,6 @@
|
|||||||
"newPresetHelp": "Enregistrez un modèle réutilisable et ses paramètres de génération.",
|
"newPresetHelp": "Enregistrez un modèle réutilisable et ses paramètres de génération.",
|
||||||
"presets": "Préréglages de modèle",
|
"presets": "Préréglages de modèle",
|
||||||
"editPreset": "Modifier le préréglage",
|
"editPreset": "Modifier le préréglage",
|
||||||
"backToPresets": "Retour aux préréglages de modèle",
|
|
||||||
"selectPreset": "Sélectionner un préréglage",
|
|
||||||
"selectPresetHelp": "Sélectionnez un préréglage dans la liste pour modifier son modèle et ses paramètres de génération.",
|
|
||||||
"presetName": "Nom du préréglage",
|
"presetName": "Nom du préréglage",
|
||||||
"presetNameHelp": "Un nom court utilisé dans les paramètres des modèles.",
|
"presetNameHelp": "Un nom court utilisé dans les paramètres des modèles.",
|
||||||
"presetNamePlaceholder": "Rédaction rapide",
|
"presetNamePlaceholder": "Rédaction rapide",
|
||||||
|
|||||||
@@ -408,9 +408,6 @@
|
|||||||
"newPresetHelp": "Simpan model yang dapat digunakan kembali beserta pengaturan generasinya.",
|
"newPresetHelp": "Simpan model yang dapat digunakan kembali beserta pengaturan generasinya.",
|
||||||
"presets": "Prasetel model",
|
"presets": "Prasetel model",
|
||||||
"editPreset": "Ubah prasetel",
|
"editPreset": "Ubah prasetel",
|
||||||
"backToPresets": "Kembali ke prasetel model",
|
|
||||||
"selectPreset": "Pilih prasetel",
|
|
||||||
"selectPresetHelp": "Pilih prasetel dari daftar untuk mengubah model dan pengaturan generasinya.",
|
|
||||||
"presetName": "Nama prasetel",
|
"presetName": "Nama prasetel",
|
||||||
"presetNameHelp": "Nama singkat yang digunakan di pengaturan model.",
|
"presetNameHelp": "Nama singkat yang digunakan di pengaturan model.",
|
||||||
"presetNamePlaceholder": "Menulis cepat",
|
"presetNamePlaceholder": "Menulis cepat",
|
||||||
|
|||||||
@@ -408,9 +408,6 @@
|
|||||||
"newPresetHelp": "再利用できるモデルと生成設定を保存します。",
|
"newPresetHelp": "再利用できるモデルと生成設定を保存します。",
|
||||||
"presets": "モデルプリセット",
|
"presets": "モデルプリセット",
|
||||||
"editPreset": "プリセットを編集",
|
"editPreset": "プリセットを編集",
|
||||||
"backToPresets": "モデルプリセットに戻る",
|
|
||||||
"selectPreset": "プリセットを選択",
|
|
||||||
"selectPresetHelp": "リストからプリセットを選択して、モデルと生成設定を編集します。",
|
|
||||||
"presetName": "プリセット名",
|
"presetName": "プリセット名",
|
||||||
"presetNameHelp": "モデル設定で使用する短い名前です。",
|
"presetNameHelp": "モデル設定で使用する短い名前です。",
|
||||||
"presetNamePlaceholder": "高速執筆",
|
"presetNamePlaceholder": "高速執筆",
|
||||||
|
|||||||
@@ -408,9 +408,6 @@
|
|||||||
"newPresetHelp": "재사용할 모델과 생성 설정을 저장합니다.",
|
"newPresetHelp": "재사용할 모델과 생성 설정을 저장합니다.",
|
||||||
"presets": "모델 프리셋",
|
"presets": "모델 프리셋",
|
||||||
"editPreset": "프리셋 편집",
|
"editPreset": "프리셋 편집",
|
||||||
"backToPresets": "모델 프리셋으로 돌아가기",
|
|
||||||
"selectPreset": "프리셋 선택",
|
|
||||||
"selectPresetHelp": "목록에서 프리셋을 선택하여 모델과 생성 설정을 편집하세요.",
|
|
||||||
"presetName": "프리셋 이름",
|
"presetName": "프리셋 이름",
|
||||||
"presetNameHelp": "모델 설정에서 사용할 짧은 이름입니다.",
|
"presetNameHelp": "모델 설정에서 사용할 짧은 이름입니다.",
|
||||||
"presetNamePlaceholder": "빠른 작성",
|
"presetNamePlaceholder": "빠른 작성",
|
||||||
|
|||||||
@@ -132,9 +132,6 @@
|
|||||||
"newPresetHelp": "Salve um modelo reutilizável e suas configurações de geração.",
|
"newPresetHelp": "Salve um modelo reutilizável e suas configurações de geração.",
|
||||||
"presets": "Predefinições de modelo",
|
"presets": "Predefinições de modelo",
|
||||||
"editPreset": "Editar predefinição",
|
"editPreset": "Editar predefinição",
|
||||||
"backToPresets": "Voltar às predefinições de modelo",
|
|
||||||
"selectPreset": "Selecionar uma predefinição",
|
|
||||||
"selectPresetHelp": "Selecione uma predefinição na lista para editar o modelo e as configurações de geração.",
|
|
||||||
"presetName": "Nome da predefinição",
|
"presetName": "Nome da predefinição",
|
||||||
"presetNameHelp": "Um nome curto usado nas configurações de modelo.",
|
"presetNameHelp": "Um nome curto usado nas configurações de modelo.",
|
||||||
"presetNamePlaceholder": "Escrita rápida",
|
"presetNamePlaceholder": "Escrita rápida",
|
||||||
|
|||||||
@@ -408,9 +408,6 @@
|
|||||||
"newPresetHelp": "Lưu mô hình có thể tái sử dụng cùng các thiết lập sinh nội dung.",
|
"newPresetHelp": "Lưu mô hình có thể tái sử dụng cùng các thiết lập sinh nội dung.",
|
||||||
"presets": "Cấu hình mô hình đặt trước",
|
"presets": "Cấu hình mô hình đặt trước",
|
||||||
"editPreset": "Sửa cấu hình đặt trước",
|
"editPreset": "Sửa cấu hình đặt trước",
|
||||||
"backToPresets": "Quay lại cấu hình mô hình đặt trước",
|
|
||||||
"selectPreset": "Chọn một cấu hình đặt trước",
|
|
||||||
"selectPresetHelp": "Chọn một cấu hình đặt trước trong danh sách để chỉnh sửa mô hình và cài đặt sinh nội dung.",
|
|
||||||
"presetName": "Tên cấu hình đặt trước",
|
"presetName": "Tên cấu hình đặt trước",
|
||||||
"presetNameHelp": "Tên ngắn dùng trong phần thiết lập mô hình.",
|
"presetNameHelp": "Tên ngắn dùng trong phần thiết lập mô hình.",
|
||||||
"presetNamePlaceholder": "Viết nhanh",
|
"presetNamePlaceholder": "Viết nhanh",
|
||||||
|
|||||||
@@ -132,9 +132,6 @@
|
|||||||
"newPresetHelp": "保存可复用的模型及其生成参数。",
|
"newPresetHelp": "保存可复用的模型及其生成参数。",
|
||||||
"presets": "模型预设",
|
"presets": "模型预设",
|
||||||
"editPreset": "编辑预设",
|
"editPreset": "编辑预设",
|
||||||
"backToPresets": "返回模型预设",
|
|
||||||
"selectPreset": "选择一个预设",
|
|
||||||
"selectPresetHelp": "从列表中选择一个预设,编辑它的模型和生成参数。",
|
|
||||||
"presetName": "预设名称",
|
"presetName": "预设名称",
|
||||||
"presetNameHelp": "在模型设置中使用的简短名称。",
|
"presetNameHelp": "在模型设置中使用的简短名称。",
|
||||||
"presetNamePlaceholder": "快速写作",
|
"presetNamePlaceholder": "快速写作",
|
||||||
|
|||||||
@@ -408,9 +408,6 @@
|
|||||||
"newPresetHelp": "儲存可重複使用的模型與生成參數。",
|
"newPresetHelp": "儲存可重複使用的模型與生成參數。",
|
||||||
"presets": "模型預設",
|
"presets": "模型預設",
|
||||||
"editPreset": "編輯預設",
|
"editPreset": "編輯預設",
|
||||||
"backToPresets": "返回模型預設",
|
|
||||||
"selectPreset": "選擇一個預設",
|
|
||||||
"selectPresetHelp": "從列表中選擇一個預設,編輯它的模型與生成參數。",
|
|
||||||
"presetName": "預設名稱",
|
"presetName": "預設名稱",
|
||||||
"presetNameHelp": "模型設定中使用的簡短名稱。",
|
"presetNameHelp": "模型設定中使用的簡短名稱。",
|
||||||
"presetNamePlaceholder": "快速寫作",
|
"presetNamePlaceholder": "快速寫作",
|
||||||
|
|||||||
@@ -2137,7 +2137,7 @@ describe("SettingsView Apps catalog", () => {
|
|||||||
expect(reasoningEffort).toHaveValue("provider-native-mode");
|
expect(reasoningEffort).toHaveValue("provider-native-mode");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("associates preset selection with the responsive detail pane", async () => {
|
it("expands the model preset editor directly below the selected row", async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
vi.fn(async (input: RequestInfo | URL) => {
|
vi.fn(async (input: RequestInfo | URL) => {
|
||||||
@@ -2157,22 +2157,23 @@ describe("SettingsView Apps catalog", () => {
|
|||||||
|
|
||||||
const row = await screen.findByTestId("model-call-order-row-primary");
|
const row = await screen.findByTestId("model-call-order-row-primary");
|
||||||
const trigger = within(row).getAllByRole("button")[0];
|
const trigger = within(row).getAllByRole("button")[0];
|
||||||
const detail = document.getElementById("model-preset-detail");
|
expect(screen.queryByTestId("model-preset-editor")).not.toBeInTheDocument();
|
||||||
expect(detail).toHaveClass("hidden", "xl:block");
|
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
||||||
expect(trigger).toHaveAttribute("aria-controls", "model-preset-detail");
|
|
||||||
|
|
||||||
fireEvent.click(trigger);
|
fireEvent.click(trigger);
|
||||||
|
|
||||||
|
const editor = screen.getByTestId("model-preset-editor");
|
||||||
expect(trigger).toHaveAttribute("aria-pressed", "true");
|
expect(trigger).toHaveAttribute("aria-pressed", "true");
|
||||||
expect(detail).not.toHaveClass("hidden");
|
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
||||||
expect(within(detail as HTMLElement).getByDisplayValue("Primary")).toBeInTheDocument();
|
expect(trigger).toHaveAttribute("aria-controls", "model-preset-editor");
|
||||||
|
expect(row.nextElementSibling).toBe(editor);
|
||||||
|
expect(editor).toHaveClass("slide-in-from-top-1");
|
||||||
|
expect(within(editor).getByDisplayValue("Primary")).toBeInTheDocument();
|
||||||
|
|
||||||
const backButton = screen.getByRole("button", { name: "Back to model presets" });
|
fireEvent.click(trigger);
|
||||||
expect(backButton).toHaveClass("xl:hidden");
|
|
||||||
fireEvent.click(backButton);
|
|
||||||
|
|
||||||
expect(trigger).toHaveAttribute("aria-pressed", "false");
|
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
||||||
expect(detail).toHaveClass("hidden", "xl:block");
|
expect(screen.queryByTestId("model-preset-editor")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("drags model presets to reorder and saves the model call order immediately", async () => {
|
it("drags model presets to reorder and saves the model call order immediately", async () => {
|
||||||
@@ -4048,10 +4049,6 @@ describe("SettingsView Apps catalog", () => {
|
|||||||
expect(screen.queryByRole("dialog", { name: "New model preset" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("dialog", { name: "New model preset" })).not.toBeInTheDocument();
|
||||||
expect(screen.getByPlaceholderText("Fast writing")).toHaveValue("");
|
expect(screen.getByPlaceholderText("Fast writing")).toHaveValue("");
|
||||||
expect(screen.queryByText("Temperature")).not.toBeInTheDocument();
|
expect(screen.queryByText("Temperature")).not.toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Back to model presets" }));
|
|
||||||
expect(screen.queryByPlaceholderText("Fast writing")).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "New model preset" }));
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /Advanced options/ }));
|
fireEvent.click(screen.getByRole("button", { name: /Advanced options/ }));
|
||||||
expect(screen.getByText("Temperature")).toBeInTheDocument();
|
expect(screen.getByText("Temperature")).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||||
|
|||||||
Reference in New Issue
Block a user