ai-chatbot/components/image-params-bar.tsx
2026-07-21 11:01:43 +08:00

918 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<ImageGenerationParams>;
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<string, number> = {
"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<string>();
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<ImageGenerationParams> | undefined,
normalized: ImageGenerationParams
) => {
if (!source) return false;
const keys: Array<keyof ImageGenerationParams> = [
"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<string, string>(
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<ImageGenerationParams>) => {
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<ImageGenerationParams>({
...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 <Scan className="h-4 w-4" />;
}
const [wRaw, hRaw] = ratioValue.split(":").map((item) => Number(item));
if (!wRaw || !hRaw) {
return <Scan className="h-4 w-4" />;
}
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 (
<span
className="inline-block rounded-[6px] border-2 border-current"
style={{ width, height }}
/>
);
};
// 仅同步展示状态;只有确实发生参数校正时才回写父层,避免和父组件互相触发更新。
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 = <K extends keyof ImageGenerationParams>(
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<ImageGenerationParams>) => {
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<string>();
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 <span className="text-[11px] leading-none">auto</span>;
}
const [wRaw, hRaw] = ratioValue.split(":").map((item) => Number(item));
if (!wRaw || !hRaw) {
return <Scan className="h-4 w-4" />;
}
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 (
<span
className="inline-block rounded-[3px] border border-current"
style={{ width, height }}
/>
);
};
return (
<Popover open={minimalParamsOpen} onOpenChange={setMinimalParamsOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="h-8 shrink-0 rounded-full bg-[#f1f1f2] px-2.5 text-[13px] font-normal text-[#20242a] hover:bg-[#e9eaec] focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-[#242529] dark:text-zinc-100 dark:hover:bg-[#2c2d33]"
title="生成参数"
>
{qualityLabel} · {selectedAspectLabel} · {selectedImageCount} img
{minimalParamsOpen ? (
<ChevronUp className="ml-1 h-3 w-3" />
) : (
<ChevronDown className="ml-1 h-3 w-3" />
)}
</Button>
</PopoverTrigger>
<PopoverContent
align="center"
side="top"
sideOffset={8}
className="w-[304px] max-w-[calc(100vw-32px)] rounded-[18px] border border-[#c7ccd5] bg-white p-3.5 text-[#252b33] shadow-[0_22px_56px_rgba(15,23,42,0.16)] dark:border-white/12 dark:bg-[#141416] dark:text-zinc-100 dark:shadow-[0_24px_64px_rgba(0,0,0,0.42)]"
>
<div className="space-y-4">
<div>
<div className="mb-2 text-[14px] font-medium leading-none">质量</div>
<div className="grid h-9 grid-cols-4 rounded-full bg-[#f4f4f5] p-0.5 dark:bg-white/8">
{qualityOptions.map((quality) => (
<button
key={quality.value}
type="button"
className={cn(
"rounded-full text-[13px] font-medium transition-colors",
params.quality === quality.value
? "border border-[#c8cdd5] bg-white shadow-[0_1px_4px_rgba(15,23,42,0.1)] dark:border-white/12 dark:bg-white/16"
: "text-[#3d434d] hover:bg-white/55 dark:text-zinc-300 dark:hover:bg-white/10"
)}
onClick={() => updateParam("quality", quality.value)}
>
{quality.label}
</button>
))}
</div>
</div>
<div>
<div className="mb-2 text-[14px] font-medium leading-none">尺寸</div>
<div className="grid grid-cols-[1fr_20px_1fr] items-center gap-2">
<div className="flex h-10 items-center gap-2 rounded-[10px] bg-[#f3f3f4] px-3 text-[15px] dark:bg-white/8">
<span className="text-[#68707c] dark:text-zinc-400">W</span>
<span>{outputSize.width}</span>
</div>
<Link2 className="mx-auto h-3.5 w-3.5 text-[#8b929c]" />
<div className="flex h-10 items-center gap-2 rounded-[10px] bg-[#f3f3f4] px-3 text-[15px] dark:bg-white/8">
<span className="text-[#68707c] dark:text-zinc-400">H</span>
<span>{outputSize.height}</span>
</div>
</div>
</div>
{sizeOptions.length > 0 && (
<div>
<div className="mb-2 text-[14px] font-medium leading-none">Size</div>
<div className="grid grid-cols-4 gap-2">
{sizeOptions.map((option) => {
const selected =
params.aspectRatio === option.aspectRatio &&
(option.aspectRatio === "auto" || params.imageSize === option.imageSize);
return (
<button
key={`${option.aspectRatio}-${option.imageSize}-${option.label}`}
type="button"
className={cn(
"flex h-[66px] flex-col items-center justify-center gap-1.5 rounded-[10px] border text-[13px] transition-colors",
selected
? "border-[#bfc4cc] bg-[#e4e4e5] text-[#171b21] dark:border-white/18 dark:bg-white/16 dark:text-zinc-50"
: "border-[#d0d3d9] bg-white text-[#4c535d] hover:bg-[#f5f5f6] dark:border-white/12 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-white/8"
)}
onClick={() =>
updateParams({
aspectRatio: option.aspectRatio,
imageSize: option.imageSize,
})
}
>
<span className="flex h-5 items-center justify-center text-[#30353c] dark:text-zinc-200">
{renderMinimalRatioGlyph(option.aspectRatio)}
</span>
<span>{option.label}</span>
</button>
);
})}
</div>
</div>
)}
<div>
<div className="mb-2 text-[14px] font-medium leading-none">Image</div>
<div className="grid grid-cols-4 gap-2">
{imageCountOptions.map((count) => (
<button
key={count}
type="button"
className={cn(
"h-9 rounded-[9px] border text-[14px] transition-colors",
selectedImageCount === count
? "border-[#bfc4cc] bg-[#e4e4e5] text-[#171b21] dark:border-white/18 dark:bg-white/16 dark:text-zinc-50"
: "border-[#d0d3d9] bg-white text-[#4c535d] hover:bg-[#f5f5f6] dark:border-white/12 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-white/8"
)}
onClick={() => updateParams({ imageCount: count })}
>
{count} img
</button>
))}
</div>
</div>
</div>
</PopoverContent>
</Popover>
);
}
return (
<div
className={cn(
"flex min-w-0 items-center",
isMinimalInline ? "ml-0 gap-2 pl-0" : "ml-1 gap-1 pl-1",
showInlineDivider &&
!isMinimalInline &&
(isDarkInline ? "border-l border-white/10" : "border-l border-border/50")
)}
>
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
className={cn(
"shrink-0 p-0",
isMinimalInline
? "h-11 w-11 rounded-full border border-transparent bg-transparent text-[#555b66] hover:bg-[#f3f4f6] hover:text-[#20242a]"
: "h-8 w-11 rounded-[12px]",
!isMinimalInline && isDarkInline
? "border border-white/12 bg-white/[0.06] text-white hover:bg-white/[0.1]"
: !isMinimalInline
? "border border-[#d6dbe2] bg-[#f3f4f6]/90 text-[#2f3b48] hover:bg-[#edeff2]/90"
: ""
)}
aria-label={`比例:${params.aspectRatio},分辨率:${params.imageSize}`}
title={`比例:${params.aspectRatio},分辨率:${params.imageSize}`}
>
<span
className={cn(
"flex h-5 w-5 items-center justify-center",
isMinimalInline
? "text-current"
: isDarkInline
? "text-white/80"
: "text-[#20262f]"
)}
>
{renderRatioGlyph(params.aspectRatio)}
</span>
</Button>
</PopoverTrigger>
<PopoverContent
align="center"
side="top"
sideOffset={10}
className={cn(
"w-[312px] max-w-[calc(100vw-24px)] rounded-[14px] p-2 shadow-[0_10px_24px_rgba(15,23,42,0.14)]",
isDarkInline
? "border border-white/12 bg-[#0d1013]/98 text-white backdrop-blur-xl"
: "border border-[#d8dde4] bg-[#f3f4f6]/90"
)}
>
<div className="space-y-2.5">
<div>
<p className={cn("text-[11px] font-medium", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>选择比例</p>
<div
className={cn(
"mt-1.5 grid grid-cols-4 gap-1.5 rounded-[12px] p-1.5 md:grid-cols-5",
isDarkInline ? "bg-white/[0.04]" : "bg-[#eceff2]/90"
)}
>
{availableAspectRatioOptions.map((option) => (
<button
key={option.value}
type="button"
className={cn(
"flex h-[46px] flex-col items-center justify-center rounded-[10px] border transition",
params.aspectRatio === option.value
? isDarkInline
? "border-emerald-400/50 bg-emerald-500/12 text-emerald-200 shadow-[0_1px_5px_rgba(15,23,42,0.18)]"
: "border-white bg-white text-[#1f2937] shadow-[0_1px_5px_rgba(15,23,42,0.08)]"
: isDarkInline
? "border-transparent text-white/62 hover:bg-white/[0.06]"
: "border-transparent text-[#5f6b7a] hover:bg-white/70"
)}
onClick={() => updateParam("aspectRatio", option.value)}
>
<span className="mb-0.5 flex h-4 items-center justify-center">
{renderRatioGlyph(option.value)}
</span>
<span className="text-[12px] font-semibold leading-none">
{option.value === "auto" ? "智能" : option.label}
</span>
</button>
))}
</div>
</div>
{availableSizeOptions.length > 0 && (
<div>
<p className={cn("text-[11px] font-medium", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>选择分辨率</p>
<div
className={cn(
"mt-1.5 grid grid-cols-2 gap-1.5 rounded-[12px] p-1",
isDarkInline ? "bg-white/[0.04]" : "bg-[#eceff2]/90"
)}
>
{availableSizeOptions.map((option) => (
<button
key={option.value}
type="button"
className={cn(
"h-8 rounded-[9px] text-[12px] font-semibold transition",
params.imageSize === option.value
? isDarkInline
? "bg-emerald-500/12 text-emerald-200 shadow-[0_1px_5px_rgba(15,23,42,0.18)]"
: "bg-white text-[#111827] shadow-[0_1px_5px_rgba(15,23,42,0.08)]"
: isDarkInline
? "bg-transparent text-white/62 hover:bg-white/[0.06]"
: "bg-transparent text-[#5f6b7a] hover:bg-white/70"
)}
onClick={() => updateParam("imageSize", option.value)}
>
<span className="inline-flex items-center gap-1">
{option.label}
</span>
</button>
))}
</div>
</div>
)}
<div>
<p className={cn("text-[11px] font-medium", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>尺寸</p>
<div className="mt-1.5 grid grid-cols-[1fr_auto_1fr_auto] items-center gap-1.5">
<div className={cn("h-7 rounded-[9px] px-2", isDarkInline ? "border border-white/10 bg-white/[0.04] text-white/68" : "border border-[#dce1e8] bg-[#eef1f4]/90 text-[#b0b8c4]")}>
<div className="flex h-full items-center justify-between gap-1.5">
<span className="text-[10px] font-semibold">W</span>
<span className="text-[12px] font-semibold">{outputSize.width}</span>
</div>
</div>
<Link2 className={cn("h-3 w-3", isDarkInline ? "text-white/35" : "text-[#adb6c3]")} />
<div className={cn("h-7 rounded-[9px] px-2", isDarkInline ? "border border-white/10 bg-white/[0.04] text-white/68" : "border border-[#dce1e8] bg-[#eef1f4]/90 text-[#b0b8c4]")}>
<div className="flex h-full items-center justify-between gap-1.5">
<span className="text-[10px] font-semibold">H</span>
<span className="text-[12px] font-semibold">{outputSize.height}</span>
</div>
</div>
<span className={cn("text-[11px] font-semibold", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>PX</span>
</div>
</div>
</div>
</PopoverContent>
</Popover>
{showImageCountSelector && (
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
className={cn(
"text-[12px] font-semibold focus-visible:ring-0 focus-visible:ring-offset-0",
isMinimalInline
? "h-11 min-w-11 rounded-full border border-transparent bg-transparent px-0 text-[#555b66] hover:bg-[#f3f4f6] hover:text-[#20242a]"
: "h-8 min-w-[56px] rounded-[12px] px-2.5",
!isMinimalInline && isDarkInline
? "border border-white/12 bg-white/[0.06] text-white hover:bg-white/[0.1]"
: !isMinimalInline
? "border border-[#d6dbe2] bg-[#f3f4f6]/90 text-[#2f3b48] hover:bg-[#edeff2]/90"
: ""
)}
title={`数量:${params.imageCount || 1}张`}
>
<Sparkles
className={cn(
isMinimalInline ? "h-5 w-5" : "mr-1 h-3.5 w-3.5",
isMinimalInline
? "text-current"
: isDarkInline
? "text-emerald-300"
: "text-[#6b7280]"
)}
/>
{isMinimalInline ? (
<span className="sr-only">{params.imageCount || 1}张</span>
) : (
`${params.imageCount || 1}张`
)}
</Button>
</PopoverTrigger>
<PopoverContent
align="center"
side="top"
sideOffset={10}
className={cn(
"w-auto min-w-[120px] rounded-[14px] p-2 shadow-[0_10px_24px_rgba(15,23,42,0.14)]",
isDarkInline
? "border border-white/12 bg-[#0d1013]/98 text-white backdrop-blur-xl"
: "border border-[#d8dde4] bg-[#f3f4f6]/95"
)}
>
<div>
<p className={cn("text-[11px] font-medium", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>生成数量</p>
<div className={cn(
"mt-1.5 grid gap-1.5 rounded-[12px] p-1.5",
isDarkInline ? "bg-white/[0.04]" : "bg-[#eceff2]/90",
imageCountOptions.length <= 3 ? "grid-cols-3" : "grid-cols-3"
)}>
{imageCountOptions.map((count) => (
<button
key={count}
type="button"
className={cn(
"flex h-8 min-w-[36px] items-center justify-center rounded-[9px] border text-[12px] font-semibold transition",
(params.imageCount || 1) === count
? isDarkInline
? "border-emerald-400/50 bg-emerald-500/12 text-emerald-200 shadow-[0_1px_5px_rgba(15,23,42,0.18)]"
: "border-white bg-white text-[#1f2937] shadow-[0_1px_5px_rgba(15,23,42,0.08)]"
: isDarkInline
? "border-transparent text-white/62 hover:bg-white/[0.06]"
: "border-transparent text-[#5f6b7a] hover:bg-white/70"
)}
onClick={() => updateParam("imageCount", count)}
>
{count}张
</button>
))}
</div>
</div>
</PopoverContent>
</Popover>
)}
{/* 🆕 阿里生图专用:提示词优化 & 反向提示词 */}
{!hideAdvancedOptions && (showPromptExtend || showNegativePrompt) && (
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
className={cn(
"shrink-0 p-0 focus-visible:ring-0 focus-visible:ring-offset-0",
isMinimalInline
? "h-11 w-11 rounded-full border border-transparent bg-transparent text-[#555b66] hover:bg-[#f3f4f6] hover:text-[#20242a]"
: "h-8 w-8 rounded-[12px]",
!isMinimalInline && isDarkInline
? "border border-white/12 bg-white/[0.06] text-white/70 hover:bg-white/[0.1]"
: !isMinimalInline
? "border border-[#d6dbe2] bg-[#f3f4f6]/90 text-[#5f6b7a] hover:bg-[#edeff2]/90"
: "",
(params.promptExtend || params.negativePrompt) &&
(isMinimalInline
? "text-[#20242a]"
: isDarkInline
? "text-emerald-300"
: "text-primary")
)}
title="高级设置"
>
<SlidersHorizontal size={14} />
<span className="sr-only">高级设置</span>
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
side="top"
sideOffset={10}
className={cn(
"w-[280px] max-w-[calc(100vw-24px)] rounded-[14px] p-2 shadow-[0_10px_24px_rgba(15,23,42,0.14)]",
isDarkInline
? "border border-white/12 bg-[#0d1013]/98 text-white backdrop-blur-xl"
: "border border-[#d8dde4] bg-[#f3f4f6]/95"
)}
>
<div className="space-y-2.5">
{/* 提示词智能优化 */}
{showPromptExtend && (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<span className={cn("text-[12px] font-semibold", isDarkInline ? "text-white/88" : "text-[#2f3b48]")}>提示词优化</span>
<p className={cn("text-[11px]", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>AI 自动优化您的描述</p>
</div>
<Switch
checked={params.promptExtend ?? true}
onCheckedChange={(checked) => updateParam("promptExtend", checked)}
/>
</div>
)}
{/* 反向提示词 */}
{showNegativePrompt && (
<div className="space-y-1">
<span className={cn("text-[12px] font-semibold", isDarkInline ? "text-white/88" : "text-[#2f3b48]")}>反向提示词</span>
<Input
placeholder="不希望出现的内容,如:低质量、模糊..."
value={params.negativePrompt || ""}
onChange={(e) => 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]"
)}
/>
</div>
)}
</div>
</PopoverContent>
</Popover>
)}
</div>
);
}
// 非内联模式(暂时不实现,因为只需要内联模式)
return null;
}