Atualização automática - 2026-06-24 10:23:23
This commit is contained in:
+72
-68
@@ -1,6 +1,7 @@
|
|||||||
import { GoogleGenAI, Type } from "@google/genai";
|
import { GoogleGenAI, Type } from "@google/genai";
|
||||||
import type { AIProvider } from '../types/providers';
|
import type { AIProvider } from '../types/providers';
|
||||||
import type { ReportData } from '../types';
|
import type { ReportData, Standard } from '../types';
|
||||||
|
import { validateCertificateAgainstStandard } from './validationEngine';
|
||||||
|
|
||||||
interface AnalyzeOptions {
|
interface AnalyzeOptions {
|
||||||
provider: AIProvider;
|
provider: AIProvider;
|
||||||
@@ -73,7 +74,7 @@ const reportSchema = {
|
|||||||
compliance: {
|
compliance: {
|
||||||
type: Type.OBJECT,
|
type: Type.OBJECT,
|
||||||
properties: {
|
properties: {
|
||||||
status: { type: Type.STRING, description: "'CONFORME' ou 'NÃO CONFORME'" },
|
status: { type: Type.STRING, description: "'CONFORME' ou 'NÃO CONFORME' (deixe vazio se não souber)" },
|
||||||
nonComplianceReason: { type: Type.STRING },
|
nonComplianceReason: { type: Type.STRING },
|
||||||
mechanical: {
|
mechanical: {
|
||||||
type: Type.ARRAY,
|
type: Type.ARRAY,
|
||||||
@@ -81,12 +82,9 @@ const reportSchema = {
|
|||||||
type: Type.OBJECT,
|
type: Type.OBJECT,
|
||||||
properties: {
|
properties: {
|
||||||
property: { type: Type.STRING },
|
property: { type: Type.STRING },
|
||||||
norm: { type: Type.STRING },
|
certificate: { type: Type.STRING }
|
||||||
certificate: { type: Type.STRING },
|
|
||||||
status: { type: Type.STRING, description: "'OK' ou 'REPROVADO'" },
|
|
||||||
differencePercentage: { type: Type.STRING }
|
|
||||||
},
|
},
|
||||||
required: ["property", "norm", "certificate", "status", "differencePercentage"]
|
required: ["property", "certificate"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
chemical: {
|
chemical: {
|
||||||
@@ -95,11 +93,9 @@ const reportSchema = {
|
|||||||
type: Type.OBJECT,
|
type: Type.OBJECT,
|
||||||
properties: {
|
properties: {
|
||||||
element: { type: Type.STRING },
|
element: { type: Type.STRING },
|
||||||
norm: { type: Type.STRING },
|
certificate: { type: Type.STRING }
|
||||||
certificate: { type: Type.STRING },
|
|
||||||
status: { type: Type.STRING, description: "'OK' ou 'REPROVADO'" }
|
|
||||||
},
|
},
|
||||||
required: ["element", "norm", "certificate", "status"]
|
required: ["element", "certificate"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
otherTests: {
|
otherTests: {
|
||||||
@@ -108,14 +104,13 @@ const reportSchema = {
|
|||||||
type: Type.OBJECT,
|
type: Type.OBJECT,
|
||||||
properties: {
|
properties: {
|
||||||
test: { type: Type.STRING },
|
test: { type: Type.STRING },
|
||||||
result: { type: Type.STRING },
|
result: { type: Type.STRING }
|
||||||
status: { type: Type.STRING }
|
|
||||||
},
|
},
|
||||||
required: ["test", "result", "status"]
|
required: ["test", "result"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ["status", "nonComplianceReason", "mechanical", "chemical", "otherTests"]
|
required: ["mechanical", "chemical", "otherTests"]
|
||||||
},
|
},
|
||||||
overPerformance: {
|
overPerformance: {
|
||||||
type: Type.ARRAY,
|
type: Type.ARRAY,
|
||||||
@@ -150,39 +145,20 @@ const arraySchema = {
|
|||||||
items: reportSchema
|
items: reportSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildAnalysisPrompt = (standardsContext?: any[]) => {
|
export const buildAnalysisPrompt = () => {
|
||||||
let prompt = `Você é o "SteelCheck", um especialista sênior em engenharia de materiais, metalurgia, calibração e controle de qualidade industrial.
|
let prompt = `Você é o "SteelCheck", um especialista sênior em extração de dados de Certificados de Qualidade.
|
||||||
|
|
||||||
Sua tarefa principal é transcrever com precisão matemática e zero alucinação os dados de um Certificado de Qualidade (Mill Test Report) e gerar um "Relatório de Análise Técnica" em formato JSON.
|
Sua tarefa principal é transcrever com precisão matemática e zero alucinação os dados de um Certificado de Qualidade (Mill Test Report) e gerar um JSON estruturado.
|
||||||
|
|
||||||
### DIRETRIZES RÍGIDAS DE TRANSCRIÇÃO (OCR DE ALTA PRECISÃO):
|
### DIRETRIZES RÍGIDAS DE TRANSCRIÇÃO (OCR DE ALTA PRECISÃO):
|
||||||
1. **Zero Alucinação de Normas:** No campo "standards", extraia APENAS e EXATAMENTE o que estiver escrito na imagem.
|
1. **Zero Alucinação de Normas:** No campo "standards", extraia APENAS e EXATAMENTE o que estiver escrito na imagem.
|
||||||
2. **Extração de Identificação:** Transcreva números de certificado, lotes, corridas e quantidades exatamente como aparecem.
|
2. **Extração de Identificação:** Transcreva o produto (incluindo dimensões/bitola, ex: "CHAPA 50MM" ou "PERFIL W360x44.6"), números de certificado, lotes, corridas e quantidades exatamente como aparecem.
|
||||||
3. **Mapeamento Preciso de Colunas:** Mapeie composição química e propriedades mecânicas com precisão absoluta.
|
3. **Mapeamento Preciso de Colunas:** Mapeie composição química e propriedades mecânicas com precisão absoluta para a chave "certificate". Não preencha "norm" nem "status".
|
||||||
4. **Múltiplas Linhas:** Se houver múltiplas linhas de teste, gere um objeto para cada uma.
|
4. **Múltiplas Linhas:** Se houver múltiplas linhas de teste, gere um objeto para cada uma.
|
||||||
5. **Orientação:** Rotacione mentalmente a imagem para ler corretamente.
|
5. **Orientação:** Rotacione mentalmente a imagem para ler corretamente.
|
||||||
|
|
||||||
### ANÁLISE DE CONFORMIDADE:
|
- No campo "analysisSource" do JSON, preencha EXATAMENTE com "Rede Neural".
|
||||||
- Compare os valores reais extraídos com os requisitos mínimos/máximos da norma técnica declarada no certificado.
|
|
||||||
- FORMATO DO CAMPO "NORM": Sempre que a norma (ou o Banco Local) estabelecer uma FAIXA (um mínimo e um máximo) para um elemento, você DEVE transcrever AMBOS os limites no campo "norm" do JSON (Exemplo: "Min: 0.005% - Max: 0.05%"). Nunca oculte o limite máximo ou mínimo se ambos existirem.
|
|
||||||
- LAUDOS COM DUPLA ESPECIFICAÇÃO (MÚLTIPLAS NORMAS): Se o certificado citar mais de uma norma (ex: "ASTM A572 GR50 / NBR 7007 AR350"), você DEVE adotar sempre o critério MAIS RÍGIDO (o mais conservador e restritivo) entre as normas citadas para garantir que o material atenda a ambas. Exemplo: se a ASTM não possui limite para Carbono Equivalente (CEQ), mas a NBR exige Máx: 0.47%, você DEVE usar Máx: 0.47% como parâmetro de reprovação.
|
|
||||||
- REGRA CRÍTICA DE STATUS GERAL: Se QUALQUER item (mecânico, químico ou outro) for avaliado como "REPROVADO", o status geral do laudo ("compliance.status") DEVE ser OBRIGATORIAMENTE "NÃO CONFORME". Só atribua "CONFORME" se ABSOLUTAMENTE TODOS os itens individuais forem "OK".`;
|
|
||||||
|
|
||||||
if (standardsContext && standardsContext.length > 0) {
|
|
||||||
prompt += `\n\n=== BANCO DE NORMAS LOCAL (USO OBRIGATÓRIO) ===\n`;
|
|
||||||
prompt += `Você DEVE utilizar as regras rígidas do seguinte banco de dados local para confrontar e validar os resultados do certificado. SE a norma citada no laudo estiver presente na lista abaixo, USE ESTRITAMENTE os valores desta lista para verificar o "status" (CONFORME / REPROVADO):\n\n`;
|
|
||||||
prompt += JSON.stringify(standardsContext, null, 2);
|
|
||||||
prompt += `\n\n- No campo "analysisSource" do JSON, preencha EXATAMENTE com "Banco Local" se você usou as regras deste banco.`;
|
|
||||||
prompt += `\n=================================================\n`;
|
|
||||||
} else {
|
|
||||||
prompt += `\n- Utilize o seu conhecimento paramétrico interno de engenharia de materiais para aplicar os limites corretos da norma encontrada no laudo.\n`;
|
|
||||||
prompt += `- No campo "analysisSource" do JSON, preencha EXATAMENTE com "Rede Neural".\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt += `
|
|
||||||
- **Fórmula de Variação (differencePercentage):** Calcule a variação percentual do valor do certificado em relação ao mínimo exigido pela norma.
|
|
||||||
USE ESTRITAMENTE A FÓRMULA: ((Valor_Certificado - Valor_Norma) / Valor_Norma) * 100.
|
|
||||||
Exemplo: Se a Norma exige Mínimo 345, e o Certificado acusa 415, o resultado é ((415-345)/345)*100 = +20.3%.
|
|
||||||
|
|
||||||
Retorne APENAS o JSON no formato do array especificado, sem explicações ou markdown.
|
Retorne APENAS o JSON no formato do array especificado, sem explicações ou markdown.
|
||||||
|
|
||||||
@@ -202,13 +178,13 @@ FORMATO DO RETORNO:
|
|||||||
"quantity": "8,560 T"
|
"quantity": "8,560 T"
|
||||||
},
|
},
|
||||||
"compliance": {
|
"compliance": {
|
||||||
"status": "CONFORME",
|
"status": "",
|
||||||
"nonComplianceReason": "",
|
"nonComplianceReason": "",
|
||||||
"mechanical": [
|
"mechanical": [
|
||||||
{ "property": "Yield Strength (Limite de Escoamento)", "norm": "Min: 345 MPa", "certificate": "415 MPa", "status": "OK", "differencePercentage": "+20.3%" }
|
{ "property": "Yield Strength (Limite de Escoamento)", "certificate": "415 MPa" }
|
||||||
],
|
],
|
||||||
"chemical": [
|
"chemical": [
|
||||||
{ "element": "C (Carbon)", "norm": "Max: 0.23%", "certificate": "0.22%", "status": "OK" }
|
{ "element": "C (Carbon)", "certificate": "0.22%" }
|
||||||
],
|
],
|
||||||
"otherTests": []
|
"otherTests": []
|
||||||
},
|
},
|
||||||
@@ -227,7 +203,7 @@ export const analyzeWithGemini = async (file: File, apiKey: string, model: strin
|
|||||||
|
|
||||||
const ai = new GoogleGenAI({ apiKey: apiKey });
|
const ai = new GoogleGenAI({ apiKey: apiKey });
|
||||||
const imagePart = await fileToGenerativePart(file);
|
const imagePart = await fileToGenerativePart(file);
|
||||||
const prompt = buildAnalysisPrompt(standardsContext);
|
const prompt = buildAnalysisPrompt();
|
||||||
|
|
||||||
const response = await ai.models.generateContent({
|
const response = await ai.models.generateContent({
|
||||||
model,
|
model,
|
||||||
@@ -241,8 +217,14 @@ export const analyzeWithGemini = async (file: File, apiKey: string, model: strin
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const text = response.text;
|
const text = response.text;
|
||||||
const data = cleanAndParseJson(text);
|
const data = cleanAndParseJson(text) as ReportData[];
|
||||||
return data as ReportData[];
|
|
||||||
|
// Apply programmatic validation
|
||||||
|
if (standardsContext && standardsContext.length > 0) {
|
||||||
|
// Assume the first standard matched is the main one for now
|
||||||
|
return data.map(report => validateCertificateAgainstStandard(report, standardsContext[0] as Standard));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -262,7 +244,7 @@ export const analyzeWithOpenAI = async (file: File, apiKey: string, model: strin
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const prompt = buildAnalysisPrompt(standardsContext);
|
const prompt = buildAnalysisPrompt();
|
||||||
|
|
||||||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -302,7 +284,11 @@ export const analyzeWithOpenAI = async (file: File, apiKey: string, model: strin
|
|||||||
throw new Error("Resposta vazia da OpenAI");
|
throw new Error("Resposta vazia da OpenAI");
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanAndParseJson(content) as ReportData[];
|
const dataJson = cleanAndParseJson(content) as ReportData[];
|
||||||
|
if (standardsContext && standardsContext.length > 0) {
|
||||||
|
return dataJson.map(report => validateCertificateAgainstStandard(report, standardsContext[0] as Standard));
|
||||||
|
}
|
||||||
|
return dataJson;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const analyzeCertificate = async (options: AnalyzeOptions): Promise<ReportData[]> => {
|
export const analyzeCertificate = async (options: AnalyzeOptions): Promise<ReportData[]> => {
|
||||||
@@ -334,7 +320,7 @@ export const analyzeWithOllama = async (file: File, endpoint: string, model: str
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const prompt = buildAnalysisPrompt(standardsContext);
|
const prompt = buildAnalysisPrompt();
|
||||||
|
|
||||||
let url = `${endpoint}/api/chat`;
|
let url = `${endpoint}/api/chat`;
|
||||||
|
|
||||||
@@ -392,7 +378,11 @@ export const analyzeWithOllama = async (file: File, endpoint: string, model: str
|
|||||||
throw new Error("Resposta vazia do Ollama/OpenWebUI");
|
throw new Error("Resposta vazia do Ollama/OpenWebUI");
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanAndParseJson(content) as ReportData[];
|
const dataJson = cleanAndParseJson(content) as ReportData[];
|
||||||
|
if (standardsContext && standardsContext.length > 0) {
|
||||||
|
return dataJson.map(report => validateCertificateAgainstStandard(report, standardsContext[0] as Standard));
|
||||||
|
}
|
||||||
|
return dataJson;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: string = 'openai/gpt-4o', standardsContext?: any[]): Promise<ReportData[]> => {
|
export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: string = 'openai/gpt-4o', standardsContext?: any[]): Promise<ReportData[]> => {
|
||||||
@@ -406,7 +396,7 @@ export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: s
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const prompt = buildAnalysisPrompt(standardsContext);
|
const prompt = buildAnalysisPrompt();
|
||||||
|
|
||||||
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -452,7 +442,11 @@ export const analyzeWithOpenRouter = async (file: File, apiKey: string, model: s
|
|||||||
throw new Error("Resposta vazia do OpenRouter");
|
throw new Error("Resposta vazia do OpenRouter");
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanAndParseJson(content) as ReportData[];
|
const dataJson = cleanAndParseJson(content) as ReportData[];
|
||||||
|
if (standardsContext && standardsContext.length > 0) {
|
||||||
|
return dataJson.map(report => validateCertificateAgainstStandard(report, standardsContext[0] as Standard));
|
||||||
|
}
|
||||||
|
return dataJson;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildStandardWithAI = async (
|
export const buildStandardWithAI = async (
|
||||||
@@ -463,32 +457,42 @@ export const buildStandardWithAI = async (
|
|||||||
const prompt = `Você é um engenheiro de materiais sênior. O usuário solicitou a criação de uma tabela de limites para a norma técnica: "${standardName}".
|
const prompt = `Você é um engenheiro de materiais sênior. O usuário solicitou a criação de uma tabela de limites para a norma técnica: "${standardName}".
|
||||||
A categoria deste material é descrita como: "${categoryDescription}".
|
A categoria deste material é descrita como: "${categoryDescription}".
|
||||||
|
|
||||||
Sua tarefa é acessar seu conhecimento sobre esta norma e gerar os limites estritos (mínimos e máximos) exigidos.
|
Sua tarefa é acessar seu conhecimento sobre esta norma e gerar os limites estritos, incluindo as variações por faixa de espessura (bitola), grupos de perfil e tolerâncias de análise de produto.
|
||||||
|
|
||||||
RESPONDA APENAS UM JSON VÁLIDO no seguinte formato EXATO:
|
RESPONDA APENAS UM JSON VÁLIDO no seguinte formato EXATO:
|
||||||
{
|
{
|
||||||
"codigo": "Nome oficial da norma (ex: ASTM A36/A36M-19)",
|
"id_norma": "id_unico_da_norma_sem_espacos",
|
||||||
"produto_especifico": "Descreva o tipo de produto que essa norma cobre",
|
"codigo": "Nome oficial da norma (ex: ASTM A572/A572M)",
|
||||||
|
"grau": "Grau se aplicável (ex: 50)",
|
||||||
|
"produto_especifico": "Descreva o tipo de produto",
|
||||||
|
"limites_quimicos_corrida": {
|
||||||
|
"produtos_gerais": [
|
||||||
|
{
|
||||||
|
"espessura_max_mm": valor_numerico_ou_null,
|
||||||
|
"carbono_max": valor_numerico,
|
||||||
|
"manganes_max": valor_numerico
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tolerancias_analise_produto_astma6": {
|
||||||
|
"carbono": {
|
||||||
|
"tabela": [
|
||||||
|
{ "limite_corrida_ate": 0.25, "tolerancia_mais": 0.03 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"parametros_controle": {
|
"parametros_controle": {
|
||||||
"mecanicos": {
|
"mecanicos": {
|
||||||
"nome_do_parametro_1": valor_numerico,
|
"limite_escoamento_min": valor_numerico
|
||||||
"nome_do_parametro_2": valor_numerico
|
|
||||||
},
|
|
||||||
"quimicos": {
|
|
||||||
"nome_do_parametro": valor_numerico
|
|
||||||
},
|
|
||||||
"fisicos": {
|
|
||||||
"nome_do_parametro": valor_numerico
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Regras Cruciais:
|
Regras Cruciais:
|
||||||
1. USE APENAS NÚMEROS (floats/ints) nos valores. NUNCA use strings como "0.25" ou "250". Use 0.25 e 250.
|
1. USE APENAS NÚMEROS (floats/ints) nos valores. NUNCA use strings como "0.25".
|
||||||
2. Se a norma não especifica um limite máximo ou mínimo para um elemento, coloque null (sem aspas).
|
2. Estruture as matrizes de limite químico considerando as faixas de espessura de acordo com a norma.
|
||||||
3. O nome da chave dentro de mecânicos/químicos/físicos deve ser em minúsculo, sem acentos, com underscores, e DEVE conter a palavra "_min_" ou "_max_" (ex: "limite_escoamento_min_mpa", "carbono_max_percentual"). Se a norma define uma FAIXA (um mínimo e um máximo) para o mesmo elemento, você DEVE criar DUAS chaves separadas (ex: "columbio_min_percentual" e "columbio_max_percentual"). NUNCA combine min e max na mesma chave.
|
3. Inclua a chave tolerancias_analise_produto_astma6 se aplicável.
|
||||||
4. Crie APENAS os grupos (mecanicos, quimicos, fisicos) que fazem sentido para a norma solicitada.
|
4. DEVOLVA APENAS O JSON, sem nenhuma formatação ou texto explicativo ao redor.`;
|
||||||
5. DEVOLVA APENAS O JSON, sem nenhuma formatação ou texto explicativo ao redor.`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let resultJson = '';
|
let resultJson = '';
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import { ReportData, Standard, ComplianceItem } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a numeric value from a string like "0.22%", "415 MPa", etc.
|
||||||
|
*/
|
||||||
|
function extractNumber(val: string): number | null {
|
||||||
|
if (!val) return null;
|
||||||
|
const match = val.match(/-?\d+(\.\d+)?/);
|
||||||
|
return match ? parseFloat(match[0]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the product string to try and find a thickness in mm.
|
||||||
|
* E.g., "CHAPA 50MM", "PLATE 25.4 MM"
|
||||||
|
*/
|
||||||
|
function parseThickness(productDesc: string): number {
|
||||||
|
if (!productDesc) return 0;
|
||||||
|
const match = productDesc.match(/(\d+(\.\d+)?)\s*(mm)/i);
|
||||||
|
if (match) return parseFloat(match[1]);
|
||||||
|
return 0; // Default or unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeElementName(element: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
'c': 'carbono',
|
||||||
|
'carbon': 'carbono',
|
||||||
|
'carbono': 'carbono',
|
||||||
|
'mn': 'manganes',
|
||||||
|
'manganese': 'manganes',
|
||||||
|
'manganes': 'manganes',
|
||||||
|
'si': 'silicio',
|
||||||
|
'silicon': 'silicio',
|
||||||
|
'silicio': 'silicio',
|
||||||
|
'p': 'fosforo',
|
||||||
|
'phosphorus': 'fosforo',
|
||||||
|
'fosforo': 'fosforo',
|
||||||
|
's': 'enxofre',
|
||||||
|
'sulfur': 'enxofre',
|
||||||
|
'enxofre': 'enxofre',
|
||||||
|
'cu': 'cobre',
|
||||||
|
'copper': 'cobre',
|
||||||
|
'cobre': 'cobre',
|
||||||
|
'ni': 'niquel',
|
||||||
|
'nickel': 'niquel',
|
||||||
|
'niquel': 'niquel',
|
||||||
|
'cr': 'cromo',
|
||||||
|
'chromium': 'cromo',
|
||||||
|
'cromo': 'cromo',
|
||||||
|
'mo': 'molibdenio',
|
||||||
|
'molybdenum': 'molibdenio',
|
||||||
|
'molibdenio': 'molibdenio',
|
||||||
|
'v': 'vanadio',
|
||||||
|
'vanadium': 'vanadio',
|
||||||
|
'vanadio': 'vanadio',
|
||||||
|
'nb': 'niobio',
|
||||||
|
'columbium': 'niobio',
|
||||||
|
'niobium': 'niobio',
|
||||||
|
'niobio': 'niobio',
|
||||||
|
'ti': 'titanio',
|
||||||
|
'titanium': 'titanio',
|
||||||
|
'titanio': 'titanio',
|
||||||
|
'al': 'aluminio',
|
||||||
|
'aluminum': 'aluminio',
|
||||||
|
'aluminio': 'aluminio'
|
||||||
|
};
|
||||||
|
|
||||||
|
const lower = element.toLowerCase().trim();
|
||||||
|
// Remove content in parentheses, e.g. "C (Carbon)" -> "c"
|
||||||
|
const clean = lower.replace(/\(.*?\)/g, '').trim();
|
||||||
|
return map[clean] || clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTolerance(elementName: string, productVal: number, thickness: number, norm: Standard): number {
|
||||||
|
if (!norm.tolerancias_analise_produto_astma6) return 0;
|
||||||
|
|
||||||
|
const tolRules = norm.tolerancias_analise_produto_astma6[elementName];
|
||||||
|
if (!tolRules) return 0;
|
||||||
|
|
||||||
|
if (tolRules.tabela && tolRules.tabela.length > 0) {
|
||||||
|
for (const faixa of tolRules.tabela) {
|
||||||
|
if (faixa.espessura_ate_mm && thickness <= faixa.espessura_ate_mm) {
|
||||||
|
return faixa.tolerancia_mais || 0;
|
||||||
|
}
|
||||||
|
if (faixa.espessura_maior_que_mm && thickness > faixa.espessura_maior_que_mm) {
|
||||||
|
return faixa.tolerancia_mais || 0;
|
||||||
|
}
|
||||||
|
if (faixa.limite_corrida_ate && productVal <= faixa.limite_corrida_ate) {
|
||||||
|
return faixa.tolerancia_mais || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tolRules.tolerancia_mais || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateCertificateAgainstStandard(extractedReport: ReportData, standard: Standard): ReportData {
|
||||||
|
const report = JSON.parse(JSON.stringify(extractedReport)) as ReportData;
|
||||||
|
const thickness = parseThickness(report.identification.product);
|
||||||
|
|
||||||
|
let allOk = true;
|
||||||
|
|
||||||
|
// Encontrar limites baseados no produto (simplificado: pegamos o primeiro grupo que atende a espessura, ou o flat limits)
|
||||||
|
let activeLimits: Record<string, number | null> = {};
|
||||||
|
|
||||||
|
if (standard.limites_quimicos_corrida) {
|
||||||
|
const grupos = Object.values(standard.limites_quimicos_corrida)[0] || [];
|
||||||
|
for (const limite of grupos) {
|
||||||
|
if (!limite.espessura_max_mm || thickness <= limite.espessura_max_mm) {
|
||||||
|
// Encontrou a faixa certa
|
||||||
|
for (const [key, val] of Object.entries(limite)) {
|
||||||
|
if (typeof val === 'number') {
|
||||||
|
activeLimits[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break; // Pega o primeiro correspondente
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (standard.parametros_controle?.quimicos) {
|
||||||
|
activeLimits = { ...standard.parametros_controle.quimicos };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Chemical
|
||||||
|
if (report.compliance.chemical) {
|
||||||
|
for (const item of report.compliance.chemical) {
|
||||||
|
if (!item.element) continue;
|
||||||
|
|
||||||
|
const normElement = normalizeElementName(item.element);
|
||||||
|
const val = extractNumber(item.certificate);
|
||||||
|
|
||||||
|
if (val !== null) {
|
||||||
|
// Look for max limit
|
||||||
|
const maxLimit = activeLimits[`${normElement}_max`] || activeLimits[normElement];
|
||||||
|
const minLimit = activeLimits[`${normElement}_min`];
|
||||||
|
|
||||||
|
let status: 'OK' | 'FALHA' = 'OK';
|
||||||
|
let normStr = [];
|
||||||
|
|
||||||
|
// Product tolerance
|
||||||
|
const tolerance = getTolerance(normElement, maxLimit as number || 0, thickness, standard);
|
||||||
|
|
||||||
|
if (minLimit !== undefined && minLimit !== null) {
|
||||||
|
normStr.push(`Min: ${minLimit}`);
|
||||||
|
if (val < (minLimit - tolerance)) status = 'FALHA';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxLimit !== undefined && maxLimit !== null) {
|
||||||
|
normStr.push(`Max: ${maxLimit}`);
|
||||||
|
if (val > (maxLimit + tolerance)) status = 'FALHA';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normStr.length > 0) {
|
||||||
|
item.norm = normStr.join(' - ');
|
||||||
|
} else {
|
||||||
|
item.norm = 'Não especificado';
|
||||||
|
}
|
||||||
|
|
||||||
|
item.status = status;
|
||||||
|
if (status === 'FALHA') allOk = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Mechanical
|
||||||
|
const mechLimits = standard.parametros_controle?.mecanicos || {};
|
||||||
|
if (report.compliance.mechanical) {
|
||||||
|
for (const item of report.compliance.mechanical) {
|
||||||
|
if (!item.property) continue;
|
||||||
|
|
||||||
|
const val = extractNumber(item.certificate);
|
||||||
|
if (val !== null) {
|
||||||
|
const propLower = item.property.toLowerCase();
|
||||||
|
let maxLimit: number | null = null;
|
||||||
|
let minLimit: number | null = null;
|
||||||
|
|
||||||
|
if (propLower.includes('yield') || propLower.includes('escoamento')) {
|
||||||
|
minLimit = mechLimits['limite_escoamento_min'] || mechLimits['yield_strength_min'] || null;
|
||||||
|
maxLimit = mechLimits['limite_escoamento_max'] || mechLimits['yield_strength_max'] || null;
|
||||||
|
} else if (propLower.includes('tensile') || propLower.includes('resistencia')) {
|
||||||
|
minLimit = mechLimits['limite_resistencia_min'] || mechLimits['tensile_strength_min'] || null;
|
||||||
|
maxLimit = mechLimits['limite_resistencia_max'] || mechLimits['tensile_strength_max'] || null;
|
||||||
|
} else if (propLower.includes('elongation') || propLower.includes('alongamento')) {
|
||||||
|
minLimit = mechLimits['alongamento_min'] || mechLimits['elongation_min'] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status: 'OK' | 'FALHA' = 'OK';
|
||||||
|
let normStr = [];
|
||||||
|
|
||||||
|
if (minLimit !== null) {
|
||||||
|
normStr.push(`Min: ${minLimit}`);
|
||||||
|
if (val < minLimit) status = 'FALHA';
|
||||||
|
item.differencePercentage = `${val >= minLimit ? '+' : ''}${(((val - minLimit) / minLimit) * 100).toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxLimit !== null) {
|
||||||
|
normStr.push(`Max: ${maxLimit}`);
|
||||||
|
if (val > maxLimit) status = 'FALHA';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normStr.length > 0) {
|
||||||
|
item.norm = normStr.join(' - ');
|
||||||
|
} else {
|
||||||
|
item.norm = 'Não especificado';
|
||||||
|
}
|
||||||
|
|
||||||
|
item.status = status;
|
||||||
|
if (status === 'FALHA') allOk = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report.compliance.status = allOk ? 'CONFORME' : 'NÃO CONFORME';
|
||||||
|
if (!allOk) {
|
||||||
|
report.compliance.nonComplianceReason = 'Valores fora dos limites da norma técnica considerando bitola e tolerâncias de produto.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
@@ -46,3 +46,46 @@ export interface ReportData {
|
|||||||
overPerformance: OverPerformanceItem[];
|
overPerformance: OverPerformanceItem[];
|
||||||
equivalents: EquivalentItem[];
|
equivalents: EquivalentItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NEW STANDARD DEFINITIONS ---
|
||||||
|
export interface ToleranciaTabela {
|
||||||
|
limite_corrida_ate?: number;
|
||||||
|
espessura_ate_mm?: number;
|
||||||
|
espessura_maior_que_mm?: number;
|
||||||
|
tolerancia_mais?: number;
|
||||||
|
tolerancia_menos?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegraTolerancia {
|
||||||
|
regra?: string;
|
||||||
|
tabela?: ToleranciaTabela[];
|
||||||
|
tolerancia_mais?: number;
|
||||||
|
tolerancia_menos?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToleranciasAnaliseProduto {
|
||||||
|
[elemento: string]: RegraTolerancia;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LimitesQuimicos {
|
||||||
|
espessura_max_mm?: number;
|
||||||
|
grupos_astm_a6?: number[];
|
||||||
|
nota_exemplo?: string;
|
||||||
|
[elemento: string]: number | string | number[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Standard {
|
||||||
|
id_norma: string;
|
||||||
|
codigo: string;
|
||||||
|
grau?: string;
|
||||||
|
produto_especifico?: string;
|
||||||
|
limites_quimicos_corrida?: {
|
||||||
|
[grupo_produto: string]: LimitesQuimicos[];
|
||||||
|
};
|
||||||
|
tolerancias_analise_produto_astma6?: ToleranciasAnaliseProduto;
|
||||||
|
parametros_controle?: {
|
||||||
|
mecanicos?: Record<string, number | null>;
|
||||||
|
quimicos?: Record<string, number | null>;
|
||||||
|
fisicos?: Record<string, number | null>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user