mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
fix(codex): keep the reasoning levels column from stretching the model catalog
Picking many reasoning levels for a Codex model catalog row pushed the levels column wider, and the rows stopped lining up with each other and with the header. - Each catalog row is its own grid, and its `1fr` tracks could not shrink below their content, so a long label such as "none,low,medium,high,xhigh,max" widened its own row. The header and row templates now use `minmax(0,1fr)`, so every row splits the width the same way and the label's existing truncation takes effect. - The trigger label collapses each run of three or more adjacent levels into "first → last": "low → max", or "none, low → max" when minimal is skipped. A range never spans a skipped level, so it cannot suggest a level that was not picked. Shorter runs, gapped picks such as "low, high, max" and lists that are not in canonical order are shown in full. - Hovering or keyboard-focusing the trigger shows every picked level and, when one is set, the default level (new codexConfig.defaultReasoningLevelTip key in all four locales).
This commit is contained in:
1 parent
20222460a1
commit
db8069ef7d
6 files changed
+119
-19
No files matched your search
@@ -232,6 +232,39 @@ const CODEX_REASONING_LEVELS = [
|
||||
"ultra",
|
||||
] as const;
|
||||
|
||||
// Trigger label for the picked levels, kept narrow for the catalog cell. Each
|
||||
// run of three or more adjacent canonical levels collapses to "first → last"
|
||||
// ("none, low → max" when minimal is skipped); a range never spans a gap, so
|
||||
// it can't imply an unpicked level. Non-canonical order stays a plain list.
|
||||
export function formatReasoningLevelsLabel(levels: string[]): string {
|
||||
const canonical = CODEX_REASONING_LEVELS as readonly string[];
|
||||
const positions = levels.map((level) => canonical.indexOf(level));
|
||||
const ordered = positions.every(
|
||||
(position, index) =>
|
||||
position >= 0 && (index === 0 || position > positions[index - 1]),
|
||||
);
|
||||
if (!ordered) return levels.join(", ");
|
||||
|
||||
const parts: string[] = [];
|
||||
let runStart = 0;
|
||||
for (let index = 1; index <= levels.length; index++) {
|
||||
if (
|
||||
index < levels.length &&
|
||||
positions[index] === positions[index - 1] + 1
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const run = levels.slice(runStart, index);
|
||||
if (run.length >= 3) {
|
||||
parts.push(`${run[0]} → ${run[run.length - 1]}`);
|
||||
} else {
|
||||
parts.push(...run);
|
||||
}
|
||||
runStart = index;
|
||||
}
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
// Sentinel for the default-level Select: Radix Select forbids empty item
|
||||
// values, so "back to Auto" needs a non-empty value mapped to undefined.
|
||||
const AUTO_DEFAULT_REASONING_LEVEL = "__auto__";
|
||||
@@ -270,28 +303,47 @@ function ReasoningLevelsEditor({
|
||||
|
||||
const triggerLabel =
|
||||
selected.length > 0
|
||||
? selected.join(",")
|
||||
? formatReasoningLevelsLabel(selected)
|
||||
: t("codexConfig.reasoningLevelsNotSet", {
|
||||
defaultValue: "Not set",
|
||||
});
|
||||
|
||||
// The label may be a range or truncated, so hovering spells out every
|
||||
// picked level plus the default one.
|
||||
const triggerTip =
|
||||
selected.length > 0 ? (
|
||||
<>
|
||||
<div>{selected.join(", ")}</div>
|
||||
{defaultLevel && (
|
||||
<div>
|
||||
{t("codexConfig.defaultReasoningLevelTip", {
|
||||
level: defaultLevel,
|
||||
defaultValue: "Default level: {{level}}",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<Popover modal open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="flex h-9 w-full items-center justify-between gap-1 rounded-md border border-border bg-surface px-3 py-1 text-sm shadow-sm focus:outline-none focus-visible:outline-none focus:border-border focus-visible:border-border focus:ring-0 focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<span
|
||||
className={cn("truncate", selected.length === 0 && "text-fg-2")}
|
||||
<HoverTip content={triggerTip}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="flex h-9 w-full items-center justify-between gap-1 rounded-md border border-border bg-surface px-3 py-1 text-sm shadow-sm focus:outline-none focus-visible:outline-none focus:border-border focus-visible:border-border focus:ring-0 focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{triggerLabel}
|
||||
</span>
|
||||
<ChevronsUpDown className="h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<span
|
||||
className={cn("truncate", selected.length === 0 && "text-fg-2")}
|
||||
>
|
||||
{triggerLabel}
|
||||
</span>
|
||||
<ChevronsUpDown className="h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
</HoverTip>
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
align="start"
|
||||
@@ -1167,8 +1219,8 @@ export function CodexFormFields({
|
||||
className={cn(
|
||||
"hidden gap-2 px-1 text-xs font-medium text-fg-2 md:grid",
|
||||
withDefault
|
||||
? "grid-cols-[36px_1fr_1fr_140px_1fr_36px]"
|
||||
: "grid-cols-[1fr_1fr_140px_1fr_36px]",
|
||||
? "grid-cols-[36px_minmax(0,1fr)_minmax(0,1fr)_140px_minmax(0,1fr)_36px]"
|
||||
: "grid-cols-[minmax(0,1fr)_minmax(0,1fr)_140px_minmax(0,1fr)_36px]",
|
||||
)}
|
||||
>
|
||||
{withDefault && <span />}
|
||||
@@ -1201,8 +1253,8 @@ export function CodexFormFields({
|
||||
className={cn(
|
||||
"grid grid-cols-1 gap-2",
|
||||
withDefault
|
||||
? "md:grid-cols-[36px_1fr_1fr_140px_1fr_36px]"
|
||||
: "md:grid-cols-[1fr_1fr_140px_1fr_36px]",
|
||||
? "md:grid-cols-[36px_minmax(0,1fr)_minmax(0,1fr)_140px_minmax(0,1fr)_36px]"
|
||||
: "md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_140px_minmax(0,1fr)_36px]",
|
||||
)}
|
||||
>
|
||||
{withDefault && renderDefaultStar(index)}
|
||||
|
||||
@@ -1824,6 +1824,7 @@
|
||||
"reasoningLevelsEmpty": "No levels",
|
||||
"defaultReasoningLevelLabel": "Default level",
|
||||
"defaultReasoningLevelPlaceholder": "Auto",
|
||||
"defaultReasoningLevelTip": "Default level: {{level}}",
|
||||
"reasoningModeToggle": "Supports Thinking Mode",
|
||||
"reasoningModeHint": "Enable when the upstream Chat Completions API supports toggling thinking on/off. Providers like Kimi, GLM and Qwen usually fall into this category.",
|
||||
"reasoningEffortToggle": "Supports Reasoning Effort",
|
||||
|
||||
@@ -1824,6 +1824,7 @@
|
||||
"reasoningLevelsEmpty": "レベルなし",
|
||||
"defaultReasoningLevelLabel": "デフォルトレベル",
|
||||
"defaultReasoningLevelPlaceholder": "自動",
|
||||
"defaultReasoningLevelTip": "デフォルトレベル:{{level}}",
|
||||
"reasoningModeToggle": "思考モードに対応",
|
||||
"reasoningModeHint": "上流の Chat Completions API が思考(thinking)のオン/オフ切り替えに対応している場合に有効化します。Kimi、GLM、Qwen などが通常このタイプに該当します。",
|
||||
"reasoningEffortToggle": "思考レベルに対応",
|
||||
|
||||
@@ -1817,6 +1817,7 @@
|
||||
"reasoningLevelsEmpty": "無等級",
|
||||
"defaultReasoningLevelLabel": "預設等級",
|
||||
"defaultReasoningLevelPlaceholder": "自動",
|
||||
"defaultReasoningLevelTip": "預設等級:{{level}}",
|
||||
"catalogDisplayNamePlaceholder": "例如: DeepSeek V4 Flash",
|
||||
"catalogModelPlaceholder": "例如: deepseek-v4-flash",
|
||||
"contextWindow": "上下文視窗",
|
||||
|
||||
@@ -1824,6 +1824,7 @@
|
||||
"reasoningLevelsEmpty": "无等级",
|
||||
"defaultReasoningLevelLabel": "默认等级",
|
||||
"defaultReasoningLevelPlaceholder": "自动",
|
||||
"defaultReasoningLevelTip": "默认等级:{{level}}",
|
||||
"reasoningModeToggle": "支持思考模式",
|
||||
"reasoningModeHint": "上游 Chat Completions 接口支持开启或关闭 thinking 时启用。Kimi、GLM、Qwen 等通常属于这一类。",
|
||||
"reasoningEffortToggle": "支持思考等级",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatReasoningLevelsLabel } from "@/components/providers/forms/CodexFormFields";
|
||||
|
||||
describe("formatReasoningLevelsLabel", () => {
|
||||
it("collapses a run of three or more adjacent levels into a range", () => {
|
||||
expect(
|
||||
formatReasoningLevelsLabel(["low", "medium", "high", "xhigh", "max"]),
|
||||
).toBe("low → max");
|
||||
});
|
||||
|
||||
it("collapses each run separately, so a skipped level stays visible", () => {
|
||||
// minimal sits between none and low: none must not join the low run.
|
||||
expect(
|
||||
formatReasoningLevelsLabel([
|
||||
"none",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
]),
|
||||
).toBe("none, low → max");
|
||||
expect(formatReasoningLevelsLabel(["none", "low", "medium", "high"])).toBe(
|
||||
"none, low → high",
|
||||
);
|
||||
});
|
||||
|
||||
it("lists short runs and gapped picks as they are", () => {
|
||||
expect(formatReasoningLevelsLabel(["high"])).toBe("high");
|
||||
expect(formatReasoningLevelsLabel(["none", "high"])).toBe("none, high");
|
||||
expect(formatReasoningLevelsLabel(["low", "high", "max"])).toBe(
|
||||
"low, high, max",
|
||||
);
|
||||
expect(formatReasoningLevelsLabel(["low", "medium", "xhigh"])).toBe(
|
||||
"low, medium, xhigh",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a non-canonical order as a plain list", () => {
|
||||
expect(formatReasoningLevelsLabel(["high", "medium", "low"])).toBe(
|
||||
"high, medium, low",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user