Files
motovaultpro/frontend/src/features/vehicles/hooks/useVinOcr.ts
Eric Gullickson d96736789e
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 6m31s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 23s
Deploy to Staging / Verify Staging (pull_request) Successful in 9s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
feat: update frontend for Gemini VIN decode (refs #228)
Rename nhtsaValue to sourceValue in frontend MatchedField type and
VinOcrReviewModal component. Update NHTSA references to vehicle
database across guide pages, hooks, and API documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:51:45 -06:00

173 lines
4.8 KiB
TypeScript

/**
* @ai-summary Hook to orchestrate VIN OCR extraction and VIN decode
* @ai-context Handles camera capture -> OCR extraction -> VIN decode flow
*/
import { useState, useCallback } from 'react';
import { apiClient } from '../../../core/api/client';
import { vehiclesApi } from '../api/vehicles.api';
import { DecodedVehicleData } from '../types/vehicles.types';
/** OCR extraction result for VIN */
export interface VinOcrResult {
vin: string;
confidence: number;
rawText: string;
}
/** Combined OCR + decode result */
export interface VinCaptureResult {
ocrResult: VinOcrResult;
decodedVehicle: DecodedVehicleData | null;
decodeError: string | null;
}
/** Hook state */
export interface UseVinOcrState {
isCapturing: boolean;
isProcessing: boolean;
processingStep: 'idle' | 'extracting' | 'decoding';
result: VinCaptureResult | null;
error: string | null;
}
/** Hook return type */
export interface UseVinOcrReturn extends UseVinOcrState {
startCapture: () => void;
cancelCapture: () => void;
processImage: (file: File, croppedFile?: File) => Promise<void>;
acceptResult: () => VinCaptureResult | null;
reset: () => void;
}
/**
* Extract VIN from image using VIN-specific OCR endpoint
*/
async function extractVinFromImage(file: File): Promise<VinOcrResult> {
const formData = new FormData();
formData.append('file', file);
const response = await apiClient.post('/ocr/extract/vin', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 120000, // 120 seconds for OCR processing
});
const data = response.data;
if (!data.success) {
throw new Error(data.error || 'VIN extraction failed');
}
if (!data.vin) {
throw new Error('No VIN found in image. Please ensure the VIN is clearly visible.');
}
return {
vin: data.vin.toUpperCase().replace(/[^A-HJ-NPR-Z0-9]/g, ''),
confidence: data.confidence,
rawText: data.vin,
};
}
/**
* Hook to orchestrate VIN photo capture, OCR, and decode
*/
export function useVinOcr(): UseVinOcrReturn {
const [isCapturing, setIsCapturing] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [processingStep, setProcessingStep] = useState<'idle' | 'extracting' | 'decoding'>('idle');
const [result, setResult] = useState<VinCaptureResult | null>(null);
const [error, setError] = useState<string | null>(null);
const startCapture = useCallback(() => {
setIsCapturing(true);
setError(null);
setResult(null);
}, []);
const cancelCapture = useCallback(() => {
setIsCapturing(false);
setError(null);
}, []);
const processImage = useCallback(async (file: File, croppedFile?: File) => {
setIsCapturing(false);
setIsProcessing(true);
setError(null);
setResult(null);
try {
// Step 1: Extract VIN from image
setProcessingStep('extracting');
const imageToProcess = croppedFile || file;
const ocrResult = await extractVinFromImage(imageToProcess);
// Validate VIN format
if (ocrResult.vin.length !== 17) {
throw new Error(
`Extracted VIN "${ocrResult.vin}" is ${ocrResult.vin.length} characters. VIN must be exactly 17 characters.`
);
}
// Step 2: Decode VIN
setProcessingStep('decoding');
let decodedVehicle: DecodedVehicleData | null = null;
let decodeError: string | null = null;
try {
decodedVehicle = await vehiclesApi.decodeVin(ocrResult.vin);
} catch (err: any) {
// VIN decode failure is not fatal - we still have the VIN
if (err.response?.data?.error === 'TIER_REQUIRED') {
decodeError = 'VIN decode requires Pro or Enterprise subscription';
} else if (err.response?.data?.error === 'INVALID_VIN') {
decodeError = 'VIN format is not recognized';
} else {
decodeError = 'Unable to decode vehicle information';
}
console.warn('VIN decode failed:', err);
}
setResult({
ocrResult,
decodedVehicle,
decodeError,
});
} catch (err: any) {
console.error('VIN OCR processing failed:', err);
const message = err.response?.data?.message || err.message || 'Failed to process image';
setError(message);
} finally {
setIsProcessing(false);
setProcessingStep('idle');
}
}, []);
const acceptResult = useCallback(() => {
const currentResult = result;
setResult(null);
return currentResult;
}, [result]);
const reset = useCallback(() => {
setIsCapturing(false);
setIsProcessing(false);
setProcessingStep('idle');
setResult(null);
setError(null);
}, []);
return {
isCapturing,
isProcessing,
processingStep,
result,
error,
startCapture,
cancelCapture,
processImage,
acceptResult,
reset,
};
}