"use client";
import {
CheckCircle2,
ChevronDown,
ImageIcon,
Loader2,
ScanFace,
Send,
Settings2,
SlidersHorizontal,
Sparkles,
} from "lucide-react";
import {
type PointerEvent as ReactPointerEvent,
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 {
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());
type EnhanceProClientConfig = {
enabled: boolean;
providerName: string;
baseUrl: string;
defaultModel: 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",
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)]";
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,
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 (
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;
onRequestAddImage?: () => void;
onApplyResult: (payload: {
imageUrl: string;
sourceImageUrl?: string;
prompt: string;
title?: string;
model?: string;
width?: number;
height?: number;
}) => void;
}
export function EnhanceProWorkspace({
sourceImageUrl,
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 [previewOffset, setPreviewOffset] = useState({ x: 0, y: 0 });
const [isPreviewDragging, setIsPreviewDragging] = useState(false);
const previewDragStateRef = useRef<{
pointerId: number;
startX: number;
startY: number;
originX: number;
originY: number;
} | null>(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(
() => (config.presets.length ? config.presets : FALLBACK_CONFIG.presets),
[config.presets]
);
const selectedPreset = useMemo(
() => presets.find((item) => item.id === selectedPresetId) || null,
[presets, selectedPresetId]
);
const selectedGeneratedImage =
selectedResultIndex >= 0
? generatedImages[selectedResultIndex] || null
: null;
const activePreviewUrl =
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(() => {
setPreviewOffset({ x: 0, y: 0 });
setIsPreviewDragging(false);
previewDragStateRef.current = null;
}, [activePreviewUrl]);
useEffect(() => {
if (formState.face_enhancement) {
setFaceOpen(true);
}
}, [formState.face_enhancement]);
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 handleSubmit = async () => {
if (!config.enabled) {
toast.error("图像增强 Pro 当前未开启,请先在系统设置中启用");
return;
}
const sourcePayload = buildEnhanceSourcePayload(sourceImageUrl);
if (!sourcePayload) {
toast.error(
"当前没有可用于增强的图片,请先选择画布图片或切回实时模式准备一张结果图"
);
return;
}
setIsSubmitting(true);
setLastErrorMessage("");
try {
const result = await imageGenerationService.enhanceImagePro({
...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: 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),
});
if (!result.success || !result.data?.images?.length) {
throw new Error(result.error || "图像增强 Pro 失败");
}
setGeneratedImages(result.data.images);
setSelectedResultIndex(0);
toast.success(
`图像增强 Pro 完成,已生成 ${result.data.images.length} 张结果`
);
} catch (error) {
const message =
error instanceof Error ? error.message : "图像增强 Pro 失败";
setLastErrorMessage(message);
toast.error(message);
} finally {
setIsSubmitting(false);
}
};
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 || 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 handlePreviewPointerDown = (
event: ReactPointerEvent
) => {
if (!activePreviewUrl || event.button !== 0) {
return;
}
previewDragStateRef.current = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
originX: previewOffset.x,
originY: previewOffset.y,
};
setIsPreviewDragging(true);
event.currentTarget.setPointerCapture(event.pointerId);
};
const handlePreviewPointerMove = (
event: ReactPointerEvent
) => {
const dragState = previewDragStateRef.current;
if (!dragState || dragState.pointerId !== event.pointerId) {
return;
}
setPreviewOffset({
x: dragState.originX + event.clientX - dragState.startX,
y: dragState.originY + event.clientY - dragState.startY,
});
};
const finishPreviewDrag = (event?: ReactPointerEvent) => {
const dragState = previewDragStateRef.current;
if (!dragState) {
return;
}
if (
event &&
event.currentTarget.hasPointerCapture(dragState.pointerId)
) {
event.currentTarget.releasePointerCapture(dragState.pointerId);
}
previewDragStateRef.current = null;
setIsPreviewDragging(false);
};
return (
{activePreviewUrl ? (
setPreviewOffset({ x: 0, y: 0 })}
onPointerCancel={finishPreviewDrag}
onPointerDown={handlePreviewPointerDown}
onPointerMove={handlePreviewPointerMove}
onPointerUp={finishPreviewDrag}
>
{activePreviewLabel}
双击回正
) : (
)}
{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}
{selectedPreset ? (
{selectedPreset.description ||
`${selectedPreset.model} · ${getSubjectDetectionLabel(selectedPreset.subjectDetection)}`}
) : null}
handleNumericField(
"width",
clampNumber(
Number.parseInt(event.target.value, 10) || 1,
1,
config.maxOutputEdge
)
)
}
type="number"
value={formState.width}
/>
handleNumericField(
"height",
clampNumber(
Number.parseInt(event.target.value, 10) || 1,
1,
config.maxOutputEdge
)
)
}
type="number"
value={formState.height}
/>
{config.allowBatchSize ? (
) : null}
handleNumericField(
"seed",
Number.parseInt(event.target.value, 10) || 1337
)
}
type="number"
value={formState.seed}
/>
handleNumericField(
"image_scaling_factor",
clampNumber(
Number.parseFloat(event.target.value) || 1,
1,
config.maxUpscaleFactor
)
)
}
step="0.5"
type="number"
value={formState.image_scaling_factor}
/>
启用放大
setFormState((prev) => ({
...prev,
upscaling_activated: checked,
}))
}
/>
裁剪铺满
setFormState((prev) => ({
...prev,
crop_to_fill: checked,
}))
}
/>
{config.enableAdvancedControls ? (
handleNumericField("sharpen", clampNumber(value, 0, 1))
}
step={0.01}
value={formState.sharpen}
formatValue={(value) => `${Math.round(value * 100)}%`}
/>
handleNumericField("denoise", clampNumber(value, 0, 1))
}
step={0.01}
value={formState.denoise}
formatValue={(value) => `${Math.round(value * 100)}%`}
/>
handleNumericField("detail", clampNumber(value, 0, 1))
}
step={0.01}
value={formState.detail}
formatValue={(value) => `${Math.round(value * 100)}%`}
/>
handleNumericField(
"creativity",
clampNumber(Math.round(value), 1, 6)
)
}
step={1}
value={formState.creativity}
formatValue={(value) => String(Math.round(value))}
/>
handleNumericField(
"texture",
clampNumber(Math.round(value), 1, 5)
)
}
step={1}
value={formState.texture}
formatValue={(value) => String(Math.round(value))}
/>
) : null}
setFormState((prev) => ({
...prev,
face_enhancement: checked,
}))
}
/>
handleNumericField(
"face_enhancement_strength",
clampNumber(value, 0, 1)
)
}
step={0.01}
value={formState.face_enhancement_strength}
formatValue={(value) => `${Math.round(value * 100)}%`}
/>
handleNumericField(
"face_enhancement_creativity",
clampNumber(value, 0, 1)
)
}
step={0.01}
value={formState.face_enhancement_creativity}
formatValue={(value) => `${Math.round(value * 100)}%`}
/>
);
}
export default EnhanceProWorkspace;