import { ChevronDown, ChevronRight, Download, ImageIcon, Package, Upload, User, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { ProgressCard } from "@/components/common/progress-card"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { useFileStore } from "@/stores/file-store"; type SubjectType = "people" | "products" | "general"; type Quality = "fast" | "balanced" | "best" | "ultra"; type BackgroundType = "transparent" | "color" | "gradient" | "image"; type BgModel = | "birefnet-general" | "birefnet-general-lite" | "birefnet-matting" | "birefnet-portrait" | "bria-rmbg" | "u2net"; const MODEL_MAP: Record>> = { people: { fast: "u2net", balanced: "birefnet-portrait", best: "birefnet-portrait", ultra: "birefnet-matting", }, products: { fast: "u2net", balanced: "bria-rmbg", best: "birefnet-general" }, general: { fast: "u2net", balanced: "birefnet-general-lite", best: "birefnet-general" }, }; const SUBJECT_OPTIONS: { value: SubjectType; label: string; icon: typeof User }[] = [ { value: "people", label: "People", icon: User }, { value: "products", label: "Products", icon: Package }, { value: "general", label: "General", icon: ImageIcon }, ]; const ALL_QUALITY_OPTIONS: { value: Quality; label: string; peopleOnly?: boolean }[] = [ { value: "fast", label: "Fast" }, { value: "balanced", label: "HD" }, { value: "best", label: "Max" }, { value: "ultra", label: "Ultra", peopleOnly: true }, ]; const COLOR_PRESETS = [ { color: "#FFFFFF", label: "White" }, { color: "#000000", label: "Black" }, { color: "#FF0000", label: "Red" }, { color: "#00FF00", label: "Green" }, { color: "#0000FF", label: "Blue" }, ]; const GRADIENT_PRESETS = [ { color1: "#667eea", color2: "#764ba2", label: "Purple" }, { color1: "#f093fb", color2: "#f5576c", label: "Pink" }, { color1: "#4facfe", color2: "#00f2fe", label: "Blue" }, { color1: "#43e97b", color2: "#38f9d7", label: "Green" }, { color1: "#fa709a", color2: "#fee140", label: "Sunset" }, { color1: "#a18cd1", color2: "#fbc2eb", label: "Lavender" }, ]; // ── Section label ── function SectionLabel({ children }: { children: React.ReactNode }) { return (

{children}

); } // ── Shared controls (used by both standalone page and pipeline steps) ── export interface RemoveBgControlsProps { settings: Record; onChange: (settings: Record) => void; } export function RemoveBgControls({ settings, onChange }: RemoveBgControlsProps) { const [subject, setSubject] = useState("people"); const [quality, setQuality] = useState("balanced"); const [isPassport, setIsPassport] = useState(true); // Background const [bgType, setBgType] = useState("transparent"); const [bgColor, setBgColor] = useState("#FFFFFF"); const [gradColor1, setGradColor1] = useState("#667eea"); const [gradColor2, setGradColor2] = useState("#764ba2"); const [gradAngle, setGradAngle] = useState(180); const [bgImageFile, setBgImageFile] = useState(null); // Effects const [blurEnabled, setBlurEnabled] = useState(false); const [blurIntensity, setBlurIntensity] = useState(50); const [shadowEnabled, setShadowEnabled] = useState(false); const [shadowOpacity, setShadowOpacity] = useState(35); // Expandable sections const [effectsOpen, setEffectsOpen] = useState(false); // Filter quality options based on subject (Ultra only for People) const qualityOptions = ALL_QUALITY_OPTIONS.filter( (opt) => !opt.peopleOnly || subject === "people", ); // If switching away from People while on Ultra, fall back to Best const effectiveQuality = quality === "ultra" && subject !== "people" ? "best" : quality; const model = isPassport && subject === "people" ? "birefnet-portrait" : MODEL_MAP[subject][effectiveQuality] || "birefnet-general"; const onChangeRef = useRef(onChange); useEffect(() => { onChangeRef.current = onChange; }); // Sync settings on every control change useEffect(() => { const next: Record = { model, backgroundType: bgType }; if (bgType === "color") next.backgroundColor = bgColor; if (bgType === "gradient") { next.gradientColor1 = gradColor1; next.gradientColor2 = gradColor2; next.gradientAngle = gradAngle; } // Blur: enabled as effect on transparent bg means "blur original background" if (blurEnabled) { next.blurEnabled = true; next.blurIntensity = blurIntensity; } if (shadowEnabled) { next.shadowEnabled = true; next.shadowOpacity = shadowOpacity; } // Pass bgImageFile reference for the standalone wrapper to include in FormData if (bgType === "image" && bgImageFile) { next._bgImageFile = bgImageFile; } onChangeRef.current(next); }, [ model, bgType, bgColor, gradColor1, gradColor2, gradAngle, bgImageFile, blurEnabled, blurIntensity, shadowEnabled, shadowOpacity, ]); return (
{/* Subject type */} Subject
{SUBJECT_OPTIONS.map((opt) => { const Icon = opt.icon; return ( ); })}
{/* Passport checkbox - only for people, default ON */} {subject === "people" && ( )} {/* Quality */} Quality
3 ? "grid-cols-4" : "grid-cols-3"}`}> {qualityOptions.map((opt) => ( ))}
{/* Background */} Background
{/* Type buttons */}
setBgType("transparent")} checkerboard label="Transparent" /> setBgType("color")} color={bgColor} label="Color" /> setBgType("gradient")} gradient={{ color1: gradColor1, color2: gradColor2 }} label="Gradient" /> setBgType("image")} label="Image" isImage />
{/* Color options */} {bgType === "color" && (
{COLOR_PRESETS.map((preset) => (
setBgColor(e.target.value)} className="w-7 h-7 rounded border border-border cursor-pointer" /> setBgColor(e.target.value)} placeholder="#FF5500" className="flex-1 px-2 py-1 rounded border border-border bg-background text-xs text-foreground" />
)} {/* Gradient options */} {bgType === "gradient" && (
{GRADIENT_PRESETS.map((preset) => (
setGradColor1(e.target.value)} className="w-7 h-7 rounded border border-border cursor-pointer" title="Start color" /> to setGradColor2(e.target.value)} className="w-7 h-7 rounded border border-border cursor-pointer" title="End color" />
Direction {gradAngle}°
setGradAngle(Number(e.target.value))} className="w-full mt-0.5" />
)} {/* Image upload */} {bgType === "image" && (
{bgImageFile ? (
{bgImageFile.name}
) : ( )}
)}
{/* Effects */} {effectsOpen && (
{/* Blur */}
{blurEnabled && (
Intensity {blurIntensity}
setBlurIntensity(Number(e.target.value))} className="w-full mt-0.5" />
)}
{/* Shadow */}
{shadowEnabled && (
Opacity {shadowOpacity}
setShadowOpacity(Number(e.target.value))} className="w-full mt-0.5" />
)}
)}
); } // ── Background type button ── function BgTypeButton({ active, onClick, label, color, gradient, checkerboard, isImage, }: { active: boolean; onClick: () => void; label: string; color?: string; gradient?: { color1: string; color2: string }; checkerboard?: boolean; isImage?: boolean; }) { let swatchStyle: React.CSSProperties = {}; if (checkerboard) { swatchStyle = { backgroundImage: "linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)", backgroundSize: "8px 8px", backgroundPosition: "0 0, 0 4px, 4px -4px, -4px 0px", }; } else if (gradient) { swatchStyle = { background: `linear-gradient(180deg, ${gradient.color1}, ${gradient.color2})`, }; } else if (color) { swatchStyle = { backgroundColor: color }; } return ( ); } // ── Standalone tool page wrapper (two-phase flow) ── interface RemoveBgSettingsProps { onBgPreview?: (state: import("@/components/common/image-viewer").BgPreviewState | null) => void; } export function RemoveBgSettings({ onBgPreview }: RemoveBgSettingsProps = {}) { const { files } = useFileStore(); const { processFiles, processAllFiles, processing, error, downloadUrl, originalSize, processedSize, progress, } = useToolProcessor("remove-background"); const [settings, setSettings] = useState>({}); // Two-phase state: after Phase 1 (bg removal), store job info for Phase 2 (effects) const [bgJobId, setBgJobId] = useState(null); const [bgFilename, setBgFilename] = useState(null); const [bgOriginalUrl, setBgOriginalUrl] = useState(null); const [effectsDownloadUrl, setEffectsDownloadUrl] = useState(null); const [applyingEffects, setApplyingEffects] = useState(false); const [effectsError, setEffectsError] = useState(null); // Create a blob URL for the uploaded background image (for CSS preview). // HEIC/HEIF files can't be displayed by browsers, so we decode them via the // server preview endpoint first. const [bgImageBlobUrl, setBgImageBlobUrl] = useState(null); const bgImageFileRef = useRef(null); useEffect(() => { const file = settings._bgImageFile as File | undefined; if (file && file !== bgImageFileRef.current) { bgImageFileRef.current = file; let revoke: (() => void) | null = null; const ext = file.name.split(".").pop()?.toLowerCase() ?? ""; const isHeic = ext === "heic" || ext === "heif" || ext === "hif"; if (isHeic) { // Decode HEIC via server preview endpoint const formData = new FormData(); formData.append("file", file); import("@/lib/api").then(({ formatHeaders }) => { fetch("/api/v1/preview", { method: "POST", headers: formatHeaders(), body: formData, }) .then((res) => (res.ok ? res.blob() : null)) .then((blob) => { if (blob && bgImageFileRef.current === file) { const url = URL.createObjectURL(blob); revoke = () => URL.revokeObjectURL(url); setBgImageBlobUrl(url); } }) .catch(() => {}); }); } else { const url = URL.createObjectURL(file); revoke = () => URL.revokeObjectURL(url); setBgImageBlobUrl(url); } return () => revoke?.(); } if (!file && bgImageFileRef.current) { bgImageFileRef.current = null; setBgImageBlobUrl(null); } }, [settings._bgImageFile]); const hasFile = files.length > 0; const bgRemoved = bgJobId !== null && !processing; // Build CSS preview state from current settings and send to tool-page useEffect(() => { if (!bgRemoved || !onBgPreview) return; const bgType = (settings.backgroundType as string) || "transparent"; const blurEnabled = settings.blurEnabled as boolean; const blurIntensity = (settings.blurIntensity as number) ?? 50; const shadowEnabled = settings.shadowEnabled as boolean; const shadowOpacity = (settings.shadowOpacity as number) ?? 35; // When no effects are active and background is transparent, show the // before/after slider instead of the CSS preview (pass null). const hasAnyEffect = blurEnabled || shadowEnabled || bgType !== "transparent"; if (!hasAnyEffect) { onBgPreview(null); return; } const preview: import("@/components/common/image-viewer").BgPreviewState = {}; const sigma = 1 + (blurIntensity / 100) * 49; // Determine background source and blur if (bgType === "image" && bgImageBlobUrl) { preview.backgroundSrc = bgImageBlobUrl; if (blurEnabled) { preview.backgroundBlur = `blur(${sigma}px)`; } } else if (blurEnabled && (bgType === "transparent" || bgType === "blur")) { preview.backgroundSrc = bgOriginalUrl || undefined; preview.backgroundBlur = `blur(${sigma}px)`; } else if (bgType === "color") { preview.containerBackground = (settings.backgroundColor as string) || "#FFFFFF"; } else if (bgType === "gradient") { const c1 = (settings.gradientColor1 as string) || "#667eea"; const c2 = (settings.gradientColor2 as string) || "#764ba2"; const angle = (settings.gradientAngle as number) ?? 180; preview.containerBackground = `linear-gradient(${angle}deg, ${c1}, ${c2})`; } else { preview.showCheckerboard = true; } // Shadow if (shadowEnabled) { const alpha = Math.round((shadowOpacity / 100) * 255) .toString(16) .padStart(2, "0"); preview.dropShadow = `drop-shadow(0px 10px 15px #000000${alpha})`; } onBgPreview(preview); }, [ bgRemoved, settings.backgroundType, settings.backgroundColor, settings.gradientColor1, settings.gradientColor2, settings.gradientAngle, settings.blurEnabled, settings.blurIntensity, settings.shadowEnabled, settings.shadowOpacity, bgOriginalUrl, bgImageBlobUrl, onBgPreview, ]); // Clear bg preview when no bg removal is active useEffect(() => { if (!bgRemoved && onBgPreview) onBgPreview(null); }, [bgRemoved, onBgPreview]); // Phase 1: Run AI background removal const handleRemoveBg = () => { // Reset Phase 2 state setBgJobId(null); setBgFilename(null); setBgOriginalUrl(null); setEffectsDownloadUrl(null); if (files.length > 1) { processAllFiles(files, settings); return; } // Custom XHR to capture the extended response (jobId, maskUrl, originalUrl) const formData = new FormData(); formData.append("file", files[0]); const cleanSettings = { ...settings }; delete cleanSettings._bgImageFile; formData.append("settings", JSON.stringify({ model: cleanSettings.model })); const clientJobId = `bg-${Date.now()}`; formData.append("clientJobId", clientJobId); // Use processFiles for the progress/SSE flow - it handles everything // But we need the extended response. Override via a fetch after processFiles completes. // Actually, let's use processFiles and then fetch the job info. processFiles(files, { model: settings.model }); }; // After processFiles completes, extract jobId from downloadUrl useEffect(() => { if (!downloadUrl || processing) return; // downloadUrl format: /api/v1/download/{jobId}/{filename} const parts = downloadUrl.split("/"); const jobId = parts[4]; // [0]='' [1]='api' [2]='v1' [3]='download' [4]=jobId [5]=filename const filename = decodeURIComponent(parts[5] || ""); if (jobId && filename) { setBgJobId(jobId); // Derive the cached filenames from the mask filename const baseName = filename.replace(/_mask\.png$|_nobg\.png$/, ""); setBgFilename(baseName || filename.replace(/\.[^.]+$/, "")); // Build original URL from the job const origFilename = `${baseName || filename.replace(/\.[^.]+$/, "")}_original.png`; setBgOriginalUrl(`/api/v1/download/${jobId}/${encodeURIComponent(origFilename)}`); } }, [downloadUrl, processing]); // Phase 2: Apply effects and download const handleDownloadWithEffects = async () => { if (!bgJobId || !bgFilename) return; setApplyingEffects(true); try { const formData = new FormData(); const effectSettings: Record = { jobId: bgJobId, filename: `${bgFilename}.png`, backgroundType: settings.backgroundType, backgroundColor: settings.backgroundColor, gradientColor1: settings.gradientColor1, gradientColor2: settings.gradientColor2, gradientAngle: settings.gradientAngle, blurEnabled: settings.blurEnabled, blurIntensity: settings.blurIntensity, shadowEnabled: settings.shadowEnabled, shadowOpacity: settings.shadowOpacity, }; formData.append("settings", JSON.stringify(effectSettings)); const bgImageFile = settings._bgImageFile as File | undefined; if (bgImageFile) { formData.append("backgroundImage", bgImageFile); } const headers = (await import("@/lib/api")).formatHeaders(); const response = await fetch("/api/v1/tools/remove-background/effects", { method: "POST", headers, body: formData, }); if (!response.ok) { const body = await response.json().catch(() => null); throw new Error(body?.details || body?.error || `Effects failed: ${response.status}`); } const result = await response.json(); setEffectsDownloadUrl(result.downloadUrl); setEffectsError(null); // Auto-trigger download const a = document.createElement("a"); a.href = result.downloadUrl; a.download = ""; document.body.appendChild(a); a.click(); document.body.removeChild(a); } catch (err) { setEffectsError(err instanceof Error ? err.message : "Effects processing failed"); } finally { setApplyingEffects(false); } }; const hasEffectsToApply = settings.blurEnabled || settings.shadowEnabled || ((settings.backgroundType as string) || "transparent") !== "transparent"; return (
{/* Errors */} {error &&

{error}

} {effectsError &&

{effectsError}

} {/* Size info */} {originalSize != null && processedSize != null && !processing && (

Original: {(originalSize / 1024).toFixed(1)} KB

Processed: {(processedSize / 1024).toFixed(1)} KB

)} {/* Phase 1: Remove Background button */} {processing ? ( ) : !bgRemoved ? ( ) : null} {/* Phase 2: Single smart download button */} {bgRemoved && files.length <= 1 && (
{hasEffectsToApply ? ( ) : ( Download )}
)}
); }