feat: multi-provider AI support with auto-detection
- Added support for Google Gemini, OpenAI, Anthropic, and Azure OpenAI - Implemented API key validation with auto model detection - Added Error Boundary for better error handling - Migrated PDF generation to native jsPDF (better quality) - Added PWA support with offline capabilities - Implemented tests with Vitest - Fixed language consistency (PT-BR) - Improved accessibility (ARIA)
This commit is contained in:
48
App.tsx
48
App.tsx
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { analyzeCertificate } from './services/geminiService';
|
||||
import { analyzeCertificate } from './services/aiService';
|
||||
import { exportAsPdf } from './services/pdfService';
|
||||
import type { ReportData } from './types';
|
||||
import type { AIProvider } from './types/providers';
|
||||
import { Header } from './components/Header';
|
||||
import { FileUpload } from './components/FileUpload';
|
||||
import { Loader } from './components/Loader';
|
||||
@@ -10,36 +11,45 @@ import { PrintableReport } from './components/PrintableReport';
|
||||
import { DownloadIcon, EyeIcon } from './components/Icons';
|
||||
import { ApiKeySetup } from './components/ApiKeySetup';
|
||||
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [apiKey, setApiKey] = useState<string>('');
|
||||
const [provider, setProvider] = useState<AIProvider>('gemini');
|
||||
const [model, setModel] = useState<string>('gemini-2.5-flash');
|
||||
const [hasKey, setHasKey] = useState<boolean>(false);
|
||||
const [reportData, setReportData] = useState<ReportData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const savedApiKey = localStorage.getItem('gemini-api-key');
|
||||
const savedApiKey = localStorage.getItem('api-key');
|
||||
const savedProvider = localStorage.getItem('ai-provider') as AIProvider;
|
||||
const savedModel = localStorage.getItem('model-' + savedProvider);
|
||||
|
||||
if (savedApiKey) {
|
||||
setApiKey(savedApiKey);
|
||||
if (savedProvider) setProvider(savedProvider);
|
||||
if (savedModel) setModel(savedModel);
|
||||
setHasKey(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleFileChange = useCallback((selectedFile: File | null) => {
|
||||
setFile(selectedFile);
|
||||
// Reset state if file is removed
|
||||
if (!selectedFile) {
|
||||
setReportData(null);
|
||||
setError(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleKeySave = useCallback((key: string) => {
|
||||
const handleKeySave = useCallback((key: string, newProvider: AIProvider, newModel: string) => {
|
||||
if (key) {
|
||||
setApiKey(key);
|
||||
localStorage.setItem('gemini-api-key', key);
|
||||
setProvider(newProvider);
|
||||
setModel(newModel);
|
||||
localStorage.setItem('api-key', key);
|
||||
localStorage.setItem('ai-provider', newProvider);
|
||||
localStorage.setItem('model-' + newProvider, newModel);
|
||||
setHasKey(true);
|
||||
}
|
||||
}, []);
|
||||
@@ -47,7 +57,7 @@ const App: React.FC = () => {
|
||||
const handleAnalyzeClick = async () => {
|
||||
if (!apiKey) {
|
||||
setError("A chave de API não foi encontrada. Por favor, configure-a novamente.");
|
||||
setHasKey(false); // Force re-entry
|
||||
setHasKey(false);
|
||||
return;
|
||||
}
|
||||
if (!file) {
|
||||
@@ -58,13 +68,18 @@ const App: React.FC = () => {
|
||||
setError(null);
|
||||
setReportData(null);
|
||||
try {
|
||||
const data = await analyzeCertificate(file, apiKey);
|
||||
const data = await analyzeCertificate({
|
||||
provider,
|
||||
apiKey,
|
||||
model,
|
||||
file
|
||||
});
|
||||
setReportData(data);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('API key not valid')) {
|
||||
if (err.message.includes('API key') || err.message.includes('not valid') || err.message.includes('invalid')) {
|
||||
setError('Erro: A chave de API fornecida não é válida. Por favor, insira uma nova chave.');
|
||||
handleClearKey(); // Clear invalid key
|
||||
handleClearKey();
|
||||
} else {
|
||||
setError(`Erro na análise: ${err.message}`);
|
||||
}
|
||||
@@ -85,22 +100,22 @@ const App: React.FC = () => {
|
||||
|
||||
const handleClearKey = useCallback(() => {
|
||||
setApiKey('');
|
||||
localStorage.removeItem('gemini-api-key');
|
||||
localStorage.removeItem('api-key');
|
||||
setHasKey(false);
|
||||
handleReset();
|
||||
}, [handleReset]);
|
||||
|
||||
const handleExport = (action: 'preview' | 'download') => {
|
||||
if (reportData) {
|
||||
const fileName = `Relatorio_SteelBase_${reportData.identification.certificateNumber || 'analise'}`;
|
||||
const fileName = `Relatorio_SteelCheck_${reportData.identification.certificateNumber || 'analise'}`;
|
||||
exportAsPdf('printable-report-container', fileName, action);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{isLoading && <Loader />}
|
||||
<Header onReset={handleReset} onClearKey={handleClearKey} hasKey={hasKey} />
|
||||
<Header onReset={handleReset} onClearKey={handleClearKey} hasKey={hasKey} provider={provider} />
|
||||
|
||||
<main className="container mx-auto p-4 sm:p-6 md:p-8 max-w-7xl">
|
||||
{!hasKey ? (
|
||||
@@ -115,6 +130,10 @@ const App: React.FC = () => {
|
||||
<p className="text-slate-500 dark:text-slate-400">
|
||||
Carregue seu certificado de qualidade e deixe nossa IA verificar a conformidade.
|
||||
</p>
|
||||
<div className="mt-4 inline-flex items-center gap-2 px-3 py-1 bg-slate-100 dark:bg-slate-700 rounded-full text-xs text-slate-600 dark:text-slate-300">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||
{provider.toUpperCase()} • {model}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FileUpload onFileChange={handleFileChange} />
|
||||
@@ -163,7 +182,6 @@ const App: React.FC = () => {
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Hidden component for PDF generation */}
|
||||
{reportData && (
|
||||
<div className="absolute -left-[9999px] top-0 opacity-0" aria-hidden="true">
|
||||
<PrintableReport report={reportData} />
|
||||
|
||||
Reference in New Issue
Block a user