Skip to content

fix(web): rebuild the Playground's model surface - #282

Closed
Menci wants to merge 12 commits into
fluent-dashboardfrom
fluent-dashboard-playground
Closed

fix(web): rebuild the Playground's model surface#282
Menci wants to merge 12 commits into
fluent-dashboardfrom
fluent-dashboard-playground

Conversation

@Menci

@Menci Menci commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Stacked on #277.

Summary

  • drop the Generation section's temperature, max output tokens, top P, frequency penalty, presence penalty and stop sequence controls; the Custom JSON editor already expresses those request fields, and reasoning effort — whose accepted values come from the model catalog — stays
  • restore the model capability summary the Vue dashboard had: provider badges, advertised token limits, and for an alias what it resolves to under the caller's upstream cap, rendered as Fluent outline badges in the Playground header
  • fix the model picker's search, which could not match while a model was selected

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.ts gains reachableTargets, shared with isModelReachable; availableModels now takes the resolved cap the header needs anyway.

Verification

  • pnpm --filter @floway-dev/web run typecheck
  • pnpm exec vitest run --project @floway-dev/web — 37 files, 200 tests passed
  • pnpm exec eslint apps/web
  • manual: seeded a local instance with multi-upstream models, single-target / multi-target / partially-reachable / unresolvable-target aliases, and a live Copilot upstream; verified every badge shape and that model search filters

Menci added 12 commits July 28, 2026 22:45
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.
@Menci

Menci commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

Codex agent: Consolidating this follow-up into #277 as requested; the branch remains available while its changes are merged and verified there.

@Menci Menci closed this Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant