/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; import { Card, Select, Typography, Button, Switch, RadioGroup, Input, Slider, } from '@douyinfe/semi-ui'; import { Sparkles, Users, ToggleLeft, X, Settings } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { formatVideoResolutionDisplayLabel, renderGroupOption, selectFilter } from '../../helpers'; import { PLAYGROUND_IMAGE_SIZE_OPTIONS, PLAYGROUND_VIDEO_DURATION_OPTIONS, } from '../../constants/playground.constants'; import ParameterControl from './ParameterControl'; /** * 体验馆分组下拉:与全局 renderGroupOption 一致,但不展示倍率角标。 * @param {Record} item Semi Select 传入的选项渲染参数 */ const renderPlaygroundGroupOption = (item) => renderGroupOption({ ...item, ratio: undefined }); import ImageUrlInput from './ImageUrlInput'; import VideoUrlInput from './VideoUrlInput'; import ConfigManager from './ConfigManager'; import CustomRequestEditor from './CustomRequestEditor'; const SettingsPanel = ({ inputs, parameterEnabled, models, modelTypes, supplierOptions, groups, styleState, showDebugPanel, customRequestMode, customRequestBody, onInputChange, onParameterToggle, onCloseSettings, onConfigImport, onConfigReset, onCustomRequestModeChange, onCustomRequestBodyChange, previewPayload, messages, userId, }) => { const { t } = useTranslation(); const currentConfig = { inputs, parameterEnabled, showDebugPanel, customRequestMode, customRequestBody, }; const displayMode = inputs.display_mode || 'text'; const isImageMode = displayMode === 'image'; const isVideoMode = displayMode === 'video'; const mediaModeEnabled = isImageMode || isVideoMode; const videoMediaHint = isVideoMode ? t( '体验馆视频素材提示', '图片地址:第 1 张为首帧,2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。', ) : ''; const applyVideoResolutionPreset = (preset) => { const [w, h] = String(preset || '1280x720') .split('x') .map((n) => Number(n)); onInputChange('video_resolution_preset', preset); if (Number.isFinite(w) && Number.isFinite(h)) { onInputChange('video_width', w); onInputChange('video_height', h); } }; return ( {/* 标题区域 - 与调试面板保持一致 */}
{t('模型配置')}
{styleState.isMobile && onCloseSettings && (
{/* 展示模式(置顶,左右切换) */}
{t('展示模式')} { const nextMode = e?.target?.value || 'text'; onInputChange('display_mode', nextMode); // 切换模式时重置类型筛选,避免残留筛选导致“明明有模型却不显示” onInputChange('model_type', ''); }} disabled={customRequestMode} />
{/* 移动端配置管理 */} {styleState.isMobile && (
)}
{/* 自定义请求体编辑器 */} {/* 分组选择 */}
{t('分组')} {customRequestMode && ( ({t('已在自定义模式中忽略')}) )}
onInputChange( 'model_type', value === undefined || value === null ? '' : value, ) } value={ inputs.model_type === undefined || inputs.model_type === null ? '' : inputs.model_type } autoComplete='new-password' optionList={modelTypes} style={{ width: '100%' }} dropdownStyle={{ width: '100%', maxWidth: '100%' }} className='!rounded-lg mb-3' disabled={customRequestMode} />
{t('模型')} {customRequestMode && ( ({t('已在自定义模式中忽略')}) )}
onInputChange( 'selected_route_slug', value === undefined || value === null ? '' : value, ) } value={ inputs.selected_route_slug === undefined || inputs.selected_route_slug === null ? '' : inputs.selected_route_slug } autoComplete='new-password' optionList={supplierOptions} style={{ width: '100%' }} dropdownStyle={{ width: '100%', maxWidth: '100%' }} className='!rounded-lg' disabled={customRequestMode} />
{/* 素材 URL:视频模式分图片地址 / 视频地址 */} {mediaModeEnabled && (
onInputChange('imageUrls', urls)} onImageEnabledChange={() => {}} allowToggle={false} disabled={customRequestMode} /> {isVideoMode && ( onInputChange('videoUrls', urls)} onVideoEnabledChange={() => {}} allowToggle={false} disabled={customRequestMode} /> )} {isVideoMode ? videoMediaHint : t('图片模式支持图片 URL 作为素材')}
)} {/* 模式参数区 */} {displayMode === 'text' && (
)} {isImageMode && (
{t('图片参数')} {t('用于文生图/图生图的核心参数')} {t('计费以实际图片质量为准')}
{t('分辨率')} onInputChange('image_n', Math.max(1, Math.min(4, Number(value) || 1))) } disabled={customRequestMode} />
)} {isVideoMode && (
{t('视频参数')} {t('用于视频生成的核心参数(时长、分辨率)')} {t('计费以实际视频质量为准')}
{t('视频时长(秒)')} applyVideoResolutionPreset(value)} disabled={customRequestMode} style={{ width: '100%' }} />
onInputChange('video_width', Number(value) || 1280)} disabled={customRequestMode} /> onInputChange('video_height', Number(value) || 720)} disabled={customRequestMode} />
{t('运动强度 motion')}
onInputChange('video_motion', Number(value ?? 0.4)) } disabled={customRequestMode} tipFormatter={(value) => `${Number(value).toFixed(1)}`} />
{t('生成数量 n')}