Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 31 additions & 18 deletions apps/web/src/components/chat/ComposerModelEffort.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import type { ComposerControlSize } from "./ComposerControl";
import { ComposerPermissionsSection, type RuntimeModeOption } from "./ComposerFooterControls";
import { composerFloatingLayerProps, focusCheckedPermission } from "./composerEventScope";
import { EffortGlyph, FastBolt, resolveEffortGlyph } from "./EffortGlyph";
import {
EffortGlyph,
effortTone,
EffortSparkles,
FastBolt,
resolveEffortGlyph,
} from "./EffortGlyph";
import { DockedPickerContext } from "./ProviderModelPicker";
import {
getTraitsSectionVisibility,
Expand Down Expand Up @@ -107,18 +113,19 @@ export function describeComposerEffort(
columnControls(traits.provider, visibility.selectDescriptors, visibility.booleanDescriptors)
.effort?.descriptor ?? null;
const ultrathink = visibility.ultrathinkPromptControlled && reasoning === primary;
const glyph =
reasoning?.type === "select"
? resolveEffortGlyph(
reasoning,
ultrathink
? "ultrathink"
: getDescriptorStringValue(reasoning, modelSelection, traits.reportedModelSelection),
)
: null;
// Workflow levels such as Ultracode always show their name; the setting names every level.
const level =
reasoning?.type !== "select"
? null
: ultrathink
? "ultrathink"
: getDescriptorStringValue(reasoning, modelSelection, traits.reportedModelSelection);
const glyph = reasoning?.type === "select" ? resolveEffortGlyph(reasoning, level) : null;
// Scales too long for bars still show their word in the level's color.
const tone = glyph?.tone ?? effortTone(level);
const word = glyph
? (glyph.special ?? (effortDisplay === "label" ? glyph.label : null))
? effortDisplay === "label"
? glyph.label
: null
: !reasoning
? null
: reasoning.type === "boolean"
Expand All @@ -141,17 +148,23 @@ export function describeComposerEffort(
suffix: (
<span
className={cn(
"flex shrink-0 items-center gap-1.5 text-muted-foreground group-hover/model-trigger:text-foreground group-data-pressed/model-trigger:text-foreground group-data-popup-open/model-trigger:text-foreground",
"flex shrink-0 items-center gap-1.5",
"text-muted-foreground group-hover/model-trigger:text-foreground group-data-pressed/model-trigger:text-foreground group-data-popup-open/model-trigger:text-foreground",
// A word space after the model name: the regular trigger's gap is wider than one.
size === "sm" && "-ms-0.5",
)}
>
{word ? (
<span data-composer-control-label className="max-w-24 truncate">
{word}
{word || glyph ? (
<span className={cn("flex items-center gap-0.5", tone)}>
{word ? (
<span data-composer-control-label className="max-w-24 truncate">
{word}
</span>
) : glyph ? (
<EffortGlyph count={glyph.count} filled={glyph.filled} size={size} />
) : null}
{glyph ? <EffortSparkles count={glyph.sparkles} /> : null}
</span>
) : glyph ? (
<EffortGlyph count={glyph.count} filled={glyph.filled} size={size} />
) : null}
{fast ? <FastBolt ultra={display.speedLabel === "Ultrafast"} /> : null}
{contextWindow ? (
Expand Down
33 changes: 24 additions & 9 deletions apps/web/src/components/chat/EffortGlyph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,45 @@ describe("resolveEffortGlyph", () => {
);

it("fills bars up to the current level and leaves max and workflow levels off the scale", () => {
expect(resolveEffortGlyph(claude, "xhigh")).toEqual({
expect(resolveEffortGlyph(claude, "xhigh")).toMatchObject({
count: 4,
filled: 4,
special: null,
label: "Xhigh",
sparkles: 0,
});
});

it("names max instead of drawing it", () => {
expect(resolveEffortGlyph(claude, "max")?.special).toBe("Max");
it("draws max and workflow levels as full bars plus sparkles", () => {
expect(resolveEffortGlyph(claude, "max")).toMatchObject({ filled: 4, sparkles: 1 });
expect(resolveEffortGlyph(claude, "ultracode")).toMatchObject({ filled: 4, sparkles: 2 });
const codex = descriptor(["low", "medium", "high", "xhigh", "max", "ultra"]);
expect(resolveEffortGlyph(codex, "ultra")).toMatchObject({ count: 4, filled: 4, sparkles: 2 });
const short = descriptor(["high", "ultra"]);
expect(resolveEffortGlyph(short, "ultra")).toMatchObject({ count: 1, filled: 1, sparkles: 2 });
for (const named of [descriptor(["max", "ultra"]), descriptor(["off", "ultra"])]) {
expect(resolveEffortGlyph(named, "ultra")).toMatchObject({ count: 1, sparkles: 2 });
}
expect(resolveEffortGlyph(claude, "ultrathink")).toMatchObject({
filled: 4,
sparkles: 2,
label: "Ultrathink",
});
});

it("names Ultracode and Ultrathink instead of drawing a level", () => {
expect(resolveEffortGlyph(claude, "ultracode")?.special).toBe("Ultracode");
expect(resolveEffortGlyph(claude, "ultrathink")?.special).toBe("Ultrathink");
it("colors only the levels that cost more than usual", () => {
expect(resolveEffortGlyph(claude, "low")?.tone).toBeNull();
expect(resolveEffortGlyph(claude, "medium")?.tone).toBeNull();
expect(resolveEffortGlyph(claude, "high")?.tone).toContain("amber");
expect(resolveEffortGlyph(claude, "xhigh")?.tone).toContain("red");
expect(resolveEffortGlyph(claude, "max")?.tone).toContain("purple");
});

it("draws the lowest level as one bar, and an off level as none", () => {
expect(resolveEffortGlyph(claude, "low")).toMatchObject({ count: 4, filled: 1 });
expect(resolveEffortGlyph(claude, "medium")).toMatchObject({ count: 4, filled: 2 });
expect(resolveEffortGlyph(descriptor(["off", "low", "high"]), "off")).toEqual({
expect(resolveEffortGlyph(descriptor(["off", "low", "high"]), "off")).toMatchObject({
count: 2,
filled: 0,
special: null,
label: "Off",
});
});
Expand Down
54 changes: 44 additions & 10 deletions apps/web/src/components/chat/EffortGlyph.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ProviderOptionDescriptor } from "@t3tools/contracts";
import { ZapIcon } from "lucide-react";
import { SparkleIcon, ZapIcon } from "lucide-react";

import type { ComposerControlSize } from "./ComposerControl";

Expand All @@ -10,34 +10,57 @@ const BASELINE_INSET = 2;

type SelectDescriptor = Extract<ProviderOptionDescriptor, { type: "select" }>;

// Ultracode changes the workflow, not only intensity, so it keeps its name like Ultrathink.
// Max is named too, so the full scale stops short of it.
const NAMED_LEVELS = new Set(["ultracode", "max"]);
// Max and workflow levels sit above the scale: full bars plus sparkles rather than more bars.
const NAMED_LEVELS = new Set(["ultra", "ultracode", "max"]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const OFF_LEVELS = new Set(["off", "none"]);
// More bars than this no longer fit the glyph's height in whole pixels.
const MAX_BARS = 7;

// Color marks cost, not difference from a default, so ordinary levels stay quiet.
// Amber, red, then purple: no green, so the ramp holds up for red/green color blindness.
const EFFORT_TONES: Record<string, string> = {
high: "text-amber-700 dark:text-amber-500",
xhigh: "text-red-600 dark:text-red-500",
max: "text-purple-600 dark:text-purple-400",
ultra: "text-fuchsia-600 dark:text-fuchsia-400",
ultracode: "text-fuchsia-600 dark:text-fuchsia-400",
ultrathink: "text-fuchsia-600 dark:text-fuchsia-400",
};

export function effortTone(level: string | null) {
return (level !== null && EFFORT_TONES[level]) || null;
}

/**
* Maps a reasoning select to bars: one per ordinary level, filled up to the current one, with an
* off level as none filled. Max, prompt-injected and workflow levels are named instead of drawn.
* off level as none filled. Max draws full bars and one sparkle; prompt-injected and workflow
* levels draw two.
*/
export function resolveEffortGlyph(descriptor: SelectDescriptor, current: string | null) {
const special = new Set([...(descriptor.promptInjectedValues ?? []), ...NAMED_LEVELS]);
const levels = descriptor.options.filter((option) => !special.has(option.id));
if (levels.length < 2 || levels.length > MAX_BARS || current === null) return null;
const startsAtOff = OFF_LEVELS.has(levels[0]!.id);
const count = startsAtOff ? levels.length - 1 : levels.length;
if (descriptor.options.length < 2 || levels.length > MAX_BARS || current === null) return null;
const startsAtOff = levels[0] !== undefined && OFF_LEVELS.has(levels[0].id);
// A scale of only named levels still draws one bar under its sparkles.
const count = Math.max(1, startsAtOff ? levels.length - 1 : levels.length);
if (special.has(current)) {
const label = descriptor.options.find((option) => option.id === current)?.label ?? current;
return { count, filled: count, special: label, label };
return {
count,
filled: count,
label,
sparkles: current === "max" ? 1 : 2,
tone: effortTone(current),
};
}
const index = levels.findIndex((option) => option.id === current);
if (index < 0) return null;
return {
count,
filled: startsAtOff ? index : index + 1,
special: null,
label: levels[index]!.label,
sparkles: 0,
tone: effortTone(current),
};
}

Expand Down Expand Up @@ -88,6 +111,17 @@ export function EffortGlyph(props: { count: number; filled: number; size?: Compo
);
}

export function EffortSparkles(props: { count: number }) {
return Array.from({ length: props.count }, (_, index) => (
<SparkleIcon
key={index}
aria-hidden="true"
data-composer-control-icon
className="size-3 fill-current text-current"
/>
));
}

/** One bolt for Fast. Ultrafast adds an outlined echo behind it, cut away by the surface color. */
export function FastBolt(props: { ultra: boolean }) {
if (!props.ultra) {
Expand Down
Loading
Loading