From 32bebe68f36067fc942ebf3b6ecd61b3cec698d5 Mon Sep 17 00:00:00 2001 From: Artjoms Stukans Date: Sun, 27 Sep 2026 19:23:58 +0200 Subject: [PATCH] Lay out the model dialog in two panes with narrow price inputs The one-column model dialog was taller than a laptop window, and each price input was half the dialog wide, far wider than any rate. Of three mockups, the operator chose two panes. - The dialog is 880px wide. The model and its API settings are on the left; the prices are in their own shaded pane on the right. - Each price is one row: the type's name, a star when it is required, a 110px right-aligned input, and "not billed" under it. The input's own name still reads "Input rate $ / 1M tokens, required". - A self-hosted model shows a note in the prices pane instead. - Below 760px the prices pane moves under the model. - The subgrid that lined up the old two-column price fields is removed. --- .../src/components/SettingsLlmModelForm.tsx | 100 ++++++++++-------- .../components/SettingsLlmModelRateFields.tsx | 44 ++++---- spire-ui/src/index.css | 36 +++++-- spire-ui/src/llmDialogs.contract.test.ts | 27 ++--- 4 files changed, 119 insertions(+), 88 deletions(-) diff --git a/spire-ui/src/components/SettingsLlmModelForm.tsx b/spire-ui/src/components/SettingsLlmModelForm.tsx index 3d0e9ead..718ada69 100644 --- a/spire-ui/src/components/SettingsLlmModelForm.tsx +++ b/spire-ui/src/components/SettingsLlmModelForm.tsx @@ -131,57 +131,65 @@ export default function SettingsLlmModelForm({ initial, onClose, onSaved }: Sett return (
-
e.stopPropagation()} role="dialog" aria-modal="true"> +
e.stopPropagation()} role="dialog" aria-modal="true">

{editing ? 'Edit model' : 'Add model'}

-
- + +
+ + + + + + - - +
+ +
+
Prices · $ per 1M tokens
+ {pricingMode === 'METERED' + ? + : Self-hosted: this model has no per-token price.} +
- - - - - {pricingMode === 'METERED' && } - - - {error &&
{error}
}
diff --git a/spire-ui/src/components/SettingsLlmModelRateFields.tsx b/spire-ui/src/components/SettingsLlmModelRateFields.tsx index cba2c376..1f8f194b 100644 --- a/spire-ui/src/components/SettingsLlmModelRateFields.tsx +++ b/spire-ui/src/components/SettingsLlmModelRateFields.tsx @@ -1,3 +1,4 @@ +import { useId } from 'react'; import type { LlmModelView, PricingMode } from '../api'; import { dollarsToMillicentsPerMillion, millicentsPerMillionToDollars } from '../money'; import { MANDATORY_RATE_TYPES, RATE_TYPES, TOKEN_TYPE_LABEL, type RateType } from '../llmPricing'; @@ -85,24 +86,28 @@ interface RateFieldProps { } /** One rate input, mapped over {@link RATE_TYPES} rather than hand-written per dimension — the label, - * the optional/mandatory marker and the placeholder all already come from shared, tested tables. */ + * the optional/mandatory marker and the placeholder all already come from shared, tested tables. The + * row shows only the type's name and a star when it is required; the input's own name still says the + * whole thing ("Input rate $ / 1M tokens, required"), which a screen reader and the tests both read. */ function RateField({ type, value, onChange, notBilled, onNotBilled }: RateFieldProps) { - const optional = !MANDATORY_RATE_TYPES.includes(type); + const id = useId(); + const mandatory = MANDATORY_RATE_TYPES.includes(type); return ( -
-