fix(web): rebuild the Playground's model surface - #282
Closed
Menci wants to merge 12 commits into
Closed
Conversation
The Playground's Generation section duplicated request fields the Custom JSON editor already expresses, with per-protocol enable/disable rules the toggles could not explain. Reasoning effort stays, because it is the one field whose accepted values come from the model catalog rather than from the caller. Drop the temperature, max output tokens, top P, frequency penalty, presence penalty and stop sequence controls along with the settings shape that carried them; generationOptions now takes the reasoning effort directly and keeps supplying the Messages token cap the protocol requires.
The Vue dashboard's model info bar stated what the selected row actually is — which upstreams a request reaches, the advertised token limits, and for an alias what it resolves to under the caller's cap. The Fluent rebuild left only the model id, so an operator picking between two similarly named rows had nothing to tell them apart. Bring the summary back as Fluent outline badges beside the existing provider badges. Badge assembly is a pure catalog walk in model-badges.ts, so alias reachability, the in-cap binding union, and the "varies" collapse for rules the targets disagree on are testable without rendering; the component only maps descriptors onto localized labels. reachability.ts gains reachableTargets, which isModelReachable now shares, and availableModels takes the resolved cap the header needs anyway instead of re-deriving it from the key.
The picker's input showed the selected model's display name and reported every keystroke as the search query, so typing produced "Atlas TextMixed" and the option list went empty — the search box could only ever match while nothing was selected. Give the query its own null state: null renders the selection, opening the list clears the field, and closing it or choosing an option restores the selection. The first keystroke now starts a query instead of extending a name.
The summary read as a dashboard-era chip strip — monospace text in
"key: value" form, sitting in the page header away from the picker whose
choice it describes.
Move it under the Model field, where it annotates the selection directly,
and restyle it as tint badges with body-weight text and prose labels
("Context 200k", "Alias of 2 of 3 models"). The badge height now matches
the provider badges beside it, so one row reads as one row.
The model picker's list flashed open on mouse down and vanished on mouse up. Fluent copies a provider's className onto the portal mount node it creates under <body>, so the page-layout classes the Playground passed to its nested FluentProvider sized that node too: `h-full` made it a full-viewport, z-index 1000000, pointer-events-auto overlay. It covered the page — elementFromPoint over the picker returned the mount node — so mouse up landed outside the combobox and Fluent's dismiss closed the list it had just opened. Give the provider `contents` instead. The element still carries the theme's custom properties for its subtree and its popups, the section grid resolves its height against <main>, and the mount node goes back to being a zero-size, inert container.
A long list opened beside the control instead of under it: Fluent lets the popup keep its natural height and flips to whichever edge can fit that height, and for the Playground's model picker the only edge tall enough was the left one, so the list flew across the page. Restrict the fallback to the opposite edge and let autoSize trim the popup to the space that edge has, with a little padding so it stops short of the viewport. The model options also stacked their name and id with no separation. Give each line the Fluent line height for its own ramp step and a gap between them, so the id reads as a caption rather than a wrapped second line.
Fluent calls Badge "a visual decoration for UI elements" and gives counts and status their own components; Tag is "a visual representation of an attribute, person, or asset". What this row carries is a set of attributes of the selected model, so Tag is the element for it — and a Tag that is not dismissible renders as a plain span, which states the fact without implying an interaction that does not exist. Switching also settles the shape: the row had two chip languages side by side, an outlined pill for the provider and a filled rounded rectangle for everything else. Every chip is now an outlined pill, and only color and icon separate provider identity from neutral metadata.
The provider badge was a hand-rolled span — pill border, 22px min height, its own padding and text ramp — so it never lined up with the Fluent tags next to it in the Playground's model summary, and it was the one chip in the dashboard that Fluent did not draw. Rebuild it on Tag, which is the element for representing an asset, and keep only what is genuinely ours: the identity tone classes, the custom hex override, and the provider icon in the icon slot. Geometry, sizing and typography now come from the same component every other chip uses, so they align by construction rather than by matching numbers.
Fluent pads a Tag's primary text for a secondary line, and a chip that supplies no secondary text still pays for it: the glyphs land a pixel above the chip's centre. Alone that reads as a slightly high label; beside a provider icon, which is centred exactly, it reads as broken alignment. Drop the padding, keeping the text spanning both grid rows — confined to the primary row it rides higher still, because the empty secondary row holds its height. Chip box, text box, glyphs and icon now share one centre line. Both chip callers were configuring the same Tag independently, so fold that into components/ui/chip.tsx and let the provider badge and the model summary differ only in the colour and weight they paint on top.
The provider list mixed brand palettes: Azure's gradient A and Claude's orange asterisk next to Copilot's and Ollama's black marks, the latter kept legible in dark mode only by inverting them. Codex wore the Codex product tile rather than the vendor it authenticates against. Paint every mark as a silhouette in the surrounding text color by masking it, so a mark picks up the identity tone inside a provider chip, plain foreground in a menu, and needs no dark-mode inversion. Masking keeps the negative space a flat recolor would fill — Copilot's eyes, Ollama's outline. Codex now carries OpenAI's mark, matching the account it signs into. The marks import with `?no-inline`: Vite inlines an asset under 4 KB as a data URI, and an unquoted `url(data:image/svg+xml,<svg …>)` is not a valid CSS value, so the mask-image declaration is dropped and the box paints solid.
Owner
Author
|
Codex agent: Consolidating this follow-up into #277 as requested; the branch remains available while its changes are merged and verified there. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #277.
Summary
Notes
Badge assembly is a pure catalog walk in
model-badges.ts, so alias reachability, the in-cap binding union and the "varies" collapse for rules the targets disagree on are covered without rendering.reachability.tsgainsreachableTargets, shared withisModelReachable;availableModelsnow takes the resolved cap the header needs anyway.Verification
pnpm --filter @floway-dev/web run typecheckpnpm exec vitest run --project @floway-dev/web— 37 files, 200 tests passedpnpm exec eslint apps/web