diff --git a/app/src/components/AuditPanel.tsx b/app/src/components/AuditPanel.tsx
index c7c949f..2357946 100644
--- a/app/src/components/AuditPanel.tsx
+++ b/app/src/components/AuditPanel.tsx
@@ -11,7 +11,7 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Play, FileDown, Loader2, AlertCircle } from 'lucide-react';
-import type { AuditConfig, AuditScenario, ScenarioResult, AuditReport, ModuleType } from '@/lib/audit/types';
+import type { AuditConfig, AuditScenario, ScenarioResult, AuditReport } from '@/lib/audit/types';
import { MODULE_LABELS } from '@/lib/audit/types';
import { generateAllScenarios } from '@/lib/audit/scenarios';
import AuditDetailPopup from './AuditDetailPopup';
diff --git a/app/src/lib/audit/export-audit-pdf.tsx b/app/src/lib/audit/export-audit-pdf.tsx
index 9deb514..f0637dd 100644
--- a/app/src/lib/audit/export-audit-pdf.tsx
+++ b/app/src/lib/audit/export-audit-pdf.tsx
@@ -65,6 +65,33 @@ function statusColor(s: CheckStatus): string {
}
}
+function sanitizeForPDF(text: string | undefined): string {
+ if (!text) return '';
+ return text
+ .replace(/φ/g, 'phi')
+ .replace(/ϕ/g, 'phi')
+ .replace(/α/g, 'alpha')
+ .replace(/θ/g, 'theta')
+ .replace(/β/g, 'beta')
+ .replace(/γ/g, 'gamma')
+ .replace(/Δ/g, 'Delta')
+ .replace(/—/g, '-')
+ .replace(/–/g, '-')
+ .replace(/“/g, '"')
+ .replace(/”/g, '"')
+ .replace(/‘/g, "'")
+ .replace(/’/g, "'")
+ .replace(/Æ/g, 'phi') // Fix specifically the one seen in user screenshot
+ .replace(/≤/g, '<=')
+ .replace(/≥/g, '>=')
+ .replace(/≈/g, '~')
+ .replace(/×/g, 'x')
+ .replace(/÷/g, '/')
+ .replace(/²/g, '^2')
+ .replace(/³/g, '^3')
+ .replace(/°/g, ' deg');
+}
+
interface ReportPDFProps {
report: AuditReport;
}
@@ -119,27 +146,27 @@ function AuditReportDocument({ report }: ReportPDFProps) {
return (
- {sc.scenarioId} — {sc.verdict === 'PASS' ? '✓' : sc.verdict === 'WARN' ? '⚠' : '✗'} {summaryTranslated}
+ {sanitizeForPDF(sc.scenarioId)} — {sc.verdict === 'PASS' ? '✓' : sc.verdict === 'WARN' ? '⚠' : '✗'} {sanitizeForPDF(summaryTranslated)}
{sc.enunciado_problema && (
- Enunciado: {sc.enunciado_problema}
+ Enunciado: {sanitizeForPDF(sc.enunciado_problema)}
)}
{sc.tabelas_nbr_consultadas && (
- Tabelas: {sc.tabelas_nbr_consultadas}
+ Tabelas: {sanitizeForPDF(sc.tabelas_nbr_consultadas)}
)}
{sc.resultado_app && (
- Avaliação do App: {sc.resultado_app}
+ Avaliação do App: {sanitizeForPDF(sc.resultado_app)}
)}
{sc.calculation_steps && (
- CoT: {sc.calculation_steps.substring(0, 300)}{sc.calculation_steps.length > 300 ? '...' : ''}
+ CoT: {sanitizeForPDF(sc.calculation_steps.substring(0, 300))}{sc.calculation_steps.length > 300 ? '...' : ''}
)}
{sc.checks.map((chk, i) => (
{chk.status}
- {chk.name}
- {chk.actual} ({chk.expected})
+ {sanitizeForPDF(chk.name)}
+ {sanitizeForPDF(chk.actual)} ({sanitizeForPDF(chk.expected)})
))}
diff --git a/app/src/lib/audit/runner.ts b/app/src/lib/audit/runner.ts
index c840979..7daadcc 100644
--- a/app/src/lib/audit/runner.ts
+++ b/app/src/lib/audit/runner.ts
@@ -185,7 +185,7 @@ async function callOpenRouter(
onProgress?: (msg: string) => void,
signal?: AbortSignal,
): Promise {
- onProgress?.('Enviando para OpenRouter...');
+ onProgress?.('Enviando para OpenRouter (streaming)...');
let url = config.baseUrl || 'https://openrouter.ai/api/v1/chat/completions';
if (config.baseUrl && !config.baseUrl.includes('/chat/completions')) {
url = config.baseUrl.replace(/\/$/, '') + '/chat/completions';
@@ -197,6 +197,7 @@ async function callOpenRouter(
{ role: 'user' as const, content: userPrompt },
],
temperature: 0,
+ stream: true,
};
const response = await fetchWithTimeout(url, {
method: 'POST',
@@ -208,15 +209,43 @@ async function callOpenRouter(
},
body: JSON.stringify(body),
}, 600000, signal);
+
if (!response.ok) {
const errText = await response.text();
throw new Error(`OpenRouter HTTP ${response.status}: ${errText}`);
}
- const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } };
- if (data.error) throw new Error(`OpenRouter error: ${data.error.message}`);
- const content = data.choices?.[0]?.message?.content;
- if (!content) throw new Error('OpenRouter returned empty response');
- return content;
+
+ if (!response.body) throw new Error('OpenRouter response body is null');
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder('utf-8');
+ let fullText = '';
+ let buffer = '';
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split('\n');
+ buffer = lines.pop() || '';
+ for (const rawLine of lines) {
+ const line = rawLine.trim();
+ if (!line.startsWith('data: ')) continue;
+ const jsonStr = line.replace(/^data: /, '').trim();
+ if (jsonStr === '[DONE]') continue;
+ try {
+ const parsed = JSON.parse(jsonStr);
+ // OpenRouter typically sends reasoning tokens or content in delta
+ const delta = parsed.choices?.[0]?.delta;
+ if (delta?.content) fullText += delta.content;
+ } catch (e) {
+ // ignore incomplete JSON
+ }
+ }
+ }
+
+ if (!fullText) throw new Error('OpenRouter returned empty streaming response');
+ return fullText;
}
async function callOllama(
@@ -292,6 +321,10 @@ export async function runAudit(
} catch (e) {
chunkError = e instanceof Error ? e : new Error(String(e));
console.warn(`Erro no chunk ${i} (tentativa ${retry}):`, chunkError);
+ if (retry < maxRetriesPerChunk) {
+ // Backoff de 3 segundos antes da próxima tentativa
+ await new Promise(r => setTimeout(r, 3000));
+ }
}
}
diff --git a/app/src/pages/CertificatePage.tsx b/app/src/pages/CertificatePage.tsx
index 2776333..70a47fb 100644
--- a/app/src/pages/CertificatePage.tsx
+++ b/app/src/pages/CertificatePage.tsx
@@ -1,8 +1,7 @@
import { useState } from 'react';
-import { ShieldCheck, FileText, CheckCircle2, ChevronRight, Download, Bot, FileCheck, X } from 'lucide-react';
-import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { ShieldCheck, FileText, CheckCircle2, Download, Bot, FileCheck } from 'lucide-react';
+import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogClose } from '@/components/ui/dialog';
const rawLaudos = import.meta.glob('/public/laudos/*.pdf', { eager: true, query: '?url', import: 'default' });