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:
2026-04-04 19:32:00 +00:00
parent bd9592eb73
commit 97eb42c243
22 changed files with 2988 additions and 966 deletions

48
App.tsx
View File

@@ -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} />