From 97eb42c24387bc26e143d96cb3bbc7cda8036e77 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Sat, 4 Apr 2026 19:32:00 +0000 Subject: [PATCH] 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) --- App.tsx | 48 +- components/ApiKeySetup.tsx | 350 ++++- components/ErrorBoundary.tsx | 71 + components/Header.tsx | 28 +- components/Icons.tsx | 6 + components/ModelSelector.tsx | 51 + components/PrintableReport.tsx | 8 +- components/ProviderSelector.tsx | 86 ++ index.html | 14 +- index.tsx | 9 +- package-lock.json | 2113 +++++++++++++++++++--------- package.json | 16 +- public/manifest.json | 22 + public/sw.js | 35 + services/aiService.ts | 353 +++++ services/apiTestService.ts | 161 +++ services/geminiService.ts | 174 --- services/pdfService.ts | 320 ++++- src/__tests__/ApiKeySetup.test.tsx | 34 + src/test/setup.ts | 1 + types/providers.ts | 42 + vitest.config.ts | 12 + 22 files changed, 2988 insertions(+), 966 deletions(-) create mode 100644 components/ErrorBoundary.tsx create mode 100644 components/ModelSelector.tsx create mode 100644 components/ProviderSelector.tsx create mode 100644 public/manifest.json create mode 100644 public/sw.js create mode 100644 services/aiService.ts create mode 100644 services/apiTestService.ts delete mode 100644 services/geminiService.ts create mode 100644 src/__tests__/ApiKeySetup.test.tsx create mode 100644 src/test/setup.ts create mode 100644 types/providers.ts create mode 100644 vitest.config.ts diff --git a/App.tsx b/App.tsx index 3df5112..697b5bc 100644 --- a/App.tsx +++ b/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(null); const [apiKey, setApiKey] = useState(''); + const [provider, setProvider] = useState('gemini'); + const [model, setModel] = useState('gemini-2.5-flash'); const [hasKey, setHasKey] = useState(false); const [reportData, setReportData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(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 (
{isLoading && } -
+
{!hasKey ? ( @@ -115,6 +130,10 @@ const App: React.FC = () => {

Carregue seu certificado de qualidade e deixe nossa IA verificar a conformidade.

+
+ + {provider.toUpperCase()} • {model} +
@@ -163,7 +182,6 @@ const App: React.FC = () => { )} - {/* Hidden component for PDF generation */} {reportData && (