fix(web): 修正在线测试模型类型筛选

移除可能将 image_to_video 误判为图像模型的子串回退,在没有匹配模型时显示空状态。

新增图像生成、图像编辑和视频模式回归测试。验证通过:前端 94 项测试、类型检查、lint 和生产构建。
This commit is contained in:
chengcheng
2026-07-21 13:12:40 +08:00
parent e533ec2367
commit 69b0c107d3
2 changed files with 56 additions and 8 deletions
@@ -0,0 +1,48 @@
import type { PlatformModel } from '@easyai-ai-gateway/contracts';
import { describe, expect, it } from 'vitest';
import { filterModelsForMode } from './PlaygroundPage';
function model(id: string, modelType: string[]) {
return { id, modelType } as PlatformModel;
}
function modelIds(models: PlatformModel[]) {
return models.map((item) => item.id);
}
describe('playground model filtering', () => {
const models = [
model('image-generation', ['image_generate']),
model('legacy-image', ['image']),
model('image-edit', ['image_edit']),
model('image-to-video', ['video_generate', 'image_to_video']),
model('text-to-video', ['text_to_video']),
];
it('only shows image generation models when no reference image is present', () => {
expect(modelIds(filterModelsForMode(models, 'image', false, 'text_to_video'))).toEqual([
'image-generation',
'legacy-image',
]);
});
it('only shows image editing models when a reference image is present', () => {
expect(modelIds(filterModelsForMode(models, 'image', true, 'text_to_video'))).toEqual([
'legacy-image',
'image-edit',
]);
});
it('does not fall back to image-to-video models when no image model is available', () => {
const videoOnlyModels = [
model('kling-3-turbo', ['video_generate', 'image_to_video']),
model('kling-1-5', ['image_to_video']),
];
expect(filterModelsForMode(videoOnlyModels, 'image', false, 'text_to_video')).toEqual([]);
});
it('keeps image-to-video models available for the matching video mode', () => {
expect(modelIds(filterModelsForMode(models, 'video', false, 'first_last_frame'))).toContain('image-to-video');
});
});
+8 -8
View File
@@ -810,7 +810,7 @@ function Composer(props: {
<Select className="playgroundModelSelect" value={props.selectedModel ?? ''} disabled={!props.modelOptions.length} onChange={(event) => props.onModelChange(event.target.value)}> <Select className="playgroundModelSelect" value={props.selectedModel ?? ''} disabled={!props.modelOptions.length} onChange={(event) => props.onModelChange(event.target.value)}>
{props.modelOptions.length ? props.modelOptions.map((item) => ( {props.modelOptions.length ? props.modelOptions.map((item) => (
<option value={item.value} key={item.value}>{modelOptionLabel(item)}</option> <option value={item.value} key={item.value}>{modelOptionLabel(item)}</option>
)) : <option value=""></option>} )) : <option value="">{props.compact ? '模型选择' : '暂无可用模型'}</option>}
</Select> </Select>
{props.mode !== 'chat' && props.mediaSettings && props.onMediaSettingsChange && ( {props.mode !== 'chat' && props.mediaSettings && props.onMediaSettingsChange && (
<MediaSettingsPopover <MediaSettingsPopover
@@ -977,25 +977,25 @@ function mediaPromptPlaceholder(mode: PlaygroundMode) {
return placeholderByMode.chat; return placeholderByMode.chat;
} }
function filterModelsForMode(models: PlatformModel[], mode: PlaygroundMode, hasReference: boolean, videoMode: VideoCreateMode) { export function filterModelsForMode(models: PlatformModel[], mode: PlaygroundMode, hasReference: boolean, videoMode: VideoCreateMode) {
if (mode === 'chat') { if (mode === 'chat') {
return filterWithFallback(models, ['text_generate', 'chat', 'responses', 'text']); return filterModelsByType(models, ['text_generate', 'chat', 'responses', 'text']);
} }
if (mode === 'image') { if (mode === 'image') {
const preferredTypes = hasReference ? ['image_edit', 'images.edits'] : ['image_generate', 'images.generations']; const preferredTypes = hasReference ? ['image_edit', 'images.edits'] : ['image_generate', 'images.generations'];
return filterWithFallback(models, [...preferredTypes, 'image']); return filterModelsByType(models, [...preferredTypes, 'image']);
} }
const videoTypesByMode: Record<VideoCreateMode, string[]> = { const videoTypesByMode: Record<VideoCreateMode, string[]> = {
first_last_frame: ['video_first_last_frame', 'image_to_video', 'video_generate'], first_last_frame: ['video_first_last_frame', 'image_to_video', 'video_generate'],
omni_reference: ['omni_video', 'video_reference', 'video_generate'], omni_reference: ['omni_video', 'video_reference', 'video_generate'],
text_to_video: ['text_to_video', 'video_generate'], text_to_video: ['text_to_video', 'video_generate'],
}; };
return filterWithFallback(models, [...videoTypesByMode[videoMode], 'video']); return filterModelsByType(models, [...videoTypesByMode[videoMode], 'video']);
} }
function filterWithFallback(models: PlatformModel[], modelTypes: string[]) { function filterModelsByType(models: PlatformModel[], modelTypes: string[]) {
const exact = models.filter((model) => model.modelType.some((type) => modelTypes.includes(type))); const acceptedTypes = new Set(modelTypes);
return exact.length ? exact : models.filter((model) => modelTypes.some((type) => model.modelType.some((modelType) => modelType.includes(type) || type.includes(modelType)))); return models.filter((model) => model.modelType.some((type) => acceptedTypes.has(type)));
} }
function buildModelOptions(models: PlatformModel[]): ModelOption[] { function buildModelOptions(models: PlatformModel[]): ModelOption[] {