Deploy Inicial do SteelCheck com Docker Build Automatizado

This commit is contained in:
2026-06-23 00:22:53 +00:00
parent 8fc62aeb0f
commit e82656e5c2
5 changed files with 111 additions and 87 deletions
+54 -44
View File
@@ -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
View File
@@ -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`);