Feat(webapp): Models page UI improvements (#3308)

Lots of layout and UI improvements to the new Models page

<img width="2282" height="1356" alt="CleanShot 2026-04-01 at 14 14 35"
src="https://github.com/user-attachments/assets/4a13a291-5d5b-415f-962f-b9cfb25b887d"
/>
This commit is contained in:
James Ritchie
2026-04-01 14:48:05 +01:00
committed by GitHub
parent f75d4d62af
commit bf736a79fc
9 changed files with 1035 additions and 415 deletions
@@ -53,7 +53,7 @@ export function AppliedFilter({
<div
className={cn("flex items-start leading-4", label === undefined ? "gap-1.5" : "gap-0.5")}
>
<div className="-mt-[0.5px] flex items-center gap-1">
<div className="-mt-[0.5px] flex items-center gap-1.5">
{icon}
{label && (
<div className="text-text-bright">
@@ -341,7 +341,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
disabled: isDisabled || !props.shortcut,
});
return (
const buttonElement = (
<button
className={cn("group/button outline-none focus-custom", props.fullWidth ? "w-full" : "")}
type={type}
@@ -353,9 +353,30 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
form={props.form}
autoFocus={autoFocus}
>
<ButtonContent {...props} />
<ButtonContent {...props} tooltip={undefined} />
</button>
);
if (props.tooltip) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className={isDisabled ? "cursor-default" : undefined}>
{buttonElement}
</span>
</TooltipTrigger>
<TooltipContent className="flex items-center gap-3 py-1.5 pl-2.5 pr-3 text-xs text-text-bright">
{props.tooltip} {props.shortcut && !props.hideShortcutKey && (
<ShortcutKey shortcut={props.shortcut} variant="medium" className="ml-1" />
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return buttonElement;
}
);
@@ -184,8 +184,8 @@ export const Checkbox = forwardRef<HTMLInputElement, SimpleCheckboxProps>(
<input
type="checkbox"
className={cn(
props.readOnly || props.disabled ? "cursor-default" : "cursor-pointer",
"read-only:border-charcoal-650 disabled:border-charcoal-650 rounded-sm border border-charcoal-600 bg-transparent transition checked:!bg-indigo-500 read-only:!bg-charcoal-700 group-hover:bg-charcoal-900 group-hover:checked:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-none focus-visible:ring-indigo-500 disabled:!bg-charcoal-700"
props.disabled ? "cursor-not-allowed" : props.readOnly ? "cursor-default" : "cursor-pointer",
"read-only:border-charcoal-650 disabled:border-charcoal-650 disabled:opacity-50 rounded-sm border border-charcoal-600 bg-transparent transition checked:!bg-indigo-500 read-only:!bg-charcoal-700 group-hover:bg-charcoal-900 group-hover:checked:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-none focus-visible:ring-indigo-500 disabled:!bg-charcoal-700"
)}
{...props}
ref={ref}
@@ -64,14 +64,12 @@ export type ModelCatalogItem = {
description: string | null;
contextWindow: number | null;
maxOutputTokens: number | null;
capabilities: string[];
/** Combined capabilities (from DB) and boolean feature flags (from catalog) as slug strings. */
features: string[];
inputPrice: number | null;
outputPrice: number | null;
/** When the model was publicly released (from startDate on LlmModel). */
releaseDate: string | null;
supportsStructuredOutput: boolean;
supportsParallelToolCalls: boolean;
supportsStreamingToolCalls: boolean;
/** Dated variants of this model (only populated on base models). */
variants: ModelVariant[];
};
@@ -98,6 +96,17 @@ export type ModelDetail = ModelCatalogItem & {
}>;
};
function buildFeatures(
capabilities: string[],
catalogEntry: { supportsStructuredOutput: boolean; supportsParallelToolCalls: boolean; supportsStreamingToolCalls: boolean } | undefined
): string[] {
const features = new Set(capabilities);
if (catalogEntry?.supportsStructuredOutput) features.add("structured_output");
if (catalogEntry?.supportsParallelToolCalls) features.add("parallel_tool_calls");
if (catalogEntry?.supportsStreamingToolCalls) features.add("streaming_tool_calls");
return Array.from(features);
}
export type ModelMetricsPoint = {
minute: string;
callCount: number;
@@ -214,13 +223,10 @@ export class ModelRegistryPresenter extends BasePresenter {
description: m.description,
contextWindow: m.contextWindow,
maxOutputTokens: m.maxOutputTokens,
capabilities: m.capabilities,
features: buildFeatures(m.capabilities, catalogEntry),
inputPrice: inputPrice ? Number(inputPrice.price) : null,
outputPrice: outputPrice ? Number(outputPrice.price) : null,
releaseDate: m.startDate ? m.startDate.toISOString().split("T")[0] : null,
supportsStructuredOutput: catalogEntry?.supportsStructuredOutput ?? false,
supportsParallelToolCalls: catalogEntry?.supportsParallelToolCalls ?? false,
supportsStreamingToolCalls: catalogEntry?.supportsStreamingToolCalls ?? false,
variants: [],
_baseModelName: m.baseModelName,
};
@@ -304,7 +310,7 @@ export class ModelRegistryPresenter extends BasePresenter {
/** Get a single model with full pricing details. */
async getModelDetail(friendlyId: string): Promise<ModelDetail | null> {
const model = await this._replica.llmModel.findUnique({
const model = await this._replica.llmModel.findFirst({
where: { friendlyId },
include: {
pricingTiers: {
@@ -331,13 +337,10 @@ export class ModelRegistryPresenter extends BasePresenter {
description: model.description,
contextWindow: model.contextWindow,
maxOutputTokens: model.maxOutputTokens,
capabilities: model.capabilities,
features: buildFeatures(model.capabilities, catalogEntry),
inputPrice: inputPrice ? Number(inputPrice.price) : null,
outputPrice: outputPrice ? Number(outputPrice.price) : null,
releaseDate: model.startDate ? model.startDate.toISOString().split("T")[0] : null,
supportsStructuredOutput: catalogEntry?.supportsStructuredOutput ?? false,
supportsParallelToolCalls: catalogEntry?.supportsParallelToolCalls ?? false,
supportsStreamingToolCalls: catalogEntry?.supportsStreamingToolCalls ?? false,
variants: [],
matchPattern: model.matchPattern,
source: model.source,
@@ -42,7 +42,7 @@ import {
formatModelPrice,
formatTokenCount,
formatModelCost,
formatCapability,
formatFeature,
formatProviderName,
} from "~/utils/modelFormatters";
@@ -312,14 +312,14 @@ function OverviewTab({
</Property.Value>
</Property.Item>
)}
{model.capabilities.length > 0 && (
{model.features.length > 0 && (
<Property.Item>
<Property.Label>Capabilities</Property.Label>
<Property.Label>Features</Property.Label>
<Property.Value>
<div className="flex flex-wrap gap-1">
{model.capabilities.map((cap) => (
<Badge key={cap} variant="outline-rounded">
{formatCapability(cap)}
{model.features.map((f) => (
<Badge key={f} variant="outline-rounded">
{formatFeature(f)}
</Badge>
))}
</div>
@@ -312,7 +312,7 @@ export default function AdminLlmModelDetailRoute() {
/>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-text-dimmed">Capabilities (comma-separated)</label>
<label className="text-xs font-medium text-text-dimmed">Features (comma-separated)</label>
<Input
name="capabilities"
value={capabilities}
@@ -269,7 +269,7 @@ export default function AdminLlmModelNewRoute() {
/>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-text-dimmed">Capabilities (comma-separated)</label>
<label className="text-xs font-medium text-text-dimmed">Features (comma-separated)</label>
<Input
name="capabilities"
value={capabilities}
+6 -3
View File
@@ -23,15 +23,18 @@ export function formatModelCost(dollars: number): string {
return `$${dollars.toFixed(2)}`;
}
/** Format a capability slug from SCREAMING_CASE to Title Case. */
export function formatCapability(cap: string): string {
return cap
/** Format a feature slug (snake_case) to Title Case. */
export function formatFeature(slug: string): string {
return slug
.toLowerCase()
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
/** @deprecated Use formatFeature instead. */
export const formatCapability = formatFeature;
/** Capitalize a provider name. */
export function formatProviderName(provider: string): string {
const names: Record<string, string> = {