From e82656e5c2c25d6897604cc1eae58d3340cbe1b4 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Tue, 23 Jun 2026 00:22:53 +0000 Subject: [PATCH] Deploy Inicial do SteelCheck com Docker Build Automatizado --- App.tsx | 26 +++++---- components/PrintableReport.tsx | 20 +++---- components/ReportDisplay.tsx | 8 +-- services/aiService.ts | 98 +++++++++++++++++++--------------- services/pdfService.ts | 46 +++++++++------- 5 files changed, 111 insertions(+), 87 deletions(-) diff --git a/App.tsx b/App.tsx index a1ef771..6d81f59 100644 --- a/App.tsx +++ b/App.tsx @@ -18,7 +18,7 @@ const App: React.FC = () => { const [provider, setProvider] = useState('gemini'); const [model, setModel] = useState('gemini-2.5-flash'); const [hasKey, setHasKey] = useState(false); - const [reportData, setReportData] = useState(null); + const [reportsData, setReportsData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(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 = () => {
{!hasKey ? ( - ) : !reportData ? ( + ) : !reportsData ? (
@@ -191,14 +191,18 @@ const App: React.FC = () => { Baixar Relatório
- +
+ {reportsData.map((report, idx) => ( + + ))} +
)}
- {reportData && ( + {reportsData && ( )} diff --git a/components/PrintableReport.tsx b/components/PrintableReport.tsx index 699ea11..41888f9 100644 --- a/components/PrintableReport.tsx +++ b/components/PrintableReport.tsx @@ -3,22 +3,22 @@ import type { ReportData } from '../types'; import { ReportDisplay } from './ReportDisplay'; interface PrintableReportProps { - report: ReportData; + reports: ReportData[]; } -export const PrintableReport: React.FC = ({ 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 = ({ reports }) => { return (
- + {reports.map((report, idx) => ( +
+ +
+ ))}
); }; \ No newline at end of file diff --git a/components/ReportDisplay.tsx b/components/ReportDisplay.tsx index 7f836e5..8a9ba9a 100644 --- a/components/ReportDisplay.tsx +++ b/components/ReportDisplay.tsx @@ -141,9 +141,9 @@ export const ReportDisplay: React.FC = ({ report }) => { return ( - {item.property} + {item.property} - {item.certificate} + {item.certificate} {item.differencePercentage ? ( {item.differencePercentage} @@ -191,9 +191,9 @@ export const ReportDisplay: React.FC = ({ report }) => { {compliance.chemical.map((item, i) => ( - {item.element} + {item.element} {item.norm} - {item.certificate} + {item.certificate} {item.status} diff --git a/services/aiService.ts b/services/aiService.ts index 7109439..8f3c49c 100644 --- a/services/aiService.ts +++ b/services/aiService.ts @@ -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 => { +export const analyzeWithGemini = async (file: File, apiKey: string, model: string = 'gemini-2.5-flash'): Promise => { 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 => { +export const analyzeWithOpenAI = async (file: File, apiKey: string, model: string = 'gpt-4o'): Promise => { 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 => { +export const analyzeWithAnthropic = async (file: File, apiKey: string, model: string = 'claude-3-sonnet-20240229'): Promise => { 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 => { +export const analyzeWithAzure = async (file: File, apiKey: string, endpoint: string, deployment: string): Promise => { 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 => { +export const analyzeCertificate = async (options: AnalyzeOptions): Promise => { const { provider, apiKey, model, file, endpoint } = options; switch (provider) { @@ -389,7 +399,7 @@ export const analyzeCertificate = async (options: AnalyzeOptions): Promise => { +export const analyzeWithOllama = async (file: File, endpoint: string, model: string = 'llama3.2-vision'): Promise => { 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 => { +export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: string = 'google/gemini-2.5-flash'): Promise => { 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 => { +export const analyzeWithMinimax = async (file: File, apiKey: string, model: string = 'minimax-2.7'): Promise => { 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[]; }; \ No newline at end of file diff --git a/services/pdfService.ts b/services/pdfService.ts index 951e5ab..377a510 100644 --- a/services/pdfService.ts +++ b/services/pdfService.ts @@ -2,37 +2,47 @@ import { jsPDF } from 'jspdf'; import html2canvas from 'html2canvas'; export const exportAsPdf = async (elementId: string, fileName: string, action: 'preview' | 'download'): Promise => { - 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`);