🚀 Auto-deploy: BrainWind atualizado em 16/07/2026 11:35:26
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Play, FileDown, Loader2, AlertCircle } from 'lucide-react';
|
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 { MODULE_LABELS } from '@/lib/audit/types';
|
||||||
import { generateAllScenarios } from '@/lib/audit/scenarios';
|
import { generateAllScenarios } from '@/lib/audit/scenarios';
|
||||||
import AuditDetailPopup from './AuditDetailPopup';
|
import AuditDetailPopup from './AuditDetailPopup';
|
||||||
|
|||||||
@@ -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 {
|
interface ReportPDFProps {
|
||||||
report: AuditReport;
|
report: AuditReport;
|
||||||
}
|
}
|
||||||
@@ -119,27 +146,27 @@ function AuditReportDocument({ report }: ReportPDFProps) {
|
|||||||
return (
|
return (
|
||||||
<View key={sc.scenarioId} style={styles.scenarioItem}>
|
<View key={sc.scenarioId} style={styles.scenarioItem}>
|
||||||
<Text style={styles.scenarioId}>
|
<Text style={styles.scenarioId}>
|
||||||
{sc.scenarioId} — {sc.verdict === 'PASS' ? '✓' : sc.verdict === 'WARN' ? '⚠' : '✗'} {summaryTranslated}
|
{sanitizeForPDF(sc.scenarioId)} — {sc.verdict === 'PASS' ? '✓' : sc.verdict === 'WARN' ? '⚠' : '✗'} {sanitizeForPDF(summaryTranslated)}
|
||||||
</Text>
|
</Text>
|
||||||
{sc.enunciado_problema && (
|
{sc.enunciado_problema && (
|
||||||
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Enunciado: {sc.enunciado_problema}</Text>
|
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Enunciado: {sanitizeForPDF(sc.enunciado_problema)}</Text>
|
||||||
)}
|
)}
|
||||||
{sc.tabelas_nbr_consultadas && (
|
{sc.tabelas_nbr_consultadas && (
|
||||||
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Tabelas: {sc.tabelas_nbr_consultadas}</Text>
|
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Tabelas: {sanitizeForPDF(sc.tabelas_nbr_consultadas)}</Text>
|
||||||
)}
|
)}
|
||||||
{sc.resultado_app && (
|
{sc.resultado_app && (
|
||||||
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Avaliação do App: {sc.resultado_app}</Text>
|
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Avaliação do App: {sanitizeForPDF(sc.resultado_app)}</Text>
|
||||||
)}
|
)}
|
||||||
{sc.calculation_steps && (
|
{sc.calculation_steps && (
|
||||||
<Text style={{ fontSize: 8, color: '#444', marginBottom: 4, fontStyle: 'italic' }}>
|
<Text style={{ fontSize: 8, color: '#444', marginBottom: 4, fontStyle: 'italic' }}>
|
||||||
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 ? '...' : ''}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{sc.checks.map((chk, i) => (
|
{sc.checks.map((chk, i) => (
|
||||||
<View key={i} style={styles.checkRow}>
|
<View key={i} style={styles.checkRow}>
|
||||||
<Text style={[styles.statusBadge, { color: statusColor(chk.status) }]}>{chk.status}</Text>
|
<Text style={[styles.statusBadge, { color: statusColor(chk.status) }]}>{chk.status}</Text>
|
||||||
<Text style={styles.checkLabel}>{chk.name}</Text>
|
<Text style={styles.checkLabel}>{sanitizeForPDF(chk.name)}</Text>
|
||||||
<Text style={styles.checkValue}>{chk.actual} ({chk.expected})</Text>
|
<Text style={styles.checkValue}>{sanitizeForPDF(chk.actual)} ({sanitizeForPDF(chk.expected)})</Text>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ async function callOpenRouter(
|
|||||||
onProgress?: (msg: string) => void,
|
onProgress?: (msg: string) => void,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
onProgress?.('Enviando para OpenRouter...');
|
onProgress?.('Enviando para OpenRouter (streaming)...');
|
||||||
let url = config.baseUrl || 'https://openrouter.ai/api/v1/chat/completions';
|
let url = config.baseUrl || 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
if (config.baseUrl && !config.baseUrl.includes('/chat/completions')) {
|
if (config.baseUrl && !config.baseUrl.includes('/chat/completions')) {
|
||||||
url = config.baseUrl.replace(/\/$/, '') + '/chat/completions';
|
url = config.baseUrl.replace(/\/$/, '') + '/chat/completions';
|
||||||
@@ -197,6 +197,7 @@ async function callOpenRouter(
|
|||||||
{ role: 'user' as const, content: userPrompt },
|
{ role: 'user' as const, content: userPrompt },
|
||||||
],
|
],
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
|
stream: true,
|
||||||
};
|
};
|
||||||
const response = await fetchWithTimeout(url, {
|
const response = await fetchWithTimeout(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -208,15 +209,43 @@ async function callOpenRouter(
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
}, 600000, signal);
|
}, 600000, signal);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errText = await response.text();
|
const errText = await response.text();
|
||||||
throw new Error(`OpenRouter HTTP ${response.status}: ${errText}`);
|
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}`);
|
if (!response.body) throw new Error('OpenRouter response body is null');
|
||||||
const content = data.choices?.[0]?.message?.content;
|
|
||||||
if (!content) throw new Error('OpenRouter returned empty response');
|
const reader = response.body.getReader();
|
||||||
return content;
|
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(
|
async function callOllama(
|
||||||
@@ -292,6 +321,10 @@ export async function runAudit(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
chunkError = e instanceof Error ? e : new Error(String(e));
|
chunkError = e instanceof Error ? e : new Error(String(e));
|
||||||
console.warn(`Erro no chunk ${i} (tentativa ${retry}):`, chunkError);
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ShieldCheck, FileText, CheckCircle2, ChevronRight, Download, Bot, FileCheck, X } from 'lucide-react';
|
import { ShieldCheck, FileText, CheckCircle2, Download, Bot, FileCheck } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogClose } from '@/components/ui/dialog';
|
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' });
|
const rawLaudos = import.meta.glob('/public/laudos/*.pdf', { eager: true, query: '?url', import: 'default' });
|
||||||
|
|||||||
Reference in New Issue
Block a user