Deploy Inicial do SteelCheck com Docker Build Automatizado
This commit is contained in:
@@ -18,7 +18,7 @@ const App: React.FC = () => {
|
||||
const [provider, setProvider] = useState<AIProvider>('gemini');
|
||||
const [model, setModel] = useState<string>('gemini-2.5-flash');
|
||||
const [hasKey, setHasKey] = useState<boolean>(false);
|
||||
const [reportData, setReportData] = useState<ReportData | null>(null);
|
||||
const [reportsData, setReportsData] = useState<ReportData[] | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -40,7 +40,7 @@ const App: React.FC = () => {
|
||||
const handleFileChange = useCallback((selectedFile: File | null) => {
|
||||
setFile(selectedFile);
|
||||
if (!selectedFile) {
|
||||
setReportData(null);
|
||||
setReportsData(null);
|
||||
setError(null);
|
||||
}
|
||||
}, []);
|
||||
@@ -79,7 +79,7 @@ const App: React.FC = () => {
|
||||
}
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setReportData(null);
|
||||
setReportsData(null);
|
||||
try {
|
||||
const data = await analyzeCertificate({
|
||||
provider,
|
||||
@@ -88,7 +88,7 @@ const App: React.FC = () => {
|
||||
file,
|
||||
endpoint: provider === 'ollama' ? endpoint : undefined
|
||||
});
|
||||
setReportData(data);
|
||||
setReportsData(data);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes('API key') || err.message.includes('not valid') || err.message.includes('invalid')) {
|
||||
@@ -107,7 +107,7 @@ const App: React.FC = () => {
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setFile(null);
|
||||
setReportData(null);
|
||||
setReportsData(null);
|
||||
setIsLoading(false);
|
||||
setError(null);
|
||||
}, []);
|
||||
@@ -120,8 +120,8 @@ const App: React.FC = () => {
|
||||
}, [handleReset]);
|
||||
|
||||
const handleExport = (action: 'preview' | 'download') => {
|
||||
if (reportData) {
|
||||
const fileName = `Relatorio_SteelCheck_${reportData.identification.certificateNumber || 'analise'}`;
|
||||
if (reportsData && reportsData.length > 0) {
|
||||
const fileName = `Relatorio_SteelCheck_${reportsData[0].identification.certificateNumber || 'analise'}`;
|
||||
exportAsPdf('printable-report-container', fileName, action);
|
||||
}
|
||||
};
|
||||
@@ -134,7 +134,7 @@ const App: React.FC = () => {
|
||||
<main className="container mx-auto p-4 sm:p-6 md:p-8 max-w-7xl">
|
||||
{!hasKey ? (
|
||||
<ApiKeySetup onKeySave={handleKeySave} />
|
||||
) : !reportData ? (
|
||||
) : !reportsData ? (
|
||||
<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">
|
||||
@@ -191,14 +191,18 @@ const App: React.FC = () => {
|
||||
Baixar Relatório
|
||||
</button>
|
||||
</div>
|
||||
<ReportDisplay report={reportData} />
|
||||
<div className="flex flex-col gap-8">
|
||||
{reportsData.map((report, idx) => (
|
||||
<ReportDisplay key={idx} report={report} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{reportData && (
|
||||
{reportsData && (
|
||||
<div className="absolute -left-[9999px] top-0 opacity-0" aria-hidden="true">
|
||||
<PrintableReport report={reportData} />
|
||||
<PrintableReport reports={reportsData} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,22 +3,22 @@ import type { ReportData } from '../types';
|
||||
import { ReportDisplay } from './ReportDisplay';
|
||||
|
||||
interface PrintableReportProps {
|
||||
report: ReportData;
|
||||
reports: ReportData[];
|
||||
}
|
||||
|
||||
export const PrintableReport: React.FC<PrintableReportProps> = ({ report }) => {
|
||||
// To ensure the generated PDF is strictly in light mode, we can wrap it in a container
|
||||
// that forces light theme classes, but since html2canvas evaluates computed styles,
|
||||
// it's best to ensure it renders offscreen without "dark" class in its hierarchy.
|
||||
// The 'light' class can help if tailwind 'dark' strategy is class-based.
|
||||
export const PrintableReport: React.FC<PrintableReportProps> = ({ reports }) => {
|
||||
return (
|
||||
<div
|
||||
id="printable-report-container"
|
||||
data-report={JSON.stringify(report)}
|
||||
className="light bg-white text-slate-900"
|
||||
style={{ width: '210mm', minHeight: '297mm', background: 'white' }}
|
||||
data-report={JSON.stringify(reports)}
|
||||
className="light bg-white text-slate-900 flex flex-col gap-[20mm]"
|
||||
style={{ width: '210mm', background: 'white' }}
|
||||
>
|
||||
<ReportDisplay report={report} />
|
||||
{reports.map((report, idx) => (
|
||||
<div key={idx} className="report-page" style={{ minHeight: '297mm' }}>
|
||||
<ReportDisplay report={report} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -141,9 +141,9 @@ export const ReportDisplay: React.FC<ReportDisplayProps> = ({ report }) => {
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td className="py-2.5 px-2 truncate">
|
||||
<span className="font-semibold block text-slate-800 dark:text-slate-200" title={item.property}>{item.property}</span>
|
||||
<span className="font-medium text-[10px] block text-slate-800 dark:text-slate-200" title={item.property}>{item.property}</span>
|
||||
</td>
|
||||
<td className="py-2.5 px-1 font-mono font-bold text-slate-700 dark:text-slate-300 truncate" title={item.certificate}>{item.certificate}</td>
|
||||
<td className="py-2.5 px-1 font-mono font-bold text-[10px] text-slate-700 dark:text-slate-300 truncate" title={item.certificate}>{item.certificate}</td>
|
||||
<td className="py-2.5 px-1 text-center truncate">
|
||||
{item.differencePercentage ? (
|
||||
<span className={`text-[9px] font-bold ${diffColor}`}>{item.differencePercentage}</span>
|
||||
@@ -191,9 +191,9 @@ export const ReportDisplay: React.FC<ReportDisplayProps> = ({ report }) => {
|
||||
<tbody className="divide-y divide-slate-50 dark:divide-slate-800/50 font-mono">
|
||||
{compliance.chemical.map((item, i) => (
|
||||
<tr key={i}>
|
||||
<td className="py-2 px-2 font-sans font-semibold text-slate-800 dark:text-slate-200 truncate" title={item.element}>{item.element}</td>
|
||||
<td className="py-2 px-2 font-sans font-medium text-[10px] text-slate-800 dark:text-slate-200 truncate" title={item.element}>{item.element}</td>
|
||||
<td className="py-2 px-1 text-slate-400 dark:text-slate-500 text-[9px] leading-tight truncate" title={item.norm}>{item.norm}</td>
|
||||
<td className="py-2 px-1 font-bold text-slate-700 dark:text-slate-300 truncate" title={item.certificate}>{item.certificate}</td>
|
||||
<td className="py-2 px-1 font-bold text-[10px] text-slate-700 dark:text-slate-300 truncate" title={item.certificate}>{item.certificate}</td>
|
||||
<td className="py-2 px-1 text-right">
|
||||
<span className={`inline-flex items-center text-[9px] font-bold px-1.5 py-0.5 rounded ${statusClass(item.status)}`}>
|
||||
{item.status}
|
||||
|
||||
+54
-44
@@ -39,11 +39,14 @@ const cleanAndParseJson = (jsonString: string): any => {
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(cleanedString);
|
||||
// Se o modelo aninhou os dados dentro de uma chave "report"
|
||||
if (parsed.report && !parsed.identification) {
|
||||
return parsed.report;
|
||||
let finalData = parsed;
|
||||
// Se o modelo aninhou os dados dentro de uma chave "report" ou "reports"
|
||||
if (parsed.reports) {
|
||||
finalData = parsed.reports;
|
||||
} else if (parsed.report && !parsed.identification) {
|
||||
finalData = parsed.report;
|
||||
}
|
||||
return parsed;
|
||||
return Array.isArray(finalData) ? finalData : [finalData];
|
||||
} catch (error) {
|
||||
console.error("Failed to parse cleaned JSON:", jsonString);
|
||||
throw new Error("A resposta da IA não estava no formato JSON esperado.");
|
||||
@@ -143,37 +146,44 @@ const reportSchema = {
|
||||
required: ["confidence", "identification", "compliance", "overPerformance", "equivalents"]
|
||||
};
|
||||
|
||||
const PROMPT_BASE = `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).
|
||||
const arraySchema = {
|
||||
type: Type.ARRAY,
|
||||
items: reportSchema
|
||||
};
|
||||
|
||||
Sua tarefa é analisar o Certificado de Qualidade fornecido e gerar um "Relatório de Análise Técnica" em formato JSON, seguindo rigorosamente o schema fornecido.
|
||||
const PROMPT_BASE = `Você é o "SteelCheck", um especialista sênior em engenharia de materiais, metalurgia e controle de qualidade.
|
||||
|
||||
**Instrução Crítica:** Verifique a orientação do documento. Se estiver rotacionado, ajuste para a posição correta antes de extrair dados.
|
||||
Sua tarefa é analisar o Certificado de Qualidade fornecido e gerar um "Relatório de Análise Técnica" em formato JSON.
|
||||
|
||||
**Instrução Crítica 1 (Orientação):** A imagem PODE estar rotacionada (deitada ou de cabeça para baixo). Compense mentalmente a orientação correta antes de ler as tabelas.
|
||||
**Instrução Crítica 2 (Múltiplos Lotes/Corridas):** Se o certificado contiver MÚLTIPLOS LOTES (Batches) ou MÚLTIPLAS CORRIDAS (Heats) com resultados INDEPENDENTES, gere múltiplos relatórios! O JSON final DEVE SER UM ARRAY contendo 1 objeto para cada lote/corrida independente encontrado.
|
||||
|
||||
Execute:
|
||||
1. **Extração de Dados (OCR):** Extraia todos os dados primários do certificado.
|
||||
2. **Identificação de Normas:** Identifique a(s) norma(s) que o certificado alega atender.
|
||||
3. **Análise de Conformidade:** Compare os resultados com os requisitos mínimos e máximos das normas. Determine status como "OK" ou "FALHA" para cada item e "CONFORME" ou "NÃO CONFORME" geral.
|
||||
4. **Propriedades Mecânicas (mechanical):** Tabela de propriedades extraídas (ex: Yield Strength, Tensile Strength, Alongamento). Para CADA propriedade, calcule o percentual de quanto o material analisado é superior (ex: "+15%") ou inferior (ex: "-5%") em resistência ao material normativo teórico, e preencha no campo "differencePercentage".
|
||||
5. **Análise de Equivalência:** Liste materiais equivalentes de outras normas (EN, DIN, JIS, NBR, etc.).
|
||||
6. **Grau de Confiança:** Forneça um % de confiança baseado na qualidade do documento.
|
||||
1. Extração de Dados: Leia a imagem atentamente, respeitando as colunas.
|
||||
2. Identificação de Normas.
|
||||
3. Análise de Conformidade: Compare resultados x requisitos.
|
||||
4. Propriedades Mecânicas (mechanical): Calcule o "differencePercentage" (+X% ou -X%) em relação ao teórico.
|
||||
5. Análise de Equivalência e Grau de Confiança.
|
||||
|
||||
Retorne APENAS o objeto JSON, sem formatação adicional, seguindo EXATAMENTE esta estrutura de chaves (em inglês):
|
||||
{
|
||||
"confidence": 95,
|
||||
"identification": {
|
||||
"product": "", "standards": "", "manufacturer": "", "certificateNumber": "", "certificateDate": "", "batches": "", "heats": "", "quantity": ""
|
||||
},
|
||||
"compliance": {
|
||||
"status": "CONFORME ou NÃO CONFORME",
|
||||
"mechanical": [ { "property": "", "norm": "", "certificate": "", "status": "OK ou FALHA", "differencePercentage": "+X% ou -X%" } ],
|
||||
"chemical": [ { "element": "", "norm": "", "certificate": "", "status": "OK ou FALHA" } ],
|
||||
"otherTests": [ { "test": "", "norm": "", "certificate": "", "status": "OK ou FALHA" } ]
|
||||
},
|
||||
"overPerformance": [ { "property": "", "value": "" } ],
|
||||
"equivalents": [ { "system": "", "norm": "" } ]
|
||||
}`;
|
||||
Retorne APENAS o JSON, sem markdown. DEVE SER UM ARRAY DE OBJETOS:
|
||||
[
|
||||
{
|
||||
"confidence": 95,
|
||||
"identification": {
|
||||
"product": "", "standards": "", "manufacturer": "", "certificateNumber": "", "certificateDate": "", "batches": "LOTE X", "heats": "CORRIDA Y", "quantity": ""
|
||||
},
|
||||
"compliance": {
|
||||
"status": "CONFORME ou NÃO CONFORME",
|
||||
"mechanical": [ { "property": "", "norm": "", "certificate": "", "status": "OK ou FALHA", "differencePercentage": "+X% ou -X%" } ],
|
||||
"chemical": [ { "element": "", "norm": "", "certificate": "", "status": "OK ou FALHA" } ],
|
||||
"otherTests": [ { "test": "", "norm": "", "certificate": "", "status": "OK ou FALHA" } ]
|
||||
},
|
||||
"overPerformance": [ { "property": "", "value": "" } ],
|
||||
"equivalents": [ { "system": "", "norm": "" } ]
|
||||
}
|
||||
]`;
|
||||
|
||||
export const analyzeWithGemini = async (file: File, apiKey: string, model: string = 'gemini-2.5-flash'): Promise<ReportData> => {
|
||||
export const analyzeWithGemini = async (file: File, apiKey: string, model: string = 'gemini-2.5-flash'): Promise<ReportData[]> => {
|
||||
if (!apiKey) {
|
||||
throw new Error("A chave de API do Gemini não foi fornecida.");
|
||||
}
|
||||
@@ -186,7 +196,7 @@ export const analyzeWithGemini = async (file: File, apiKey: string, model: strin
|
||||
contents: { parts: [imagePart, { text: PROMPT_BASE }] },
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: reportSchema,
|
||||
responseSchema: arraySchema,
|
||||
temperature: 0.1,
|
||||
}
|
||||
});
|
||||
@@ -194,7 +204,7 @@ export const analyzeWithGemini = async (file: File, apiKey: string, model: strin
|
||||
try {
|
||||
const text = response.text;
|
||||
const data = cleanAndParseJson(text);
|
||||
return data as ReportData;
|
||||
return data as ReportData[];
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw e;
|
||||
@@ -203,7 +213,7 @@ export const analyzeWithGemini = async (file: File, apiKey: string, model: strin
|
||||
}
|
||||
};
|
||||
|
||||
export const analyzeWithOpenAI = async (file: File, apiKey: string, model: string = 'gpt-4o'): Promise<ReportData> => {
|
||||
export const analyzeWithOpenAI = async (file: File, apiKey: string, model: string = 'gpt-4o'): Promise<ReportData[]> => {
|
||||
if (!apiKey) {
|
||||
throw new Error("A chave de API da OpenAI não foi fornecida.");
|
||||
}
|
||||
@@ -252,10 +262,10 @@ export const analyzeWithOpenAI = async (file: File, apiKey: string, model: strin
|
||||
throw new Error("Resposta vazia da OpenAI");
|
||||
}
|
||||
|
||||
return cleanAndParseJson(content) as ReportData;
|
||||
return cleanAndParseJson(content) as ReportData[];
|
||||
};
|
||||
|
||||
export const analyzeWithAnthropic = async (file: File, apiKey: string, model: string = 'claude-3-sonnet-20240229'): Promise<ReportData> => {
|
||||
export const analyzeWithAnthropic = async (file: File, apiKey: string, model: string = 'claude-3-sonnet-20240229'): Promise<ReportData[]> => {
|
||||
if (!apiKey) {
|
||||
throw new Error("A chave de API da Anthropic não foi fornecida.");
|
||||
}
|
||||
@@ -311,10 +321,10 @@ export const analyzeWithAnthropic = async (file: File, apiKey: string, model: st
|
||||
throw new Error("Resposta vazia da Anthropic");
|
||||
}
|
||||
|
||||
return cleanAndParseJson(content) as ReportData;
|
||||
return cleanAndParseJson(content) as ReportData[];
|
||||
};
|
||||
|
||||
export const analyzeWithAzure = async (file: File, apiKey: string, endpoint: string, deployment: string): Promise<ReportData> => {
|
||||
export const analyzeWithAzure = async (file: File, apiKey: string, endpoint: string, deployment: string): Promise<ReportData[]> => {
|
||||
if (!apiKey || !endpoint) {
|
||||
throw new Error("A chave de API e endpoint do Azure são necessários.");
|
||||
}
|
||||
@@ -363,10 +373,10 @@ export const analyzeWithAzure = async (file: File, apiKey: string, endpoint: str
|
||||
throw new Error("Resposta vazia do Azure");
|
||||
}
|
||||
|
||||
return cleanAndParseJson(content) as ReportData;
|
||||
return cleanAndParseJson(content) as ReportData[];
|
||||
};
|
||||
|
||||
export const analyzeCertificate = async (options: AnalyzeOptions): Promise<ReportData> => {
|
||||
export const analyzeCertificate = async (options: AnalyzeOptions): Promise<ReportData[]> => {
|
||||
const { provider, apiKey, model, file, endpoint } = options;
|
||||
|
||||
switch (provider) {
|
||||
@@ -389,7 +399,7 @@ export const analyzeCertificate = async (options: AnalyzeOptions): Promise<Repor
|
||||
}
|
||||
};
|
||||
|
||||
export const analyzeWithOllama = async (file: File, endpoint: string, model: string = 'llama3.2-vision'): Promise<ReportData> => {
|
||||
export const analyzeWithOllama = async (file: File, endpoint: string, model: string = 'llama3.2-vision'): Promise<ReportData[]> => {
|
||||
if (!endpoint) {
|
||||
throw new Error("O endpoint do Ollama é necessário. Configure o endereço da sua VPS.");
|
||||
}
|
||||
@@ -456,10 +466,10 @@ export const analyzeWithOllama = async (file: File, endpoint: string, model: str
|
||||
throw new Error("Resposta vazia do Ollama/OpenWebUI");
|
||||
}
|
||||
|
||||
return cleanAndParseJson(content) as ReportData;
|
||||
return cleanAndParseJson(content) as ReportData[];
|
||||
};
|
||||
|
||||
export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: string = 'google/gemini-2.5-flash'): Promise<ReportData> => {
|
||||
export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: string = 'google/gemini-2.5-flash'): Promise<ReportData[]> => {
|
||||
if (!apiKey) {
|
||||
throw new Error("A chave de API do OpenRouter não foi fornecida.");
|
||||
}
|
||||
@@ -508,10 +518,10 @@ export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: s
|
||||
throw new Error("Resposta vazia do OpenRouter");
|
||||
}
|
||||
|
||||
return cleanAndParseJson(content) as ReportData;
|
||||
return cleanAndParseJson(content) as ReportData[];
|
||||
};
|
||||
|
||||
export const analyzeWithMinimax = async (file: File, apiKey: string, model: string = 'minimax-2.7'): Promise<ReportData> => {
|
||||
export const analyzeWithMinimax = async (file: File, apiKey: string, model: string = 'minimax-2.7'): Promise<ReportData[]> => {
|
||||
if (!apiKey) {
|
||||
throw new Error("A chave de API do MiniMax não foi fornecida.");
|
||||
}
|
||||
@@ -558,5 +568,5 @@ export const analyzeWithMinimax = async (file: File, apiKey: string, model: stri
|
||||
throw new Error("Resposta vazia do MiniMax");
|
||||
}
|
||||
|
||||
return cleanAndParseJson(content) as ReportData;
|
||||
return cleanAndParseJson(content) as ReportData[];
|
||||
};
|
||||
+28
-18
@@ -2,37 +2,47 @@ import { jsPDF } from 'jspdf';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
export const exportAsPdf = async (elementId: string, fileName: string, action: 'preview' | 'download'): Promise<void> => {
|
||||
const element = document.getElementById(elementId);
|
||||
const container = document.getElementById(elementId);
|
||||
|
||||
if (!element) {
|
||||
if (!container) {
|
||||
console.error(`Element with id ${elementId} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// We temporarily make the element visible to html2canvas if it's hidden by CSS
|
||||
// by cloning it or just capturing it directly (html2canvas can capture off-screen if it's positioned absolute)
|
||||
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2, // Higher resolution
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
backgroundColor: '#ffffff' // Ensure white background
|
||||
});
|
||||
const pages = container.querySelectorAll('.report-page');
|
||||
if (pages.length === 0) {
|
||||
console.error('No report pages found');
|
||||
return;
|
||||
}
|
||||
|
||||
const imgData = canvas.toDataURL('image/jpeg', 0.95);
|
||||
|
||||
// A4 dimensions in mm: 210 x 297
|
||||
try {
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4',
|
||||
});
|
||||
|
||||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const pageEl = pages[i] as HTMLElement;
|
||||
|
||||
const canvas = await html2canvas(pageEl, {
|
||||
scale: 2, // Higher resolution
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
backgroundColor: '#ffffff' // Ensure white background
|
||||
});
|
||||
|
||||
pdf.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight);
|
||||
const imgData = canvas.toDataURL('image/jpeg', 0.95);
|
||||
|
||||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
|
||||
|
||||
if (i > 0) {
|
||||
pdf.addPage();
|
||||
}
|
||||
|
||||
pdf.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight);
|
||||
}
|
||||
|
||||
if (action === 'download') {
|
||||
pdf.save(`${fileName}.pdf`);
|
||||
|
||||
Reference in New Issue
Block a user