Fix OCR configuration and error handling in scanner

🔧 Scanner OCR Fixes:
- AutoScanningCamera now properly initializes OCR with saved API keys
- Checks both localStorage and user preferences for OpenAI API key
- Prevents auto-scanning when OCR is not configured
- Better error messages for different failure scenarios:
  • 'OCR not configured' when no API key found
  • 'Invalid OpenAI API key' for 401 errors
  • 'API rate limit exceeded' for 429 errors
  • 'OCR service unavailable' for general failures

🎯 User Experience Improvements:
- Yellow warning banner when OCR not configured
- Manual scan button disabled until OCR configured
- Clear instructions to configure API key in Settings
- Fallback from OpenAI to Ollama if available
- Extended error message display time (4 seconds)

✅ This should resolve the 'Scan failed, try again' issue
Users now get clear guidance on what to do when OCR isn't working
This commit is contained in:
Randall Stillwell 2025-07-22 12:19:08 -05:00
parent cf05ea8daf
commit e720ffdc70

View file

@ -1,6 +1,6 @@
import React, { useRef, useState, useEffect, useCallback } from 'react'; import React, { useRef, useState, useEffect, useCallback } from 'react';
import ScanningToast from './ScanningToast'; import ScanningToast from './ScanningToast';
import { aiCardOCR, type CardOCRResult } from '../../services/aiOcr'; import { aiCardOCR, ollamaCardOCR, type CardOCRResult } from '../../services/aiOcr';
interface ScannedCard { interface ScannedCard {
id: string; id: string;
@ -44,6 +44,60 @@ const AutoScanningCamera: React.FC<AutoScanningCameraProps> = ({
const [queueCount, setQueueCount] = useState(0); const [queueCount, setQueueCount] = useState(0);
const [recentScans, setRecentScans] = useState<Set<string>>(new Set()); const [recentScans, setRecentScans] = useState<Set<string>>(new Set());
const [isOcrConfigured, setIsOcrConfigured] = useState(false);
// Initialize OCR services with saved settings
useEffect(() => {
const initializeOCR = async () => {
try {
// Check for saved API key in localStorage (from OCRSettings component)
const savedApiKey = localStorage.getItem('openai_api_key');
// Also check user preferences (from Settings page)
const token = localStorage.getItem('tcg_vault_token');
if (token) {
try {
const response = await fetch('/api/user/preferences', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
const userApiKey = data.preferences?.ocrSettings?.openai_api_key;
if (userApiKey) {
aiCardOCR.setApiKey(userApiKey);
setIsOcrConfigured(true);
console.log('✅ OCR configured with user preferences API key');
return;
}
}
} catch (error) {
console.log('Could not load user preferences, checking localStorage...');
}
}
// Fallback to localStorage API key
if (savedApiKey) {
aiCardOCR.setApiKey(savedApiKey);
setIsOcrConfigured(true);
console.log('✅ OCR configured with localStorage API key');
} else {
console.log('❌ No OpenAI API key found. OCR scanning will not work.');
setIsOcrConfigured(false);
}
} catch (error) {
console.error('Error initializing OCR:', error);
setIsOcrConfigured(false);
}
};
initializeOCR();
}, []);
// Initialize camera // Initialize camera
useEffect(() => { useEffect(() => {
@ -60,7 +114,7 @@ const AutoScanningCamera: React.FC<AutoScanningCameraProps> = ({
// Auto-scan detection // Auto-scan detection
useEffect(() => { useEffect(() => {
if (!isStreaming || !isActive) return; if (!isStreaming || !isActive || !isOcrConfigured) return;
const detectCardStabilization = () => { const detectCardStabilization = () => {
if (scanState.isProcessing || queueCount >= maxQueueSize) return; if (scanState.isProcessing || queueCount >= maxQueueSize) return;
@ -115,7 +169,7 @@ const AutoScanningCamera: React.FC<AutoScanningCameraProps> = ({
clearTimeout(scanTimeoutRef.current); clearTimeout(scanTimeoutRef.current);
} }
}; };
}, [isStreaming, isActive, scanState.isProcessing, queueCount, maxQueueSize]); }, [isStreaming, isActive, scanState.isProcessing, queueCount, maxQueueSize, isOcrConfigured]);
const startCamera = async () => { const startCamera = async () => {
try { try {
@ -180,6 +234,24 @@ const AutoScanningCamera: React.FC<AutoScanningCameraProps> = ({
const triggerScan = useCallback(async () => { const triggerScan = useCallback(async () => {
if (scanState.isProcessing || queueCount >= maxQueueSize) return; if (scanState.isProcessing || queueCount >= maxQueueSize) return;
// Check if OCR is configured
if (!isOcrConfigured) {
setScanState(prev => ({
...prev,
isScanning: false,
isProcessing: false,
message: 'OCR not configured - please set up your API key in Settings',
}));
setTimeout(() => {
setScanState(prev => ({
...prev,
message: '',
}));
}, 4000);
return;
}
setScanState(prev => ({ setScanState(prev => ({
...prev, ...prev,
isScanning: false, isScanning: false,
@ -194,8 +266,23 @@ const AutoScanningCamera: React.FC<AutoScanningCameraProps> = ({
throw new Error('Failed to capture image'); throw new Error('Failed to capture image');
} }
// Process with AI OCR // Process with AI OCR - try OpenAI first, fallback to Ollama
const cardData = await aiCardOCR.analyzeCard(imageDataUrl); let cardData: CardOCRResult;
try {
console.log('🤖 Trying OpenAI Vision API...');
cardData = await aiCardOCR.analyzeCard(imageDataUrl);
console.log('✅ OpenAI Vision result:', cardData);
} catch (openaiError) {
console.log('❌ OpenAI failed, trying Ollama:', openaiError);
try {
cardData = await ollamaCardOCR.analyzeCard(imageDataUrl);
console.log('✅ Ollama Vision result:', cardData);
} catch (ollamaError) {
console.error('❌ All AI OCR methods failed');
throw new Error('AI OCR services unavailable. Please configure OpenAI API key or run Ollama locally.');
}
}
// Create unique ID for this scan // Create unique ID for this scan
const cardId = `scan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const cardId = `scan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@ -253,10 +340,24 @@ const AutoScanningCamera: React.FC<AutoScanningCameraProps> = ({
} catch (error: any) { } catch (error: any) {
console.error('Scan processing error:', error); console.error('Scan processing error:', error);
let errorMessage = 'Scan failed, try again';
// Provide more specific error messages
if (error.message.includes('OpenAI API key not configured')) {
errorMessage = 'OpenAI API key not configured - check Settings';
} else if (error.message.includes('OpenAI API error: 401')) {
errorMessage = 'Invalid OpenAI API key - check Settings';
} else if (error.message.includes('OpenAI API error: 429')) {
errorMessage = 'API rate limit exceeded - wait a moment';
} else if (error.message.includes('AI OCR services unavailable')) {
errorMessage = 'OCR service unavailable - configure API key';
}
setScanState(prev => ({ setScanState(prev => ({
...prev, ...prev,
isProcessing: false, isProcessing: false,
message: 'Scan failed, try again', message: errorMessage,
})); }));
setTimeout(() => { setTimeout(() => {
@ -264,9 +365,9 @@ const AutoScanningCamera: React.FC<AutoScanningCameraProps> = ({
...prev, ...prev,
message: '', message: '',
})); }));
}, 3000); }, 4000);
} }
}, [captureImage, onCardScanned, queueCount, maxQueueSize, recentScans, scanState.isProcessing]); }, [captureImage, onCardScanned, queueCount, maxQueueSize, recentScans, scanState.isProcessing, isOcrConfigured]);
const manualScan = () => { const manualScan = () => {
if (!scanState.isProcessing && queueCount < maxQueueSize) { if (!scanState.isProcessing && queueCount < maxQueueSize) {
@ -281,9 +382,24 @@ const AutoScanningCamera: React.FC<AutoScanningCameraProps> = ({
isVisible={scanState.isScanning || scanState.isProcessing || !!scanState.message} isVisible={scanState.isScanning || scanState.isProcessing || !!scanState.message}
message={scanState.message} message={scanState.message}
progress={scanState.isScanning ? ((3 - scanState.countdown) / 3) * 100 : undefined} progress={scanState.isScanning ? ((3 - scanState.countdown) / 3) * 100 : undefined}
type={scanState.isProcessing ? 'processing' : scanState.message.includes('Added:') ? 'success' : 'scanning'} type={scanState.isProcessing ? 'processing' : scanState.message.includes('Added:') ? 'success' : scanState.message.includes('not configured') || scanState.message.includes('unavailable') ? 'error' : 'scanning'}
/> />
{/* OCR Configuration Warning */}
{!isOcrConfigured && isStreaming && (
<div className="mb-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-center">
<span className="text-yellow-600 text-xl mr-3">⚠️</span>
<div>
<p className="font-medium text-yellow-900">OCR Not Configured</p>
<p className="text-yellow-700 text-sm">
Please configure your OpenAI API key in Settings to enable card scanning.
</p>
</div>
</div>
</div>
)}
{/* Camera Preview */} {/* Camera Preview */}
<div className="relative bg-black rounded-lg overflow-hidden"> <div className="relative bg-black rounded-lg overflow-hidden">
<video <video
@ -302,7 +418,9 @@ const AutoScanningCamera: React.FC<AutoScanningCameraProps> = ({
<div className="absolute inset-4 border-2 border-white border-dashed rounded-lg flex items-center justify-center"> <div className="absolute inset-4 border-2 border-white border-dashed rounded-lg flex items-center justify-center">
<div className="bg-black bg-opacity-50 text-white px-4 py-2 rounded-lg text-sm text-center"> <div className="bg-black bg-opacity-50 text-white px-4 py-2 rounded-lg text-sm text-center">
<div className="font-medium">Position card within this area</div> <div className="font-medium">Position card within this area</div>
<div className="text-xs opacity-75">Auto-scan in 2-3 seconds</div> <div className="text-xs opacity-75">
{isOcrConfigured ? 'Auto-scan in 2-3 seconds' : 'Configure OCR in Settings first'}
</div>
</div> </div>
</div> </div>
@ -337,11 +455,17 @@ const AutoScanningCamera: React.FC<AutoScanningCameraProps> = ({
<div className="mt-4 text-center"> <div className="mt-4 text-center">
<button <button
onClick={manualScan} onClick={manualScan}
disabled={scanState.isProcessing || queueCount >= maxQueueSize} disabled={scanState.isProcessing || queueCount >= maxQueueSize || !isOcrConfigured}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white px-6 py-2 rounded-lg font-medium transition-colors" className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white px-6 py-2 rounded-lg font-medium transition-colors"
> >
{scanState.isProcessing ? 'Processing...' : 'Manual Scan'} {scanState.isProcessing ? 'Processing...' : !isOcrConfigured ? 'Configure OCR First' : 'Manual Scan'}
</button> </button>
{!isOcrConfigured && (
<p className="text-sm text-gray-600 mt-2">
Go to Settings → OCR to configure your OpenAI API key
</p>
)}
</div> </div>
)} )}
</div> </div>