diff --git a/apps/web/src/routes/dashboard/search.tsx b/apps/web/src/routes/dashboard/search.tsx index e016001..9f24632 100644 --- a/apps/web/src/routes/dashboard/search.tsx +++ b/apps/web/src/routes/dashboard/search.tsx @@ -61,6 +61,7 @@ function SearchPage() { const candidatesShownAtRef = useRef(null); const lastAttemptedVinRef = useRef(null); const attemptCountRef = useRef(0); + const preDecodedVinRef = useRef(null); const [vin, setVin] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -147,6 +148,36 @@ function SearchPage() { return () => controller.abort(); }, [vin]); + // ─── Speculative pre-decode (the moment the VIN is valid) ────────────────── + // Warm the full decode in the background before the user clicks "Şase Çöz", so + // the button-click decode hits the backend lock / 24h positive cache and returns + // near-instantly — the dealer at the counter loses no time. Fire-and-forget: no + // state, no analytics, no navigation (the button path owns all of that). Deduped + // per VIN, debounced + aborted so typing/paste-then-edit doesn't spam decodes. + useEffect(() => { + const cleanVin = vin.toUpperCase().trim(); + if (!isValidVin(cleanVin) || preDecodedVinRef.current === cleanVin) return; + + const controller = new AbortController(); + const timer = window.setTimeout(() => { + preDecodedVinRef.current = cleanVin; + // Warm the backend decode chain; the result is intentionally discarded. + fetch("/api/vehicles/decode", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ vin: cleanVin }), + signal: controller.signal, + }).catch(() => { + /* speculative warm — ignore result/errors; the button-click decode owns the outcome */ + }); + }, 350); + + return () => { + window.clearTimeout(timer); + controller.abort(); + }; + }, [vin]); + // ─── Auto-focus ──────────────────────────────────────────────────────────── useEffect(() => { inputRef.current?.focus();