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 }} + + + +