Deploy Inicial do SteelCheck com Docker Build Automatizado
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
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;
|
||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
# Estágio de Build
|
||||
FROM node:20-alpine as build-stage
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install --frozen-lockfile || npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Estágio de Produção
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
20
README.md
Normal file
20
README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/drive/1QJyTnh0ssUreUbbVXIVSPagOQEsRajVV
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
68
components/ApiKeySetup.tsx
Normal file
68
components/ApiKeySetup.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import { KeyIcon, SaveIcon } from './Icons';
|
||||
|
||||
interface ApiKeySetupProps {
|
||||
onKeySave: (key: string) => void;
|
||||
}
|
||||
|
||||
export const ApiKeySetup: React.FC<ApiKeySetupProps> = ({ onKeySave }) => {
|
||||
const [localApiKey, setLocalApiKey] = useState('');
|
||||
|
||||
const handleSave = () => {
|
||||
if (localApiKey.trim()) {
|
||||
onKeySave(localApiKey.trim());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto animate-fade-in pt-10">
|
||||
<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-8 rounded-2xl relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-2xl font-display font-bold text-center mb-3 text-slate-800 dark:text-white">
|
||||
Configurar Chave de API
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 text-center mb-8">
|
||||
Para começar, insira sua chave de API do Google Gemini. Ela será mantida segura no seu navegador.
|
||||
</p>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor="api-key-setup" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Sua Chave de API
|
||||
</label>
|
||||
<div className="relative group/input">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<KeyIcon className="h-5 w-5 text-slate-400 group-focus-within/input:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="api-key-setup"
|
||||
className="block w-full rounded-xl border-slate-300 dark:border-slate-600 bg-white/50 dark:bg-slate-700/50 py-3 pl-10 pr-3 text-slate-900 dark:text-slate-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm backdrop-blur-sm transition-all"
|
||||
placeholder="Cole sua chave aqui"
|
||||
value={localApiKey}
|
||||
onChange={(e) => setLocalApiKey(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-center text-slate-500 dark:text-slate-400">
|
||||
Não tem uma chave? <a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noopener noreferrer" className="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline">Obtenha no Google AI Studio</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!localApiKey.trim()}
|
||||
className="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-3.5 px-4 rounded-xl shadow-lg hover:shadow-blue-500/30 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.98]"
|
||||
>
|
||||
<SaveIcon className="h-5 w-5" />
|
||||
<span>Salvar e Continuar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
114
components/FileUpload.tsx
Normal file
114
components/FileUpload.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { FileIcon, XIcon, UploadIcon } from './Icons';
|
||||
|
||||
interface FileUploadProps {
|
||||
onFileChange: (file: File | null) => void;
|
||||
}
|
||||
|
||||
export const FileUpload: React.FC<FileUploadProps> = ({ onFileChange }) => {
|
||||
const [fileName, setFileName] = useState<string | null>(null);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileSelect = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
setFileName(file.name);
|
||||
onFileChange(file);
|
||||
}
|
||||
}, [onFileChange]);
|
||||
|
||||
const handleRemoveFile = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setFileName(null);
|
||||
onFileChange(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}, [onFileChange]);
|
||||
|
||||
const onDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
|
||||
const onDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) {
|
||||
setFileName(file.name);
|
||||
onFileChange(file);
|
||||
}
|
||||
}, [onFileChange]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{!fileName ? (
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
className={`
|
||||
group relative flex flex-col items-center justify-center w-full h-64
|
||||
rounded-2xl border-2 border-dashed cursor-pointer transition-all duration-300
|
||||
${isDragOver
|
||||
? 'border-blue-500 bg-blue-50/50 dark:bg-blue-900/20 scale-[0.99]'
|
||||
: 'border-slate-300 dark:border-slate-600 hover:border-blue-400 dark:hover:border-blue-500 hover:bg-slate-50/50 dark:hover:bg-slate-800/50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center pt-5 pb-6 text-center">
|
||||
<div className={`
|
||||
p-4 rounded-full mb-4 transition-transform duration-300 group-hover:scale-110
|
||||
${isDragOver ? 'bg-blue-100 text-blue-600' : 'bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400 group-hover:bg-blue-50 group-hover:text-blue-500 dark:group-hover:bg-slate-700'}
|
||||
`}>
|
||||
<UploadIcon className="w-8 h-8" />
|
||||
</div>
|
||||
<p className="mb-2 text-lg font-medium text-slate-700 dark:text-slate-200">
|
||||
Clique ou arraste seu arquivo aqui
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
PDF, JPG ou PNG (máx. 10MB)
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
id="file-upload"
|
||||
name="file-upload"
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
accept="image/*,application/pdf"
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
<div className="group relative w-full p-4 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-sm hover:shadow-md transition-all flex items-center justify-between animate-fade-in">
|
||||
<div className="flex items-center gap-4 overflow-hidden">
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/30 rounded-lg text-blue-600 dark:text-blue-400">
|
||||
<FileIcon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-sm font-semibold text-slate-700 dark:text-slate-200 truncate pr-4">{fileName}</span>
|
||||
<span className="text-xs text-green-600 dark:text-green-400 font-medium">Pronto para análise</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRemoveFile}
|
||||
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-full transition-all"
|
||||
title="Remover arquivo"
|
||||
>
|
||||
<XIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
72
components/Header.tsx
Normal file
72
components/Header.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { LogoBase64, SunIcon, MoonIcon, RefreshIcon, SignOutIcon } from './Icons';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
|
||||
interface HeaderProps {
|
||||
onReset: () => void;
|
||||
onClearKey: () => void;
|
||||
hasKey: boolean;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ onReset, onClearKey, hasKey }) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 transition-all duration-300 backdrop-blur-md bg-white/70 dark:bg-slate-900/70 border-b border-white/20 dark:border-slate-800 shadow-sm">
|
||||
<div className="container mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative group cursor-pointer">
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-full blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
||||
<img src={LogoBase64} alt="SteelBase Logo" className="relative w-10 h-10 object-contain" />
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block">
|
||||
<h1 className="text-2xl font-display font-bold bg-clip-text text-transparent bg-gradient-to-r from-slate-900 to-slate-700 dark:from-white dark:to-slate-300">
|
||||
SteelBase
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 sm:gap-4">
|
||||
{hasKey && (
|
||||
<button
|
||||
onClick={onClearKey}
|
||||
className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium text-slate-500 hover:text-red-500 hover:bg-red-50 dark:text-slate-400 dark:hover:bg-red-900/20 transition-all border border-transparent hover:border-red-200 dark:hover:border-red-900"
|
||||
title="Alterar Chave de API"
|
||||
>
|
||||
<SignOutIcon className="w-4 h-4" />
|
||||
<span>Sair</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1 bg-slate-100 dark:bg-slate-800/50 p-1 rounded-full border border-slate-200 dark:border-slate-700">
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="p-2 rounded-full text-slate-500 dark:text-slate-400 hover:bg-white dark:hover:bg-slate-700 hover:shadow-sm focus:outline-none transition-all"
|
||||
aria-label="Carregar outro arquivo"
|
||||
title="Novo Relatório"
|
||||
>
|
||||
<RefreshIcon className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="w-px h-4 bg-slate-300 dark:bg-slate-700 mx-1"></div>
|
||||
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-full text-slate-500 dark:text-slate-400 hover:bg-white dark:hover:bg-slate-700 hover:shadow-sm focus:outline-none transition-all relative overflow-hidden"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
<div className="relative z-10">
|
||||
{theme === 'light' ? (
|
||||
<MoonIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<SunIcon className="w-5 h-5" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
92
components/Icons.tsx
Normal file
92
components/Icons.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
|
||||
// FIX: Truncated the corrupted base64 string and properly terminated it.
|
||||
// The original string was malformed, containing parts of another component's code,
|
||||
// which caused a syntax error and prevented any exports from this file.
|
||||
export const LogoBase64 = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACACAYAAAC6XbIEAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAvvSURBVHhe7Z1/aBxlHcd/7542cZPEJmliEyfxtEm825a0aVunbYy22bpp/6AtdGklbKFVqVT8QSoqCCIVqYACKlQEXQVBUBHFg4KogCiIAr+IKioqPlQEFV+kiD5UDC/8QUT0g4pYtGmbdvEmaRM38SZpEyfJzb7Pnd2d7M7szsiuuyf7/X7enXfZnZmdnZ25nZ2Z3ZlxQIIECRIk6F+S+v+vJEiQIEGCJEmQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEiQIEGCBAkSJEi`;
|
||||
|
||||
export const SunIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MoonIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const RefreshIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0011.664 0l3.181-3.183m-11.664 0l4.5-4.5a3.75 3.75 0 00-5.303 0l-4.5 4.5m11.664 0l-3.182 3.182a8.25 8.25 0 01-11.664 0l-3.182-3.182m11.664 0l-4.5-4.5a3.75 3.75 0 015.303 0l4.5 4.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const UploadIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
export const EyeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.432 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DownloadIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FileIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m.158 10.308l-3.182-3.182A4.5 4.5 0 004.5 12.5v2.25A3.375 3.375 0 007.875 18h8.25A3.375 3.375 0 0019.5 14.75v-2.25a2.25 2.25 0 00-2.25-2.25H15M12 12.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const XIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CheckCircleIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const XCircleIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const InfoIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const KeyIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SaveIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15M9 12l3 3m0 0l3-3m-3 3V2.25" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SignOutIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||
</svg>
|
||||
);
|
||||
30
components/Loader.tsx
Normal file
30
components/Loader.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const loadingMessages = [
|
||||
"Analisando o certificado...",
|
||||
"Extraindo dados com OCR...",
|
||||
"Consultando normas técnicas...",
|
||||
"Verificando propriedades mecânicas...",
|
||||
"Analisando composição química...",
|
||||
"Compilando o relatório final...",
|
||||
];
|
||||
|
||||
export const Loader: React.FC = () => {
|
||||
const [messageIndex, setMessageIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setMessageIndex((prevIndex) => (prevIndex + 1) % loadingMessages.length);
|
||||
}, 2500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm flex flex-col justify-center items-center z-50">
|
||||
<div className="w-16 h-16 border-4 border-dashed rounded-full animate-spin border-blue-500"></div>
|
||||
<p className="mt-4 text-gray-800 dark:text-white text-lg font-semibold">{loadingMessages[messageIndex]}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
105
components/PrintableReport.tsx
Normal file
105
components/PrintableReport.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import type { ReportData, ComplianceItem } from '../types';
|
||||
|
||||
interface PrintableReportProps {
|
||||
report: ReportData;
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
// Tries to parse common date formats like DD.MM.YYYY or YYYY-MM-DD
|
||||
const parts = dateString.split(/[.\-/]/);
|
||||
if (parts.length === 3) {
|
||||
// Assuming DD.MM.YYYY
|
||||
if (parts[0].length === 2) return `${parts[0]}/${parts[1]}/${parts[2]}`;
|
||||
// Assuming YYYY-MM-DD
|
||||
if (parts[0].length === 4) return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
}
|
||||
return dateString; // fallback
|
||||
};
|
||||
|
||||
const TableRow: React.FC<{item: ComplianceItem}> = ({item}) => (
|
||||
<tr className="border-b text-xs">
|
||||
<td className="py-1 px-2 font-medium">{item.property || item.element || item.test}</td>
|
||||
<td className="py-1 px-2">{item.norm}</td>
|
||||
<td className="py-1 px-2 font-bold">{item.certificate}</td>
|
||||
<td className={`py-1 px-2 font-bold ${item.status === 'OK' ? 'text-green-700' : 'text-red-700'}`}>{item.status}</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
export const PrintableReport: React.FC<PrintableReportProps> = ({ report }) => {
|
||||
const { identification, compliance, overPerformance, equivalents, confidence } = report;
|
||||
|
||||
return (
|
||||
<div id="printable-report-container" style={{ width: '210mm', height: '297mm' }} className="bg-white text-gray-900 p-8 font-sans flex flex-col">
|
||||
<header className="flex justify-between items-start pb-2 border-b-2 border-gray-400">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-blue-800">SteelBase</h1>
|
||||
<p className="text-sm">Análise de Qualidade Industrial com IA</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<h2 className="text-2xl font-bold">Relatório de Análise Técnica</h2>
|
||||
<p className="text-sm">Documento gerado em: {new Date().toLocaleDateString('pt-BR')}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-grow">
|
||||
<section className="mt-4 border border-gray-300 rounded p-3 grid grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
||||
<div><strong>Produto:</strong> {identification.product}</div>
|
||||
<div><strong>Norma Principal:</strong> {identification.standards}</div>
|
||||
<div><strong>Fabricante:</strong> {identification.manufacturer}</div>
|
||||
<div><strong>Nº Certificado:</strong> {identification.certificateNumber}</div>
|
||||
<div><strong>Lotes:</strong> {identification.batches}</div>
|
||||
<div><strong>Corridas:</strong> {identification.heats}</div>
|
||||
<div><strong>Quantidade:</strong> {identification.quantity}</div>
|
||||
<div><strong>Data Emissão:</strong> {formatDate(identification.certificateDate)}</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-4">
|
||||
<h3 className="text-base font-bold text-gray-800">Resumo da Análise</h3>
|
||||
<div className="mt-1 border border-gray-300 rounded p-2 text-xs">
|
||||
<p>O material de {identification.product} atende aos requisitos da norma {identification.standards}. Status Geral de Conformidade: <span className={`font-extrabold ${compliance.status === 'CONFORME' ? 'text-green-700' : 'text-red-700'}`}>{compliance.status}</span>.</p>
|
||||
{compliance.status === 'CONFORME' && overPerformance.length > 0 && (
|
||||
<p className="mt-1">O material excede os requisitos normativos em pontos-chave, como {overPerformance.map(item => item.property).join(', ')}.</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-4">
|
||||
<h3 className="text-base font-bold text-gray-800">Análise de Composição Química</h3>
|
||||
<table className="w-full mt-1 border-collapse border border-gray-300">
|
||||
<thead className="bg-gray-200 text-left text-xs uppercase">
|
||||
<tr><th className="py-1 px-2">Elemento</th><th className="py-1 px-2">Norma</th><th className="py-1 px-2">Certificado</th><th className="py-1 px-2">Status</th></tr>
|
||||
</thead>
|
||||
<tbody>{compliance.chemical.map((item, i) => <TableRow key={i} item={item} />)}</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-4">
|
||||
<h3 className="text-base font-bold text-gray-800">Análise de Propriedades Mecânicas</h3>
|
||||
<table className="w-full mt-1 border-collapse border border-gray-300">
|
||||
<thead className="bg-gray-200 text-left text-xs uppercase">
|
||||
<tr><th className="py-1 px-2">Propriedade</th><th className="py-1 px-2">Norma</th><th className="py-1 px-2">Certificado</th><th className="py-1 px-2">Status</th></tr>
|
||||
</thead>
|
||||
<tbody>{compliance.mechanical.map((item, i) => <TableRow key={i} item={item} />)}</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-4">
|
||||
<h3 className="text-base font-bold text-gray-800">Normas Equivalentes</h3>
|
||||
<div className="mt-1 grid grid-cols-4 gap-2">
|
||||
{equivalents.map((item, i) => (
|
||||
<div key={i} className="bg-gray-100 border border-gray-300 rounded p-2 text-center">
|
||||
<p className="text-xs font-semibold text-gray-600">{item.system}</p>
|
||||
<p className="text-sm font-bold text-blue-800">{item.norm}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="text-center text-xs text-gray-500 pt-2 border-t border-gray-300">
|
||||
Relatório gerado por SteelBase | Confiança da Análise: {confidence}%
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
213
components/ReportDisplay.tsx
Normal file
213
components/ReportDisplay.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
|
||||
import React from 'react';
|
||||
import type { ReportData, ComplianceItem } from '../types';
|
||||
import { CheckCircleIcon, XCircleIcon, InfoIcon } from './Icons';
|
||||
|
||||
interface ReportDisplayProps {
|
||||
report: ReportData;
|
||||
}
|
||||
|
||||
const statusClass = (status: 'OK' | 'FALHA' | 'CONFORME' | 'NÃO CONFORME') => {
|
||||
switch (status) {
|
||||
case 'OK':
|
||||
case 'CONFORME':
|
||||
return 'text-emerald-600 bg-emerald-50 dark:bg-emerald-900/20 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800';
|
||||
case 'FALHA':
|
||||
case 'NÃO CONFORME':
|
||||
return 'text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400 border border-red-200 dark:border-red-800';
|
||||
default:
|
||||
return 'text-slate-600 bg-slate-50 dark:bg-slate-800/50 dark:text-slate-400 border border-slate-200 dark:border-slate-700';
|
||||
}
|
||||
};
|
||||
|
||||
const statusIcon = (status: 'OK' | 'FALHA') => {
|
||||
return status === 'OK'
|
||||
? <CheckCircleIcon className="w-4 h-4" />
|
||||
: <XCircleIcon className="w-4 h-4" />;
|
||||
};
|
||||
|
||||
|
||||
const ComplianceTableRow: React.FC<{ item: ComplianceItem; headerLabel: string }> = ({ item, headerLabel }) => (
|
||||
<tr className="block md:table-row mb-4 md:mb-0 bg-white/50 dark:bg-slate-800/50 md:bg-transparent rounded-xl md:rounded-none shadow-sm md:shadow-none border border-slate-100 dark:border-slate-700 md:border-b md:border-slate-200/50 md:dark:border-slate-700/50 last:border-0 hover:bg-white/80 dark:hover:bg-slate-800/80 transition-colors">
|
||||
<td className="p-4 md:py-4 md:px-6 flex justify-between items-center border-b border-slate-100 dark:border-slate-700 md:border-0 md:table-cell">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 md:hidden">{headerLabel}</span>
|
||||
<span className="font-medium text-slate-700 dark:text-slate-300">{item.property || item.element || item.test}</span>
|
||||
</td>
|
||||
<td className="p-4 md:py-4 md:px-6 flex justify-between items-center border-b border-slate-100 dark:border-slate-700 md:border-0 md:table-cell">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 md:hidden">Norma</span>
|
||||
<span className="text-slate-500 dark:text-slate-400 text-sm font-mono">{item.norm}</span>
|
||||
</td>
|
||||
<td className="p-4 md:py-4 md:px-6 flex justify-between items-center border-b border-slate-100 dark:border-slate-700 md:border-0 md:table-cell">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 md:hidden">Certificado</span>
|
||||
<span className="text-slate-800 dark:text-slate-200 font-semibold font-mono">{item.certificate}</span>
|
||||
</td>
|
||||
<td className="p-4 md:py-4 md:px-6 flex justify-between items-center md:table-cell">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 md:hidden">Status</span>
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold uppercase tracking-wide ${statusClass(item.status)}`}>
|
||||
{statusIcon(item.status)}
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
export const ReportDisplay: React.FC<ReportDisplayProps> = ({ report }) => {
|
||||
const { identification, compliance, overPerformance, equivalents, confidence } = report;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-12">
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-3xl md:text-4xl font-display font-bold text-slate-900 dark:text-white">Relatório Técnico de Qualidade</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400">Análise de conformidade normativa assistida por IA</p>
|
||||
</div>
|
||||
|
||||
<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-8 rounded-2xl text-center relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500"></div>
|
||||
<h3 className="text-lg font-medium text-slate-500 dark:text-slate-400 mb-2 uppercase tracking-wide">Grau de Confiança da Análise</h3>
|
||||
<div className="flex justify-center items-end gap-2">
|
||||
<span className="text-6xl sm:text-7xl font-display font-bold bg-clip-text text-transparent bg-gradient-to-br from-blue-600 to-indigo-600 dark:from-blue-400 dark:to-indigo-400">{confidence}</span>
|
||||
<span className="text-3xl font-bold text-slate-400 mb-2">%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Identification Section */}
|
||||
<div className="bg-white/70 dark:bg-slate-800/60 backdrop-blur-md border border-white/20 dark:border-slate-700/50 shadow-xl rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/50 border-b border-slate-200/50 dark:border-slate-700/50 backdrop-blur-sm">
|
||||
<h3 className="text-xl font-display font-bold text-slate-800 dark:text-white">1. Dados de Identificação</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4">
|
||||
{Object.entries(identification).map(([key, value]) => (
|
||||
<li key={key} className="flex flex-col border-b border-slate-100 dark:border-slate-800/50 pb-3 last:border-0 last:pb-0">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-1">{key.replace(/([A-Z])/g, ' $1')}</span>
|
||||
<span className="text-lg font-medium text-slate-800 dark:text-slate-200">{value}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compliance Section */}
|
||||
<div className="bg-white/70 dark:bg-slate-800/60 backdrop-blur-md border border-white/20 dark:border-slate-700/50 shadow-xl rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/50 border-b border-slate-200/50 dark:border-slate-700/50 backdrop-blur-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<h3 className="text-xl font-display font-bold text-slate-800 dark:text-white">2. Verificação de Conformidade</h3>
|
||||
<div className={`px-4 py-1.5 rounded-full font-bold text-sm uppercase tracking-wide border shadow-sm ${statusClass(compliance.status)}`}>
|
||||
{compliance.status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
<div>
|
||||
<h4 className="flex items-center gap-2 font-display font-semibold text-lg mb-4 text-slate-800 dark:text-slate-200">
|
||||
<span className="w-1.5 h-6 bg-blue-500 rounded-full"></span>
|
||||
Análise de Propriedades Mecânicas
|
||||
</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead className="hidden md:table-header-group text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b-2 border-slate-100 dark:border-slate-700">
|
||||
<tr>
|
||||
<th className="py-3 px-6 font-semibold">Propriedade</th>
|
||||
<th className="py-3 px-6 font-semibold">Norma</th>
|
||||
<th className="py-3 px-6 font-semibold">Certificado</th>
|
||||
<th className="py-3 px-6 font-semibold w-32">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="md:divide-y md:divide-slate-100 dark:md:divide-slate-700/50">
|
||||
{compliance.mechanical.map((item, i) => <ComplianceTableRow key={i} item={item} headerLabel="Propriedade" />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="flex items-center gap-2 font-display font-semibold text-lg mb-4 text-slate-800 dark:text-slate-200">
|
||||
<span className="w-1.5 h-6 bg-purple-500 rounded-full"></span>
|
||||
Análise de Composição Química (%)
|
||||
</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead className="hidden md:table-header-group text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b-2 border-slate-100 dark:border-slate-700">
|
||||
<tr>
|
||||
<th className="py-3 px-6 font-semibold">Elemento</th>
|
||||
<th className="py-3 px-6 font-semibold">Norma</th>
|
||||
<th className="py-3 px-6 font-semibold">Certificado</th>
|
||||
<th className="py-3 px-6 font-semibold w-32">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="md:divide-y md:divide-slate-100 dark:md:divide-slate-700/50">
|
||||
{compliance.chemical.map((item, i) => <ComplianceTableRow key={i} item={item} headerLabel="Elemento" />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{compliance.otherTests.length > 0 && (
|
||||
<div>
|
||||
<h4 className="flex items-center gap-2 font-display font-semibold text-lg mb-4 text-slate-800 dark:text-slate-200">
|
||||
<span className="w-1.5 h-6 bg-emerald-500 rounded-full"></span>
|
||||
Outros Testes
|
||||
</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead className="hidden md:table-header-group text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b-2 border-slate-100 dark:border-slate-700">
|
||||
<tr>
|
||||
<th className="py-3 px-6 font-semibold">Teste</th>
|
||||
<th className="py-3 px-6 font-semibold">Norma</th>
|
||||
<th className="py-3 px-6 font-semibold">Certificado</th>
|
||||
<th className="py-3 px-6 font-semibold w-32">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="md:divide-y md:divide-slate-100 dark:md:divide-slate-700/50">
|
||||
{compliance.otherTests.map((item, i) => <ComplianceTableRow key={i} item={item} headerLabel="Teste" />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Over-Performance Section */}
|
||||
{compliance.status === 'CONFORME' && overPerformance.length > 0 && (
|
||||
<div className="bg-white/70 dark:bg-slate-800/60 backdrop-blur-md border border-white/20 dark:border-slate-700/50 shadow-xl rounded-2xl overflow-hidden border-l-4 border-l-green-500">
|
||||
<div className="px-6 py-4 bg-green-50/50 dark:bg-green-900/10 border-b border-green-100 dark:border-green-900/30">
|
||||
<h3 className="text-xl font-display font-bold text-green-800 dark:text-green-300">3. Destaques de Desempenho</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400 mb-6">Este material excede os requisitos mínimos normativos nos seguintes aspectos:</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{overPerformance.map((item, i) => (
|
||||
<div key={i} className="flex items-start gap-4 p-4 rounded-xl bg-white/50 dark:bg-slate-800/50 border border-green-100 dark:border-green-900/30 shadow-sm">
|
||||
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-full text-green-600 dark:text-green-400 shrink-0">
|
||||
<CheckCircleIcon className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="block font-semibold text-slate-800 dark:text-slate-200">{item.property}</span>
|
||||
<span className="text-green-600 dark:text-green-400 text-sm font-medium">{item.value} superior ao mínimo.</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Equivalents Section */}
|
||||
<div className="glass-panel rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 bg-slate-50/50 dark:bg-slate-800/50 border-b border-slate-200/50 dark:border-slate-700/50 backdrop-blur-sm">
|
||||
<h3 className="text-xl font-display font-bold text-slate-800 dark:text-white">4. Normas Equivalentes</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mb-6">Equivalências internacionais para <span className="font-semibold text-slate-700 dark:text-slate-300">{identification.standards}</span>:</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{equivalents.map((item, i) => (
|
||||
<div key={i} className="group p-4 rounded-xl bg-gradient-to-br from-slate-50 to-white dark:from-slate-800 dark:to-slate-800/50 border border-slate-200 dark:border-slate-700 shadow-sm hover:shadow-md transition-all hover:-translate-y-1">
|
||||
<p className="text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-1 group-hover:text-blue-500 transition-colors">{item.system}</p>
|
||||
<p className="font-bold text-slate-800 dark:text-slate-200 font-mono text-lg">{item.norm}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
45
context/ThemeContext.tsx
Normal file
45
context/ThemeContext.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const storedTheme = localStorage.getItem('theme');
|
||||
if (storedTheme) {
|
||||
return storedTheme as Theme;
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove('light', 'dark');
|
||||
root.classList.add(theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
31
index.css
Normal file
31
index.css
Normal file
@@ -0,0 +1,31 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-slate-100 dark:bg-slate-900 text-slate-900 dark:text-slate-100 transition-colors duration-300 font-sans;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
}
|
||||
.dark body {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.glass-panel {
|
||||
@apply bg-white/70 dark:bg-slate-800/60 backdrop-blur-md border border-white/20 dark:border-slate-700/50 shadow-xl;
|
||||
}
|
||||
.glass-card {
|
||||
@apply bg-white/50 dark:bg-slate-800/40 backdrop-blur-sm border border-white/30 dark:border-slate-700/30 hover:bg-white/60 dark:hover:bg-slate-800/50 transition-all duration-300;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.5s ease-out forwards;
|
||||
}
|
||||
62
index.html
Normal file
62
index.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SteelBase - Análise de Qualidade Industrial com IA</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;700;800&display=swap"
|
||||
rel="stylesheet">
|
||||
<script>
|
||||
// Initialize theme from localStorage to prevent flash of unstyled content
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
display: ['Outfit', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
glass: {
|
||||
100: 'rgba(255, 255, 255, 0.1)',
|
||||
200: 'rgba(255, 255, 255, 0.2)',
|
||||
300: 'rgba(255, 255, 255, 0.3)',
|
||||
dark: 'rgba(15, 23, 42, 0.6)',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react/": "https://aistudiocdn.com/react@^19.2.0/",
|
||||
"react": "https://aistudiocdn.com/react@^19.2.0",
|
||||
"@google/genai": "https://aistudiocdn.com/@google/genai@^1.29.1",
|
||||
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
|
||||
"html2canvas": "https://aistudiocdn.com/html2canvas@^1.4.1",
|
||||
"jspdf": "https://aistudiocdn.com/jspdf@^3.0.3"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="bg-slate-100 dark:bg-slate-900 text-slate-900 dark:text-slate-100 transition-colors duration-300 font-sans">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
19
index.tsx
Normal file
19
index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { ThemeProvider } from './context/ThemeContext';
|
||||
import './index.css';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
5
metadata.json
Normal file
5
metadata.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "SteelBase",
|
||||
"description": "A foundation for the SteelBase industrial analysis application.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
2838
package-lock.json
generated
Normal file
2838
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "steelbase",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.0",
|
||||
"@google/genai": "^1.29.1",
|
||||
"react-dom": "^19.2.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
43
push_steelcheck.py
Normal file
43
push_steelcheck.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import subprocess
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
# Configurações do Gitea
|
||||
GITEA_USER = "admtracksteel"
|
||||
GITEA_PASS = "@@Gi05Br;;"
|
||||
GITEA_URL = "git.reifonas.cloud:22222"
|
||||
GITEA_REPO = "SteelCheck"
|
||||
|
||||
# URL codificada para evitar problemas com caracteres especiais
|
||||
encoded_pass = urllib.parse.quote(GITEA_PASS)
|
||||
remote_url = f"https://{GITEA_USER}:{encoded_pass}@{GITEA_URL}/{GITEA_USER}/{GITEA_REPO}.git"
|
||||
|
||||
def run_command(command, description):
|
||||
print(f"Executando: {description}...")
|
||||
try:
|
||||
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
||||
print(f"Sucesso em {description}")
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Erro em {description}: {e.stderr}")
|
||||
return None
|
||||
|
||||
# Mudar para o diretório do projeto
|
||||
os.chdir(r"m:\OFICIAIS E FUNCIONANDO\SteelCheck_base")
|
||||
|
||||
# Inicializar Git se necessário
|
||||
if not os.path.exists(".git"):
|
||||
run_command("git init", "inicializar git")
|
||||
|
||||
# Configurar Git localmente para evitar erros de autenticação
|
||||
run_command(f'git remote remove origin', "limpar remote antigo")
|
||||
run_command(f'git remote add origin {remote_url}', "adicionar novo remote")
|
||||
|
||||
# Add e Commit
|
||||
run_command("git add .", "adicionar arquivos")
|
||||
run_command('git commit -m "Deploy Inicial do SteelCheck com Docker Build Automatizado"', "fazer commit inicial")
|
||||
|
||||
# Push final
|
||||
run_command("git push -u origin main --force", "fazer push para o Gitea")
|
||||
|
||||
print("\n--- Processo SteelCheck Concluído! ---")
|
||||
174
services/geminiService.ts
Normal file
174
services/geminiService.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import type { ReportData } from '../types';
|
||||
|
||||
const fileToGenerativePart = async (file: File) => {
|
||||
const base64EncodedDataPromise = new Promise<string>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve((reader.result as string).split(',')[1]);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
return {
|
||||
inlineData: { data: await base64EncodedDataPromise, mimeType: file.type },
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleans and parses a JSON string, removing potential markdown code blocks.
|
||||
* @param jsonString The raw string from the AI response.
|
||||
* @returns The parsed JSON object.
|
||||
*/
|
||||
const cleanAndParseJson = (jsonString: string): any => {
|
||||
const cleanedString = jsonString
|
||||
.replace(/^```json\s*/, '')
|
||||
.replace(/```$/, '')
|
||||
.trim();
|
||||
|
||||
try {
|
||||
return JSON.parse(cleanedString);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse cleaned JSON. Original string:", jsonString);
|
||||
console.error("Cleaned string:", cleanedString);
|
||||
// Re-throw the parsing error to be caught by the caller
|
||||
throw new Error("A resposta da IA não estava no formato JSON esperado, mesmo após a limpeza.");
|
||||
}
|
||||
}
|
||||
|
||||
const reportSchema = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
confidence: { type: Type.NUMBER, description: "Grau de Confiança da Análise em percentual (ex: 98)." },
|
||||
identification: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
product: { type: Type.STRING, description: "Nome do produto/material (ex: Chapa de Aço)." },
|
||||
standards: { type: Type.STRING, description: "Norma(s) principal(is) citada(s) no certificado." },
|
||||
manufacturer: { type: Type.STRING, description: "Nome do fabricante ou revendedor." },
|
||||
certificateNumber: { type: Type.STRING, description: "Número do certificado." },
|
||||
certificateDate: { type: Type.STRING, description: "Data do certificado." },
|
||||
batches: { type: Type.STRING, description: "Lista de lotes (batches)." },
|
||||
heats: { type: Type.STRING, description: "Lista de corridas (heats)." },
|
||||
quantity: { type: Type.STRING, description: "Volume, peso ou medida do material." },
|
||||
},
|
||||
required: ["product", "standards", "manufacturer", "certificateNumber", "certificateDate", "batches", "heats", "quantity"]
|
||||
},
|
||||
compliance: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
status: { type: Type.STRING, enum: ["CONFORME", "NÃO CONFORME"], description: "Status geral de conformidade." },
|
||||
mechanical: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
property: { type: Type.STRING, description: "Propriedade mecânica (ex: Limite de Escoamento)." },
|
||||
norm: { type: Type.STRING, description: "Valor exigido pela norma (ex: Mín: 350 MPa)." },
|
||||
certificate: { type: Type.STRING, description: "Valor encontrado no certificado." },
|
||||
status: { type: Type.STRING, enum: ["OK", "FALHA"], description: "Status da propriedade." },
|
||||
},
|
||||
required: ["property", "norm", "certificate", "status"]
|
||||
}
|
||||
},
|
||||
chemical: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
element: { type: Type.STRING, description: "Elemento químico (ex: Carbono (C))." },
|
||||
norm: { type: Type.STRING, description: "Valor exigido pela norma (ex: Máx: 0.25%)." },
|
||||
certificate: { type: Type.STRING, description: "Valor encontrado no certificado." },
|
||||
status: { type: Type.STRING, enum: ["OK", "FALHA"], description: "Status do elemento." },
|
||||
},
|
||||
required: ["element", "norm", "certificate", "status"]
|
||||
}
|
||||
},
|
||||
otherTests: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
test: { type: Type.STRING, description: "Outro teste relevante (ex: Teste de Impacto Charpy)." },
|
||||
norm: { type: Type.STRING, description: "Valor exigido pela norma." },
|
||||
certificate: { type: Type.STRING, description: "Valor encontrado no certificado." },
|
||||
status: { type: Type.STRING, enum: ["OK", "FALHA"], description: "Status do teste." },
|
||||
},
|
||||
required: ["test", "norm", "certificate", "status"]
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ["status", "mechanical", "chemical", "otherTests"]
|
||||
},
|
||||
overPerformance: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
property: { type: Type.STRING, description: "Ponto-chave de superdesempenho." },
|
||||
value: { type: Type.STRING, description: "Percentual acima do mínimo normativo (ex: 12.5%)." },
|
||||
},
|
||||
required: ["property", "value"]
|
||||
},
|
||||
description: "Lista de pontos onde o material excede os requisitos mínimos. Preencher apenas se o status for 'CONFORME'."
|
||||
},
|
||||
equivalents: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
system: { type: Type.STRING, description: "Sistema da norma (ex: EN (Europa))." },
|
||||
norm: { type: Type.STRING, description: "Norma equivalente (ex: S355J2)." },
|
||||
},
|
||||
required: ["system", "norm"]
|
||||
},
|
||||
description: "Lista de normas internacionais equivalentes."
|
||||
}
|
||||
},
|
||||
required: ["confidence", "identification", "compliance", "overPerformance", "equivalents"]
|
||||
};
|
||||
|
||||
export const analyzeCertificate = async (file: File, apiKey: string): Promise<ReportData> => {
|
||||
if (!apiKey) {
|
||||
throw new Error("A chave de API do Gemini não foi fornecida.");
|
||||
}
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey: apiKey });
|
||||
const imagePart = await fileToGenerativePart(file);
|
||||
|
||||
const prompt = `Você é o "SteelCheck", um especialista sênior em engenharia de materiais, metalurgia e controle de qualidade, com profundo conhecimento em aços estruturais (ASTM, EN, DIN), parafusos (ASTM, ISO), consumíveis de soldagem (AWS, ISO) e revestimentos industriais (ISO, NACE, SSPC).
|
||||
|
||||
Sua tarefa é analisar o Certificado de Qualidade fornecido (que pode ser uma imagem ou um PDF) e gerar um "Relatório de Análise Técnica" em formato JSON, seguindo rigorosamente o schema fornecido.
|
||||
|
||||
**Instrução Crítica:** Antes de qualquer outra coisa, verifique a orientação do documento. Se ele estiver rotacionado (de lado ou de cabeça para baixo), ajuste-o mentalmente para a posição de leitura correta antes de iniciar a extração de dados. A análise correta depende da orientação adequada.
|
||||
|
||||
Execute as seguintes etapas:
|
||||
1. **Extração de Dados (OCR):** Extraia todos os dados primários do certificado, já considerando a orientação correta.
|
||||
2. **Identificação de Normas:** Identifique a(s) norma(s) que o certificado alega atender.
|
||||
3. **Análise de Conformidade:** Baseado no seu conhecimento profundo, compare os resultados dos testes (Propriedades Mecânicas, Composição Química, etc.) com os requisitos mínimos e máximos especificados pela(s) norma(s). Determine o status como "OK" ou "FALHA" para cada item e um "status" geral como "CONFORME" ou "NÃO CONFORME".
|
||||
4. **Análise de Superdesempenho:** Se o status geral for "CONFORME", calcule o quanto (em porcentagem) ele excede os requisitos-chave (ex: "18% acima do mínimo"). Se for "NÃO CONFORME", deixe a lista de superdesempenho vazia.
|
||||
5. **Análise de Equivalência:** Pesquise e liste materiais funcionalmente equivalentes de outras normas importantes (EN, DIN, JIS, NBR, etc.).
|
||||
6. **Grau de Confiança:** Forneça um Grau de Confiança (em %) sobre a precisão da sua análise, baseado na qualidade do documento.
|
||||
|
||||
Retorne APENAS o objeto JSON, sem nenhum texto ou formatação adicional.`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: { parts: [imagePart, { text: prompt }] },
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: reportSchema,
|
||||
temperature: 0.1,
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const text = response.text;
|
||||
const data = cleanAndParseJson(text);
|
||||
return data as ReportData;
|
||||
} catch (e) {
|
||||
console.error("Failed to parse JSON response:", e);
|
||||
// Lançar o erro original ou um erro mais específico
|
||||
if (e instanceof Error) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("A resposta da IA não estava no formato JSON esperado.");
|
||||
}
|
||||
};
|
||||
59
services/pdfService.ts
Normal file
59
services/pdfService.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import jsPDF from 'jspdf';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
export const exportAsPdf = async (elementId: string, fileName: string, action: 'preview' | 'download'): Promise<void> => {
|
||||
const input = document.getElementById(elementId);
|
||||
if (!input) {
|
||||
console.error(`Element with id ${elementId} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const canvas = await html2canvas(input, {
|
||||
scale: 2, // Higher scale improves quality
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
|
||||
// A4 dimensions in mm: 210 x 297
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4',
|
||||
});
|
||||
|
||||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
const pdfHeight = pdf.internal.pageSize.getHeight();
|
||||
const canvasWidth = canvas.width;
|
||||
const canvasHeight = canvas.height;
|
||||
|
||||
// Calculate the aspect ratio
|
||||
const ratio = canvasWidth / canvasHeight;
|
||||
let imgWidth = pdfWidth;
|
||||
let imgHeight = imgWidth / ratio;
|
||||
|
||||
// If the calculated height is greater than the page height, scale down
|
||||
if (imgHeight > pdfHeight) {
|
||||
imgHeight = pdfHeight;
|
||||
imgWidth = imgHeight * ratio;
|
||||
}
|
||||
|
||||
const x = (pdfWidth - imgWidth) / 2;
|
||||
const y = 0;
|
||||
|
||||
pdf.addImage(imgData, 'PNG', x, y, imgWidth, imgHeight);
|
||||
|
||||
if (action === 'download') {
|
||||
pdf.save(`${fileName}.pdf`);
|
||||
} else {
|
||||
const pdfBlob = pdf.output('blob');
|
||||
const pdfUrl = URL.createObjectURL(pdfBlob);
|
||||
window.open(pdfUrl, '_blank');
|
||||
URL.revokeObjectURL(pdfUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error generating PDF:", error);
|
||||
}
|
||||
};
|
||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
45
types.ts
Normal file
45
types.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
export interface IdentificationData {
|
||||
product: string;
|
||||
standards: string;
|
||||
manufacturer: string;
|
||||
certificateNumber: string;
|
||||
certificateDate: string;
|
||||
batches: string;
|
||||
heats: string;
|
||||
quantity: string;
|
||||
}
|
||||
|
||||
export interface ComplianceItem {
|
||||
property?: string;
|
||||
element?: string;
|
||||
test?: string;
|
||||
norm: string;
|
||||
certificate: string;
|
||||
status: 'OK' | 'FALHA';
|
||||
}
|
||||
|
||||
export interface ComplianceData {
|
||||
status: 'CONFORME' | 'NÃO CONFORME';
|
||||
mechanical: ComplianceItem[];
|
||||
chemical: ComplianceItem[];
|
||||
otherTests: ComplianceItem[];
|
||||
}
|
||||
|
||||
export interface OverPerformanceItem {
|
||||
property: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface EquivalentItem {
|
||||
system: string;
|
||||
norm: string;
|
||||
}
|
||||
|
||||
export interface ReportData {
|
||||
confidence: number;
|
||||
identification: IdentificationData;
|
||||
compliance: ComplianceData;
|
||||
overPerformance: OverPerformanceItem[];
|
||||
equivalents: EquivalentItem[];
|
||||
}
|
||||
23
vite.config.ts
Normal file
23
vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user