Skip to content

Commit e51ecbc

Browse files
feat(ui): full-color model logos in model selector; GLM uses the actual Z.ai mark
The model dropdown now renders each model's full-color creator logo beside its name (dropdown rows and closed trigger). MultiSelect options gain an optional leading icon. - GLM models and the 'Z.ai (Zhipu AI)' developer mapping now use the actual Z.ai "Z" mark (new self-contained white-on-black app tile at logos/models/zai.svg) instead of the legacy Zhipu asterisk mark; the landing AgentX ledger row is updated too and the stale logos/models/zhipu.svg is removed. - Kimi models switch from the monochrome dark-inverted glyph to the full-color Kimi app tile (logos/models/kimi.svg); kimi.svg leaves MONOCHROME_LOGO_FILES. - New unit tests pin GLM -> models/zai.svg and assert every model logo is full-color (no currentColor fills) except monochrome-by-design marks (OpenAI). 中文:模型下拉选择器现在在模型名称旁显示全彩开发商 Logo(下拉选项与已选状态均支持)。GLM 系列改用 Z.ai 官方 "Z" 标志(新增自包含的黑底白字应用图标 logos/models/zai.svg),替换旧的智谱标志;首页 AgentX 结果列表同步更新。Kimi 系列改用全彩 Kimi 应用图标,不再依赖深色模式反色。新增单元测试确保 GLM 使用 Z.ai 标志、且所有模型 Logo 均为全彩。
1 parent 93a525b commit e51ecbc

8 files changed

Lines changed: 68 additions & 38 deletions

File tree

Lines changed: 1 addition & 0 deletions
Loading

‎packages/app/public/logos/models/zhipu.svg‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎packages/app/src/components/compare/agentx-compare-hero.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { CompareIndexTrackedLink } from './compare-index-tracked-link';
1414
const MODEL_LOGOS: Record<string, string> = {
1515
'kimi-k3': '/logos/models/kimi.svg',
1616
'deepseek-v4': '/logos/models/deepseek.svg',
17-
'glm-5-2': '/logos/models/zhipu.svg',
17+
'glm-5-2': '/logos/models/zai.svg',
1818
'minimax-m3': '/logos/models/minimax.svg',
1919
'qwen-3-5': '/logos/models/qwen.svg',
2020
};

‎packages/app/src/components/ui/chart-selectors.tsx‎

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Info } from 'lucide-react';
44

