"use client"; import { CheckCircle2, ChevronDown, History as HistoryIcon, ImageIcon, Loader2, ScanFace, Send, Settings2, SlidersHorizontal, Sparkles, Upload, X, } from "lucide-react"; import { type PointerEvent as ReactPointerEvent, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { toast } from "sonner"; import useSWR from "swr"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Slider } from "@/components/ui/slider"; import { Textarea } from "@/components/ui/textarea"; import { ENHANCE_PRO_KREA_MODELS, defaultEnhanceProPresets, type EnhanceProKreaModel, type EnhanceProOutputFormat, type EnhanceProPreset, type EnhanceProSubjectDetection, } from "@/lib/enhance-pro/config"; import { buildImageProxyUrl } from "@/lib/image-delivery"; import { calculateEnhanceProPointsEstimate, defaultPointsSystemConfig, type EnhanceProPointsPricingConfig, } from "@/lib/points/config"; import { cn } from "@/lib/utils"; import { type EnhanceProImageResult, type EnhanceProRequestParams, imageGenerationService, } 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: EnhanceProRequestParams) { return JSON.stringify({ imageUrl: params.imageUrl || null, image_base64: params.image_base64 || null, mimeType: params.mimeType || null, prompt: params.prompt || "", width: params.width, height: params.height, batchSize: params.batchSize || 1, seed: params.seed || 0, output_format: params.output_format || null, subject_detection: params.subject_detection || null, face_enhancement: params.face_enhancement === true, face_enhancement_creativity: params.face_enhancement_creativity ?? null, face_enhancement_strength: params.face_enhancement_strength ?? null, crop_to_fill: params.crop_to_fill === true, model: params.model || null, upscaling_activated: params.upscaling_activated === true, image_scaling_factor: params.image_scaling_factor ?? 1, creativity: params.creativity ?? null, texture: params.texture ?? null, sharpen: params.sharpen ?? null, denoise: params.denoise ?? null, detail: params.detail ?? null, }); } 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 EnhanceProClientPointsConfig = { enabled: boolean; blockOnInsufficient: boolean; enhanceProPricing: EnhanceProPointsPricingConfig; }; 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(), }; const FALLBACK_POINTS_CONFIG: EnhanceProClientPointsConfig = { enabled: false, blockOnInsufficient: true, enhanceProPricing: defaultPointsSystemConfig.enhanceProPricing, }; const PANEL_SHELL_CLASS = "pointer-events-auto rounded-[28px] border border-[#e5e5e5] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_20px_40px_rgba(0,0,0,0.06)]"; const SOLID_PANEL_FIELD_CLASS = "border-[#e5e5e5] bg-white text-[#111111] shadow-[inset_0_1px_0_rgba(255,255,255,0.95),0_1px_2px_rgba(0,0,0,0.04)]"; const SOLID_PANEL_CARD_CLASS = "rounded-[22px] border border-[#ececec] bg-white shadow-[inset_0_1px_0_rgba(255,255,255,0.95),0_10px_22px_rgba(0,0,0,0.04)]"; const PANEL_SECTION_TITLE_CLASS = "font-semibold text-[15px] tracking-[-0.04em] text-[#111111]"; const PANEL_SECTION_NOTE_CLASS = "text-[12px] leading-5 text-[#666666]"; const SOLID_PANEL_SELECT_CONTENT_CLASS = "border-[#e5e5e5] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_18px_38px_rgba(0,0,0,0.08)]"; const PRIMARY_ACTION_BUTTON_CLASS = "border border-[#111111] bg-[#111111] text-white shadow-[0_16px_30px_rgba(0,0,0,0.18)] transition hover:bg-black hover:border-black disabled:border-[#e5e5e5] disabled:bg-[#f1f1f1] disabled:text-[#9a9a9a] disabled:shadow-none"; const SECONDARY_ACTION_BUTTON_CLASS = "border border-[#d9d9d9] bg-white text-[#111111] shadow-[0_0_0_1px_rgba(0,0,0,0.02),0_10px_22px_rgba(0,0,0,0.03)] transition hover:border-[#111111] hover:bg-[#fafafa] hover:text-black disabled:border-[#e5e5e5] disabled:bg-[#f7f7f7] disabled:text-[#a5a5a5] disabled:shadow-none"; const COMPACT_FIELD_CLASS = `${SOLID_PANEL_FIELD_CLASS} h-11 rounded-[16px] text-sm`; const COMPACT_LABEL_CLASS = "font-medium text-[#666666] text-[11px] leading-4 tracking-[0.01em]"; const COMPACT_TOGGLE_CARD_CLASS = "flex items-center justify-between gap-2.5 px-3.5 py-3"; const SLIDER_TRACK_CLASS = "h-2.5 bg-[#ededed] shadow-inner"; const SLIDER_RANGE_CLASS = "bg-[#111111]"; const SLIDER_THUMB_CLASS = "h-4 w-4 border border-[#d4d4d4] bg-white shadow-[0_0_0_4px_rgba(0,0,0,0.08)]"; const SIDEBAR_SCROLL_CLASS = "pr-1.5 [scrollbar-width:thin] [scrollbar-color:#c8c8c8_transparent] [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[#c8c8c8] hover:[&::-webkit-scrollbar-thumb]:bg-[#a8a8a8]"; const SIDEBAR_SECTION_CARD_CLASS = "rounded-[28px] border border-[#e5e5e5] bg-white p-3.5 shadow-[0_18px_36px_rgba(0,0,0,0.05)]"; const SECTION_EYEBROW_CLASS = "inline-flex items-center gap-1.5 rounded-full border border-[#ebebeb] bg-[#fafafa] px-2.5 py-1 font-semibold text-[10px] tracking-[0.14em] text-[#666666] uppercase"; const INNER_PANEL_CLASS = "rounded-[22px] border border-[#efefef] bg-[#fbfbfb] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.92)]"; const ENHANCE_PRO_HISTORY_STORAGE_KEY = "ai-chatbot:enhance-pro-history:v1"; const ENHANCE_PRO_HISTORY_LIMIT = 4; const ENHANCE_PRO_HISTORY_TOTAL_LIMIT = 48; type EnhanceProHistoryEntry = { id: string; scopeKey: string; createdAt: number; prompt: string; model: string; image: EnhanceProImageResult; }; function clampNumber(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); } function hashEnhanceHistoryValue(value: string) { let hash = 2_166_136_261; for (let index = 0; index < value.length; index += 1) { hash ^= value.charCodeAt(index); hash = Math.imul(hash, 16_777_619); } return (hash >>> 0).toString(36); } function createEnhanceProHistoryScopeKey(value?: string | null) { const raw = String(value || "").trim(); return raw ? `enhance-scope-${hashEnhanceHistoryValue(raw)}` : "enhance-scope-global"; } function normalizeEnhanceProHistoryEntry( value: unknown ): EnhanceProHistoryEntry | null { if (!value || typeof value !== "object") return null; const raw = value as Partial & { image?: Partial; }; const originalUrl = String(raw.image?.originalUrl || "").trim(); if (!originalUrl) return null; return { id: String(raw.id || "").trim() || `enhance-history-${hashEnhanceHistoryValue(originalUrl)}`, scopeKey: String(raw.scopeKey || "").trim() || createEnhanceProHistoryScopeKey(), createdAt: Number.isFinite(Number(raw.createdAt)) ? Number(raw.createdAt) : Date.now(), prompt: String(raw.prompt || "").trim(), model: String(raw.model || "").trim(), image: { originalUrl, thumbnailUrl: String(raw.image?.thumbnailUrl || originalUrl).trim(), width: Number.isFinite(Number(raw.image?.width)) ? Number(raw.image?.width) : undefined, height: Number.isFinite(Number(raw.image?.height)) ? Number(raw.image?.height) : undefined, }, }; } function sortEnhanceHistoryEntries(entries: EnhanceProHistoryEntry[]) { return [...entries].sort((left, right) => right.createdAt - left.createdAt); } function dedupeEnhanceHistoryEntries(entries: EnhanceProHistoryEntry[]) { const seen = new Set(); const nextEntries: EnhanceProHistoryEntry[] = []; for (const entry of sortEnhanceHistoryEntries(entries)) { const originalUrl = String(entry.image.originalUrl || "").trim(); const dedupeKey = `${entry.scopeKey}::${originalUrl}`; if (!originalUrl || seen.has(dedupeKey)) continue; seen.add(dedupeKey); nextEntries.push(entry); } return nextEntries; } function readPersistedEnhanceProHistory() { if (typeof window === "undefined") return [] as EnhanceProHistoryEntry[]; try { const raw = window.localStorage.getItem(ENHANCE_PRO_HISTORY_STORAGE_KEY); if (!raw) return []; const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return []; return dedupeEnhanceHistoryEntries( parsed .map((entry) => normalizeEnhanceProHistoryEntry(entry)) .filter((entry): entry is EnhanceProHistoryEntry => Boolean(entry)) ).slice(0, ENHANCE_PRO_HISTORY_TOTAL_LIMIT); } catch { return []; } } function writePersistedEnhanceProHistory(entries: EnhanceProHistoryEntry[]) { if (typeof window === "undefined") return; window.localStorage.setItem( ENHANCE_PRO_HISTORY_STORAGE_KEY, JSON.stringify(entries) ); } function mergePersistedEnhanceProHistory( currentEntries: EnhanceProHistoryEntry[], scopeKey: string, nextEntries: EnhanceProHistoryEntry[] ) { const currentScopeEntries = dedupeEnhanceHistoryEntries([ ...nextEntries.filter((entry) => entry.scopeKey === scopeKey), ...currentEntries.filter((entry) => entry.scopeKey === scopeKey), ]).slice(0, ENHANCE_PRO_HISTORY_LIMIT); const otherScopeEntries = dedupeEnhanceHistoryEntries( currentEntries.filter((entry) => entry.scopeKey !== scopeKey) ); return sortEnhanceHistoryEntries([ ...currentScopeEntries, ...otherScopeEntries, ]).slice(0, ENHANCE_PRO_HISTORY_TOTAL_LIMIT); } 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 getDisplayImageUrl(url?: string | null, width = 1400) { const raw = String(url || "").trim(); if (!raw) return ""; return /^https?:\/\//i.test(raw) ? buildImageProxyUrl(raw, { width, quality: 88, format: "webp", }) : raw; } function getSubjectDetectionLabel( subjectDetection: EnhanceProSubjectDetection ) { if (subjectDetection === "Foreground") return "前景"; if (subjectDetection === "Background") return "背景"; return "全部"; } function getActiveResultLabel(index: number) { return `结果 ${index + 1}`; } type ParameterSliderRowProps = { label: string; value: number; min: number; max: number; step: number; onChange: (value: number) => void; formatValue?: (value: number) => string; disabled?: boolean; }; function ParameterSliderRow({ label, value, min, max, step, onChange, formatValue, disabled = false, }: ParameterSliderRowProps) { const displayValue = formatValue ? formatValue(value) : String(value); return (
{label}
{displayValue}
onChange(next ?? value)} rangeClassName={SLIDER_RANGE_CLASS} step={step} thumbClassName={SLIDER_THUMB_CLASS} trackClassName={SLIDER_TRACK_CLASS} value={[value]} />
); } function buildEnhanceSourcePayload( sourceImageUrl?: string | null ): Pick< EnhanceProRequestParams, "imageUrl" | "image_base64" | "mimeType" > | null { const raw = String(sourceImageUrl || "").trim(); if (!raw) return null; const dataUrlMatch = raw.match(/^data:([^;]+);base64,(.+)$/); if (dataUrlMatch) { return { image_base64: dataUrlMatch[2], mimeType: dataUrlMatch[1] || "image/png", }; } if (/^https?:\/\//i.test(raw)) { return { imageUrl: raw, }; } return null; } interface EnhanceProWorkspaceProps { sourceImageUrl?: string | null; historyScopeKey?: string | null; onRequestAddImage?: () => void; onApplyResult: (payload: { imageUrl: string; sourceImageUrl?: string; prompt: string; title?: string; model?: string; width?: number; height?: number; }) => void; } export function EnhanceProWorkspace({ sourceImageUrl, historyScopeKey, onRequestAddImage, onApplyResult, }: EnhanceProWorkspaceProps) { const [isSubmitting, setIsSubmitting] = useState(false); const [naturalSize, setNaturalSize] = useState({ width: 1024, height: 1024 }); const [selectedPresetId, setSelectedPresetId] = useState(""); const [selectedResultIndex, setSelectedResultIndex] = useState(-1); const [formState, setFormState] = useState(() => createDefaultFormState(FALLBACK_CONFIG, 1024, 1024) ); const [generatedImages, setGeneratedImages] = useState< EnhanceProImageResult[] >([]); const [lastErrorMessage, setLastErrorMessage] = useState(""); const [settingsOpen, setSettingsOpen] = useState(true); const [adjustmentsOpen, setAdjustmentsOpen] = useState(true); const [faceOpen, setFaceOpen] = useState(true); const [comparePosition, setComparePosition] = useState(50); const [persistedHistoryEntries, setPersistedHistoryEntries] = useState< EnhanceProHistoryEntry[] >(() => readPersistedEnhanceProHistory()); const compareDragPointerIdRef = useRef(null); const requestIdRef = useRef(null); const requestSignatureRef = useRef(null); const { data: configPayload, isLoading } = useSWR<{ success?: boolean; config?: EnhanceProClientConfig; pointsConfig?: EnhanceProClientPointsConfig; }>("/api/enhance/pro/config", fetcher); const config = useMemo( () => configPayload?.config || FALLBACK_CONFIG, [configPayload?.config] ); const pointsConfig = useMemo( () => configPayload?.pointsConfig || FALLBACK_POINTS_CONFIG, [configPayload?.pointsConfig] ); 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] ); const selectedPreset = useMemo( () => presets.find((item) => item.id === selectedPresetId) || null, [presets, selectedPresetId] ); const selectedGeneratedImage = selectedResultIndex >= 0 ? generatedImages[selectedResultIndex] || null : null; const resolvedHistoryScopeInput = useMemo( () => String(historyScopeKey || sourceImageUrl || "").trim(), [historyScopeKey, sourceImageUrl] ); const resolvedHistoryScopeKey = useMemo( () => createEnhanceProHistoryScopeKey(resolvedHistoryScopeInput), [resolvedHistoryScopeInput] ); const visibleHistoryEntries = useMemo(() => { const orderedEntries = sortEnhanceHistoryEntries(persistedHistoryEntries); if (!resolvedHistoryScopeInput) { return orderedEntries.slice(0, ENHANCE_PRO_HISTORY_LIMIT); } return orderedEntries .filter((entry) => entry.scopeKey === resolvedHistoryScopeKey) .slice(0, ENHANCE_PRO_HISTORY_LIMIT); }, [ persistedHistoryEntries, resolvedHistoryScopeInput, resolvedHistoryScopeKey, ]); const activePreviewUrl = selectedGeneratedImage?.originalUrl || sourceImageUrl || ""; const showComparisonPreview = Boolean( selectedGeneratedImage?.originalUrl && sourceImageUrl ); const activePreviewLabel = selectedGeneratedImage && selectedResultIndex >= 0 ? getActiveResultLabel(selectedResultIndex) : "原图"; const actionHint = selectedGeneratedImage ? `已选中 ${activePreviewLabel}` : generatedImages.length > 0 ? `已生成 ${generatedImages.length} 张候选结果` : "生成后可直接发送回画布"; const pointsEstimate = useMemo( () => calculateEnhanceProPointsEstimate( { ...defaultPointsSystemConfig, enabled: pointsConfig.enabled, blockOnInsufficient: pointsConfig.blockOnInsufficient, enhanceProPricing: pointsConfig.enhanceProPricing, }, { width: formState.width, height: formState.height, batchSize: formState.batchSize, model: formState.model, upscalingActivated: formState.upscaling_activated, scaleFactor: formState.image_scaling_factor, } ), [ formState.batchSize, formState.height, formState.image_scaling_factor, formState.model, formState.upscaling_activated, formState.width, pointsConfig, ] ); useEffect(() => { const imageSource = String(sourceImageUrl || "").trim(); if (!imageSource) { setNaturalSize({ width: 1024, height: 1024 }); 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 = imageSource; }, [sourceImageUrl]); useEffect(() => { 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, presets]); useEffect(() => { setGeneratedImages([]); setSelectedResultIndex(-1); setLastErrorMessage(""); }, [sourceImageUrl]); useEffect(() => { setComparePosition(50); }, [selectedResultIndex]); useEffect(() => { if (formState.face_enhancement) { setFaceOpen(true); } }, [formState.face_enhancement]); const persistHistoryEntries = useCallback( (updater: (entries: EnhanceProHistoryEntry[]) => EnhanceProHistoryEntry[]) => { setPersistedHistoryEntries((currentEntries) => { const nextEntries = dedupeEnhanceHistoryEntries( updater(currentEntries) ).slice(0, ENHANCE_PRO_HISTORY_TOTAL_LIMIT); writePersistedEnhanceProHistory(nextEntries); return nextEntries; }); }, [] ); const handleNumericField = ( key: K, nextValue: number ) => { setFormState((prev) => ({ ...prev, [key]: nextValue, })); }; const handleSelectPreset = (presetId: string) => { const preset = presets.find((item) => item.id === presetId); setSelectedPresetId(presetId); setFormState( applyPresetToForm(config, naturalSize.width, naturalSize.height, preset) ); }; const handleRequestChangeImage = useCallback(() => { requestIdRef.current = null; requestSignatureRef.current = null; setLastErrorMessage(""); onRequestAddImage?.(); }, [onRequestAddImage]); const handleSubmit = async () => { if (!config.enabled) { toast.error("图像增强 Pro 当前未开启,请先在系统设置中启用"); return; } const sourcePayload = buildEnhanceSourcePayload(sourceImageUrl); if (!sourcePayload) { toast.error( "当前没有可用于增强的图片,请先选择画布图片或切回实时模式准备一张结果图" ); return; } setIsSubmitting(true); setLastErrorMessage(""); const requestParams: EnhanceProRequestParams = { ...sourcePayload, 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 失败"); } const nextImages = result.data.images; const historyCreatedAt = Date.now(); setGeneratedImages(nextImages); setSelectedResultIndex(0); persistHistoryEntries((currentEntries) => mergePersistedEnhanceProHistory( currentEntries, resolvedHistoryScopeKey, nextImages.map((image, index) => ({ id: [ resolvedHistoryScopeKey, historyCreatedAt.toString(36), String(index), hashEnhanceHistoryValue(image.originalUrl || image.thumbnailUrl), ].join("-"), scopeKey: resolvedHistoryScopeKey, createdAt: historyCreatedAt + index, prompt: requestParams.prompt || "", model: requestParams.model || resolveEnhanceProModel(config, config.defaultModel), image, })) ) ); requestIdRef.current = null; requestSignatureRef.current = null; toast.success( `图像增强 Pro 完成,已生成 ${nextImages.length} 张结果` ); } catch (error) { const message = error instanceof Error ? error.message : "图像增强 Pro 失败"; if (!shouldPreserveEnhanceProRequestId(message)) { requestIdRef.current = null; requestSignatureRef.current = null; } setLastErrorMessage(message); toast.error(message); } finally { setIsSubmitting(false); } }; const handleLoadHistoryEntry = useCallback((entry: EnhanceProHistoryEntry) => { requestIdRef.current = null; requestSignatureRef.current = null; setGeneratedImages([entry.image]); setSelectedResultIndex(0); setLastErrorMessage(""); toast.success("已加载历史增强图片"); }, []); const handleDeleteHistoryEntry = useCallback( (entryId: string) => { persistHistoryEntries((currentEntries) => currentEntries.filter((entry) => entry.id !== entryId) ); toast.success("已删除历史增强图片"); }, [persistHistoryEntries] ); const handleApply = () => { if (!selectedGeneratedImage?.originalUrl) { toast.error("请先生成结果并选择一张增强图"); return; } onApplyResult({ imageUrl: selectedGeneratedImage.originalUrl, sourceImageUrl: sourceImageUrl || undefined, prompt: formState.prompt.trim() || "图像增强 Pro", title: "图像增强 Pro 结果", model: selectedPreset?.model || resolveEnhanceProModel(config, config.defaultModel), width: selectedGeneratedImage.width, height: selectedGeneratedImage.height, }); }; const sidePreviewItems = useMemo(() => { const items: Array<{ key: string; label: string; url: string; isSource?: boolean; }> = []; if (generatedImages.length > 0 && sourceImageUrl) { items.push({ key: "source", label: "原图", url: sourceImageUrl, isSource: true, }); } generatedImages.forEach((image, index) => { items.push({ key: `result-${index}`, label: getActiveResultLabel(index), url: image.thumbnailUrl || image.originalUrl, }); }); return items; }, [generatedImages, sourceImageUrl]); const updateComparePositionFromPointer = ( event: ReactPointerEvent ) => { const rect = event.currentTarget.getBoundingClientRect(); if (rect.width <= 0) return; const nextPosition = ((event.clientX - rect.left) / rect.width) * 100; setComparePosition(clampNumber(nextPosition, 0, 100)); }; const handleComparePointerDown = ( event: ReactPointerEvent ) => { if (event.button !== 0) return; event.preventDefault(); event.stopPropagation(); compareDragPointerIdRef.current = event.pointerId; event.currentTarget.setPointerCapture(event.pointerId); updateComparePositionFromPointer(event); }; const handleComparePointerMove = ( event: ReactPointerEvent ) => { if (compareDragPointerIdRef.current !== event.pointerId) return; event.preventDefault(); event.stopPropagation(); updateComparePositionFromPointer(event); }; const finishCompareDrag = (event: ReactPointerEvent) => { if (compareDragPointerIdRef.current !== event.pointerId) return; event.preventDefault(); event.stopPropagation(); if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } compareDragPointerIdRef.current = null; }; return (
{activePreviewUrl ? (
{activePreviewLabel} {showComparisonPreview && sourceImageUrl ? ( <>
原图对比
event.stopPropagation()} onPointerCancel={finishCompareDrag} onPointerDown={handleComparePointerDown} onPointerMove={handleComparePointerMove} onPointerUp={finishCompareDrag} >
) : null} {showComparisonPreview ? ( <>
原图
增强后
) : null}
{activePreviewLabel}
) : (
选择本地图片后即可开始图像增强 Pro
)} {generatedImages.length > 0 && sidePreviewItems.length > 0 ? (
{sidePreviewItems.map((item, index) => { const isActive = item.isSource ? selectedResultIndex < 0 : selectedResultIndex === index - 1; return ( ); })}
) : null}
{selectedGeneratedImage ? ( ) : null} {actionHint}
{pointsEstimate.chargeable ? (
预计消耗 {pointsEstimate.cost} 积分 {pointsEstimate.chargeMode === "resolution_bucket" && pointsEstimate.bucketName ? ` · ${pointsEstimate.bucketName}` : ""} {pointsEstimate.chargeMode === "scale_factor" ? ` · ${pointsEstimate.scaleFactor}x` : ""}
) : null}
{isLoading ? (
正在加载增强配置...
) : null} {!config.enabled && !isLoading ? (
当前未启用图像增强 Pro。请先在系统设置的“本地AI设置”里完成 Krea 配置。
) : null} {lastErrorMessage ? (
{lastErrorMessage}
) : null} {visibleHistoryEntries.length > 0 ? (
Recent Enhancements
最近增强
{visibleHistoryEntries.length}/{ENHANCE_PRO_HISTORY_LIMIT}
{visibleHistoryEntries.map((entry, index) => { const isActive = selectedGeneratedImage?.originalUrl === entry.image.originalUrl; const createdAtLabel = new Date(entry.createdAt).toLocaleString( "zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", } ); return (
); })}
) : null}
{selectedPreset ? (
{selectedPreset.description || `${selectedPreset.model} · ${getSubjectDetectionLabel(selectedPreset.subjectDetection)}`}
) : null}