import { useEffect, useMemo, useState, type ReactNode } from 'react'; import { Brain, Image, ListChecks, Plus, Trash2, Video } from 'lucide-react'; import { Button, Checkbox, Input, Select } from '../../components/ui'; import { addCapabilityType, defaultCapabilityConfig, modelTypeGroup, modelTypeLabel, removeCapabilityType, updateCapabilityConfig, type CapabilityEditorState, } from './base-model-capabilities'; type FieldType = | 'boolean' | 'number' | 'text' | 'list' | 'numberList' | 'range' | 'ratioRange' | 'scopedList' | 'scopedNumberList' | 'scopedRange' | 'scopedNumber'; type FieldScope = 'mode' | 'resolution' | 'mixed'; type FieldDefinition = { key: string; label: string; hint?: string; placeholder?: string; type: FieldType; scope?: FieldScope; }; type ValueOption = { label: string; value: string }; const textFields: FieldDefinition[] = [ { key: 'supportTool', label: '工具调用', hint: 'function calling / tools', type: 'boolean' }, { key: 'supportStructuredOutput', label: '结构化输出', hint: 'JSON Schema 等输出', type: 'boolean' }, { key: 'supportThinking', label: '推理能力', hint: '支持 reasoning / thinking 参数', type: 'boolean' }, { key: 'supportThinkingModeSwitch', label: '思考开关', hint: '可按请求切换', type: 'boolean' }, { key: 'supportWebSearch', label: '联网搜索', type: 'boolean' }, { key: 'max_context_tokens', label: '上下文 Token', placeholder: '128000', type: 'number' }, { key: 'max_input_tokens', label: '最大输入 Token', placeholder: '64000', type: 'number' }, { key: 'max_output_tokens', label: '最大输出 Token', placeholder: '8192', type: 'number' }, { key: 'max_thinking_tokens', label: '最大思考 Token', placeholder: '32768', type: 'number' }, { key: 'thinkingEffortLevels', label: '推理深度', hint: '声明模型支持的 reasoning_effort 取值,可填写 max 等供应商自定义值', placeholder: 'none, minimal, low, medium, high, xhigh, max', type: 'list' }, ]; const embeddingFields: FieldDefinition[] = [ { key: 'dimensions', label: '向量维度', placeholder: '1024, 768, 512', type: 'numberList' }, ]; const imageFields: FieldDefinition[] = [ { key: 'support_base64_input', label: 'Base64 输入', type: 'boolean' }, { key: 'support_url_input', label: 'URL 输入', type: 'boolean' }, { key: 'input_multiple_images', label: '多图输入', type: 'boolean' }, { key: 'input_max_images_count', label: '最多输入图片', placeholder: '10', type: 'number' }, { key: 'output_multiple_images', label: '多图输出', type: 'boolean' }, { key: 'output_max_images_count', label: '最多输出图片', placeholder: '4', type: 'number' }, { key: 'output_resolutions', label: '输出分辨率', placeholder: '1K, 2K, 4K', type: 'list' }, { key: 'output_max_size', label: '最大输出尺寸', placeholder: '16777216', type: 'number' }, { key: 'aspect_ratio_allowed', label: '支持宽高比', placeholder: '16:9, 1:1, 9:16', type: 'list' }, { key: 'aspect_ratio_range', label: '宽高比范围', hint: '按宽/高数值限制,如 0.125 - 8', placeholder: '0.125 - 8', type: 'ratioRange' }, ]; const videoFields: FieldDefinition[] = [ { key: 'support_base64_input', label: 'Base64 输入', type: 'boolean' }, { key: 'support_url_input', label: 'URL 输入', type: 'boolean' }, { key: 'output_resolutions', label: '输出分辨率', hint: '可直接配置,也可按首帧/首尾帧等模式配置', placeholder: '720p, 1080p', type: 'scopedList', scope: 'mode' }, { key: 'aspect_ratio_allowed', label: '支持宽高比', hint: '支持按分辨率配置可选宽高比', placeholder: '16:9, 9:16, 1:1', type: 'scopedList', scope: 'resolution' }, { key: 'aspect_ratio_range', label: '宽高比范围', hint: '按宽/高数值限制,如 0.125 - 8', placeholder: '0.125 - 8', type: 'ratioRange' }, { key: 'duration_range', label: '时长范围', hint: '支持按分辨率或模式配置不同范围', placeholder: '5 - 10', type: 'scopedRange', scope: 'mixed' }, { key: 'duration_options', label: '可选时长', hint: '与时长范围二选一;只允许这些秒数', placeholder: '5, 10', type: 'scopedNumberList', scope: 'mixed' }, { key: 'duration_step', label: '时长间隔', hint: '自定义时长按该秒数递增或取整', placeholder: '1', type: 'scopedNumber', scope: 'mixed' }, { key: 'input_audio', label: '音频输入', type: 'boolean' }, { key: 'output_audio', label: '输出音频', type: 'boolean' }, { key: 'output_bgm', label: '背景音乐', type: 'boolean' }, { key: 'input_first_frame', label: '首帧输入', type: 'boolean' }, { key: 'input_last_frame', label: '尾帧输入', type: 'boolean' }, { key: 'input_first_last_frame', label: '首尾帧模式', type: 'boolean' }, { key: 'input_reference_generate_single', label: '单参考图', type: 'boolean' }, { key: 'input_reference_generate_multiple', label: '多参考图', type: 'boolean' }, { key: 'input_smart_multi_frame', label: '智能多帧', type: 'boolean' }, { key: 'smart_multi_frame_range', label: '智能多帧数量', placeholder: '2 - 9', type: 'range' }, { key: 'smart_multi_frame_mode', label: '智能多帧模式', placeholder: 'native 或 stitch', type: 'text' }, { key: 'smart_multi_frame_duration_range', label: '智能多帧每段时长', placeholder: '2 - 7', type: 'range' }, { key: 'support_video_effect_template', label: '视频特效模板', type: 'boolean' }, { key: 'supported_modes', label: '支持模式', placeholder: 'text_to_video, image_reference', type: 'list' }, { key: 'max_videos', label: '最多视频', placeholder: '1', type: 'number' }, { key: 'max_images', label: '最多图片', placeholder: '4', type: 'number' }, { key: 'max_audios', label: '最多音频', placeholder: '1', type: 'number' }, { key: 'max_elements', label: '最多主体', placeholder: '4', type: 'number' }, { key: 'max_images_and_elements', label: '图片主体合计', placeholder: '4', type: 'number' }, { key: 'support_instruction_edit', label: '指令编辑', type: 'boolean' }, ]; const model3dFields: FieldDefinition[] = [ { key: 'support_texture', label: '纹理生成', type: 'boolean' }, { key: 'support_part_generation', label: '分部生成', type: 'boolean' }, { key: 'support_image_autofix', label: '图片自动优化', type: 'boolean' }, { key: 'support_quad', label: '四边面输出', type: 'boolean' }, { key: 'support_smart_low_poly', label: '智能低模', type: 'boolean' }, { key: 'geometry_quality_options', label: '几何质量', placeholder: 'standard, detailed', type: 'list' }, { key: 'max_face_limit', label: '三角面上限', placeholder: '20000', type: 'number' }, { key: 'max_face_limit_quad', label: '四边面上限', placeholder: '10000', type: 'number' }, ]; export function BaseModelCapabilityEditor(props: { modelType: string; typeOptions: string[]; value: CapabilityEditorState; onChange: (value: CapabilityEditorState) => void; }) { const enabledTypes = props.value.types; const [activeType, setActiveType] = useState(enabledTypes[0] ?? props.modelType); const [pendingType, setPendingType] = useState(''); const availableTypes = useMemo( () => Array.from(new Set([props.modelType, ...props.typeOptions].filter(Boolean))), [props.modelType, props.typeOptions], ); const addableTypes = availableTypes.filter((type) => !enabledTypes.includes(type)); const activeConfig = props.value.typeConfigs[activeType] ?? defaultCapabilityConfig(activeType); const fieldGroups = capabilityFieldGroups(activeType); const capabilitySummary = useMemo( () => summarizeEnabledCapabilities(enabledTypes, props.value.typeConfigs), [enabledTypes, props.value.typeConfigs], ); useEffect(() => { if (!enabledTypes.includes(activeType)) setActiveType(enabledTypes[0] ?? props.modelType); }, [activeType, enabledTypes, props.modelType]); function addType() { const nextType = pendingType || addableTypes[0]; if (!nextType) return; props.onChange(addCapabilityType(props.value, nextType)); setActiveType(nextType); setPendingType(''); } function removeType(type: string) { const next = removeCapabilityType(props.value, type); props.onChange(next); setActiveType(next.types[0] ?? props.modelType); } function patchConfig(key: string, value: unknown) { const nextConfig = { ...activeConfig }; if (value === undefined) delete nextConfig[key]; else nextConfig[key] = value; const exclusiveKey = exclusiveCapabilityFields[key]; if (exclusiveKey && hasMeaningfulValue(value)) delete nextConfig[exclusiveKey]; props.onChange(updateCapabilityConfig(props.value, activeType, sanitizeCapabilityConfig(nextConfig))); } return (
模型能力 左侧维护已启用能力,右侧编辑该能力的默认配置。
{fieldGroups.length ? ( fieldGroups.map((group) => ( {group.fields.map((field) => ( patchConfig(field.key, value)} /> ))} )) ) : (
当前能力暂无通用表单 仍会保存该能力条目,并保留原始能力字段;后续可按平台专属能力继续扩展。
)} } title="保留字段">
{preservedConfigCount(activeConfig, fieldGroups)} 个未展示字段 这些字段保存时会继续保留在 {activeType} 能力配置内。
); } function CapabilityFormGroup(props: { children: ReactNode; icon: ReactNode; title: string }) { return (
{props.icon} {props.title}
{props.children}
); } function CapabilityFormRow(props: { config: Record; defaultConfig: Record; field: FieldDefinition; onChange: (value: unknown) => void; }) { const value = props.config[props.field.key]; return (
{props.field.label} {props.field.hint && {props.field.hint}}
); } function FieldControl(props: { config: Record; defaultValue: unknown; field: FieldDefinition; value: unknown; onChange: (value: unknown) => void; }) { if (props.field.type === 'boolean') { return ; } if (props.field.type === 'range') { const range = Array.isArray(props.value) ? props.value : []; return (
props.onChange([numberValue(event.target.value), numberValue(range[1])])} /> props.onChange([numberValue(range[0]), numberValue(event.target.value)])} />
); } if (props.field.type === 'ratioRange') { const range = Array.isArray(props.value) ? props.value : []; return (
props.onChange([numberValue(event.target.value), numberValue(range[1])])} /> props.onChange([numberValue(range[0]), numberValue(event.target.value)])} />
); } if (props.field.type === 'number') { return props.onChange(numberValue(event.target.value))} />; } if (isScopedField(props.field)) { return ; } if (props.field.type === 'text') { return props.onChange(event.target.value)} />; } return ; } function BooleanFieldControl(props: { defaultText: string; onChange: (value: unknown) => void; value: unknown; }) { return ( ); } function ScopedFieldControl(props: { config: Record; field: FieldDefinition; value: unknown; onChange: (value: unknown) => void; }) { const grouped = isPlainRecord(props.value); const directValue = grouped ? defaultDirectValue(props.field) : props.value; const scopeOptions = scopeOptionsForField(props.field, props.config, props.value); const entries = grouped ? Object.entries(props.value as Record) : []; const canGroup = scopeOptions.length > 0 || entries.length > 0; function switchMode(nextGrouped: boolean) { if (nextGrouped === grouped) return; if (!nextGrouped) { props.onChange(entries[0]?.[1] ?? defaultDirectValue(props.field)); return; } const firstKey = firstAvailableScopeKey(scopeOptions, []); props.onChange({ [firstKey]: normalizeScopedValue(props.field, props.value) }); } function setEntryKey(oldKey: string, nextKey: string) { if (!nextKey || !grouped) return; const next = { ...(props.value as Record) }; const current = next[oldKey]; delete next[oldKey]; next[nextKey] = current; props.onChange(next); } function setEntryValue(key: string, nextValue: unknown) { props.onChange({ ...(props.value as Record), [key]: nextValue }); } function removeEntry(key: string) { const next = { ...(props.value as Record) }; delete next[key]; props.onChange(Object.keys(next).length ? next : defaultDirectValue(props.field)); } function addEntry() { const used = entries.map(([key]) => key); const nextKey = firstAvailableScopeKey(scopeOptions, used); const current = grouped ? (props.value as Record) : {}; props.onChange({ ...current, [nextKey]: defaultDirectValue(props.field) }); } return (
{!grouped ? ( ) : (
{entries.map(([key, value]) => (
setEntryValue(key, nextValue)} />
))}
)}
); } function ScopedValueInput(props: { config: Record; field: FieldDefinition; onChange: (value: unknown) => void; placeholder?: string; value: unknown; }) { if (props.field.type === 'scopedRange') { const range = Array.isArray(props.value) ? props.value : []; return (
props.onChange([numberValue(event.target.value), numberValue(range[1])])} /> props.onChange([numberValue(range[0]), numberValue(event.target.value)])} />
); } if (props.field.type === 'scopedNumber') { return props.onChange(numberValue(event.target.value))} />; } return ; } function MultiValueControl(props: { config: Record; field: FieldDefinition; onChange: (value: unknown) => void; value: unknown; }) { const [customValue, setCustomValue] = useState(''); const selected = valueArray(props.value); const options = multiOptionsForField(props.field, props.config, props.value); const selectedSet = new Set(selected); function emit(nextValues: string[]) { const values = uniqueStrings(nextValues); props.onChange(isNumberListField(props.field) ? values.map(Number).filter(Number.isFinite) : values); } function toggle(value: string) { emit(selectedSet.has(value) ? selected.filter((item) => item !== value) : [...selected, value]); } function addCustom() { const values = customValue.split(/[,,]/).map((item) => item.trim()).filter(Boolean); if (!values.length) return; emit([...selected, ...values]); setCustomValue(''); } return (
{options.map((option) => ( ))}
setCustomValue(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); addCustom(); } }} />
); } type ScopeOption = { label: string; requires?: string; value: string }; const modeScopeOptions: ScopeOption[] = [ { value: 'input_first_frame', label: '首帧模式 (input_first_frame)', requires: 'input_first_frame' }, { value: 'input_first_last_frame', label: '首尾帧模式 (input_first_last_frame)', requires: 'input_first_last_frame' }, { value: 'input_last_frame', label: '尾帧模式 (input_last_frame)', requires: 'input_last_frame' }, { value: 'input_smart_multi_frame', label: '智能多帧 (input_smart_multi_frame)', requires: 'input_smart_multi_frame' }, ]; const videoResolutionOptions = ['480p', '720p', '1080p', '1440p', '2160p']; const imageResolutionOptions = ['1K', '2K', '3K', '4K', '8K']; const videoAspectRatioOptions = ['adaptive', '16:9', '9:16', '1:1', '4:3', '3:4']; const imageAspectRatioOptions = [ 'adaptive', '1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3', '5:4', '4:5', '5:3', '3:5', '21:9', '9:21', '2:1', '1:2', '4:1', '1:4', '8:1', '1:8', '7:4', '4:7', ]; const thinkingEffortOptions = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']; const omniVideoModeOptions = ['text_to_video', 'image_reference', 'element_reference', 'first_last_frame', 'video_reference', 'video_edit', 'multi_shot']; const durationOptionValues = ['1', '2', '3', '4', '5', '6', '8', '10', '15', '20', '25', '30']; const exclusiveCapabilityFields: Record = { duration_range: 'duration_options', duration_options: 'duration_range', }; function isScopedField(field: FieldDefinition) { return field.type === 'scopedList' || field.type === 'scopedNumberList' || field.type === 'scopedRange' || field.type === 'scopedNumber'; } function scopeOptionsForField(field: FieldDefinition, config: Record, value: unknown): ScopeOption[] { const options: ScopeOption[] = []; if (field.scope === 'mode' || field.scope === 'mixed') { options.push(...modeScopeOptions.filter((option) => !option.requires || config[option.requires] === true)); flattenStringValues(config.supported_modes).forEach((mode) => options.push({ value: mode, label: `${friendlyScopeLabel(mode)} (${mode})` })); } if (field.scope === 'resolution' || field.scope === 'mixed') { [...flattenStringValues(config.output_resolutions), ...videoResolutionOptions].forEach((resolution) => { options.push({ value: resolution, label: `${friendlyScopeLabel(resolution)} (${resolution})` }); }); } if (isPlainRecord(value)) { Object.keys(value).forEach((key) => options.push({ value: key, label: `${friendlyScopeLabel(key)} (${key})` })); } return dedupeScopeOptions(options); } function ensureScopeOption(options: ScopeOption[], value: string) { if (!value || options.some((option) => option.value === value)) return options; return [...options, { value, label: `${friendlyScopeLabel(value)} (${value})` }]; } function firstAvailableScopeKey(options: ScopeOption[], used: string[]) { return options.find((option) => !used.includes(option.value))?.value ?? `condition_${used.length + 1}`; } function sanitizeCapabilityConfig(config: Record) { const supportedModeKeys = new Set([ ...modeScopeOptions.filter((option) => !option.requires || config[option.requires] === true).map((option) => option.value), ...flattenStringValues(config.supported_modes), ]); const keepScopedModeKey = (key: string) => !isKnownModeScopeKey(key) || supportedModeKeys.has(key); return Object.fromEntries( Object.entries(config).map(([key, value]) => { if (!isScopedConfigKey(key) || !isPlainRecord(value)) return [key, value]; const filtered = Object.fromEntries(Object.entries(value).filter(([scopeKey]) => keepScopedModeKey(scopeKey))); if (Object.keys(filtered).length > 0) return [key, filtered]; return [key, defaultDirectValueForConfigKey(key)]; }), ); } function isScopedConfigKey(key: string) { return key === 'output_resolutions' || key === 'aspect_ratio_allowed' || key === 'duration_range' || key === 'duration_options' || key === 'duration_step'; } function isKnownModeScopeKey(key: string) { return modeScopeOptions.some((option) => option.value === key) || ['text_to_video', 'image_reference', 'element_reference', 'first_last_frame', 'video_reference', 'video_edit', 'multi_shot'].includes(key); } function defaultDirectValueForConfigKey(key: string) { if (key === 'duration_range') return [0, 0]; if (key === 'duration_step') return 0; return []; } function multiOptionsForField(field: FieldDefinition, config: Record, value: unknown): ValueOption[] { let options: string[] = []; if (field.key === 'output_resolutions') options = field.scope ? videoResolutionOptions : imageResolutionOptions; if (field.key === 'aspect_ratio_allowed') options = field.scope ? videoAspectRatioOptions : imageAspectRatioOptions; if (field.key === 'supported_modes') options = omniVideoModeOptions; if (field.key === 'thinkingEffortLevels') options = thinkingEffortOptions; if (field.key === 'duration_options') options = durationOptionValues; if (field.key === 'dimensions') options = ['3072', '1536', '1024', '768', '512', '256']; if (field.key === 'geometry_quality_options') options = ['standard', 'detailed']; options = orderedOptionValues(options, [...valueArray(value), ...flattenStringValues(config[field.key])], field); return options.map((item) => ({ value: item, label: friendlyOptionLabel(field, item) })); } function orderedOptionValues(baseOptions: string[], extraOptions: string[], field: FieldDefinition) { const base = uniqueStrings(baseOptions); const baseSet = new Set(base); const extras = uniqueStrings(extraOptions).filter((item) => !baseSet.has(item)); if (field.key === 'duration_options') { return uniqueStrings([...base, ...extras]).sort((a, b) => Number(a) - Number(b)); } const sortedExtras = isNumberListField(field) ? extras.sort((a, b) => Number(a) - Number(b)) : extras.sort((a, b) => a.localeCompare(b)); return [...base, ...sortedExtras]; } function friendlyOptionLabel(field: FieldDefinition, value: string) { if (field.key === 'duration_options') return `${value}秒`; if (field.key === 'supported_modes') return friendlyScopeLabel(value); return value; } function valueArray(value: unknown): string[] { if (Array.isArray(value)) return value.map(String).filter(Boolean); if (value === undefined || value === null || value === '') return []; return [String(value)]; } function isNumberListField(field: FieldDefinition) { return field.type === 'numberList' || field.type === 'scopedNumberList'; } function friendlyScopeLabel(key: string) { const labels: Record = { input_first_frame: '首帧模式', input_first_last_frame: '首尾帧模式', input_last_frame: '尾帧模式', input_smart_multi_frame: '智能多帧', text_to_video: '文生视频', image_reference: '图片参考', element_reference: '主体参考', first_last_frame: '首尾帧', video_reference: '视频参考', video_edit: '视频编辑', multi_shot: '多镜头', }; return labels[key] ?? key; } function dedupeScopeOptions(options: ScopeOption[]) { const seen = new Set(); return options.filter((option) => { if (!option.value || seen.has(option.value)) return false; seen.add(option.value); return true; }); } function flattenStringValues(value: unknown): string[] { if (Array.isArray(value)) return value.map(String).filter(Boolean); if (!isPlainRecord(value)) return []; return Object.values(value).flatMap((item) => Array.isArray(item) ? item.map(String).filter(Boolean) : []); } function defaultDirectValue(field: FieldDefinition) { if (field.type === 'scopedRange') return [0, 0]; if (field.type === 'scopedNumber') return 0; return []; } function normalizeScopedValue(field: FieldDefinition, value: unknown) { if (isPlainRecord(value)) return Object.values(value)[0] ?? defaultDirectValue(field); if (field.type === 'scopedRange') return Array.isArray(value) ? value : defaultDirectValue(field); if (field.type === 'scopedNumber') return typeof value === 'number' || typeof value === 'string' ? numberValue(value) : 0; return Array.isArray(value) ? value : defaultDirectValue(field); } function hasMeaningfulValue(value: unknown): boolean { if (value === undefined || value === null || value === '') return false; if (Array.isArray(value)) return value.length > 0; if (isPlainRecord(value)) return Object.keys(value).length > 0; return true; } function capabilityFieldGroups(type: string): Array<{ title: string; icon: ReactNode; fields: FieldDefinition[] }> { if (isTextType(type)) return [{ title: '文本默认能力', icon: , fields: textFields }]; if (type === 'text_embedding') return [{ title: '向量默认能力', icon: , fields: embeddingFields }]; if (isImageType(type)) return [{ title: '图像默认能力', icon: , fields: imageFieldsFor(type) }]; if (isVideoType(type)) return [{ title: '视频默认能力', icon: