"use client"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { ChevronDown, ChevronUp, Link2, Scan, SlidersHorizontal, Sparkles } from "lucide-react"; import { getGrsaiGptImageOutputSize } from "@/lib/ai/grsai-gpt-image-sizes"; import { cn } from "@/lib/utils"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; // 图片生成参数接口 export interface ImageGenerationParams { aspectRatio: string; imageSize: string; imageCount?: number; // 生成图片数量 quality?: "auto" | "low" | "medium" | "high"; // 🆕 阿里生图专用参数 size?: string; // 自定义分辨率,如 "1024*1024" negativePrompt?: string; // 反向提示词 promptExtend?: boolean; // 提示词智能优化 watermark?: boolean; // 水印 seed?: number; // 随机种子 } interface ImageParamsBarProps { modelName?: string; initialParams?: Partial; onParamsChange?: (params: ImageGenerationParams) => void; inline?: boolean; // 内联模式:只显示参数按钮,不显示外层容器 providerType?: string; // 模型类型:gemini, volcano-vision, alibaba-image, ruxa-image 等 hideImageCount?: boolean; // 隐藏图片数量按钮(仅个别页面需要) hideAdvancedOptions?: boolean; // 隐藏高级设置按钮 inlineTheme?: "light" | "dark" | "minimal"; showInlineDivider?: boolean; // 🆕 图片生成模型特性配置(从模型配置中读取) imageGenConfig?: { maxImages?: number; supportsSize?: boolean; supports1K?: boolean; supports2K?: boolean; supports4K?: boolean; supportsPromptExtend?: boolean; supportsNegativePrompt?: boolean; supportsWatermark?: boolean; supportsSeed?: boolean; supportsQuality?: boolean; supportedAspectRatios?: string[]; supportedImageSizes?: string[]; }; } const DEFAULT_ASPECT_RATIO_OPTIONS = [ { value: "auto", label: "自动" }, { value: "1:1", label: "1:1" }, { value: "16:9", label: "16:9" }, { value: "9:16", label: "9:16" }, { value: "4:3", label: "4:3" }, { value: "3:4", label: "3:4" }, { value: "3:2", label: "3:2" }, { value: "2:3", label: "2:3" }, { value: "5:4", label: "5:4" }, { value: "4:5", label: "4:5" }, { value: "21:9", label: "21:9" }, ] as const; type AspectRatioValue = string; const DEFAULT_IMAGE_SIZE_OPTIONS = [ { value: "1K", label: "1K", disabled: false }, { value: "2K", label: "2K", disabled: false }, { value: "4K", label: "4K", disabled: false }, ] as const; type ImageSizeValue = string; function normalizeImageSizeValue(value?: string, fallback: ImageSizeValue = "2K"): ImageSizeValue { const trimmed = String(value || "").trim(); if (!trimmed) return fallback; const kMatch = trimmed.match(/^(\d+(?:\.\d+)?)\s*k$/i); if (kMatch?.[1]) { return `${Number(kMatch[1]).toString()}K`; } return trimmed; } const IMAGE_SIZE_PIXEL_MAP: Record = { "0.5K": 512, "1K": 1024, "2K": 2048, "4K": 4096, "8K": 8192, "10K": 10240, "12K": 12288, }; const DEFAULT_IMAGE_SIZE_VALUE = "2K"; function getImageSizePixelValue(imageSize: string) { const normalized = normalizeImageSizeValue(imageSize, DEFAULT_IMAGE_SIZE_VALUE); if (IMAGE_SIZE_PIXEL_MAP[normalized]) { return IMAGE_SIZE_PIXEL_MAP[normalized]; } const kMatch = normalized.match(/^(\d+(?:\.\d+)?)K$/i); if (kMatch?.[1]) { return Math.max(1, Math.round(Number(kMatch[1]) * 1024)); } return IMAGE_SIZE_PIXEL_MAP[DEFAULT_IMAGE_SIZE_VALUE]; } function uniqueValues(values: string[]) { const seen = new Set(); const result: string[] = []; for (const value of values) { const normalized = String(value || "").trim(); if (!normalized || seen.has(normalized)) continue; seen.add(normalized); result.push(normalized); } return result; } export function ImageParamsBar({ modelName = "Gemini 图片生成", initialParams, onParamsChange, inline = false, providerType = "gemini", // 默认 gemini hideImageCount = false, hideAdvancedOptions = false, inlineTheme = "light", showInlineDivider = true, imageGenConfig, }: ImageParamsBarProps) { const hasNormalizedParamDiff = ( source: Partial | undefined, normalized: ImageGenerationParams ) => { if (!source) return false; const keys: Array = [ "aspectRatio", "imageSize", "imageCount", "quality", "size", "negativePrompt", "promptExtend", "watermark", "seed", ]; return keys.some( (key) => source[key] !== undefined && source[key] !== normalized[key] ); }; const getImageCountOptions = () => { if (imageGenConfig?.maxImages) { return Array.from({ length: imageGenConfig.maxImages }, (_, i) => i + 1); } if (inlineTheme === "minimal") { return Array.from({ length: 10 }, (_, i) => i + 1); } if (providerType === "ruxa-image") { return [1]; } if ( providerType === "openai-image" || providerType === "bananaimage" || providerType === "visionary" || providerType === "atlascloud" || providerType === "stepfun-image" || providerType === "midjourney" ) { return [1]; } // 否则根据 providerType 判断 if (providerType === "alibaba-image") { // 阿里生图:1~6 张(qwen-image-edit-plus) return [1, 2, 3, 4, 5, 6]; } if (providerType === "volcano-vision") { // 即梦4.5(豆包):1~3 张 return [1, 2, 3]; } // Gemini:1~2 张 return [1, 2]; }; const configuredImageSizes = uniqueValues( Array.isArray(imageGenConfig?.supportedImageSizes) ? imageGenConfig.supportedImageSizes.map((size) => normalizeImageSizeValue(size, "") ) : [] ); const buildImageSizeOptions = () => { if (imageGenConfig?.supportsSize === false) { return []; } if (configuredImageSizes.length > 0) { return configuredImageSizes.map((value) => ({ value, label: value, disabled: false })); } return DEFAULT_IMAGE_SIZE_OPTIONS.filter((option) => { if (option.disabled) return false; if (!imageGenConfig) return true; if (option.value === "1K") return imageGenConfig.supports1K !== false; if (option.value === "2K") return imageGenConfig.supports2K !== false; if (option.value === "4K") return imageGenConfig.supports4K !== false; return true; }); }; const availableSizeOptions = buildImageSizeOptions(); const isImageSizeAllowed = (s: ImageSizeValue) => { if (availableSizeOptions.length === 0) return true; const normalized = normalizeImageSizeValue(s, ""); return availableSizeOptions.some((option) => option.value === normalized); }; const getFallbackImageSize = () => ["2K", "1K", "4K", DEFAULT_IMAGE_SIZE_VALUE].find(isImageSizeAllowed) || availableSizeOptions[0]?.value || DEFAULT_IMAGE_SIZE_VALUE; const configuredAspectRatios = uniqueValues( Array.isArray(imageGenConfig?.supportedAspectRatios) ? imageGenConfig.supportedAspectRatios.map((ratio) => String(ratio || "").trim()) : [] ); const defaultAspectRatioLabel = new Map( DEFAULT_ASPECT_RATIO_OPTIONS.map((option) => [option.value, option.label]) ); const availableAspectRatioOptions = ( configuredAspectRatios.length > 0 ? configuredAspectRatios : DEFAULT_ASPECT_RATIO_OPTIONS.map((option) => option.value) ).map((value) => ({ value, label: defaultAspectRatioLabel.get(value) || value, })); const isAspectRatioAllowed = (aspectRatio: string) => availableAspectRatioOptions.some((option) => option.value === aspectRatio); const getFallbackAspectRatio = (): AspectRatioValue => availableAspectRatioOptions.find((option) => option.value === "1:1")?.value || availableAspectRatioOptions[0]?.value || "1:1"; const clampParamsByProvider = (raw: ImageGenerationParams): ImageGenerationParams => { const options = getImageCountOptions(); const fallbackCount = options[0] ?? 1; const imageCount = options.includes(raw.imageCount || fallbackCount) ? (raw.imageCount || fallbackCount) : fallbackCount; let aspectRatio = raw.aspectRatio; if (!isAspectRatioAllowed(aspectRatio)) { aspectRatio = getFallbackAspectRatio(); } // 根据模型配置校正分辨率:如果当前选择的档位被禁用,则降级到可用的档位 let imageSize = normalizeImageSizeValue(raw.imageSize, getFallbackImageSize()); if (availableSizeOptions.length > 0 && !isImageSizeAllowed(imageSize)) { imageSize = getFallbackImageSize(); } return { ...raw, aspectRatio, imageSize, imageCount, }; }; const isSameParams = (a: ImageGenerationParams, b: ImageGenerationParams) => a.aspectRatio === b.aspectRatio && a.imageSize === b.imageSize && (a.imageCount || 1) === (b.imageCount || 1) && (a.quality || "medium") === (b.quality || "medium") && a.size === b.size && a.negativePrompt === b.negativePrompt && a.promptExtend === b.promptExtend && a.watermark === b.watermark && a.seed === b.seed; const buildBaseParams = (source?: Partial) => { const hasSourceAspectRatio = typeof source?.aspectRatio === "string" && source.aspectRatio.trim().length > 0; return clampParamsByProvider({ aspectRatio: inlineTheme === "minimal" ? "1:1" : "auto", imageSize: "2K", imageCount: 1, quality: "medium", promptExtend: true, watermark: false, ...source, ...(inlineTheme === "minimal" && !hasSourceAspectRatio ? { aspectRatio: "1:1" } : {}), }); }; const [params, setParams] = useState({ ...buildBaseParams(initialParams), }); const [minimalParamsOpen, setMinimalParamsOpen] = useState(false); const qualityOptions = [ { label: "自动", value: "auto" }, { label: "高", value: "high" }, { label: "中", value: "medium" }, { label: "低", value: "low" }, ] as const; const qualityLabel = qualityOptions.find((option) => option.value === params.quality)?.label || "中"; const imageCountOptions = getImageCountOptions(); const showImageCountSelector = !hideImageCount && imageCountOptions.length > 1; // 是否显示阿里生图专用参数 const isAlibabaImage = providerType === "alibaba-image"; const showPromptExtend = isAlibabaImage || imageGenConfig?.supportsPromptExtend; const showNegativePrompt = isAlibabaImage || imageGenConfig?.supportsNegativePrompt; const getComputedOutputSize = (aspectRatio: string, imageSize: string) => { if (providerType === "grsai-gpt-image") { return getGrsaiGptImageOutputSize(aspectRatio, imageSize); } const size = getImageSizePixelValue(imageSize); if (aspectRatio === "auto" || aspectRatio === "1:1") { return { width: size, height: size }; } const [wRaw, hRaw] = aspectRatio.split(":").map((item) => Number(item)); if (!wRaw || !hRaw) { return { width: size, height: size }; } if (wRaw >= hRaw) { return { width: size, height: Math.max(1, Math.round((size * hRaw) / wRaw)), }; } return { width: Math.max(1, Math.round((size * wRaw) / hRaw)), height: size, }; }; const renderRatioGlyph = (ratioValue: string) => { if (ratioValue === "auto") { return ; } const [wRaw, hRaw] = ratioValue.split(":").map((item) => Number(item)); if (!wRaw || !hRaw) { return ; } const maxSide = 14; let width = maxSide; let height = maxSide; if (wRaw >= hRaw) { width = maxSide; height = Math.max(6, Math.round((maxSide * hRaw) / wRaw)); } else { height = maxSide; width = Math.max(6, Math.round((maxSide * wRaw) / hRaw)); } return ( ); }; // 仅同步展示状态;只有确实发生参数校正时才回写父层,避免和父组件互相触发更新。 useEffect(() => { const nextParams = buildBaseParams(initialParams); setParams((prevParams) => (isSameParams(prevParams, nextParams) ? prevParams : nextParams)); if (onParamsChange && hasNormalizedParamDiff(initialParams, nextParams)) { onParamsChange(nextParams); } }, [ imageGenConfig?.maxImages, imageGenConfig?.supports1K, imageGenConfig?.supports2K, imageGenConfig?.supports4K, imageGenConfig?.supportsSize, imageGenConfig?.supportedAspectRatios, imageGenConfig?.supportedImageSizes, initialParams, onParamsChange, providerType, ]); const updateParam = ( key: K, value: ImageGenerationParams[K] ) => { const newParams = clampParamsByProvider({ ...params, [key]: value }); setParams((prevParams) => (isSameParams(prevParams, newParams) ? prevParams : newParams)); if (!isSameParams(params, newParams)) { onParamsChange?.(newParams); } }; const updateParams = (patch: Partial) => { const newParams = clampParamsByProvider({ ...params, ...patch }); setParams((prevParams) => (isSameParams(prevParams, newParams) ? prevParams : newParams)); if (!isSameParams(params, newParams)) { onParamsChange?.(newParams); } }; // 内联模式:只返回参数按钮,嵌入工具栏 if (inline) { const isDarkInline = inlineTheme === "dark"; const isMinimalInline = inlineTheme === "minimal"; const outputSize = getComputedOutputSize(params.aspectRatio, params.imageSize); if (isMinimalInline) { const currentImageSize = params.imageSize || getFallbackImageSize(); const currentAspectRatio = isAspectRatioAllowed(params.aspectRatio) ? params.aspectRatio : getFallbackAspectRatio(); const sizeOptionKeySet = new Set(); const sizeOptions = [ ...availableAspectRatioOptions.map((option) => ({ label: option.value === "auto" ? "auto" : option.label, aspectRatio: option.value, imageSize: currentImageSize, })), ...availableSizeOptions.map((option) => ({ label: option.label, aspectRatio: currentAspectRatio, imageSize: option.value, })), ].filter((option) => { const key = `${option.aspectRatio}:${option.imageSize}`; if (sizeOptionKeySet.has(key)) return false; sizeOptionKeySet.add(key); return true; }); const imageCountOptions = getImageCountOptions(); const selectedAspectLabel = params.aspectRatio === "auto" ? "auto" : params.aspectRatio; const selectedImageCount = params.imageCount || 1; const renderMinimalRatioGlyph = (ratioValue: string) => { if (ratioValue === "auto") { return auto; } const [wRaw, hRaw] = ratioValue.split(":").map((item) => Number(item)); if (!wRaw || !hRaw) { return ; } const maxSide = 18; let width = maxSide; let height = maxSide; if (wRaw >= hRaw) { width = maxSide; height = Math.max(7, Math.round((maxSide * hRaw) / wRaw)); } else { height = maxSide; width = Math.max(7, Math.round((maxSide * wRaw) / hRaw)); } return ( ); }; return (
质量
{qualityOptions.map((quality) => ( ))}
尺寸
W {outputSize.width}
H {outputSize.height}
{sizeOptions.length > 0 && (
Size
{sizeOptions.map((option) => { const selected = params.aspectRatio === option.aspectRatio && (option.aspectRatio === "auto" || params.imageSize === option.imageSize); return ( ); })}
)}
Image
{imageCountOptions.map((count) => ( ))}
); } return (

选择比例

{availableAspectRatioOptions.map((option) => ( ))}
{availableSizeOptions.length > 0 && (

选择分辨率

{availableSizeOptions.map((option) => ( ))}
)}

尺寸

W {outputSize.width}
H {outputSize.height}
PX
{showImageCountSelector && (

生成数量

{imageCountOptions.map((count) => ( ))}
)} {/* 🆕 阿里生图专用:提示词优化 & 反向提示词 */} {!hideAdvancedOptions && (showPromptExtend || showNegativePrompt) && (
{/* 提示词智能优化 */} {showPromptExtend && (
提示词优化

AI 自动优化您的描述

updateParam("promptExtend", checked)} />
)} {/* 反向提示词 */} {showNegativePrompt && (
反向提示词 updateParam("negativePrompt", e.target.value)} className={cn( "h-7 rounded-[8px] text-[12px]", isDarkInline ? "border-white/12 bg-white/[0.04] text-white placeholder:text-white/28" : "border-[#dce1e8] bg-[#eef1f4]/90 text-[#2f3b48] placeholder:text-[#b0b8c4]" )} />
)}
)}
); } // 非内联模式(暂时不实现,因为只需要内联模式) return null; }