181 lines
6.4 KiB
TypeScript
181 lines
6.4 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
import { Check, Image, Ratio, Maximize2, Images } from "lucide-react";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
// 图片生成参数接口
|
||
export interface ImageGenerationParams {
|
||
aspectRatio: string;
|
||
imageSize: string;
|
||
imageCount?: number; // 🆕 生成图片数量
|
||
}
|
||
|
||
interface ImageParamsBarProps {
|
||
modelName?: string;
|
||
initialParams?: Partial<ImageGenerationParams>;
|
||
onParamsChange?: (params: ImageGenerationParams) => void;
|
||
inline?: boolean; // 内联模式:只显示参数按钮,不显示外层容器
|
||
providerType?: string; // 🆕 模型类型:gemini, volcano-vision 等
|
||
}
|
||
|
||
// 图片比例选项(根据 Nano Banana 文档)
|
||
const 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;
|
||
|
||
// 图片大小选项(仅支持 nano-banana-pro)
|
||
const IMAGE_SIZE_OPTIONS = [
|
||
{ value: "1K", label: "1K", disabled: false },
|
||
{ value: "2K", label: "2K ⚡", disabled: false }, // 默认推荐
|
||
{ value: "4K", label: "4K 🐌", disabled: false }, // 4K较慢
|
||
] as const;
|
||
|
||
export function ImageParamsBar({
|
||
modelName = "Gemini 图片生成",
|
||
initialParams,
|
||
onParamsChange,
|
||
inline = false,
|
||
providerType = "gemini", // 默认 gemini
|
||
}: ImageParamsBarProps) {
|
||
const [params, setParams] = useState<ImageGenerationParams>({
|
||
aspectRatio: "auto",
|
||
imageSize: "2K", // 默认 2K
|
||
imageCount: 1, // 🆕 默认 1 张
|
||
...initialParams,
|
||
});
|
||
|
||
// 🆕 根据模型类型确定可选的图片数量
|
||
const getImageCountOptions = () => {
|
||
if (providerType === "volcano-vision") {
|
||
// 即梦4.5(豆包):1~3 张
|
||
return [1, 2, 3];
|
||
}
|
||
// Gemini:1~2 张
|
||
return [1, 2];
|
||
};
|
||
|
||
const imageCountOptions = getImageCountOptions();
|
||
|
||
// 🆕 监听 initialParams 变化,确保状态同步
|
||
useEffect(() => {
|
||
console.log("[ImageParamsBar] Mount/Update - initialParams:", initialParams);
|
||
if (initialParams) {
|
||
setParams({
|
||
aspectRatio: "auto",
|
||
imageSize: "2K",
|
||
imageCount: 1,
|
||
...initialParams,
|
||
});
|
||
}
|
||
}, [initialParams]);
|
||
|
||
const updateParam = <K extends keyof ImageGenerationParams>(
|
||
key: K,
|
||
value: ImageGenerationParams[K]
|
||
) => {
|
||
const newParams = { ...params, [key]: value };
|
||
console.log("[ImageParamsBar] Updating param:", key, "=", value);
|
||
console.log("[ImageParamsBar] New params:", newParams);
|
||
setParams(newParams);
|
||
onParamsChange?.(newParams);
|
||
};
|
||
|
||
// 内联模式:只返回参数按钮,嵌入工具栏
|
||
if (inline) {
|
||
return (
|
||
<div className="flex items-center ml-1 border-l border-border/50 pl-1">
|
||
{/* 图片比例 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0">
|
||
<Ratio size={14} />
|
||
<span className="font-medium text-xs">{params.aspectRatio}</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="w-48">
|
||
<div className="p-1.5 grid grid-cols-3 gap-1">
|
||
{ASPECT_RATIO_OPTIONS.map((option) => (
|
||
<Button
|
||
key={option.value}
|
||
variant={params.aspectRatio === option.value ? "secondary" : "ghost"}
|
||
size="sm"
|
||
className="h-7 text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
|
||
onClick={() => updateParam("aspectRatio", option.value)}
|
||
>
|
||
{option.label}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 分辨率 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0">
|
||
<Maximize2 size={14} />
|
||
<span className="font-medium text-xs">{params.imageSize}</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="min-w-[100px]">
|
||
{IMAGE_SIZE_OPTIONS.map((option) => (
|
||
<DropdownMenuItem
|
||
key={option.value}
|
||
onClick={() => updateParam("imageSize", option.value)}
|
||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
|
||
>
|
||
<Check className={cn("mr-2 h-4 w-4", params.imageSize === option.value ? "opacity-100" : "opacity-0")} />
|
||
{option.label}
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 🆕 图片数量 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0">
|
||
<Images size={14} />
|
||
<span className="font-medium text-xs">{params.imageCount || 1}张</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="min-w-[80px]">
|
||
{imageCountOptions.map((count) => (
|
||
<DropdownMenuItem
|
||
key={count}
|
||
onClick={() => updateParam("imageCount", count)}
|
||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
|
||
>
|
||
<Check className={cn("mr-2 h-4 w-4", (params.imageCount || 1) === count ? "opacity-100" : "opacity-0")} />
|
||
{count}张
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 非内联模式(暂时不实现,因为只需要内联模式)
|
||
return null;
|
||
}
|
||
|