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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user