import { Download, Loader2 } from "lucide-react"; import { useState } from "react"; import { formatHeaders } from "@/lib/api"; import { useFileStore } from "@/stores/file-store"; type ColorMode = "bw" | "color"; type PathMode = "none" | "polygon" | "spline"; type Detail = "low" | "medium" | "high"; type Preset = "logo" | "illustration" | "photo" | "sketch" | "custom"; interface VectorizeState { colorMode: ColorMode; threshold: number; colorPrecision: number; layerDifference: number; filterSpeckle: number; pathMode: PathMode; cornerThreshold: number; invert: boolean; } const PRESET_SETTINGS: Record, VectorizeState> = { logo: { colorMode: "bw", threshold: 128, colorPrecision: 6, layerDifference: 6, filterSpeckle: 2, pathMode: "spline", cornerThreshold: 60, invert: false, }, illustration: { colorMode: "color", threshold: 128, colorPrecision: 4, layerDifference: 16, filterSpeckle: 8, pathMode: "spline", cornerThreshold: 60, invert: false, }, photo: { colorMode: "color", threshold: 128, colorPrecision: 8, layerDifference: 5, filterSpeckle: 4, pathMode: "spline", cornerThreshold: 60, invert: false, }, sketch: { colorMode: "bw", threshold: 100, colorPrecision: 6, layerDifference: 6, filterSpeckle: 1, pathMode: "polygon", cornerThreshold: 60, invert: false, }, }; const DETAIL_TO_SPECKLE: Record = { low: 16, medium: 4, high: 1 }; function speckleToDetail(speckle: number): Detail { if (speckle >= 10) return "low"; if (speckle >= 3) return "medium"; return "high"; } export function VectorizeSettings() { const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } = useFileStore(); const [preset, setPreset] = useState("logo"); const [colorMode, setColorMode] = useState("bw"); const [threshold, setThreshold] = useState(128); const [colorPrecision, setColorPrecision] = useState(6); const [layerDifference, setLayerDifference] = useState(6); const [filterSpeckle, setFilterSpeckle] = useState(2); const [pathMode, setPathMode] = useState("spline"); const [cornerThreshold, setCornerThreshold] = useState(60); const [invert, setInvert] = useState(false); const [downloadUrl, setDownloadUrl] = useState(null); const [originalSize, setOriginalSize] = useState(null); const [processedSize, setProcessedSize] = useState(null); const applyPreset = (p: Preset) => { setPreset(p); if (p === "custom") return; const s = PRESET_SETTINGS[p]; setColorMode(s.colorMode); setThreshold(s.threshold); setColorPrecision(s.colorPrecision); setLayerDifference(s.layerDifference); setFilterSpeckle(s.filterSpeckle); setPathMode(s.pathMode); setCornerThreshold(s.cornerThreshold); setInvert(s.invert); }; const updateSetting = (setter: (v: T) => void) => { return (v: T) => { setPreset("custom"); setter(v); }; }; const handleProcess = async () => { if (files.length === 0) return; setProcessing(true); setError(null); setDownloadUrl(null); try { const formData = new FormData(); formData.append("file", files[0]); formData.append( "settings", JSON.stringify({ colorMode, threshold, colorPrecision, layerDifference, filterSpeckle, pathMode, cornerThreshold, invert, }), ); const res = await fetch("/api/v1/tools/vectorize", { method: "POST", headers: formatHeaders(), body: formData, }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error || `Failed: ${res.status}`); } const result = await res.json(); setJobId(result.jobId); setProcessedUrl(result.downloadUrl); setDownloadUrl(result.downloadUrl); setOriginalSize(result.originalSize); setProcessedSize(result.processedSize); setSizes(result.originalSize, result.processedSize); } catch (err) { setError(err instanceof Error ? err.message : "Vectorization failed"); } finally { setProcessing(false); } }; const hasFile = files.length > 0; const detail = speckleToDetail(filterSpeckle); return (
{/* Preset */}

Preset

{(["logo", "illustration", "photo"] as const).map((p) => ( ))}
{(["sketch", "custom"] as const).map((p) => ( ))}
{/* Color Mode */}

Color Mode

{(["bw", "color"] as const).map((m) => ( ))}
{/* Color-specific settings */} {colorMode === "color" && ( <>
{colorPrecision}
updateSetting(setColorPrecision)(Number(e.target.value))} className="w-full mt-1" />
Fewer colors More colors
{layerDifference}
updateSetting(setLayerDifference)(Number(e.target.value))} className="w-full mt-1" />
Smooth gradients Flat colors
)} {/* B&W-specific settings */} {colorMode === "bw" && (
{threshold}
updateSetting(setThreshold)(Number(e.target.value))} className="w-full mt-1" />
More white More black
)}
{/* Detail */}

Detail

{(["low", "medium", "high"] as const).map((d) => ( ))}
{/* Smoothing */}

Smoothing

{(["none", "polygon", "spline"] as const).map((m) => ( ))}
{/* Corner Threshold (color mode) */} {colorMode === "color" && (
{cornerThreshold}deg
updateSetting(setCornerThreshold)(Number(e.target.value))} className="w-full mt-1" />
More corners Smoother
)} {/* Invert toggle */}
Invert Colors
{/* Error */} {error &&

{error}

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

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

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

)} {/* Submit */} {/* Download */} {downloadUrl && ( Download SVG )}
); }