Deploy Inicial do SteelCheck com Docker Build Automatizado

This commit is contained in:
Marcos
2026-03-22 17:19:10 -03:00
commit 5e7dd082e3
24 changed files with 4324 additions and 0 deletions

View 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
View 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
View 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
View 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
View 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>
);
};

View 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> 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>
);
};

View 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>
);
};