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
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>
173 lines
4.8 KiB
TypeScript
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,
|
|
};
|
|
}
|