"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(null); const requestSignatureRef = useRef(null); 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( () => { 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 = (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 ( { 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}