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 [provider, setProvider] = useState<AIProvider>('gemini');
|
||||||
const [model, setModel] = useState<string>('gemini-2.5-flash');
|
const [model, setModel] = useState<string>('gemini-2.5-flash');
|
||||||
const [hasKey, setHasKey] = useState<boolean>(false);
|
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 [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ const App: React.FC = () => {
|
|||||||
const handleFileChange = useCallback((selectedFile: File | null) => {
|
const handleFileChange = useCallback((selectedFile: File | null) => {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
if (!selectedFile) {
|
if (!selectedFile) {
|
||||||
setReportData(null);
|
setReportsData(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -79,7 +79,7 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setReportData(null);
|
setReportsData(null);
|
||||||
try {
|
try {
|
||||||
const data = await analyzeCertificate({
|
const data = await analyzeCertificate({
|
||||||
provider,
|
provider,
|
||||||
@@ -88,7 +88,7 @@ const App: React.FC = () => {
|
|||||||
file,
|
file,
|
||||||
endpoint: provider === 'ollama' ? endpoint : undefined
|
endpoint: provider === 'ollama' ? endpoint : undefined
|
||||||
});
|
});
|
||||||
setReportData(data);
|
setReportsData(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
if (err.message.includes('API key') || err.message.includes('not valid') || err.message.includes('invalid')) {
|
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(() => {
|
const handleReset = useCallback(() => {
|
||||||
setFile(null);
|
setFile(null);
|
||||||
setReportData(null);
|
setReportsData(null);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -120,8 +120,8 @@ const App: React.FC = () => {
|
|||||||
}, [handleReset]);
|
}, [handleReset]);
|
||||||
|
|
||||||
const handleExport = (action: 'preview' | 'download') => {
|
const handleExport = (action: 'preview' | 'download') => {
|
||||||
if (reportData) {
|
if (reportsData && reportsData.length > 0) {
|
||||||
const fileName = `Relatorio_SteelCheck_${reportData.identification.certificateNumber || 'analise'}`;
|
const fileName = `Relatorio_SteelCheck_${reportsData[0].identification.certificateNumber || 'analise'}`;
|
||||||
exportAsPdf('printable-report-container', fileName, action);
|
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">
|
<main className="container mx-auto p-4 sm:p-6 md:p-8 max-w-7xl">
|
||||||
{!hasKey ? (
|
{!hasKey ? (
|
||||||
<ApiKeySetup onKeySave={handleKeySave} />
|
<ApiKeySetup onKeySave={handleKeySave} />
|
||||||
) : !reportData ? (
|
) : !reportsData ? (
|
||||||
<div className="max-w-2xl mx-auto pt-8">
|
<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="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">
|
<div className="text-center mb-10">
|
||||||
@@ -191,14 +191,18 @@ const App: React.FC = () => {
|
|||||||
Baixar Relatório
|
Baixar Relatório
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ReportDisplay report={reportData} />
|
<div className="flex flex-col gap-8">
|
||||||
|
{reportsData.map((report, idx) => (
|
||||||
|
<ReportDisplay key={idx} report={report} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{reportData && (
|
{reportsData && (
|
||||||
<div className="absolute -left-[9999px] top-0 opacity-0" aria-hidden="true">
|
<div className="absolute -left-[9999px] top-0 opacity-0" aria-hidden="true">
|
||||||
<PrintableReport report={reportData} />
|
<PrintableReport reports={reportsData} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ import type { ReportData } from '../types';
|
|||||||
import { ReportDisplay } from './ReportDisplay';
|
import { ReportDisplay } from './ReportDisplay';
|
||||||
|
|
||||||
interface PrintableReportProps {
|
interface PrintableReportProps {
|
||||||
report: ReportData;
|
reports: ReportData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PrintableReport: React.FC<PrintableReportProps> = ({ report }) => {
|
export const PrintableReport: React.FC<PrintableReportProps> = ({ reports }) => {
|
||||||
// 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.
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="printable-report-container"
|
id="printable-report-container"
|
||||||
data-report={JSON.stringify(report)}
|
data-report={JSON.stringify(reports)}
|
||||||
className="light bg-white text-slate-900"
|
className="light bg-white text-slate-900 flex flex-col gap-[20mm]"
|
||||||
style={{ width: '210mm', minHeight: '297mm', background: 'white' }}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -141,9 +141,9 @@ export const ReportDisplay: React.FC<ReportDisplayProps> = ({ report }) => {
|
|||||||
return (
|
return (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<td className="py-2.5 px-2 truncate">
|
<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>
|
||||||
<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">
|
<td className="py-2.5 px-1 text-center truncate">
|
||||||
{item.differencePercentage ? (
|
{item.differencePercentage ? (
|
||||||
<span className={`text-[9px] font-bold ${diffColor}`}>{item.differencePercentage}</span>
|
<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">
|
<tbody className="divide-y divide-slate-50 dark:divide-slate-800/50 font-mono">
|
||||||
{compliance.chemical.map((item, i) => (
|
{compliance.chemical.map((item, i) => (
|
||||||
<tr key={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 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">
|
<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)}`}>
|
<span className={`inline-flex items-center text-[9px] font-bold px-1.5 py-0.5 rounded ${statusClass(item.status)}`}>
|
||||||
{item.status}
|
{item.status}
|
||||||
|
|||||||
+54
-44
@@ -39,11 +39,14 @@ const cleanAndParseJson = (jsonString: string): any => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(cleanedString);
|
const parsed = JSON.parse(cleanedString);
|
||||||
// Se o modelo aninhou os dados dentro de uma chave "report"
|
let finalData = parsed;
|
||||||
if (parsed.report && !parsed.identification) {
|
// Se o modelo aninhou os dados dentro de uma chave "report" ou "reports"
|
||||||
return parsed.report;
|
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) {
|
} catch (error) {
|
||||||
console.error("Failed to parse cleaned JSON:", jsonString);
|
console.error("Failed to parse cleaned JSON:", jsonString);
|
||||||
throw new Error("A resposta da IA não estava no formato JSON esperado.");
|
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"]
|
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:
|
Execute:
|
||||||
1. **Extração de Dados (OCR):** Extraia todos os dados primários do certificado.
|
1. Extração de Dados: Leia a imagem atentamente, respeitando as colunas.
|
||||||
2. **Identificação de Normas:** Identifique a(s) norma(s) que o certificado alega atender.
|
2. Identificação de Normas.
|
||||||
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.
|
3. Análise de Conformidade: Compare resultados x requisitos.
|
||||||
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".
|
4. Propriedades Mecânicas (mechanical): Calcule o "differencePercentage" (+X% ou -X%) em relação ao teórico.
|
||||||
5. **Análise de Equivalência:** Liste materiais equivalentes de outras normas (EN, DIN, JIS, NBR, etc.).
|
5. Análise de Equivalência e Grau de Confiança.
|
||||||
6. **Grau de Confiança:** Forneça um % de confiança baseado na qualidade do documento.
|
|
||||||
|
|
||||||
Retorne APENAS o objeto JSON, sem formatação adicional, seguindo EXATAMENTE esta estrutura de chaves (em inglês):
|
Retorne APENAS o JSON, sem markdown. DEVE SER UM ARRAY DE OBJETOS:
|
||||||
{
|
[
|
||||||
"confidence": 95,
|
{
|
||||||
"identification": {
|
"confidence": 95,
|
||||||
"product": "", "standards": "", "manufacturer": "", "certificateNumber": "", "certificateDate": "", "batches": "", "heats": "", "quantity": ""
|
"identification": {
|
||||||
},
|
"product": "", "standards": "", "manufacturer": "", "certificateNumber": "", "certificateDate": "", "batches": "LOTE X", "heats": "CORRIDA Y", "quantity": ""
|
||||||
"compliance": {
|
},
|
||||||
"status": "CONFORME ou NÃO CONFORME",
|
"compliance": {
|
||||||
"mechanical": [ { "property": "", "norm": "", "certificate": "", "status": "OK ou FALHA", "differencePercentage": "+X% ou -X%" } ],
|
"status": "CONFORME ou NÃO CONFORME",
|
||||||
"chemical": [ { "element": "", "norm": "", "certificate": "", "status": "OK ou FALHA" } ],
|
"mechanical": [ { "property": "", "norm": "", "certificate": "", "status": "OK ou FALHA", "differencePercentage": "+X% ou -X%" } ],
|
||||||
"otherTests": [ { "test": "", "norm": "", "certificate": "", "status": "OK ou FALHA" } ]
|
"chemical": [ { "element": "", "norm": "", "certificate": "", "status": "OK ou FALHA" } ],
|
||||||
},
|
"otherTests": [ { "test": "", "norm": "", "certificate": "", "status": "OK ou FALHA" } ]
|
||||||
"overPerformance": [ { "property": "", "value": "" } ],
|
},
|
||||||
"equivalents": [ { "system": "", "norm": "" } ]
|
"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) {
|
if (!apiKey) {
|
||||||
throw new Error("A chave de API do Gemini não foi fornecida.");
|
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 }] },
|
contents: { parts: [imagePart, { text: PROMPT_BASE }] },
|
||||||
config: {
|
config: {
|
||||||
responseMimeType: "application/json",
|
responseMimeType: "application/json",
|
||||||
responseSchema: reportSchema,
|
responseSchema: arraySchema,
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -194,7 +204,7 @@ 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);
|
||||||
return data as ReportData;
|
return data as ReportData[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
throw e;
|
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) {
|
if (!apiKey) {
|
||||||
throw new Error("A chave de API da OpenAI não foi fornecida.");
|
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");
|
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) {
|
if (!apiKey) {
|
||||||
throw new Error("A chave de API da Anthropic não foi fornecida.");
|
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");
|
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) {
|
if (!apiKey || !endpoint) {
|
||||||
throw new Error("A chave de API e endpoint do Azure são necessários.");
|
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");
|
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;
|
const { provider, apiKey, model, file, endpoint } = options;
|
||||||
|
|
||||||
switch (provider) {
|
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) {
|
if (!endpoint) {
|
||||||
throw new Error("O endpoint do Ollama é necessário. Configure o endereço da sua VPS.");
|
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");
|
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) {
|
if (!apiKey) {
|
||||||
throw new Error("A chave de API do OpenRouter não foi fornecida.");
|
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");
|
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) {
|
if (!apiKey) {
|
||||||
throw new Error("A chave de API do MiniMax não foi fornecida.");
|
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");
|
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';
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
export const exportAsPdf = async (elementId: string, fileName: string, action: 'preview' | 'download'): Promise<void> => {
|
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.`);
|
console.error(`Element with id ${elementId} not found.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const pages = container.querySelectorAll('.report-page');
|
||||||
// We temporarily make the element visible to html2canvas if it's hidden by CSS
|
if (pages.length === 0) {
|
||||||
// by cloning it or just capturing it directly (html2canvas can capture off-screen if it's positioned absolute)
|
console.error('No report pages found');
|
||||||
|
return;
|
||||||
const canvas = await html2canvas(element, {
|
}
|
||||||
scale: 2, // Higher resolution
|
|
||||||
useCORS: true,
|
|
||||||
logging: false,
|
|
||||||
backgroundColor: '#ffffff' // Ensure white background
|
|
||||||
});
|
|
||||||
|
|
||||||
const imgData = canvas.toDataURL('image/jpeg', 0.95);
|
try {
|
||||||
|
|
||||||
// A4 dimensions in mm: 210 x 297
|
|
||||||
const pdf = new jsPDF({
|
const pdf = new jsPDF({
|
||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
unit: 'mm',
|
unit: 'mm',
|
||||||
format: 'a4',
|
format: 'a4',
|
||||||
});
|
});
|
||||||
|
|
||||||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
for (let i = 0; i < pages.length; i++) {
|
||||||
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
|
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') {
|
if (action === 'download') {
|
||||||
pdf.save(`${fileName}.pdf`);
|
pdf.save(`${fileName}.pdf`);
|
||||||
|
|||||||
Reference in New Issue
Block a user