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:
@@ -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,
|
||||
|
||||
+6
-6
@@ -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>
|
||||
|
||||
+979
-386
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
Reference in New Issue
Block a user