import { ApiError, api } from "@/lib/api-client"; import { useTranslation } from "@/lib/i18n"; import { prepareImageForOcr } from "@/lib/image"; import { capture } from "@/lib/posthog"; import { toast } from "@/lib/toast"; import { Camera, Loader2 } from "lucide-react"; import { useRef, useState } from "react"; interface VinOcrResponse { vin: string | null; confidence?: "high" | "medium" | "low"; } interface VinOcrButtonProps { /** Geçerli bir VIN okunduğunda çağrılır — decode'u başlatmaz, kutuyu doldurur. */ onVin: (vin: string, confidence?: string) => void; disabled?: boolean; className?: string; } /** * VIN kutusunun sağındaki "fotoğraftan şase okut" butonu. Dosya seçtirir * (mobilde kamera dahil), görseli küçültüp /vehicles/decode/ocr'a yükler ve * dönen VIN'i kutuya yazdırır. Decode her zaman kullanıcının "Şase Çöz" * tıklamasıyla başlar — OCR yalnızca yazma zahmetini alır. */ export function VinOcrButton({ onVin, disabled, className }: VinOcrButtonProps) { const { t } = useTranslation(); const fileRef = useRef(null); const [busy, setBusy] = useState(false); async function handleFile(file: File | undefined) { if (!file || busy) return; setBusy(true); const startedAt = performance.now(); try { const blob = await prepareImageForOcr(file); const form = new FormData(); form.append("image", blob, "vin.jpg"); const res = await api.upload("/vehicles/decode/ocr", form); const durationMs = Math.round(performance.now() - startedAt); if (res?.vin) { onVin(res.vin, res.confidence); if (res.confidence === "low") { toast.warning(t("search.ocr.lowConfidence"), { description: t("search.ocr.lowConfidenceHint"), }); } else { toast.success(t("search.ocr.success")); } capture("vin_ocr_used", { success: true, confidence: res.confidence ?? null, duration_ms: durationMs, }); } else { toast.error(t("search.ocr.notFound"), { description: t("search.ocr.notFoundHint") }); capture("vin_ocr_used", { success: false, reason: "not_found", duration_ms: durationMs }); } } catch (err) { const durationMs = Math.round(performance.now() - startedAt); const tooLarge = err instanceof Error && err.message === "IMAGE_TOO_LARGE"; // Sunucu mesajları kullanıcıya dönük Türkçe (503 "şu anda kullanılamıyor", // 400 "desteklenmeyen tür" vb.) — genel metnin altında aynen göster ki // gerçek neden kaybolmasın. 429'un Nest mesajı İngilizce, onu çeviriyoruz. let description: string | undefined = t("search.ocr.errorHint"); if (tooLarge) { description = undefined; } else if (err instanceof ApiError) { description = err.status === 429 ? t("search.ocr.throttled") : err.message; } toast.error(tooLarge ? t("search.ocr.tooLarge") : t("search.ocr.error"), { description }); capture("vin_ocr_used", { success: false, reason: tooLarge ? "too_large" : err instanceof ApiError ? `http_${err.status}` : "error", duration_ms: durationMs, }); } finally { setBusy(false); // Aynı dosya tekrar seçilebilsin diye input'u sıfırla if (fileRef.current) fileRef.current.value = ""; } } return ( <> {/* Not: locator-stability sözleşmesi gereği data-testid KULLANILMAZ — testler input[type="file"] seçicisiyle erişir. */} handleFile(e.target.files?.[0])} /> ); }