feat(web): 模型分组 - 同显示名模型族按参数路由最低价模型且模型始终可切换
This commit is contained in:
@@ -8,7 +8,7 @@ import { CreditSymbol, requestCreditCost } from "@/constant/credits";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
|
||||
import { modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue } from "@/lib/model-capabilities";
|
||||
import { modelCompatibilityError, resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { modelCompatibilityError, resolveCompatibleModel, defaultImageParamsForModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
@@ -153,7 +153,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
|
||||
<div className="mb-2 rounded-lg px-2 py-2 text-[var(--fs-label)]" style={{ background: theme.node.fill, color: theme.node.muted }}>将使用当前默认模型与生成参数</div>
|
||||
) : (
|
||||
<div className={`mb-2 grid min-w-0 cursor-default items-center gap-2 ${mode === "image" || mode === "video" || mode === "audio" ? "grid-cols-[minmax(0,1fr)_148px]" : "grid-cols-1"}`} onMouseDown={(event) => event.stopPropagation()}>
|
||||
<ModelPicker className="canvas-compact-control h-10" config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability={mode} requirements={requirements} onMissingConfig={() => navigateToSettings({ continueCreation: true })} fullWidth showSelectedPrice={creditsEnabled} />
|
||||
<ModelPicker className="canvas-compact-control h-10" config={config} value={config.model} onChange={(model) => onConfigChange(node.id, mode === "image" ? { model, ...defaultImageParamsForModel(config, model) } : { model })} capability={mode} requirements={requirements} onMissingConfig={() => navigateToSettings({ continueCreation: true })} fullWidth showSelectedPrice={creditsEnabled} />
|
||||
{mode === "video" ? (
|
||||
<CanvasVideoSettingsPopover config={config} placement="topRight" buttonClassName="canvas-compact-control !h-10 !w-full !justify-start !rounded-lg !px-2" onConfigChange={(key, value) => onConfigChange(node.id, videoConfigPatch(key, value))} />
|
||||
) : mode === "image" ? (
|
||||
@@ -227,7 +227,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
|
||||
const fallbackModel = mode === "image" ? defaultConfig.imageModel : mode === "video" ? defaultConfig.videoModel : mode === "audio" ? defaultConfig.audioModel : defaultConfig.textModel;
|
||||
const storedModel = node.metadata?.model;
|
||||
const preferredModel = storedModel && configuredModelMatchesCapability(globalConfig, storedModel, mode) ? storedModel : defaultModel && configuredModelMatchesCapability(globalConfig, defaultModel, mode) ? defaultModel : fallbackModel;
|
||||
const model = resolveCompatibleModel(globalConfig, preferredModel, requirements) || preferredModel;
|
||||
const model = resolveCompatibleModel(globalConfig, preferredModel, mode === "image" ? { ...requirements, imageSize: node.metadata?.size || globalConfig.size || defaultConfig.size } : requirements) || preferredModel;
|
||||
const imageProfile = mode === "image" ? modelCapabilityConfigFor(globalConfig, model).image! : undefined;
|
||||
const normalizedImage = imageProfile ? normalizeImageValue(imageProfile, { size: node.metadata?.size || globalConfig.size || defaultConfig.size, quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality, transparentBackground: node.metadata?.transparentBackground || globalConfig.transparentBackground, count: String(node.metadata?.count || globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count) }) : undefined;
|
||||
const videoProfile = mode === "video" ? modelCapabilityConfigFor(globalConfig, model).video! : undefined;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { resolveCanvasGenerationModel } from "@/lib/canvas/canvas-project-genera
|
||||
import { CreditSymbol, requestCreditCost } from "@/constant/credits";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
|
||||
import { resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { resolveCompatibleModel, defaultImageParamsForModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
@@ -238,7 +238,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
fullWidth
|
||||
config={config}
|
||||
value={config.model}
|
||||
onChange={(model) => onConfigChange(node.id, { model })}
|
||||
onChange={(model) => onConfigChange(node.id, mode === "image" ? { model, ...defaultImageParamsForModel(config, model) } : { model })}
|
||||
capability={mode}
|
||||
requirements={requirements}
|
||||
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
|
||||
@@ -546,7 +546,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
|
||||
const defaultModel = mode === "image" ? globalConfig.imageModel : mode === "video" ? globalConfig.videoModel : mode === "audio" ? globalConfig.audioModel : globalConfig.textModel;
|
||||
const fallbackModel = mode === "image" ? defaultConfig.imageModel : mode === "video" ? defaultConfig.videoModel : mode === "audio" ? defaultConfig.audioModel : defaultConfig.textModel;
|
||||
const preferredModel = resolveCanvasGenerationModel(globalConfig, node.metadata?.model, mode) || resolveCanvasGenerationModel(globalConfig, defaultModel, mode) || fallbackModel;
|
||||
const model = resolveCompatibleModel(globalConfig, preferredModel, requirements) || preferredModel;
|
||||
const model = resolveCompatibleModel(globalConfig, preferredModel, mode === "image" ? { ...requirements, imageSize: node.metadata?.size || globalConfig.size || defaultConfig.size } : requirements) || preferredModel;
|
||||
return {
|
||||
...globalConfig,
|
||||
model,
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ConfigProvider, Switch } from "antd";
|
||||
|
||||
import { type CanvasTheme } from "@/lib/canvas-theme";
|
||||
import { buildImageResolutionOptions, formatImageResolutionSize, imageRatioForSize, imageResolutionChoices, imageResolutionOption, imageSizeForResolution, supportsImageResolutionPresets, type ImageResolutionChoice } from "@/lib/image-resolution-tiers";
|
||||
import { modelCapabilityConfigFor, normalizeImageValue, type ImageCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { normalizeImageValue, type ImageCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { mergedImageCapabilityConfig } from "@/lib/model-selection";
|
||||
import { type AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
const qualityOptions = [
|
||||
@@ -50,7 +51,7 @@ type ImageSettingsPanelProps = {
|
||||
|
||||
export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle = true, showCount = true, className = "w-[304px] space-y-3 rounded-2xl px-1 py-0.5", maxCount = 15, quickCount = 3 }: ImageSettingsPanelProps) {
|
||||
const [snapDimensionToStep, setSnapDimensionToStep] = useState(true);
|
||||
const profile = modelCapabilityConfigFor(config, config.model || config.imageModel).image!;
|
||||
const profile = mergedImageCapabilityConfig(config, config.model || config.imageModel);
|
||||
const normalized = normalizeImageValue(profile, config);
|
||||
const quality = normalized.quality;
|
||||
const transparentBackground = normalized.transparentBackground === "true";
|
||||
|
||||
@@ -145,28 +145,26 @@ export function ModelPicker({ config, value, onChange, capability, className, fu
|
||||
<div className="grid min-w-0 gap-1">
|
||||
{group.models.map((modelGroup) => {
|
||||
const selected = modelGroup.models.includes(current);
|
||||
const model = compatibleModelInGroup(config, modelGroup.models, requirements, selected ? current : undefined);
|
||||
const displayModel = model || (selected ? current : modelGroup.models[0]);
|
||||
const disabledReason = model ? "" : modelCompatibilityError(config, modelGroup.models[0], requirements) || "当前输入不符合该模型能力";
|
||||
const compatibleModel = compatibleModelInGroup(config, modelGroup.models, requirements, selected ? current : undefined);
|
||||
const model = compatibleModel || modelGroup.models[0];
|
||||
const displayModel = model;
|
||||
const incompatibleReason = compatibleModel ? "" : modelCompatibilityError(config, modelGroup.models[0], requirements) || "当前输入不符合该模型能力,切换后将重置参数";
|
||||
return (
|
||||
<button
|
||||
key={modelGroup.key}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
aria-disabled={Boolean(disabledReason)}
|
||||
disabled={Boolean(disabledReason)}
|
||||
title={disabledReason || pickerModelOptionLabel(config, displayModel, showConfiguredModelName)}
|
||||
className="canvas-model-picker-option disabled:cursor-not-allowed disabled:opacity-45"
|
||||
title={incompatibleReason || pickerModelOptionLabel(config, displayModel, showConfiguredModelName)}
|
||||
className="canvas-model-picker-option"
|
||||
style={{ background: "transparent", color: theme.node.text }}
|
||||
onClick={() => {
|
||||
if (!model) return;
|
||||
onChange(model);
|
||||
setOpen(false);
|
||||
window.requestAnimationFrame(() => triggerRef.current?.focus());
|
||||
}}
|
||||
>
|
||||
<ModelLabel config={config} model={displayModel} capability={capability} theme={theme} creationVariant={creationVariant} showConfiguredModelName={showConfiguredModelName} showPrice={creditsEnabled} disabledReason={disabledReason} />
|
||||
<ModelLabel config={config} model={displayModel} capability={capability} theme={theme} creationVariant={creationVariant} showConfiguredModelName={showConfiguredModelName} showPrice={creditsEnabled} disabledReason={incompatibleReason} />
|
||||
{selected ? <Check className="canvas-model-picker-option-check" style={{ color: theme.node.activeStroke }} /> : null}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -372,7 +372,8 @@ export function buildGenerationConfig(config: AiConfig, node: CanvasNodeData | u
|
||||
const fallbackModel = mode === "image" ? defaultConfig.imageModel : mode === "video" ? defaultConfig.videoModel : mode === "audio" ? defaultConfig.audioModel : defaultConfig.textModel;
|
||||
const storedModel = resolveCanvasGenerationModel(config, node?.metadata?.model, mode);
|
||||
const preferredModel = storedModel || resolveCanvasGenerationModel(config, defaultModel, mode) || fallbackModel;
|
||||
const model = resolveCompatibleModel(config, preferredModel, requirements) || preferredModel;
|
||||
const imageSize = mode === "image" ? node?.metadata?.size || config.size || defaultConfig.size : undefined;
|
||||
const model = resolveCompatibleModel(config, preferredModel, imageSize ? { ...requirements, imageSize } : requirements) || preferredModel;
|
||||
const imageProfile = mode === "image" ? modelCapabilityConfigFor(config, model).image! : undefined;
|
||||
const normalizedImage = imageProfile
|
||||
? normalizeImageValue(imageProfile, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { modelCapabilityConfigFor, videoDurationAllowed } from "@/lib/model-capabilities";
|
||||
import { defaultImageCapabilityConfig, modelCapabilityConfigFor, videoDurationAllowed, type ImageCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { modelOptionName, resolveModelChannel, selectableModelsByCapability, type AiConfig, type ModelCapability } from "@/stores/use-config-store";
|
||||
|
||||
export type ModelInputSummary = {
|
||||
@@ -14,6 +14,7 @@ export type ModelRequirements = {
|
||||
input?: ModelInputSummary;
|
||||
videoOperation?: string;
|
||||
videoSeconds?: string;
|
||||
imageSize?: string;
|
||||
};
|
||||
|
||||
export type DisplayModelGroup = {
|
||||
@@ -56,8 +57,9 @@ export function modelCompatibilityError(config: AiConfig, model: string, require
|
||||
if (capability === "image") {
|
||||
if (input.videoCount > 0) return "图片模型不支持参考视频";
|
||||
if (input.audioCount > 0) return "图片模型不支持参考音频";
|
||||
const maxImages = modelCapabilityConfigFor(config, model).image!.references.maxImages;
|
||||
if (visualInputCount > maxImages) return `最多支持 ${maxImages} 张参考图`;
|
||||
const image = modelCapabilityConfigFor(config, model).image!;
|
||||
if (visualInputCount > image.references.maxImages) return `最多支持 ${image.references.maxImages} 张参考图`;
|
||||
if (requirements.imageSize && !image.size.allowCustom && !image.size.values.includes(requirements.imageSize)) return "不支持当前尺寸";
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -81,8 +83,15 @@ export function modelCompatibilityError(config: AiConfig, model: string, require
|
||||
}
|
||||
|
||||
export function compatibleModelInGroup(config: AiConfig, models: string[], requirements?: ModelRequirements, preferred?: string) {
|
||||
const ordered = preferred && models.includes(preferred) ? [preferred, ...models.filter((model) => model !== preferred)] : models;
|
||||
return ordered.find((model) => !modelCompatibilityError(config, model, requirements)) || "";
|
||||
const compatible = models.filter((model) => !modelCompatibilityError(config, model, requirements));
|
||||
if (!compatible.length) return "";
|
||||
if (compatible.length === 1) return compatible[0];
|
||||
const priceOf = (model: string) => {
|
||||
const channel = resolveModelChannel(config, model);
|
||||
const cost = channel.modelCosts?.find((item) => item.model === modelOptionName(model));
|
||||
return cost && Number.isFinite(cost.unitPriceMicrocredits) ? cost.unitPriceMicrocredits : Number.POSITIVE_INFINITY;
|
||||
};
|
||||
return compatible.sort((left, right) => priceOf(left) - priceOf(right) || (left === preferred ? -1 : right === preferred ? 1 : 0))[0];
|
||||
}
|
||||
|
||||
export function resolveCompatibleModel(config: AiConfig, selected: string, requirements?: ModelRequirements) {
|
||||
@@ -94,6 +103,29 @@ export function resolveCompatibleModel(config: AiConfig, selected: string, requi
|
||||
return compatibleModelInGroup(config, selectedGroup.models, requirements, selected);
|
||||
}
|
||||
|
||||
// 同显示名分组的模型族:尺寸/比例/分辨率选项取组内全部模型配置的并集,
|
||||
// 让用户能看到并选择任意成员支持的能力,选中后由兼容路由落到具体模型。
|
||||
export function mergedImageCapabilityConfig(config: AiConfig, selected: string): ImageCapabilityConfig {
|
||||
const options = selectableModelsByCapability(config, "image");
|
||||
const group = options.length ? groupModelsByDisplayName(config, options).find((item) => item.models.includes(selected)) : undefined;
|
||||
const models = group?.models.length ? group.models : [selected];
|
||||
const profiles = models.map((model) => modelCapabilityConfigFor(config, model).image).filter((profile): profile is ImageCapabilityConfig => Boolean(profile));
|
||||
if (profiles.length <= 1) return profiles[0] || defaultImageCapabilityConfig();
|
||||
const values = Array.from(new Set(profiles.flatMap((profile) => profile.size.values)));
|
||||
return { ...profiles[0], size: { ...profiles[0].size, values } };
|
||||
}
|
||||
|
||||
// 切换模型后初始化图片参数为该模型能力默认值,避免旧参数在目标模型族不兼容导致无法切换。
|
||||
export function defaultImageParamsForModel(config: AiConfig, model: string): Pick<AiConfig, "size" | "quality" | "transparentBackground"> {
|
||||
const image = modelCapabilityConfigFor(config, model).image;
|
||||
if (!image) return { size: "1:1", quality: "auto", transparentBackground: "false" };
|
||||
return {
|
||||
size: image.size.default || image.size.values[0] || "1:1",
|
||||
quality: image.quality.default || "auto",
|
||||
transparentBackground: String(image.transparentBackground.default ?? false),
|
||||
};
|
||||
}
|
||||
|
||||
export function maxModelInputCapacity(config: AiConfig, capability: "image" | "video", kind: "image" | "video" | "audio") {
|
||||
const options = selectableModelsByCapability(config, capability);
|
||||
if (!options.length) return null;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useCopyText } from "@/hooks/use-copy-text";
|
||||
import { buildImageResolutionOptions, formatImageResolutionSize, imageRatioForSize, imageResolutionChoices, imageResolutionOption, imageSizeForResolution, supportsImageResolutionPresets, type ImageResolutionChoice } from "@/lib/image-resolution-tiers";
|
||||
import { VIDEO_RESOLUTION_OPTIONS } from "@/lib/video-generation-options";
|
||||
import { modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, videoDurationAllowed, videoDurationOptions, type ImageCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { resolveCompatibleModel, mergedImageCapabilityConfig, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { isGenerationTaskCancelled, runBackendGenerationTask, runBackendGenerationTaskBatch, type BackendGenerationResult } from "@/services/api/generation-task";
|
||||
import { requestImageQuestion, type AiTextContentPart } from "@/services/api/image";
|
||||
import { listAddedSkills, type Skill } from "@/services/api/skills";
|
||||
@@ -173,7 +173,8 @@ export default function CreatePage() {
|
||||
characterCount: 0,
|
||||
},
|
||||
videoSeconds: seconds,
|
||||
}), [attachments, hasPrompt, mode, seconds]);
|
||||
imageSize: mode === "image" ? ratio : undefined,
|
||||
}), [attachments, hasPrompt, mode, ratio, seconds]);
|
||||
const selectedModel = resolveCompatibleModel(config, preferredModel, modelRequirements) || preferredModel;
|
||||
const imageProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).image!, [config, selectedModel]);
|
||||
const videoProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).video!, [config, selectedModel]);
|
||||
@@ -1127,15 +1128,17 @@ function GenerationSettingsMenu(props: ComposerProps) {
|
||||
const [customRatioOpen, setCustomRatioOpen] = useState(!ratioOptions.some((option) => option.value === props.ratio));
|
||||
const activeQualityOptions = props.imageProfile.quality.values.map((value) => qualityOptions.find((item) => item.value === value) || { value, label: value.toUpperCase(), description: "模型支持的质量/分辨率" });
|
||||
const qualityLabel = activeQualityOptions.find((item) => item.value === props.quality)?.label || qualityOptions.find((item) => item.value === props.quality)?.label || props.quality || "自动";
|
||||
const usesImageResolutionPicker = props.mode === "image" && supportsImageResolutionPresets(props.imageProfile.size);
|
||||
const imageResolutionOptions = usesImageResolutionPicker ? buildImageResolutionOptions(props.imageProfile.size.values) : [];
|
||||
// 尺寸/比例/分辨率选项取同显示名分组内全部模型的并集,路由模型只决定发送参数。
|
||||
const mergedProfile = mergedImageCapabilityConfig(props.config, props.model || props.config.imageModel);
|
||||
const usesImageResolutionPicker = props.mode === "image" && supportsImageResolutionPresets(mergedProfile.size);
|
||||
const imageResolutionOptions = usesImageResolutionPicker ? buildImageResolutionOptions(mergedProfile.size.values) : [];
|
||||
const activeImageResolution = usesImageResolutionPicker ? imageResolutionOption(imageResolutionOptions, props.ratio) : undefined;
|
||||
const activeImageRatio = activeImageResolution?.ratio || imageRatioForSize(props.ratio) || (props.ratio.includes(":") ? props.ratio : "1:1");
|
||||
const activeImageResolutionChoice: ImageResolutionChoice = activeImageResolution?.tier || "auto";
|
||||
const imageResolutionChoiceOptions = usesImageResolutionPicker ? imageResolutionChoices(props.imageProfile.size.values) : [];
|
||||
const imageResolutionChoiceOptions = usesImageResolutionPicker ? imageResolutionChoices(mergedProfile.size.values) : [];
|
||||
const imageRatios = usesImageResolutionPicker
|
||||
? Array.from(new Set(imageResolutionOptions.filter((item) => !activeImageResolution || item.tier === activeImageResolution.tier).map((item) => item.ratio)))
|
||||
: props.imageProfile.size.values.length ? props.imageProfile.size.values : ratioOptions.map((item) => item.value);
|
||||
: mergedProfile.size.values.length ? mergedProfile.size.values : ratioOptions.map((item) => item.value);
|
||||
const ratios = props.mode === "video" ? props.videoProfile.ratios : imageRatios;
|
||||
const resolutions = props.mode === "video" ? props.videoProfile.resolutions.map((value) => ({ value: value.replace(/p$/i, ""), label: videoResolutionLabel(value) })) : resolutionOptions;
|
||||
const selectImageRatio = (nextRatio: string) => {
|
||||
@@ -1147,20 +1150,20 @@ function GenerationSettingsMenu(props: ComposerProps) {
|
||||
};
|
||||
const selectImageResolution = (choice: ImageResolutionChoice) => {
|
||||
if (choice === "auto") {
|
||||
props.setRatio(props.imageProfile.size.values.includes("auto") ? "auto" : activeImageRatio);
|
||||
props.setRatio(mergedProfile.size.values.includes("auto") ? "auto" : activeImageRatio);
|
||||
return;
|
||||
}
|
||||
const nextSize = imageSizeForResolution(imageResolutionOptions, choice, activeImageRatio) || imageResolutionOptions.find((item) => item.tier === choice)?.size;
|
||||
if (nextSize) props.setRatio(nextSize);
|
||||
};
|
||||
const imageSummary = [
|
||||
...(props.imageProfile.size.parameter !== "none" ? [usesImageResolutionPicker ? formatImageResolutionSize(props.ratio, imageResolutionOptions) : props.ratio] : []),
|
||||
...(mergedProfile.size.parameter !== "none" ? [usesImageResolutionPicker ? formatImageResolutionSize(props.ratio, imageResolutionOptions) : props.ratio] : []),
|
||||
...(props.imageProfile.quality.supported ? [qualityLabel] : []),
|
||||
...(props.imageProfile.maxOutputs > 1 ? [props.count] : []),
|
||||
].join(" · ");
|
||||
const summary = props.mode === "video" ? `${props.ratio} · ${videoResolutionLabel(props.videoQuality)}` : imageSummary;
|
||||
const panel = <div className="creation-parameter-menu">
|
||||
{props.mode === "video" || props.imageProfile.size.parameter !== "none" ? <SettingSection title="画幅" value={props.mode === "image" && usesImageResolutionPicker ? activeImageRatio : props.ratio}><div className="creation-parameter-content"><div className="creation-choice-grid is-ratio">{ratios.map((value) => { const selected = props.mode === "image" && usesImageResolutionPicker ? value === activeImageRatio : value === props.ratio; return <button key={value} type="button" aria-pressed={selected} className={selected ? "is-selected" : ""} onClick={() => { if (props.mode === "image") selectImageRatio(value); else props.setRatio(value); setCustomRatioOpen(false); }}><span className="creation-ratio-preview"><span style={ratioPreviewStyle(value)} /></span><span>{value}</span></button>; })}</div>{props.mode !== "video" && props.imageProfile.size.allowCustom && (customRatioOpen ? <label className="creation-custom-value"><span>宽 x 高</span><input value={props.ratio} onFocus={(event) => event.currentTarget.select()} onChange={(event) => props.setRatio(event.target.value)} placeholder="1920x1080 或 2:1" aria-label="自定义图片尺寸或比例" /></label> : <button type="button" className="creation-custom-trigger" onClick={() => setCustomRatioOpen(true)}><Plus />输入自定义尺寸</button>)}</div></SettingSection> : null}
|
||||
{props.mode === "video" || mergedProfile.size.parameter !== "none" ? <SettingSection title="画幅" value={props.mode === "image" && usesImageResolutionPicker ? activeImageRatio : props.ratio}><div className="creation-parameter-content"><div className="creation-choice-grid is-ratio">{ratios.map((value) => { const selected = props.mode === "image" && usesImageResolutionPicker ? value === activeImageRatio : value === props.ratio; return <button key={value} type="button" aria-pressed={selected} className={selected ? "is-selected" : ""} onClick={() => { if (props.mode === "image") selectImageRatio(value); else props.setRatio(value); setCustomRatioOpen(false); }}><span className="creation-ratio-preview"><span style={ratioPreviewStyle(value)} /></span><span>{value}</span></button>; })}</div>{props.mode !== "video" && mergedProfile.size.allowCustom && (customRatioOpen ? <label className="creation-custom-value"><span>宽 x 高</span><input value={props.ratio} onFocus={(event) => event.currentTarget.select()} onChange={(event) => props.setRatio(event.target.value)} placeholder="1920x1080 或 2:1" aria-label="自定义图片尺寸或比例" /></label> : <button type="button" className="creation-custom-trigger" onClick={() => setCustomRatioOpen(true)}><Plus />输入自定义尺寸</button>)}</div></SettingSection> : null}
|
||||
{props.mode === "video" ? <SettingSection title="清晰度" value={videoResolutionLabel(props.videoQuality)}><div className="creation-choice-grid is-resolution">{resolutions.map((option) => <button key={option.value} type="button" aria-pressed={option.value === props.videoQuality} className={option.value === props.videoQuality ? "is-selected" : ""} onClick={() => props.setVideoQuality(option.value)}>{option.label}</button>)}</div></SettingSection> : <>
|
||||
{imageResolutionChoiceOptions.length ? <SettingSection title="分辨率" value={activeImageResolutionChoice === "auto" ? "自动" : activeImageResolutionChoice.toUpperCase()}><div className="creation-choice-grid is-resolution">{imageResolutionChoiceOptions.map((choice) => <button key={choice} type="button" aria-pressed={choice === activeImageResolutionChoice} className={choice === activeImageResolutionChoice ? "is-selected" : ""} onClick={() => selectImageResolution(choice)}>{choice === "auto" ? "自动" : choice.toUpperCase()}</button>)}</div></SettingSection> : null}
|
||||
{props.imageProfile.quality.supported ? <SettingSection title={activeQualityOptions.some((item) => item.value === "1k" || item.value === "2k") ? "分辨率" : "图片质量"} value={qualityLabel}><div className="creation-choice-grid is-quality">{activeQualityOptions.map((option) => <button key={option.value} type="button" aria-pressed={option.value === props.quality} className={option.value === props.quality ? "is-selected" : ""} onClick={() => props.setQuality(option.value)}><span>{option.label}</span><small>{option.description}</small></button>)}</div></SettingSection> : null}
|
||||
|
||||
Reference in New Issue
Block a user