55
import { LabelWithTooltip } from '@/components/ui/label-with-tooltip';
66
import { track } from '@/lib/analytics';
7+
import { ModelLogo } from '@/components/ui/model-logo';
78
import { MultiSelect } from '@/components/ui/multi-select';
89
import {
910
Select,
@@ -153,6 +154,15 @@ function AgenticScenarioInfo({
153154
);
154155
}
155156

157+
// Full-color creator logo beside each model name, in the dropdown rows and
158+
// the closed trigger alike. `ModelLogo` renders nothing for models without
159+
// a configured logo, so rows degrade to plain text instead of breaking.
160+
const toOption = (model: string) => ({
161+
value: model,
162+
label: getModelLabel(model as Model),
163+
icon: <ModelLogo model={model as Model} />,
164+
});
165+
156166
interface ModelSelectorProps {
157167
id?: string;
158168
value: string;
@@ -177,20 +187,14 @@ export function ModelSelector({
177187
const sections = [
178188
{
179189
id: 'default',
180-
options: groups.default.map((model) => ({
181-
value: model,
182-
label: getModelLabel(model as Model),
183-
})),
190+
options: groups.default.map(toOption),
184191
},
185192
...(groups.experimental.length > 0
186193
? [
187194
{
188195
id: 'experimental',
189196
header: t.experimentalSupport,
190-
options: groups.experimental.map((model) => ({
191-
value: model,
192-
label: getModelLabel(model as Model),
193-
})),
197+
options: groups.experimental.map(toOption),
194198
},
195199
]
196200
: []),
@@ -205,10 +209,7 @@ export function ModelSelector({
205209
reason={t.maintenanceReason}
206210
/>
207211
),
208-
options: groups.maintenance.map((model) => ({
209-
value: model,
210-
label: getModelLabel(model as Model),
211-
})),
212+
options: groups.maintenance.map(toOption),
212213
},
213214
]
214215
: []),
@@ -223,10 +224,7 @@ export function ModelSelector({
223224
reason={t.deprecatedModelReason}
224225
/>
225226
),
226-
options: groups.deprecated.map((model) => ({
227-
value: model,
228-
label: getModelLabel(model as Model),
229-
})),
227+
options: groups.deprecated.map(toOption),
230228
},
231229
]
232230
: []),

‎packages/app/src/components/ui/multi-select.tsx‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const STRINGS = {
3535
interface MultiSelectOption {
3636
value: string;
3737
label: string;
38+
/** Optional leading visual (e.g. a brand logo) rendered before the label. */
39+
icon?: React.ReactNode;
3840
}
3941

4042
export interface MultiSelectSection {
@@ -270,10 +272,10 @@ function MultiSelect({
270272
};
271273

272274
// Preserve the order of selected values, not the order of options
273-
const selectedLabels = value.map((val) => {
274-
const option = flatOptions.find((opt) => opt.value === val);
275-
return option ? option.label : val;
276-
});
275+
const selectedOptions = value.map(
276+
(val) => flatOptions.find((opt) => opt.value === val) ?? { value: val, label: val },
277+
);
278+
const selectedLabels = selectedOptions.map((opt) => opt.label);
277279

278280
return (
279281
<div ref={containerRef} className="relative">
@@ -298,15 +300,17 @@ function MultiSelect({
298300
<div className="flex gap-1 flex-1 min-w-0 items-center min-h-5 flex-wrap">
299301
{value.length > 0 ? (
300302
plainSelectedText ? (
301-
<span className="text-foreground block min-w-0 truncate">
302-
{selectedLabels.join(', ')}
303+
<span className="text-foreground flex min-w-0 items-center gap-1.5">
304+
{selectedOptions.length === 1 && selectedOptions[0].icon}
305+
<span className="block min-w-0 truncate">{selectedLabels.join(', ')}</span>
303306
</span>
304307
) : (
305308
selectedLabels.map((label, index) => (
306309
<span
307310
key={value[index]}
308311
className="bg-transparent text-foreground border border-border dark:bg-[#0a6ca8] dark:border-border inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium transition-colors shrink-0"
309312
>
313+
{selectedOptions[index]?.icon}
310314
{label}
311315
<span
312316
role="button"
@@ -456,7 +460,10 @@ function MultiSelect({
456460
<span className="absolute right-2 flex size-3.5 items-center justify-center">
457461
{isSelected && <CheckIcon className="size-4 text-primary" />}
458462
</span>
459-
<span className="flex items-center gap-2">{option.label}</span>
463+
<span className="flex items-center gap-2">
464+
{option.icon}
465+
{option.label}
466+
</span>
460467
</div>
461468
);
462469
})}
@@ -485,7 +492,10 @@ function MultiSelect({
485492
<span className="absolute right-2 flex size-3.5 items-center justify-center">
486493
{isSelected && <CheckIcon className="size-4 text-primary" />}
487494
</span>
488-
<span className="flex items-center gap-2">{option.label}</span>
495+
<span className="flex items-center gap-2">
496+
{option.icon}
497+
{option.label}
498+
</span>
489499
</div>
490500
);
491501
})}

‎packages/app/src/lib/data-mappings.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ const MODEL_CONFIG: Record<Model, ModelConfig> = {
145145
label: 'Kimi K3 2.8T',
146146
prefix: 'kimik3',
147147
category: 'default',
148-
logo: 'kimi.svg',
148+
logo: 'models/kimi.svg',
149149
},
150150
[Model.Kimi_K2_5]: {
151151
// K2.5, K2.6, and K2.7-Code share an architecture, so the dropdown surfaces
@@ -161,7 +161,7 @@ const MODEL_CONFIG: Record<Model, ModelConfig> = {
161161
label: 'Kimi K2.5/2.6/2.7-Code 1T',
162162
prefix: 'kimik2.5',
163163
category: 'deprecated',
164-
logo: 'kimi.svg',
164+
logo: 'models/kimi.svg',
165165
},
166166
[Model.MiniMax_M3]: {
167167
label: 'MiniMax M3 428B',
@@ -179,15 +179,15 @@ const MODEL_CONFIG: Record<Model, ModelConfig> = {
179179
label: 'GLM5/5.1 744B',
180180
prefix: 'glm5',
181181
category: 'deprecated',
182-
logo: 'zhipu-color.svg',
182+
logo: 'models/zai.svg',
183183
},
184184
// GLM-5.2 and GLM-5.3 share the same architecture and inference profile, so
185185
// the selector presents both releases over the existing GLM-5.2 data bucket.
186186
[Model.GLM_5_2]: {
187187
label: 'GLM5.2/GLM5.3 744B',
188188
prefix: 'glm5.2',
189189
category: 'default',
190-
logo: 'zhipu-color.svg',
190+
logo: 'models/zai.svg',
191191
},
192192
[Model.Qwen3_5]: {
193193
label: 'Qwen3.5 397B',

‎packages/app/src/lib/model-logos.test.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import matter from 'gray-matter';
55
import { describe, expect, it } from 'vitest';
66

77
import { Model, getModelLogo } from '@/lib/data-mappings';
8-
import { MODEL_DEVELOPER_LOGOS, getModelDeveloperLogo } from '@/lib/model-logos';
8+
import { MODEL_DEVELOPER_LOGOS, getModelDeveloperLogo, isMonochromeLogo } from '@/lib/model-logos';
99

1010
const MODELS_DIR = path.join(process.cwd(), 'content', 'models');
1111
const LOGOS_DIR = path.join(process.cwd(), 'public', 'logos');
@@ -52,4 +52,24 @@ describe('model developer logos', () => {
5252
it('returns undefined for unknown developers', () => {
5353
expect(getModelDeveloperLogo('Unknown Lab')).toBeUndefined();
5454
});
55+
56+
it('uses the actual Z.ai mark for GLM models', () => {
57+
expect(getModelLogo(Model.GLM_5)).toBe('models/zai.svg');
58+
expect(getModelLogo(Model.GLM_5_2)).toBe('models/zai.svg');
59+
expect(getModelDeveloperLogo('Z.ai (Zhipu AI)')).toBe('models/zai.svg');
60+
});
61+
62+
it('keeps every model logo full-color except monochrome-by-design marks', () => {
63+
// `currentColor` fills are the monochrome giveaway: a full-color mark
64+
// ships its own brand fills and never inherits the text color.
65+
for (const model of Object.values(Model)) {
66+
const logo = getModelLogo(model);
67+
if (!logo || !logo.endsWith('.svg') || isMonochromeLogo(logo)) continue;
68+
const svg = fs.readFileSync(path.join(LOGOS_DIR, logo), 'utf8');
69+
expect(
70+
svg.includes('currentColor'),
71+
`logo '${logo}' for model '${model}' is not full-color`,
72+
).toBe(false);
73+
}
74+
});
5575
});

‎packages/app/src/lib/model-logos.ts‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Full-color logos render as-is in both themes. Shared by every surface that
55
* renders model/developer logos (`/model` pages, inference chart captions).
66
*/
7-
export const MONOCHROME_LOGO_FILES: ReadonlySet<string> = new Set(['kimi.svg', 'openai.svg']);
7+
export const MONOCHROME_LOGO_FILES: ReadonlySet<string> = new Set(['openai.svg']);
88

99
/** Whether a logo file needs a dark-mode invert to stay visible. */
1010
export function isMonochromeLogo(file: string): boolean {
@@ -22,14 +22,16 @@ export const MODEL_DEVELOPER_LOGOS: Record<string, string> = {
2222
DeepSeek: 'deepseek-color.svg',
2323
Meta: 'meta-color.svg',
2424
MiniMax: 'minimax-color.svg',
25-
// Moonshot AI's model pages are all Kimi models; the Kimi product mark is
26-
// the recognizable brand, and like Moonshot's own mark it is monochrome by
27-
// design (the color variant is a white-on-blue app tile that vanishes on
28-
// light backgrounds).
29-
'Moonshot AI': 'kimi.svg',
25+
// Moonshot AI's model pages are all Kimi models. The full-color Kimi mark
26+
// is white-on-blue, so it ships as a self-contained dark app tile
27+
// (`models/kimi.svg`) that stays legible on light and dark backgrounds.
28+
'Moonshot AI': 'models/kimi.svg',
3029
// OpenAI's brand mark is monochrome by design — no color variant exists.
3130
OpenAI: 'openai.svg',
32-
'Z.ai (Zhipu AI)': 'zhipu-color.svg',
31+
// Zhipu rebranded as Z.ai; the actual Z.ai mark is the "Z" glyph (brand
32+
// color #000), shipped as a self-contained white-on-black app tile so it
33+
// renders full-color on both themes without an invert.
34+
'Z.ai (Zhipu AI)': 'models/zai.svg',
3335
};
3436

3537
/** Logo filename under `/logos/` for a model developer, if one exists. */

0 commit comments

Comments
 (0)