"use client"; import { useEffect, useMemo, 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 { 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()); type EnhanceProClientConfig = { enabled: boolean; providerName: string; baseUrl: string; defaultModel: EnhanceProKreaModel; defaultOutputFormat: EnhanceProOutputFormat; allowBatchSize: boolean; maxBatchSize: 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", defaultOutputFormat: "jpg", allowBatchSize: true, maxBatchSize: 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 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: 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: preset.model, upscaling_activated: preset.upscalingActivated, image_scaling_factor: clampNumber(preset.imageScalingFactor, 1, 32), 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 [formState, setFormState] = useState(() => 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( () => (config.presets.length ? config.presets : FALLBACK_CONFIG.presets), [config.presets], ); 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 = (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); try { const result = await imageGenerationService.enhanceImagePro({ 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: formState.model, upscaling_activated: formState.upscaling_activated, image_scaling_factor: clampNumber(formState.image_scaling_factor, 1, 32), 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), }); if (!result.success || !result.data?.images?.length) { throw new Error(result.error || "图像增强 Pro 失败"); } onComplete?.(result.data.images); toast.success(`图像增强 Pro 完成,已生成 ${result.data.images.length} 张结果`); onOpenChange(false); } catch (error) { toast.error(error instanceof Error ? error.message : "图像增强 Pro 失败"); } finally { setIsSubmitting(false); } }; return ( { if (isSubmitting) event.preventDefault(); }} onEscapeKeyDown={(event) => { if (isSubmitting) event.preventDefault(); }} > 图像增强 Pro 使用 {config.providerName} 对当前图片执行更可控的增强与重建。处理完成后会把结果直接添加到无限画布。
{isLoading ? (
正在加载图像增强 Pro 配置...
) : null} {!config.enabled && !isLoading ? (
当前未启用图像增强 Pro。请前往系统设置的“本地AI设置”页签完成 Krea 配置后再使用。
) : null}
原图尺寸:{naturalSize.width} x {naturalSize.height} | 当前最大输出长边:{config.maxOutputEdge}px
{presets.length ? (

先选一个更贴近场景的预设,再按图片情况继续微调参数。

{presets.map((preset) => { const isSelected = preset.id === selectedPresetId; return ( ); })}
) : null}