From 41bf358d1ea6a56b396bd5944bcadc542b0cdedd Mon Sep 17 00:00:00 2001 From: elkaix Date: Mon, 17 Aug 2026 03:39:03 -0400 Subject: [PATCH] feat(web): show model capabilities as badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The picker rendered capabilities as `capabilities.join(', ')`, so a model with three of them read as one run-on line of text. Give each known capability a muted glyph with a translated tooltip, and keep an unrecognised string visible as text rather than dropping it. A model that reasons adaptively gets its own mark, so it no longer reads as the same thing as one that exposes an explicit thinking capability. Rows and the search field move onto the app's metrics, derived from `--ui-font-size` rather than pinned, so the font-size setting keeps working. Off-screen rows use `content-visibility` instead of a virtual list. The picker stays a modal: it is reached from the composer quick-switch through "More models…", and the dialog is the better form on a narrow window. --- .changeset/web-model-badges.md | 5 + .../src/components/ModelPicker.vue | 179 ++++++++++++++++-- .../src/i18n/locales/en/model.ts | 15 ++ apps/pythinker-web/test/model-picker.test.ts | 138 ++++++++++++++ 4 files changed, 325 insertions(+), 12 deletions(-) create mode 100644 .changeset/web-model-badges.md diff --git a/.changeset/web-model-badges.md b/.changeset/web-model-badges.md new file mode 100644 index 00000000..5ad63da9 --- /dev/null +++ b/.changeset/web-model-badges.md @@ -0,0 +1,5 @@ +--- +"@pymodel/pythinker-code": patch +--- + +Show model capabilities as badges in the model picker instead of a comma-separated string, and bring its rows and search field onto the app's row metrics, sized from `--ui-font-size` so the font-size setting still scales them. A model that reasons adaptively is now distinguishable from one that exposes an explicit thinking capability, and an unrecognised capability still renders rather than being dropped. diff --git a/apps/pythinker-web/src/components/ModelPicker.vue b/apps/pythinker-web/src/components/ModelPicker.vue index 18abe1b0..7366d77d 100644 --- a/apps/pythinker-web/src/components/ModelPicker.vue +++ b/apps/pythinker-web/src/components/ModelPicker.vue @@ -10,6 +10,70 @@ import { formatTokens } from '../lib/formatTokens'; const { t } = useI18n(); +interface CapabilityBadgeMeta { + readonly labelKey: string; + readonly path: string; +} + +const capabilityBadgeMeta: Record = { + image_in: { + labelKey: 'model.capabilities.imageIn', + path: 'M3 3.5h10v9H3z M4.5 10l2.3-2.3 2 2 1.4-1.4 2.8 2.8', + }, + image_out: { + labelKey: 'model.capabilities.imageOut', + path: 'M2.5 4h8v8h-8z M8 2.5h5v5 M10 2.5l3 3', + }, + vision: { + labelKey: 'model.capabilities.vision', + path: 'M2.5 8s2-3 5.5-3 5.5 3 5.5 3-2 3-5.5 3-5.5-3-5.5-3z M8 6.7a1.3 1.3 0 1 0 0 2.6 1.3 1.3 0 0 0 0-2.6z', + }, + video_in: { + labelKey: 'model.capabilities.videoIn', + path: 'M2.5 4.5h8v7h-8z M10.5 7l3-2v6l-3-2z', + }, + audio_in: { + labelKey: 'model.capabilities.audioIn', + path: 'M6 3v6a2 2 0 1 1-4 0V7 M2 9a6 6 0 0 0 12 0 M8 15v-3 M5.5 15h5', + }, + audio_out: { + labelKey: 'model.capabilities.audioOut', + path: 'M3 6h2.5L9 3v10l-3.5-3H3z M11 6a3 3 0 0 1 0 4 M12.5 4.5a5 5 0 0 1 0 7', + }, + thinking: { + labelKey: 'model.capabilities.thinking', + path: 'M5.2 10.7a4 4 0 1 1 5.6 0c-.5.4-.8.8-.8 1.3H6c0-.5-.3-.9-.8-1.3z M6 14h4', + }, + always_thinking: { + labelKey: 'model.capabilities.alwaysThinking', + path: 'M8 2.5a5.5 5.5 0 1 0 5.5 5.5 M8 5.5v3l2 1', + }, + tool_use: { + labelKey: 'model.capabilities.toolUse', + path: 'M3 3.5h10v9H3z M5 6.5l2 1.5-2 1.5 M8.5 9.5h2', + }, + fast_mode: { + labelKey: 'model.capabilities.fastMode', + path: 'M9.5 1.8 3.5 9h4l-1 5.2L12.5 7h-4z', + }, +}; + +const adaptiveThinkingBadge: CapabilityBadgeMeta = { + labelKey: 'model.capabilities.adaptiveThinking', + path: 'M3 8a5 5 0 0 1 8.7-3.4 M11.7 4.6V2.5 M11.7 4.6H9.6 M13 8a5 5 0 0 1-8.7 3.4 M4.3 11.4v2.1 M4.3 11.4h2.1', +}; + +function capabilityGlyph(capability: string): string | undefined { + return capabilityBadgeMeta[capability]?.path; +} + +function capabilityTitle(capability: string): string { + const meta = capabilityBadgeMeta[capability]; + return meta === undefined + ? t('model.capabilities.unknown', { capability }) + : t(meta.labelKey); +} + const props = defineProps<{ models: AppModel[]; current: string; @@ -223,8 +287,57 @@ function selectTab(tabId: string): void { {{ m.provider }} {{ t('model.contextSuffix', { size: formatTokens(m.maxContextSize) }) }} - - {{ m.capabilities.join(', ') }} + + + + {{ capability }} + + + +