fix(site): tidy AI model provider configuration layout (#27843)

This commit is contained in:
Danielle Maywood
2026-08-04 14:40:48 +01:00
committed by GitHub
parent 60161fd375
commit 4aec1ea592
2 changed files with 132 additions and 38 deletions
@@ -0,0 +1,111 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import { withToaster } from "#/testHelpers/storybook";
import {
MockAnthropicProviderState,
MockOpenAIProviderState,
} from "../testFixtures";
import { ModelForm } from "./ModelForm";
const meta: Meta<typeof ModelForm> = {
title: "pages/AISettingsPage/ModelsPage/ModelForm",
component: ModelForm,
decorators: [withToaster],
args: {
providerStates: [MockOpenAIProviderState, MockAnthropicProviderState],
selectedProviderState: MockOpenAIProviderState,
onProviderChange: fn(),
isSaving: false,
isDeleting: false,
onCreateModel: fn(async () => undefined),
onUpdateModel: fn(async () => undefined),
},
parameters: {
reactRouter: reactRouterParameters({
location: { path: "/ai/settings/models/add" },
routing: [
{ path: "/ai/settings/models/add", useStoryElement: true },
{ path: "/ai/settings/models", element: <div>Models</div> },
],
}),
},
};
export default meta;
type Story = StoryObj<typeof ModelForm>;
const openProviderConfig = (canvasElement: HTMLElement) => {
const canvas = within(canvasElement);
return userEvent.click(
canvas.getByRole("button", { name: /provider configuration/i }),
);
};
// String enums render as dropdowns, booleans as on/off/default switches, and
// every control shares the same column width with its label on top. OpenAI is
// the densest provider, so it exercises the mixed grid layout.
export const ProviderConfigOpenAI: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await openProviderConfig(canvasElement);
// String enums are dropdowns.
for (const name of [
/reasoning summary/i,
/text verbosity/i,
/service tier/i,
]) {
await expect(canvas.getByRole("combobox", { name })).toBeInTheDocument();
}
// Booleans keep the on/off/default segmented switch.
for (const name of [
/parallel tool calls/i,
/store/i,
/web search enabled/i,
]) {
const group = canvas.getByRole("radiogroup", { name });
await expect(group).toBeInTheDocument();
for (const option of ["Off", "On", "Default"]) {
await expect(
within(group).getByRole("radio", { name: option }),
).toBeInTheDocument();
}
}
},
};
// Anthropic is boolean-heavy, so it exercises the stacked tri-state switches.
export const ProviderConfigAnthropic: Story = {
args: {
selectedProviderState: MockAnthropicProviderState,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await openProviderConfig(canvasElement);
await expect(
canvas.getByRole("combobox", { name: /thinking display/i }),
).toBeInTheDocument();
await expect(
canvas.getByRole("radiogroup", { name: /send reasoning/i }),
).toBeInTheDocument();
},
};
// Enabling web search reveals the gated search_context_size dropdown and the
// full-width allowed_domains JSON field.
export const ProviderConfigOpenAIWebSearch: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await openProviderConfig(canvasElement);
const webSearch = canvas.getByRole("radiogroup", {
name: /web search enabled/i,
});
await userEvent.click(within(webSearch).getByRole("radio", { name: "On" }));
await expect(
canvas.getByRole("combobox", { name: /search context size/i }),
).toBeInTheDocument();
await expect(canvas.getByLabelText(/allowed domains/i)).toBeInTheDocument();
},
};
@@ -296,12 +296,25 @@ const SegmentedField: FC<
const currentValue = (getIn(form.values, fieldKey) as string) || "";
return (
<div className="flex min-w-0 flex-wrap items-center gap-2 self-stretch">
<div className="flex min-w-0 flex-col gap-1.5 self-stretch">
<div className="flex items-center gap-1 text-sm font-normal leading-6 text-content-primary">
<span>{label}</span>
{description && (
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="size-3 text-content-secondary" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[240px]">
{description}
</TooltipContent>
</Tooltip>
)}
</div>
<div
role="radiogroup"
aria-label={label}
className={cn(
"flex items-center gap-0.75 rounded-lg border border-solid border-border p-2",
"flex w-full items-center gap-0.75 rounded-lg border border-solid border-border p-2",
fieldError && "border-content-destructive",
)}
>
@@ -315,7 +328,7 @@ const SegmentedField: FC<
aria-checked={isActive}
disabled={disabled}
className={cn(
"flex h-6 cursor-pointer items-center justify-center gap-2.5 rounded-xl border-0 px-2 pb-px text-sm font-normal leading-6 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
"flex h-6 flex-1 cursor-pointer items-center justify-center gap-2.5 rounded-xl border-0 px-2 pb-px text-sm font-normal leading-6 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
isActive
? "rounded bg-surface-tertiary text-content-primary"
: "bg-transparent text-content-secondary hover:text-content-primary",
@@ -328,19 +341,6 @@ const SegmentedField: FC<
);
})}
</div>
<div className="flex items-center gap-1 text-sm font-normal leading-6 text-content-primary">
<span>{label}</span>
{description && (
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="size-3 text-content-secondary" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[240px]">
{description}
</TooltipContent>
</Tooltip>
)}
</div>
{fieldError && (
<p id={errorId} className="m-0 w-full text-xs text-content-destructive">
{fieldError}
@@ -439,6 +439,8 @@ const SchemaField: FC<SchemaFieldProps> = ({
/>
);
case "select": {
// Booleans keep the on/off/default segmented switch; every string
// enum renders as a dropdown so the switch stays a tri-state control.
if (field.type === "boolean") {
return (
<SegmentedField
@@ -452,22 +454,6 @@ const SchemaField: FC<SchemaFieldProps> = ({
);
}
const options: readonly string[] = field.enum ?? [];
const maxSegmented = 6;
if (options.length > 0 && options.length <= maxSegmented) {
return (
<SegmentedField
{...ctx}
fieldKey={fieldKey}
errorKey={errorKey}
label={label}
description={field.description}
options={options.map((value) => ({
label: capitalize(value),
value,
}))}
/>
);
}
return (
<SelectField
{...ctx}
@@ -499,14 +485,11 @@ const SchemaField: FC<SchemaFieldProps> = ({
/**
* How many grid columns a field should span in the 3-col layout.
* 1 = default (inputs, small enums)
* 3 = full-width (booleans, large enums, json textareas)
* 1 = default (inputs, selects, boolean switches)
* 3 = full-width (json textareas, which need room for multi-line content)
*/
function colSpan(field: FieldSchema): 1 | 3 {
if (field.type === "boolean" || field.input_type === "json") {
return 3;
}
if (field.input_type === "select" && (field.enum?.length ?? 0) > 3) {
if (field.input_type === "json") {
return 3;
}
return 1;