ai-chatbot/components/enhance-pro-dialog.tsx
Your Name 66fd760b9a
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: update enhance pro configuration
2026-05-07 19:54:02 +08:00

870 lines
31 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, useMemo, useRef, useState } from "react";
import useSWR from "swr";
import { Loader2, Sparkles } from "lucide-react";
import { toast } from "sonner";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import {
ENHANCE_PRO_KREA_MODELS,
defaultEnhanceProPresets,
type EnhanceProKreaModel,
type EnhanceProOutputFormat,
type EnhanceProPreset,
type EnhanceProSubjectDetection,
} from "@/lib/enhance-pro/config";
import { imageGenerationService, type EnhanceProImageResult } from "@/services/canvas";
const fetcher = (url: string) => fetch(url).then((res) => res.json());
function createEnhanceProClientRequestId() {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return `enhance-pro-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}
function shouldPreserveEnhanceProRequestId(message: string) {
return /任务.*已提交|避免重复提交|稍后重试查看结果|failed to fetch|network/i.test(
String(message || "")
);
}
function buildEnhanceProRequestSignature(params: {
imageUrl: string;
prompt: string;
width: number;
height: number;
batchSize: number;
seed: number;
output_format: EnhanceProOutputFormat;
subject_detection: EnhanceProSubjectDetection;
face_enhancement: boolean;
face_enhancement_creativity: number;
face_enhancement_strength: number;
crop_to_fill: boolean;
model: EnhanceProKreaModel;
upscaling_activated: boolean;
image_scaling_factor: number;
creativity: number;
texture: number;
sharpen: number;
denoise: number;
detail: number;
}) {
return JSON.stringify(params);
}
type EnhanceProClientConfig = {
enabled: boolean;
providerName: string;
baseUrl: string;
defaultModel: EnhanceProKreaModel;
availableModels: EnhanceProKreaModel[];
defaultOutputFormat: EnhanceProOutputFormat;
allowBatchSize: boolean;
maxBatchSize: number;
maxUpscaleFactor: number;
maxOutputEdge: number;
enableAdvancedControls: boolean;
presets: EnhanceProPreset[];
};
type FormState = {
prompt: string;
width: number;
height: number;
batchSize: number;
seed: number;
output_format: EnhanceProOutputFormat;
subject_detection: EnhanceProSubjectDetection;
face_enhancement: boolean;
face_enhancement_creativity: number;
face_enhancement_strength: number;
crop_to_fill: boolean;
model: EnhanceProKreaModel;
upscaling_activated: boolean;
image_scaling_factor: number;
creativity: number;
texture: number;
sharpen: number;
denoise: number;
detail: number;
};
const FALLBACK_CONFIG: EnhanceProClientConfig = {
enabled: false,
providerName: "Krea Topaz Generative",
baseUrl: "https://api.krea.ai",
defaultModel: "Redefine",
availableModels: [...ENHANCE_PRO_KREA_MODELS],
defaultOutputFormat: "jpg",
allowBatchSize: true,
maxBatchSize: 4,
maxUpscaleFactor: 4,
maxOutputEdge: 4096,
enableAdvancedControls: true,
presets: defaultEnhanceProPresets(),
};
function clampNumber(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
function fitDimensions(width: number, height: number, maxOutputEdge: number) {
const safeWidth = Math.max(1, Math.round(width || 1024));
const safeHeight = Math.max(1, Math.round(height || 1024));
const maxEdge = Math.max(512, Math.round(maxOutputEdge || FALLBACK_CONFIG.maxOutputEdge));
const currentMax = Math.max(safeWidth, safeHeight);
if (currentMax <= maxEdge) {
return { width: safeWidth, height: safeHeight };
}
const scale = maxEdge / currentMax;
return {
width: Math.max(1, Math.round(safeWidth * scale)),
height: Math.max(1, Math.round(safeHeight * scale)),
};
}
function getAvailableEnhanceProModels(config: EnhanceProClientConfig) {
return config.availableModels?.length
? config.availableModels
: [...ENHANCE_PRO_KREA_MODELS];
}
function resolveEnhanceProModel(
config: EnhanceProClientConfig,
model?: EnhanceProKreaModel,
) {
const availableModels = getAvailableEnhanceProModels(config);
if (model && availableModels.includes(model)) {
return model;
}
if (availableModels.includes(config.defaultModel)) {
return config.defaultModel;
}
return availableModels[0] || "Redefine";
}
function createDefaultFormState(config: EnhanceProClientConfig, width: number, height: number): FormState {
const size = fitDimensions(width, height, config.maxOutputEdge);
return {
prompt: "",
width: size.width,
height: size.height,
batchSize: 1,
seed: 1337,
output_format: config.defaultOutputFormat,
subject_detection: "All",
face_enhancement: false,
face_enhancement_creativity: 0.5,
face_enhancement_strength: 0.5,
crop_to_fill: false,
model: resolveEnhanceProModel(config, config.defaultModel),
upscaling_activated: false,
image_scaling_factor: 1,
creativity: 3,
texture: 3,
sharpen: 0.5,
denoise: 0.5,
detail: 0.5,
};
}
function applyPresetToForm(
config: EnhanceProClientConfig,
width: number,
height: number,
preset?: EnhanceProPreset,
): FormState {
const baseState = createDefaultFormState(config, width, height);
if (!preset) {
return baseState;
}
return {
...baseState,
prompt: preset.prompt,
batchSize: config.allowBatchSize
? clampNumber(Math.round(preset.batchSize || 1), 1, config.maxBatchSize)
: 1,
seed: Number.isFinite(Number(preset.seed)) ? Math.round(Number(preset.seed)) : baseState.seed,
output_format: preset.outputFormat,
subject_detection: preset.subjectDetection,
face_enhancement: preset.faceEnhancement,
face_enhancement_creativity: clampNumber(preset.faceEnhancementCreativity, 0, 1),
face_enhancement_strength: clampNumber(preset.faceEnhancementStrength, 0, 1),
crop_to_fill: preset.cropToFill,
model: resolveEnhanceProModel(config, preset.model),
upscaling_activated: preset.upscalingActivated,
image_scaling_factor: clampNumber(
preset.imageScalingFactor,
1,
config.maxUpscaleFactor
),
creativity: clampNumber(Math.round(preset.creativity), 1, 6),
texture: clampNumber(Math.round(preset.texture), 1, 5),
sharpen: clampNumber(preset.sharpen, 0, 1),
denoise: clampNumber(preset.denoise, 0, 1),
detail: clampNumber(preset.detail, 0, 1),
};
}
function getSubjectDetectionLabel(subjectDetection: EnhanceProSubjectDetection) {
if (subjectDetection === "Foreground") {
return "前景";
}
if (subjectDetection === "Background") {
return "背景";
}
return "全部";
}
function buildImageLabel(index: number, total: number) {
return total > 1 ? `图像增强 Pro #${index + 1}` : "图像增强 Pro";
}
interface EnhanceProDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
imageUrl: string;
onComplete?: (images: EnhanceProImageResult[]) => void;
}
export function EnhanceProDialog({
open,
onOpenChange,
imageUrl,
onComplete,
}: EnhanceProDialogProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [naturalSize, setNaturalSize] = useState({ width: 1024, height: 1024 });
const [selectedPresetId, setSelectedPresetId] = useState("");
const requestIdRef = useRef<string | null>(null);
const requestSignatureRef = useRef<string | null>(null);
const [formState, setFormState] = useState<FormState>(() =>
createDefaultFormState(FALLBACK_CONFIG, 1024, 1024),
);
const { data: configPayload, isLoading } = useSWR<{ success?: boolean; config?: EnhanceProClientConfig }>(
open ? "/api/enhance/pro/config" : null,
fetcher,
);
const config = useMemo(
() => configPayload?.config || FALLBACK_CONFIG,
[configPayload?.config],
);
const presets = useMemo(
() => {
const availableModels = getAvailableEnhanceProModels(config);
return (config.presets.length ? config.presets : FALLBACK_CONFIG.presets)
.filter((preset) => availableModels.includes(preset.model));
},
[config],
);
const availableModels = useMemo(
() => getAvailableEnhanceProModels(config),
[config],
);
useEffect(() => {
if (!open || !imageUrl) return;
const image = new window.Image();
image.onload = () => {
setNaturalSize({
width: image.naturalWidth || 1024,
height: image.naturalHeight || 1024,
});
};
image.onerror = () => {
setNaturalSize({ width: 1024, height: 1024 });
};
image.src = imageUrl;
}, [open, imageUrl]);
useEffect(() => {
if (!open) return;
const defaultPreset = presets.find((preset) => preset.isDefault) || presets[0];
setSelectedPresetId(defaultPreset?.id || "");
setFormState(applyPresetToForm(config, naturalSize.width, naturalSize.height, defaultPreset));
}, [config, naturalSize.height, naturalSize.width, open, presets]);
const handleNumericField = <K extends keyof FormState>(key: K, nextValue: number) => {
setFormState((prev) => ({
...prev,
[key]: nextValue,
}));
};
const handleOpenChange = (nextOpen: boolean) => {
if (isSubmitting) return;
onOpenChange(nextOpen);
};
const handleSelectPreset = (presetId: string) => {
const preset = presets.find((item) => item.id === presetId);
setSelectedPresetId(presetId);
setFormState(applyPresetToForm(config, naturalSize.width, naturalSize.height, preset));
};
const handleSubmit = async () => {
if (!config.enabled) {
toast.error("图像增强 Pro 当前未开启,请先在系统设置中启用");
return;
}
setIsSubmitting(true);
const requestParams = {
imageUrl,
prompt: formState.prompt.trim() || "图像增强 Pro",
width: clampNumber(Math.round(formState.width), 1, config.maxOutputEdge),
height: clampNumber(Math.round(formState.height), 1, config.maxOutputEdge),
batchSize: config.allowBatchSize
? clampNumber(Math.round(formState.batchSize), 1, config.maxBatchSize)
: 1,
seed: Math.round(formState.seed),
output_format: formState.output_format,
subject_detection: formState.subject_detection,
face_enhancement: formState.face_enhancement,
face_enhancement_creativity: clampNumber(formState.face_enhancement_creativity, 0, 1),
face_enhancement_strength: clampNumber(formState.face_enhancement_strength, 0, 1),
crop_to_fill: formState.crop_to_fill,
model: resolveEnhanceProModel(config, formState.model),
upscaling_activated: formState.upscaling_activated,
image_scaling_factor: clampNumber(
formState.image_scaling_factor,
1,
config.maxUpscaleFactor
),
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
texture: clampNumber(Math.round(formState.texture), 1, 5),
sharpen: clampNumber(formState.sharpen, 0, 1),
denoise: clampNumber(formState.denoise, 0, 1),
detail: clampNumber(formState.detail, 0, 1),
};
const requestSignature = buildEnhanceProRequestSignature(requestParams);
const clientRequestId =
requestIdRef.current && requestSignatureRef.current === requestSignature
? requestIdRef.current
: createEnhanceProClientRequestId();
requestIdRef.current = clientRequestId;
requestSignatureRef.current = requestSignature;
try {
const result = await imageGenerationService.enhanceImagePro({
...requestParams,
clientRequestId,
});
if (!result.success || !result.data?.images?.length) {
throw new Error(result.error || "图像增强 Pro 失败");
}
onComplete?.(result.data.images);
requestIdRef.current = null;
requestSignatureRef.current = null;
toast.success(`图像增强 Pro 完成,已生成 ${result.data.images.length} 张结果`);
onOpenChange(false);
} catch (error) {
const message = error instanceof Error ? error.message : "图像增强 Pro 失败";
if (!shouldPreserveEnhanceProRequestId(message)) {
requestIdRef.current = null;
requestSignatureRef.current = null;
}
toast.error(message);
} finally {
setIsSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent
overlayClassName="bg-black"
className="sm:max-w-[840px] border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-950"
onInteractOutside={(event) => {
if (isSubmitting) event.preventDefault();
}}
onEscapeKeyDown={(event) => {
if (isSubmitting) event.preventDefault();
}}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5" />
图像增强 Pro
</DialogTitle>
<DialogDescription>
使用 {config.providerName} 对当前图片执行更可控的增强与重建。处理完成后会把结果直接添加到无限画布。
</DialogDescription>
</DialogHeader>
<div className="max-h-[70vh] space-y-6 overflow-y-auto pr-1">
{isLoading ? (
<div className="flex items-center gap-2 rounded-lg border p-4 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
正在加载图像增强 Pro 配置...
</div>
) : null}
{!config.enabled && !isLoading ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
当前未启用图像增强 Pro。请前往系统设置的“本地AI设置”页签完成 Krea 配置后再使用。
</div>
) : null}
<div className="rounded-lg border bg-slate-50/60 p-4 text-sm text-muted-foreground dark:bg-slate-900/40">
原图尺寸:{naturalSize.width} x {naturalSize.height}
<span className="mx-2">|</span>
当前最大输出长边:{config.maxOutputEdge}px
</div>
{presets.length ? (
<div className="space-y-3">
<div>
<Label className="text-base">增强预设</Label>
<p className="mt-1 text-xs text-muted-foreground">
先选一个更贴近场景的预设,再按图片情况继续微调参数。
</p>
</div>
<div className="grid gap-3 md:grid-cols-3">
{presets.map((preset) => {
const isSelected = preset.id === selectedPresetId;
return (
<button
key={preset.id}
type="button"
className={`rounded-xl border p-4 text-left transition ${
isSelected
? "border-sky-500 bg-sky-50 shadow-sm"
: "border-slate-200 bg-white hover:border-slate-300"
}`}
onClick={() => handleSelectPreset(preset.id)}
>
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<div className="font-medium text-slate-900">{preset.name}</div>
<p className="text-xs text-muted-foreground">
{preset.description || "使用这一组默认参数快速开始增强。"}
</p>
</div>
{preset.isDefault ? (
<span className="rounded-full bg-slate-900 px-2 py-0.5 text-[11px] text-white">
默认
</span>
) : null}
</div>
<div className="mt-3 text-[11px] text-slate-600">
{preset.model} · {getSubjectDetectionLabel(preset.subjectDetection)}
{preset.faceEnhancement ? " · 人像增强" : ""}
</div>
</button>
);
})}
</div>
</div>
) : null}
<div className="space-y-2">
<Label htmlFor="enhance-pro-prompt">增强提示词</Label>
<Textarea
id="enhance-pro-prompt"
rows={3}
value={formState.prompt}
onChange={(event) =>
setFormState((prev) => ({
...prev,
prompt: event.target.value,
}))
}
placeholder="例如:保持原图风格不变,提升细节层次,减少压缩噪点,让人物皮肤更自然。"
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="enhance-pro-width">输出宽度</Label>
<Input
id="enhance-pro-width"
type="number"
min="1"
max={config.maxOutputEdge}
value={formState.width}
onChange={(event) =>
handleNumericField(
"width",
clampNumber(Number.parseInt(event.target.value, 10) || 1, 1, config.maxOutputEdge),
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="enhance-pro-height">输出高度</Label>
<Input
id="enhance-pro-height"
type="number"
min="1"
max={config.maxOutputEdge}
value={formState.height}
onChange={(event) =>
handleNumericField(
"height",
clampNumber(Number.parseInt(event.target.value, 10) || 1, 1, config.maxOutputEdge),
)
}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-2">
<Label>增强模式</Label>
<Select
value={formState.model}
onValueChange={(value) =>
setFormState((prev) => ({
...prev,
model: value as FormState["model"],
}))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableModels.map((model) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>输出格式</Label>
<Select
value={formState.output_format}
onValueChange={(value) =>
setFormState((prev) => ({
...prev,
output_format: value as FormState["output_format"],
}))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="jpg">JPG</SelectItem>
<SelectItem value="png">PNG</SelectItem>
<SelectItem value="webp">WEBP</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>作用区域</Label>
<Select
value={formState.subject_detection}
onValueChange={(value) =>
setFormState((prev) => ({
...prev,
subject_detection: value as FormState["subject_detection"],
}))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">全部</SelectItem>
<SelectItem value="Foreground">前景</SelectItem>
<SelectItem value="Background">背景</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-4 md:grid-cols-3">
{config.allowBatchSize ? (
<div className="space-y-2">
<Label>候选结果数</Label>
<Select
value={String(formState.batchSize)}
onValueChange={(value) =>
handleNumericField(
"batchSize",
clampNumber(Number.parseInt(value, 10) || 1, 1, config.maxBatchSize),
)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: config.maxBatchSize }, (_, index) => index + 1).map((item) => (
<SelectItem key={item} value={String(item)}>
{item} 张
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
<div className="space-y-2">
<Label htmlFor="enhance-pro-seed">Seed</Label>
<Input
id="enhance-pro-seed"
type="number"
value={formState.seed}
onChange={(event) =>
handleNumericField("seed", Number.parseInt(event.target.value, 10) || 1337)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="enhance-pro-image-scaling">放大倍数</Label>
<Input
id="enhance-pro-image-scaling"
type="number"
min="1"
max={config.maxUpscaleFactor}
step="0.25"
value={formState.image_scaling_factor}
onChange={(event) =>
handleNumericField(
"image_scaling_factor",
clampNumber(
Number.parseFloat(event.target.value) || 1,
1,
config.maxUpscaleFactor
),
)
}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-3">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5 pr-4">
<Label className="text-sm">人脸增强</Label>
<div className="text-xs text-muted-foreground">适合人像、半身图和证件风修复。</div>
</div>
<Switch
checked={formState.face_enhancement}
onCheckedChange={(checked) =>
setFormState((prev) => ({ ...prev, face_enhancement: checked }))
}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5 pr-4">
<Label className="text-sm">启用放大</Label>
<div className="text-xs text-muted-foreground">允许模型在增强时同时执行更强的上采样。</div>
</div>
<Switch
checked={formState.upscaling_activated}
onCheckedChange={(checked) =>
setFormState((prev) => ({ ...prev, upscaling_activated: checked }))
}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5 pr-4">
<Label className="text-sm">裁剪铺满</Label>
<div className="text-xs text-muted-foreground">输出尺寸与原图比例不同时,裁切并铺满目标画布。</div>
</div>
<Switch
checked={formState.crop_to_fill}
onCheckedChange={(checked) =>
setFormState((prev) => ({ ...prev, crop_to_fill: checked }))
}
/>
</div>
</div>
{config.enableAdvancedControls ? (
<>
<Separator />
<div className="space-y-3">
<div>
<Label className="text-base">高级调节</Label>
<p className="mt-1 text-xs text-muted-foreground">
这些参数会直接映射到 Krea Topaz Generative,对结果风格与细节影响更明显。
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="enhance-pro-creativity">Creativity (1-6)</Label>
<Input
id="enhance-pro-creativity"
type="number"
min="1"
max="6"
value={formState.creativity}
onChange={(event) =>
handleNumericField(
"creativity",
clampNumber(Number.parseInt(event.target.value, 10) || 3, 1, 6),
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="enhance-pro-texture">Texture (1-5)</Label>
<Input
id="enhance-pro-texture"
type="number"
min="1"
max="5"
value={formState.texture}
onChange={(event) =>
handleNumericField(
"texture",
clampNumber(Number.parseInt(event.target.value, 10) || 3, 1, 5),
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="enhance-pro-detail">Detail (0-1)</Label>
<Input
id="enhance-pro-detail"
type="number"
min="0"
max="1"
step="0.1"
value={formState.detail}
onChange={(event) =>
handleNumericField(
"detail",
clampNumber(Number.parseFloat(event.target.value) || 0, 0, 1),
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="enhance-pro-sharpen">Sharpen (0-1)</Label>
<Input
id="enhance-pro-sharpen"
type="number"
min="0"
max="1"
step="0.1"
value={formState.sharpen}
onChange={(event) =>
handleNumericField(
"sharpen",
clampNumber(Number.parseFloat(event.target.value) || 0, 0, 1),
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="enhance-pro-denoise">Denoise (0-1)</Label>
<Input
id="enhance-pro-denoise"
type="number"
min="0"
max="1"
step="0.1"
value={formState.denoise}
onChange={(event) =>
handleNumericField(
"denoise",
clampNumber(Number.parseFloat(event.target.value) || 0, 0, 1),
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="enhance-pro-face-strength">Face Strength (0-1)</Label>
<Input
id="enhance-pro-face-strength"
type="number"
min="0"
max="1"
step="0.1"
value={formState.face_enhancement_strength}
onChange={(event) =>
handleNumericField(
"face_enhancement_strength",
clampNumber(Number.parseFloat(event.target.value) || 0, 0, 1),
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="enhance-pro-face-creativity">Face Creativity (0-1)</Label>
<Input
id="enhance-pro-face-creativity"
type="number"
min="0"
max="1"
step="0.1"
value={formState.face_enhancement_creativity}
onChange={(event) =>
handleNumericField(
"face_enhancement_creativity",
clampNumber(Number.parseFloat(event.target.value) || 0, 0, 1),
)
}
/>
</div>
</div>
</div>
</>
) : null}
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4 text-xs text-blue-900">
<p>处理完成后,结果会保存到相册和存储桶,并作为新图片节点添加到当前画布。</p>
{config.allowBatchSize ? (
<p className="mt-1">
当前设置允许返回多张候选结果。画布中会按 {buildImageLabel(0, formState.batchSize)}
的方式依次创建新节点。
</p>
) : null}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSubmitting}>
取消
</Button>
<Button onClick={handleSubmit} disabled={isSubmitting || isLoading || !config.enabled}>
{isSubmitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Sparkles className="mr-2 h-4 w-4" />}
开始增强 Pro
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export default EnhanceProDialog;