Deploy Inicial do SteelCheck com Docker Build Automatizado
This commit is contained in:
176
App.tsx
Normal file
176
App.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { analyzeCertificate } from './services/geminiService';
|
||||
import { exportAsPdf } from './services/pdfService';
|
||||
import type { ReportData } from './types';
|
||||
import { Header } from './components/Header';
|
||||
import { FileUpload } from './components/FileUpload';
|
||||
import { Loader } from './components/Loader';
|
||||
import { ReportDisplay } from './components/ReportDisplay';
|
||||
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 [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');
|
||||
if (savedApiKey) {
|
||||
setApiKey(savedApiKey);
|
||||
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) => {
|
||||
if (key) {
|
||||
setApiKey(key);
|
||||
localStorage.setItem('gemini-api-key', key);
|
||||
setHasKey(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAnalyzeClick = async () => {
|
||||
if (!apiKey) {
|
||||
setError("A chave de API não foi encontrada. Por favor, configure-a novamente.");
|
||||
setHasKey(false); // Force re-entry
|
||||
return;
|
||||
}
|
||||
if (!file) {
|
||||
setError("Por favor, selecione um arquivo primeiro.");
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setReportData(null);
|
||||
try {
|
||||
const data = await analyzeCertificate(file, apiKey);
|
||||
setReportData(data);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('API key not valid')) {
|
||||
setError('Erro: A chave de API fornecida não é válida. Por favor, insira uma nova chave.');
|
||||
handleClearKey(); // Clear invalid key
|
||||
} else {
|
||||
setError(`Erro na análise: ${err.message}`);
|
||||
}
|
||||
} else {
|
||||
setError("Ocorreu um erro desconhecido durante a análise.");
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setFile(null);
|
||||
setReportData(null);
|
||||
setIsLoading(false);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const handleClearKey = useCallback(() => {
|
||||
setApiKey('');
|
||||
localStorage.removeItem('gemini-api-key');
|
||||
setHasKey(false);
|
||||
handleReset();
|
||||
}, [handleReset]);
|
||||
|
||||
const handleExport = (action: 'preview' | 'download') => {
|
||||
if (reportData) {
|
||||
const fileName = `Relatorio_SteelBase_${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} />
|
||||
|
||||
<main className="container mx-auto p-4 sm:p-6 md:p-8 max-w-7xl">
|
||||
{!hasKey ? (
|
||||
<ApiKeySetup onKeySave={handleKeySave} />
|
||||
) : !reportData ? (
|
||||
<div className="max-w-2xl mx-auto pt-8">
|
||||
<div className="bg-white/70 dark:bg-slate-800/60 backdrop-blur-md border border-white/20 dark:border-slate-700/50 shadow-xl p-6 sm:p-10 rounded-2xl">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-3xl font-display font-bold text-slate-900 dark:text-white mb-2">
|
||||
Análise Técnica Inteligente
|
||||
</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400">
|
||||
Carregue seu certificado de qualidade e deixe nossa IA verificar a conformidade.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FileUpload onFileChange={handleFileChange} />
|
||||
|
||||
{file && (
|
||||
<button
|
||||
onClick={handleAnalyzeClick}
|
||||
disabled={isLoading || !file}
|
||||
title={!file ? "Por favor, selecione um arquivo para analisar" : "Analisar Certificado"}
|
||||
className="mt-8 w-full flex justify-center items-center gap-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-bold py-4 px-6 rounded-xl shadow-lg shadow-blue-500/20 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 transform active:scale-[0.99]"
|
||||
>
|
||||
Analisar Documento
|
||||
</button>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-6 text-center text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-4 rounded-xl border border-red-100 dark:border-red-900/30 text-sm">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-center text-xs text-slate-400 dark:text-slate-600 mt-6 font-medium">
|
||||
IA Assistiva • Verificação profissional recomendada
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-end mb-8">
|
||||
<button
|
||||
onClick={() => handleExport('preview')}
|
||||
className="flex items-center justify-center gap-2 w-full sm:w-auto bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 font-medium py-2.5 px-5 rounded-lg border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 transition-all shadow-sm"
|
||||
>
|
||||
<EyeIcon className="w-5 h-5 text-slate-500" />
|
||||
Visualizar PDF
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExport('download')}
|
||||
className="flex items-center justify-center gap-2 w-full sm:w-auto bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-semibold py-2.5 px-5 rounded-lg hover:opacity-90 transition-all shadow-lg shadow-slate-900/10"
|
||||
>
|
||||
<DownloadIcon className="w-5 h-5" />
|
||||
Baixar Relatório
|
||||
</button>
|
||||
</div>
|
||||
<ReportDisplay report={reportData} />
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Hidden component for PDF generation */}
|
||||
{reportData && (
|
||||
<div className="absolute -left-[9999px] top-0 opacity-0" aria-hidden="true">
|
||||
<PrintableReport report={reportData} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user