deckhearth/components/OCRSettings.js

464 lines
17 KiB
JavaScript
Raw Normal View History

import { useState, useEffect } from 'react';
import { aiCardOCR, ollamaCardOCR, geminiCardOCR } from '../lib/ai-ocr';
export default function OCRSettings({ isOpen, onClose }) {
const [settings, setSettings] = useState({
service: 'gemini', // Default to free Gemini
openaiApiKey: '',
geminiApiKey: '',
ollamaUrl: 'http://localhost:11434'
});
const [isTesting, setIsTesting] = useState(false);
const [testResult, setTestResult] = useState(null);
// Load settings from localStorage on mount
useEffect(() => {
const loadSettings = async () => {
const savedSettings = localStorage.getItem('ocrSettings');
let currentSettings = {
service: 'gemini',
openaiApiKey: '',
geminiApiKey: '',
ollamaUrl: 'http://localhost:11434'
};
if (savedSettings) {
try {
const parsed = JSON.parse(savedSettings);
currentSettings = { ...currentSettings, ...parsed };
} catch (error) {
console.error('Error loading OCR settings:', error);
}
}
// Try to auto-load Gemini API key from environment if not already set
if (!currentSettings.geminiApiKey) {
try {
const response = await fetch('/api/config/gemini');
if (response.ok) {
const data = await response.json();
if (data.hasKey && data.apiKey) {
currentSettings.geminiApiKey = data.apiKey;
currentSettings.service = 'gemini'; // Default to Gemini if key is available
}
}
} catch (error) {
console.log('Could not auto-load Gemini API key:', error);
}
}
setSettings(currentSettings);
};
loadSettings();
}, []);
const saveSettings = () => {
try {
localStorage.setItem('ocrSettings', JSON.stringify(settings));
// Configure AI services
if (settings.openaiApiKey) {
aiCardOCR.setApiKey(settings.openaiApiKey);
}
if (settings.geminiApiKey) {
geminiCardOCR.setApiKey(settings.geminiApiKey);
}
if (settings.ollamaUrl) {
ollamaCardOCR.setBaseUrl(settings.ollamaUrl);
}
setTestResult({ type: 'success', message: 'Settings saved successfully!' });
setTimeout(() => setTestResult(null), 3000);
} catch (error) {
console.error('Error saving OCR settings:', error);
setTestResult({ type: 'error', message: 'Failed to save settings' });
}
};
const testConnection = async () => {
setIsTesting(true);
setTestResult(null);
try {
if (settings.service === 'puter') {
// Test Puter.js connection
try {
// Try to load Puter.js if not already loaded
if (typeof window !== 'undefined' && !window.puter) {
const script = document.createElement('script');
script.src = 'https://js.puter.com/v2/';
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
if (window.puter && window.puter.ai) {
setTestResult({ type: 'success', message: 'Puter.js loaded successfully! Ready for free AI vision.' });
} else {
setTestResult({ type: 'error', message: 'Failed to load Puter.js AI capabilities' });
}
} catch (error) {
setTestResult({ type: 'error', message: 'Could not connect to Puter.js service' });
}
} else if (settings.service === 'openai') {
if (!settings.openaiApiKey) {
setTestResult({ type: 'error', message: 'Please enter your OpenAI API key' });
return;
}
// Test with a simple request
const response = await fetch('https://api.openai.com/v1/models', {
headers: {
'Authorization': `Bearer ${settings.openaiApiKey}`,
}
});
if (response.ok) {
setTestResult({ type: 'success', message: 'OpenAI API connection successful!' });
} else {
const error = await response.json().catch(() => ({}));
setTestResult({
type: 'error',
message: `OpenAI API error: ${error.error?.message || response.statusText}`
});
}
} else if (settings.service === 'gemini') {
if (!settings.geminiApiKey) {
setTestResult({ type: 'error', message: 'Please enter your Gemini API key' });
return;
}
// Test with a simple request
const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models', {
headers: {
'x-goog-api-key': settings.geminiApiKey,
}
});
if (response.ok) {
const data = await response.json();
const hasVisionModel = data.models?.some(model =>
model.name.includes('gemini') && model.supportedGenerationMethods?.includes('generateContent')
);
if (hasVisionModel) {
setTestResult({ type: 'success', message: 'Gemini API connection successful with vision support!' });
} else {
setTestResult({
type: 'warning',
message: 'Gemini connected but vision capabilities unclear.'
});
}
} else {
const error = await response.json().catch(() => ({}));
setTestResult({
type: 'error',
message: `Gemini API error: ${error.error?.message || response.statusText}`
});
}
} else if (settings.service === 'ollama') {
// Test Ollama connection
const response = await fetch(`${settings.ollamaUrl}/api/tags`);
if (response.ok) {
const data = await response.json();
const hasVisionModel = data.models?.some(model =>
model.name.includes('llava') || model.name.includes('vision')
);
if (hasVisionModel) {
setTestResult({ type: 'success', message: 'Ollama connection successful with vision models!' });
} else {
setTestResult({
type: 'warning',
message: 'Ollama connected but no vision models found. Install llava:latest for card scanning.'
});
}
} else {
setTestResult({ type: 'error', message: 'Could not connect to Ollama server' });
}
}
} catch (error) {
console.error('Connection test error:', error);
setTestResult({
type: 'error',
message: `Connection failed: ${error.message}`
});
} finally {
setIsTesting(false);
}
};
const handleInputChange = (field, value) => {
setSettings(prev => ({
...prev,
[field]: value
}));
setTestResult(null);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="rounded-xl border max-w-md w-full max-h-[90vh] overflow-y-auto" style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border)' }}>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b" style={{ borderColor: 'var(--border)' }}>
<h2 className="text-xl font-semibold" style={{ color: 'var(--text-primary)' }}>
🤖 OCR Settings
</h2>
<button
onClick={onClose}
className="p-2 rounded-lg hover:opacity-70 transition-opacity"
style={{ color: 'var(--text-secondary)' }}
>
</button>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Service Selection */}
<div>
<label className="block text-sm font-medium mb-3" style={{ color: 'var(--text-primary)' }}>
OCR Service
</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="radio"
name="service"
value="puter"
checked={settings.service === 'puter'}
onChange={(e) => handleInputChange('service', e.target.value)}
className="mr-3"
style={{ accentColor: 'var(--accent-ember)' }}
/>
<div>
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>Puter.js</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Free GPT-4o vision - requires signing in to puter.com first
</div>
</div>
</label>
<label className="flex items-center">
<input
type="radio"
name="service"
value="openai"
checked={settings.service === 'openai'}
onChange={(e) => handleInputChange('service', e.target.value)}
className="mr-3"
style={{ accentColor: 'var(--accent-ember)' }}
/>
<div>
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>OpenAI Vision API</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
High accuracy, requires API key (~$0.01 per scan)
</div>
</div>
</label>
<label className="flex items-center">
<input
type="radio"
name="service"
value="gemini"
checked={settings.service === 'gemini'}
onChange={(e) => handleInputChange('service', e.target.value)}
className="mr-3"
style={{ accentColor: 'var(--accent-ember)' }}
/>
<div>
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>Google Gemini (Recommended)</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Free tier available, high accuracy, requires API key
</div>
</div>
</label>
<label className="flex items-center">
<input
type="radio"
name="service"
value="ollama"
checked={settings.service === 'ollama'}
onChange={(e) => handleInputChange('service', e.target.value)}
className="mr-3"
style={{ accentColor: 'var(--accent-ember)' }}
/>
<div>
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>Ollama Local</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Private & free, requires local Ollama + LLaVA model
</div>
</div>
</label>
</div>
</div>
{/* OpenAI Settings */}
{settings.service === 'openai' && (
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
OpenAI API Key
</label>
<input
type="password"
placeholder="sk-..."
value={settings.openaiApiKey}
onChange={(e) => handleInputChange('openaiApiKey', e.target.value)}
className="w-full px-3 py-2 rounded-lg border"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
/>
<div className="mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
Get your API key from{' '}
<a
href="https://platform.openai.com/api-keys"
target="_blank"
rel="noopener noreferrer"
className="underline hover:opacity-70"
style={{ color: 'var(--accent-ember)' }}
>
OpenAI Platform
</a>
</div>
</div>
)}
{/* Gemini Settings */}
{settings.service === 'gemini' && (
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Gemini API Key
</label>
<input
type="password"
placeholder="AIza..."
value={settings.geminiApiKey}
onChange={(e) => handleInputChange('geminiApiKey', e.target.value)}
className="w-full px-3 py-2 rounded-lg border"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
/>
<div className="mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
Get your free API key from{' '}
<a
href="https://ai.google.dev/gemini-api/docs/api-key"
target="_blank"
rel="noopener noreferrer"
className="underline hover:opacity-70"
style={{ color: 'var(--accent-ember)' }}
>
Google AI Studio
</a>
</div>
</div>
)}
{/* Ollama Settings */}
{settings.service === 'ollama' && (
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Ollama Server URL
</label>
<input
type="url"
placeholder="http://localhost:11434"
value={settings.ollamaUrl}
onChange={(e) => handleInputChange('ollamaUrl', e.target.value)}
className="w-full px-3 py-2 rounded-lg border"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
/>
<div className="mt-2 text-sm space-y-1" style={{ color: 'var(--text-secondary)' }}>
<div>Install Ollama and run: <code className="px-1 py-0.5 rounded text-xs" style={{ backgroundColor: 'var(--bg-tertiary)' }}>ollama pull llava:latest</code></div>
<div>
Setup guide:{' '}
<a
href="https://ollama.ai"
target="_blank"
rel="noopener noreferrer"
className="underline hover:opacity-70"
style={{ color: 'var(--accent-ember)' }}
>
ollama.ai
</a>
</div>
</div>
</div>
)}
{/* Test Result */}
{testResult && (
<div className={`p-3 rounded-lg border ${
testResult.type === 'success' ? 'border-green-500 bg-green-50 dark:bg-green-900/20' :
testResult.type === 'warning' ? 'border-yellow-500 bg-yellow-50 dark:bg-yellow-900/20' :
'border-red-500 bg-red-50 dark:bg-red-900/20'
}`}>
<div className={`text-sm ${
testResult.type === 'success' ? 'text-green-700 dark:text-green-300' :
testResult.type === 'warning' ? 'text-yellow-700 dark:text-yellow-300' :
'text-red-700 dark:text-red-300'
}`}>
{testResult.message}
</div>
</div>
)}
{/* Actions */}
<div className="flex gap-3">
<button
onClick={testConnection}
disabled={isTesting}
className="flex-1 px-4 py-2 rounded-lg border font-medium transition-all duration-200 hover:opacity-80 disabled:opacity-50"
style={{
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
{isTesting ? 'Testing...' : 'Test Connection'}
</button>
<button
onClick={saveSettings}
className="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-200 hover:opacity-90"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
>
Save Settings
</button>
</div>
{/* Usage Tips */}
<div className="rounded-lg p-4" style={{ backgroundColor: 'var(--bg-tertiary)' }}>
<div className="font-medium mb-2" style={{ color: 'var(--text-primary)' }}>💡 Tips</div>
<ul className="text-sm space-y-1" style={{ color: 'var(--text-secondary)' }}>
<li> Puter.js offers free GPT-4o vision with no setup required</li>
<li> OpenAI Vision API offers highest accuracy for card recognition</li>
<li> Ollama is free and private but requires local setup</li>
<li> Test your connection before scanning cards</li>
<li> Settings are saved locally in your browser</li>
</ul>
</div>
</div>
</div>
</div>
);
}