feat(web): speculative pre-decode on a valid VIN (faster counter lookups)

The moment the VIN reaches 17 valid chars, warm the full decode in the background
(350ms debounce, deduped per VIN, fire-and-forget) so the "Şase Çöz" click hits
the backend lock / 24h positive cache and returns near-instantly. If the warm is
still in flight when the dealer clicks, the button's decode lock-waits on it and
navigates the moment it lands.

No analytics/state/navigation from the pre-decode — vin_decoded and the
success/candidates/error events still fire only from the button path, so the CRO
funnel stays = real intent. Raw fetch + AbortController (api.post takes no signal),
mirroring the existing 17-char preview effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 00:54:22 +03:00
parent f5e4de2aab
commit c2960013f0

View File

@@ -61,6 +61,7 @@ function SearchPage() {
const candidatesShownAtRef = useRef<number | null>(null);
const lastAttemptedVinRef = useRef<string | null>(null);
const attemptCountRef = useRef<number>(0);
const preDecodedVinRef = useRef<string | null>(null);
const [vin, setVin] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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();