Skip to content

Commit b968869

Browse files
committed
Merge branch 'feat/web-model-selector'
2 parents 2ad2024 + 41bf358 commit b968869

4 files changed

Lines changed: 325 additions & 12 deletions

File tree

.changeset/web-model-badges.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
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.

apps/pythinker-web/src/components/ModelPicker.vue

Lines changed: 167 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,70 @@ import { formatTokens } from '../lib/formatTokens';
1010
1111
const { t } = useI18n();
1212
13+
interface CapabilityBadgeMeta {
14+
readonly labelKey: string;
15+
readonly path: string;
16+
}
17+
18+
const capabilityBadgeMeta: Record<string, CapabilityBadgeMeta> = {
19+
image_in: {
20+
labelKey: 'model.capabilities.imageIn',
21+
path: 'M3 3.5h10v9H3z M4.5 10l2.3-2.3 2 2 1.4-1.4 2.8 2.8',
22+
},
23+
image_out: {
24+
labelKey: 'model.capabilities.imageOut',
25+
path: 'M2.5 4h8v8h-8z M8 2.5h5v5 M10 2.5l3 3',
26+
},
27+
vision: {
28+
labelKey: 'model.capabilities.vision',
29+
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',
30+
},
31+
video_in: {
32+
labelKey: 'model.capabilities.videoIn',
33+
path: 'M2.5 4.5h8v7h-8z M10.5 7l3-2v6l-3-2z',
34+
},
35+
audio_in: {
36+
labelKey: 'model.capabilities.audioIn',
37+
path: 'M6 3v6a2 2 0 1 1-4 0V7 M2 9a6 6 0 0 0 12 0 M8 15v-3 M5.5 15h5',
38+
},
39+
audio_out: {
40+
labelKey: 'model.capabilities.audioOut',
41+
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',
42+
},
43+
thinking: {
44+
labelKey: 'model.capabilities.thinking',
45+
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',
46+
},
47+
always_thinking: {
48+
labelKey: 'model.capabilities.alwaysThinking',
49+
path: 'M8 2.5a5.5 5.5 0 1 0 5.5 5.5 M8 5.5v3l2 1',
50+
},
51+
tool_use: {
52+
labelKey: 'model.capabilities.toolUse',
53+
path: 'M3 3.5h10v9H3z M5 6.5l2 1.5-2 1.5 M8.5 9.5h2',
54+
},
55+
fast_mode: {
56+
labelKey: 'model.capabilities.fastMode',
57+
path: 'M9.5 1.8 3.5 9h4l-1 5.2L12.5 7h-4z',
58+
},
59+
};
60+
61+
const adaptiveThinkingBadge: CapabilityBadgeMeta = {
62+
labelKey: 'model.capabilities.adaptiveThinking',
63+
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',
64+
};
65+
66+
function capabilityGlyph(capability: string): string | undefined {
67+
return capabilityBadgeMeta[capability]?.path;
68+
}
69+
70+
function capabilityTitle(capability: string): string {
71+
const meta = capabilityBadgeMeta[capability];
72+
return meta === undefined
73+
? t('model.capabilities.unknown', { capability })
74+
: t(meta.labelKey);
75+
}
76+
1377
const props = defineProps<{
1478
models: AppModel[];
1579
current: string;
@@ -223,8 +287,57 @@ function selectTab(tabId: string): void {
223287
</span>
224288
<span class="model-provider">{{ m.provider }}</span>
225289
<span class="model-ctx">{{ t('model.contextSuffix', { size: formatTokens(m.maxContextSize) }) }}</span>
226-
<span v-if="m.capabilities && m.capabilities.length > 0" class="caps">
227-
{{ m.capabilities.join(', ') }}
290+
<span
291+
v-if="(m.capabilities && m.capabilities.length > 0) || m.adaptiveThinking"
292+
class="caps"
293+
:aria-label="t('model.capabilities.label')"
294+
>
295+
<span
296+
v-for="(capability, capabilityIndex) in m.capabilities ?? []"
297+
:key="`${capability}-${capabilityIndex}`"
298+
class="cap-badge"
299+
:class="{ 'is-unknown': capabilityGlyph(capability) === undefined }"
300+
:data-capability="capability"
301+
:title="capabilityTitle(capability)"
302+
:aria-label="capabilityTitle(capability)"
303+
>
304+
<svg
305+
v-if="capabilityGlyph(capability)"
306+
viewBox="0 0 16 16"
307+
width="12"
308+
height="12"
309+
fill="none"
310+
stroke="currentColor"
311+
stroke-width="1.25"
312+
stroke-linecap="round"
313+
stroke-linejoin="round"
314+
aria-hidden="true"
315+
>
316+
<path :d="capabilityGlyph(capability)" />
317+
</svg>
318+
<span v-else>{{ capability }}</span>
319+
</span>
320+
<span
321+
v-if="m.adaptiveThinking"
322+
class="cap-badge is-adaptive"
323+
data-capability="adaptive-thinking"
324+
:title="t(adaptiveThinkingBadge.labelKey)"
325+
:aria-label="t(adaptiveThinkingBadge.labelKey)"
326+
>
327+
<svg
328+
viewBox="0 0 16 16"
329+
width="12"
330+
height="12"
331+
fill="none"
332+
stroke="currentColor"
333+
stroke-width="1.25"
334+
stroke-linecap="round"
335+
stroke-linejoin="round"
336+
aria-hidden="true"
337+
>
338+
<path :d="adaptiveThinkingBadge.path" />
339+
</svg>
340+
</span>
228341
</span>
229342
<button
230343
type="button"
@@ -316,16 +429,19 @@ function selectTab(tabId: string): void {
316429
flex: none;
317430
}
318431
.search-input {
432+
/* Default 14px: 14 + 13 = 27px. */
319433
width: 100%;
434+
height: calc(var(--ui-font-size) + 13px);
320435
box-sizing: border-box;
321436
font-family: var(--mono);
322-
font-size: calc(var(--ui-font-size) - 1.5px);
323-
padding: 5px 8px;
437+
font-size: calc(var(--ui-font-size) - 1px);
438+
padding: 0 8px;
324439
border: 1px solid var(--line);
325-
border-radius: 3px;
440+
border-radius: var(--r-sm);
326441
background: var(--panel);
327442
color: var(--ink);
328443
outline: none;
444+
line-height: 1;
329445
}
330446
331447
.tab-strip {
@@ -369,14 +485,21 @@ function selectTab(tabId: string): void {
369485
}
370486
371487
.model-row {
488+
/* Default 14px: 14 + 18 = 32px; 14 - 1 = 13px. */
372489
display: flex;
373490
align-items: center;
491+
box-sizing: border-box;
492+
height: calc(var(--ui-font-size) + 18px);
374493
gap: 8px;
375-
padding: 7px 14px;
494+
padding: 0 8px;
495+
border-radius: var(--r-md);
376496
cursor: pointer;
377-
font-size: calc(var(--ui-font-size) - 1.5px);
497+
font-size: calc(var(--ui-font-size) - 1px);
498+
line-height: 1;
378499
color: var(--text);
379500
min-width: 0;
501+
content-visibility: auto;
502+
contain-intrinsic-size: calc(var(--ui-font-size) + 18px);
380503
}
381504
.model-row:hover, .model-row.is-selected {
382505
background: var(--soft);
@@ -430,13 +553,45 @@ function selectTab(tabId: string): void {
430553
flex: none;
431554
}
432555
.caps {
433-
color: var(--blue);
556+
display: inline-flex;
557+
align-items: center;
558+
gap: 4px;
559+
flex: none;
560+
min-width: 0;
561+
max-width: 180px;
562+
overflow: hidden;
563+
color: var(--muted);
564+
}
565+
.cap-badge {
566+
display: inline-flex;
567+
align-items: center;
568+
justify-content: center;
569+
flex: none;
570+
box-sizing: border-box;
571+
min-width: 16px;
572+
height: 18px;
573+
padding: 0 4px;
574+
border: 1px solid color-mix(in srgb, var(--line) 65%, var(--panel));
575+
border-radius: var(--r-xs);
576+
background: color-mix(in srgb, var(--panel2) 45%, var(--panel));
577+
color: var(--muted);
434578
font-size: max(9px, calc(var(--ui-font-size) - 4px));
435-
border: 1px solid var(--bd);
436-
border-radius: 3px;
437-
padding: 1px 5px;
579+
line-height: 1;
580+
white-space: nowrap;
581+
}
582+
.cap-badge svg {
583+
display: block;
438584
flex: none;
439585
}
586+
.cap-badge.is-adaptive {
587+
border-style: dashed;
588+
color: var(--dim);
589+
}
590+
.cap-badge.is-unknown {
591+
max-width: 120px;
592+
overflow: hidden;
593+
text-overflow: ellipsis;
594+
}
440595
.star-btn {
441596
flex: none;
442597
display: flex;
@@ -505,7 +660,7 @@ function selectTab(tabId: string): void {
505660
max-height: calc(100dvh - 24px);
506661
}
507662
.model-provider,
508-
.caps {
663+
.model-provider {
509664
display: none;
510665
}
511666
}

apps/pythinker-web/src/i18n/locales/en/model.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,19 @@ export default {
1313
starTitle: 'Add to favorites',
1414
unstarTitle: 'Remove from favorites',
1515
footerHint: '↑↓ Navigate · Enter Select · Esc Close',
16+
capabilities: {
17+
label: 'Model capabilities',
18+
imageIn: 'Image input',
19+
imageOut: 'Image output',
20+
vision: 'Vision',
21+
videoIn: 'Video input',
22+
audioIn: 'Audio input',
23+
audioOut: 'Audio output',
24+
thinking: 'Thinking',
25+
alwaysThinking: 'Always thinking',
26+
adaptiveThinking: 'Adaptive reasoning',
27+
toolUse: 'Tool use',
28+
fastMode: 'Fast mode',
29+
unknown: 'Capability: {capability}',
30+
},
1631
} as const;

0 commit comments

Comments
 (0)