feat(audit): adicionar módulo de auditoria + rebranding BrainWind

- AuditPanel, AuditDetailPopup, parser, runner, prompt-builder, serializer, scenarios, export-audit-pdf
- i18n: strings novas para auditoria
- Settings: integração com módulo de auditoria
- types: tipos compartilhados do módulo audit
This commit is contained in:
2026-07-10 11:38:12 +00:00
parent dc3721e12b
commit d40b3b0c9c
12 changed files with 1657 additions and 14 deletions
+283
View File
@@ -0,0 +1,283 @@
import { useState, useCallback, useRef } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Play, FileDown, Loader2, AlertCircle } from 'lucide-react';
import type { AuditConfig, AuditScenario, ScenarioResult, AuditReport } from '@/lib/audit/types';
import { generateAllScenarios } from '@/lib/audit/scenarios';
import AuditDetailPopup from './AuditDetailPopup';
const PROVIDERS: { value: string; label: string }[] = [
{ value: 'openai', label: 'OpenAI' },
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'minimax', label: 'MiniMax' },
{ value: 'openrouter', label: 'OpenRouter' },
{ value: 'ollama', label: 'Ollama' },
];
const DEFAULT_MODELS: Record<string, string> = {
openai: 'gpt-4o',
anthropic: 'claude-3-5-sonnet-latest',
minimax: 'minimax-m3.0',
openrouter: 'openai/gpt-4o',
ollama: 'deepseek-r1',
};
export default function AuditPanel() {
const [config, setConfig] = useState<AuditConfig>({
provider: 'openai',
model: 'gpt-4o',
apiKey: '',
baseUrl: '',
});
const [status, setStatus] = useState<'idle' | 'generating' | 'running' | 'done' | 'error'>('idle');
const [progress, setProgress] = useState(0);
const [total, setTotal] = useState(0);
const [report, setReport] = useState<AuditReport | null>(null);
const [error, setError] = useState<string | null>(null);
const abortRef = useRef(false);
const [popupOpen, setPopupOpen] = useState(false);
const [selectedScenario, setSelectedScenario] = useState<AuditScenario | null>(null);
const [selectedResult, setSelectedResult] = useState<ScenarioResult | null>(null);
const handleProviderChange = (val: string) => {
setConfig((c) => ({
...c,
provider: val as AuditConfig['provider'],
model: DEFAULT_MODELS[val] ?? c.model,
}));
};
const handleRun = useCallback(async () => {
if (!config.apiKey && config.provider !== 'ollama') {
setError('Informe uma API key para continuar.');
setStatus('error');
return;
}
abortRef.current = false;
setError(null);
setReport(null);
setProgress(0);
setStatus('generating');
const scenarios = generateAllScenarios();
setTotal(scenarios.length);
setStatus('running');
try {
const { runAudit } = await import('@/lib/audit/runner');
const reportResult = await runAudit(
config,
scenarios,
(p) => {
setProgress(p);
if (abortRef.current) throw new Error('Abortado pelo usuário');
},
);
setReport(reportResult);
setStatus('done');
} catch (e) {
if (e instanceof Error && e.message === 'Abortado pelo usuário') {
setStatus('idle');
return;
}
setError(e instanceof Error ? e.message : 'Erro desconhecido');
setStatus('error');
}
}, [config]);
const handleExportPDF = () => {
if (!report) return;
import('@/lib/audit/export-audit-pdf').then((mod) => mod.exportAuditReportToPDF(report));
};
const grouped = report && report.modules
? report.modules.map((m: { module: string; moduleLabel: string; totalScenarios: number; passed: number; failed: number; warnings: number }) => ({
module: m.module,
label: m.moduleLabel,
total: m.totalScenarios,
passed: m.passed,
failed: m.failed,
warnings: m.warnings,
}))
: [];
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-lg">Auditoria LLM</CardTitle>
<CardDescription>
Validação dos cálculos de vento usando LLMs sequenciais. 100+ cenários cobrindo todos os módulos.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="text-sm font-medium">Provedor</label>
<Select value={config.provider} onValueChange={handleProviderChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PROVIDERS.map((p) => (
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">Modelo</label>
<Input
value={config.model}
onChange={(e) => setConfig((c) => ({ ...c, model: e.target.value }))}
placeholder="gpt-4o, claude-3-5-sonnet-..."
/>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">API Key</label>
<Input
type="password"
value={config.apiKey}
onChange={(e) => setConfig((c) => ({ ...c, apiKey: e.target.value }))}
placeholder="sk-..."
/>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">Base URL (opcional)</label>
<Input
value={config.baseUrl ?? ''}
onChange={(e) => setConfig((c) => ({ ...c, baseUrl: e.target.value || undefined }))}
placeholder="https://api.openai.com/v1"
/>
</div>
</div>
<div className="flex items-center gap-2 flex-wrap">
<Button
onClick={handleRun}
disabled={status === 'running' || status === 'generating'}
>
{status === 'running' || status === 'generating' ? (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
) : (
<Play className="w-4 h-4 mr-1.5" />
)}
Executar Auditoria
</Button>
{(status === 'running' || status === 'generating') && (
<Button variant="outline" size="sm" onClick={() => { abortRef.current = true; }}>
Cancelar
</Button>
)}
{status === 'done' && report && (
<Button variant="outline" size="sm" onClick={handleExportPDF}>
<FileDown className="w-3.5 h-3.5 mr-1.5" />
Exportar PDF
</Button>
)}
</div>
{(status === 'running' || status === 'generating') && total > 0 && (
<div className="space-y-1">
<div className="flex justify-between text-xs text-muted-foreground">
<span>{status === 'generating' ? 'Gerando cenários...' : `Processando cenários...`}</span>
<span>{progress} / {total}</span>
</div>
<div className="w-full bg-muted rounded-full h-2 overflow-hidden">
<div
className="bg-purple-600 h-full rounded-full transition-all duration-300"
style={{ width: `${(progress / total) * 100}%` }}
/>
</div>
</div>
)}
{status === 'error' && error && (
<div className="rounded-md border border-red-200 bg-red-50 dark:bg-red-950 dark:border-red-800 p-3 text-sm text-red-700 dark:text-red-300 flex items-start gap-2">
<AlertCircle className="w-4 h-4 mt-0.5 shrink-0" />
<span>{error}</span>
</div>
)}
</CardContent>
</Card>
{status === 'done' && report && (
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center justify-between">
<span>Resultados</span>
<span className="text-sm font-normal text-muted-foreground">
{report.totalPassed}/{report.totalScenarios} PASS
W:{report.totalWarnings} F:{report.totalFailed}
</span>
</CardTitle>
<CardDescription>
Provedor: {report.provider} ({report.model}) {new Date(report.timestamp).toLocaleString('pt-BR')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-muted/50">
<tr>
<th className="p-2 text-left">Módulo</th>
<th className="p-2 text-center">Total</th>
<th className="p-2 text-center">PASS</th>
<th className="p-2 text-center">WARN</th>
<th className="p-2 text-center">FAIL</th>
</tr>
</thead>
<tbody>
{grouped.map((g: { module: string; label: string; total: number; passed: number; failed: number; warnings: number }) => (
<tr key={g.module} className="border-t border-border cursor-pointer hover:bg-muted/30"
onClick={() => {
const mod = report.modules.find((m) => m.module === g.module);
if (!mod) return;
const modScenarios = generateAllScenarios().filter((s) => s.module === g.module);
const dialog = mod.scenarios[0];
if (modScenarios.length > 0 && dialog) {
setSelectedScenario(modScenarios[0]);
setSelectedResult(dialog);
setPopupOpen(true);
}
}}>
<td className="p-2 font-medium">{g.label}</td>
<td className="p-2 text-center">{g.total}</td>
<td className="p-2 text-center"><Badge className="bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300">{g.passed}</Badge></td>
<td className="p-2 text-center"><Badge className="bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300">{g.warnings}</Badge></td>
<td className="p-2 text-center"><Badge className="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300">{g.failed}</Badge></td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-xs text-muted-foreground mt-2">
Clique em um módulo para ver detalhes dos cenários e verificações.
</p>
</CardContent>
</Card>
)}
<AuditDetailPopup
open={popupOpen}
onOpenChange={setPopupOpen}
scenario={selectedScenario}
result={selectedResult}
/>
</div>
);
}