- Fix BatchProcessor constructor to avoid server blocking during startup - Add comprehensive integration tests for all modular combinations - Enhance CLAUDE.md documentation with new test commands - Update SelectiveLayers configuration for better LLM allocation - Add AutoReporter system for test automation - Include production workflow validation tests 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
921 lines
39 KiB
JavaScript
921 lines
39 KiB
JavaScript
/**
|
|
* AUTO-REPORTER POUR TESTS - VERSION SIMPLIFIÉE
|
|
* Se connecte automatiquement au système de logging pour capturer tout
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
class AutoReporter {
|
|
constructor() {
|
|
this.testResults = [];
|
|
this.llmCalls = [];
|
|
this.phases = []; // Nouveau: tracking des phases
|
|
this.currentTestName = null;
|
|
this.testStartTime = null;
|
|
this.reportDir = path.join(process.cwd(), 'reports');
|
|
this.globalStartTime = Date.now();
|
|
this.reportGenerated = false;
|
|
this.lastActivity = Date.now();
|
|
this.timeoutId = null;
|
|
|
|
// Nouvelles propriétés pour capture prompts/réponses
|
|
this.currentLLMCall = null;
|
|
this.capturingPrompt = false;
|
|
this.capturingResponse = false;
|
|
|
|
// Créer le dossier reports s'il n'existe pas
|
|
if (!fs.existsSync(this.reportDir)) {
|
|
fs.mkdirSync(this.reportDir, { recursive: true });
|
|
}
|
|
|
|
this.hookIntoLogs();
|
|
this.setupAutoGeneration();
|
|
}
|
|
|
|
setupAutoGeneration() {
|
|
// Hook sur la fermeture du processus
|
|
process.on('exit', () => {
|
|
this.finalize();
|
|
});
|
|
|
|
// Hook sur SIGINT (Ctrl+C)
|
|
process.on('SIGINT', () => {
|
|
this.finalize();
|
|
process.exit(0);
|
|
});
|
|
|
|
// Hook sur SIGTERM
|
|
process.on('SIGTERM', () => {
|
|
this.finalize();
|
|
process.exit(0);
|
|
});
|
|
|
|
// Timer pour générer le rapport après une période de calme
|
|
this.startIdleTimer();
|
|
}
|
|
|
|
updateActivity() {
|
|
this.lastActivity = Date.now();
|
|
|
|
// Reset du timer - on reporte la génération
|
|
if (this.timeoutId) {
|
|
clearTimeout(this.timeoutId);
|
|
}
|
|
|
|
// Redémarrer le timer d'inactivité
|
|
this.startIdleTimer();
|
|
}
|
|
|
|
startIdleTimer() {
|
|
// Générer le rapport après 15 secondes d'inactivité (tests longs avec plusieurs appels LLM)
|
|
this.timeoutId = setTimeout(() => {
|
|
if (this.testResults.length > 0 && !this.reportGenerated) {
|
|
console.log('\n🎯 AutoReporter: Période d\'inactivité détectée, génération du rapport...');
|
|
this.finalize();
|
|
}
|
|
}, 15000);
|
|
}
|
|
|
|
finalize() {
|
|
if (this.reportGenerated || this.testResults.length === 0) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.generateReport();
|
|
this.reportGenerated = true;
|
|
} catch (error) {
|
|
console.error('❌ Erreur lors de la génération du rapport:', error.message);
|
|
}
|
|
}
|
|
|
|
hookIntoLogs() {
|
|
const originalWrite = process.stdout.write;
|
|
const originalError = process.stderr.write;
|
|
|
|
// Use original write to avoid recursion
|
|
const debugLog = (msg) => {
|
|
originalWrite.call(process.stdout, msg + '\n');
|
|
};
|
|
|
|
// Function to process test lines (for both stdout and stderr)
|
|
const processTestLine = (str) => {
|
|
// Capturer résultats de tests depuis les logs JSON avec contexte test
|
|
// Les vrais tests ont des appels LLM qui fournissent le contexte
|
|
if (str.includes('"msg":"📊 Stats:')) {
|
|
try {
|
|
const logLine = JSON.parse(str.trim());
|
|
const statsMatch = logLine.msg.match(/📊 Stats: ({.*})/);
|
|
if (statsMatch) {
|
|
const stats = JSON.parse(statsMatch[1]);
|
|
const testContext = this.currentTestName;
|
|
|
|
// Si on a un contexte de test valide, on peut créer/mettre à jour le test
|
|
if (testContext && !this.testResults.find(t => t.name === testContext)) {
|
|
this.testResults.push({
|
|
name: testContext,
|
|
status: 'passed', // Si LLM call réussit, test probablement passé
|
|
duration: 0, // Sera calculé plus tard
|
|
timestamp: Date.now(),
|
|
llmCallsCount: 0
|
|
});
|
|
|
|
debugLog(`✅ RAPPORT: Test inféré depuis LLM call - "${testContext}"`);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Ignore parsing errors
|
|
}
|
|
this.updateActivity(); // Activité détectée
|
|
}
|
|
|
|
// Capturer aussi les tests Node.js si possible (passés et échoués)
|
|
if ((str.includes('✔') || str.includes('✖')) && str.includes('ms)') && !str.includes('JSON') && !str.includes('RAPPORT')) {
|
|
const testMatch = str.match(/[✔✖]\s+(.+?)\s+\((\d+(?:\.\d+)?)ms\)/);
|
|
if (testMatch) {
|
|
const [fullMatch, testName, duration] = testMatch;
|
|
const cleanName = testName.trim();
|
|
const status = str.includes('✔') ? 'passed' : 'failed';
|
|
|
|
if (!this.testResults.find(t => t.name === cleanName)) {
|
|
this.testResults.push({
|
|
name: cleanName,
|
|
status: status,
|
|
duration: parseFloat(duration),
|
|
timestamp: Date.now(),
|
|
llmCallsCount: 0
|
|
});
|
|
|
|
debugLog(`✅ RAPPORT: Test Node.js ${status} capturé - "${cleanName}" (${duration}ms)`);
|
|
}
|
|
}
|
|
this.updateActivity(); // Activité détectée
|
|
}
|
|
|
|
if (str.includes('✗') && str.includes('(') && str.includes('ms)')) {
|
|
const testMatch = str.match(/✗\s+(.+?)\s+\((\d+(?:\.\d+)?)ms\)/);
|
|
if (testMatch) {
|
|
const [, testName, duration] = testMatch;
|
|
const cleanName = testName.trim();
|
|
|
|
if (!this.testResults.find(t => t.name === cleanName)) {
|
|
this.testResults.push({
|
|
name: cleanName,
|
|
status: 'failed',
|
|
duration: parseFloat(duration),
|
|
timestamp: Date.now(),
|
|
llmCallsCount: this.llmCalls.filter(call =>
|
|
call.testContext && call.testContext.includes(cleanName)
|
|
).length
|
|
});
|
|
|
|
debugLog(`📊 Test capturé: ${cleanName} (FAILED ${duration}ms)`);
|
|
}
|
|
}
|
|
this.updateActivity(); // Activité détectée
|
|
}
|
|
};
|
|
|
|
process.stdout.write = (chunk, encoding, callback) => {
|
|
const str = chunk.toString();
|
|
processTestLine(str);
|
|
|
|
// Capturer stats LLM depuis logs JSON et texte simple
|
|
if (str.includes('📊 Stats:')) {
|
|
let stats = null;
|
|
try {
|
|
// Tenter de parser comme JSON d'abord
|
|
if (str.includes('"msg":"📊 Stats:')) {
|
|
const logLine = JSON.parse(str.trim());
|
|
const statsMatch = logLine.msg.match(/📊 Stats: ({.*})/);
|
|
if (statsMatch) {
|
|
stats = JSON.parse(statsMatch[1]);
|
|
}
|
|
} else {
|
|
// Parser le format texte direct
|
|
const statsMatch = str.match(/📊 Stats: ({.*})/);
|
|
if (statsMatch) {
|
|
stats = JSON.parse(statsMatch[1]);
|
|
}
|
|
}
|
|
|
|
if (stats) {
|
|
// Créer l'objet LLM call avec les stats + prompt/réponse si disponibles
|
|
const llmCall = {
|
|
provider: stats.provider,
|
|
model: stats.model,
|
|
duration: stats.duration,
|
|
tokens: {
|
|
prompt: stats.promptTokens,
|
|
response: stats.responseTokens
|
|
},
|
|
timestamp: stats.timestamp,
|
|
testContext: this.currentTestName || 'unknown'
|
|
};
|
|
|
|
// Ajouter prompt/réponse si capturés
|
|
if (this.currentLLMCall) {
|
|
if (this.currentLLMCall.prompt) {
|
|
llmCall.prompt = this.currentLLMCall.prompt;
|
|
}
|
|
if (this.currentLLMCall.response) {
|
|
llmCall.response = this.currentLLMCall.response;
|
|
}
|
|
// Reset currentLLMCall
|
|
this.currentLLMCall = null;
|
|
}
|
|
|
|
this.llmCalls.push(llmCall);
|
|
this.updateActivity(); // Activité détectée
|
|
}
|
|
} catch (e) {
|
|
// Ignore parsing errors
|
|
}
|
|
}
|
|
|
|
// Capturer les phases du pipeline
|
|
if (str.includes('PHASE') && (str.includes('/4:') || str.includes('1/4') || str.includes('2/4') || str.includes('3/4') || str.includes('4/4'))) {
|
|
const phaseMatch = str.match(/PHASE (\d)\/4:\s*([^(\n]+)/);
|
|
if (phaseMatch) {
|
|
const [, phaseNumber, phaseName] = phaseMatch;
|
|
const phase = {
|
|
number: parseInt(phaseNumber),
|
|
name: phaseName.trim(),
|
|
timestamp: new Date().toISOString(),
|
|
testContext: this.currentTestName || 'unknown',
|
|
status: 'started',
|
|
inputElements: [],
|
|
outputElements: [],
|
|
llmCalls: [],
|
|
metrics: {}
|
|
};
|
|
this.phases.push(phase);
|
|
this.updateActivity();
|
|
}
|
|
}
|
|
|
|
// Capturer les fins de phases
|
|
if (str.includes('✅ Phase') && str.includes('terminé')) {
|
|
const phaseEndMatch = str.match(/✅ Phase (\d+):\s*([^(\n]+)/);
|
|
if (phaseEndMatch) {
|
|
const [, phaseNumber, phaseName] = phaseEndMatch;
|
|
// Trouver la phase correspondante et la marquer comme terminée
|
|
const phase = this.phases.find(p => p.number === parseInt(phaseNumber) && p.status === 'started');
|
|
if (phase) {
|
|
phase.status = 'completed';
|
|
phase.endTimestamp = new Date().toISOString();
|
|
// Calculer la durée
|
|
if (phase.timestamp) {
|
|
phase.duration = new Date(phase.endTimestamp) - new Date(phase.timestamp);
|
|
}
|
|
}
|
|
this.updateActivity();
|
|
}
|
|
}
|
|
|
|
// Capturer métriques des phases
|
|
if (str.includes('📊') && (str.includes('éléments') || str.includes('modifications') || str.includes('améliorés'))) {
|
|
// Capturer nombre d'éléments traités
|
|
const elementsMatch = str.match(/(\d+)\s+éléments/);
|
|
const modificationsMatch = str.match(/(\d+)\s+modifications?/);
|
|
const amelioresMatch = str.match(/(\d+)\/(\d+)\s+améliorés/);
|
|
|
|
// Associer à la dernière phase en cours
|
|
const currentPhase = this.phases.find(p => p.status === 'started') || this.phases[this.phases.length - 1];
|
|
if (currentPhase) {
|
|
if (elementsMatch) {
|
|
currentPhase.metrics.totalElements = parseInt(elementsMatch[1]);
|
|
}
|
|
if (modificationsMatch) {
|
|
currentPhase.metrics.modifications = parseInt(modificationsMatch[1]);
|
|
}
|
|
if (amelioresMatch) {
|
|
currentPhase.metrics.processed = parseInt(amelioresMatch[1]);
|
|
currentPhase.metrics.total = parseInt(amelioresMatch[2]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Capturer les phases Human Touch spécifiquement
|
|
if (str.includes('👤 PHASE 4/4: Human Touch') || str.includes('Human Touch (Simulation Humaine)')) {
|
|
const phase = {
|
|
number: 4,
|
|
name: 'Human Touch (Simulation Humaine)',
|
|
timestamp: new Date().toISOString(),
|
|
testContext: this.currentTestName || 'unknown',
|
|
status: 'started'
|
|
};
|
|
this.phases.push(phase);
|
|
this.updateActivity();
|
|
}
|
|
|
|
// Capturer prompts LLM complets
|
|
if (str.includes('🔍 ===== PROMPT ENVOYÉ À')) {
|
|
const providerMatch = str.match(/PROMPT ENVOYÉ À (\w+) \(([^)]+)\)/);
|
|
if (providerMatch) {
|
|
this.currentLLMCall = {
|
|
provider: providerMatch[1],
|
|
model: providerMatch[2],
|
|
startTime: Date.now(),
|
|
prompt: '', // Sera rempli par les lignes suivantes
|
|
response: ''
|
|
};
|
|
this.capturingPrompt = true;
|
|
}
|
|
}
|
|
|
|
// Capturer le contenu du prompt depuis le JSON log
|
|
if (this.capturingPrompt && this.currentLLMCall && str.includes('"msg"')) {
|
|
try {
|
|
const logLine = JSON.parse(str.trim());
|
|
if (logLine.msg && !logLine.msg.includes('🔍') && !logLine.msg.includes('📤')) {
|
|
// C'est le contenu du prompt
|
|
this.currentLLMCall.prompt = logLine.msg;
|
|
this.capturingPrompt = false; // Un seul message contient tout le prompt
|
|
}
|
|
} catch (e) {
|
|
// Pas du JSON, ignorer
|
|
}
|
|
}
|
|
|
|
// Arrêter la capture du prompt quand on voit la requête LLM
|
|
if (str.includes('📤 LLM REQUEST') && this.capturingPrompt) {
|
|
this.capturingPrompt = false;
|
|
}
|
|
|
|
// Capturer réponses LLM complètes
|
|
if (str.includes('📥 LLM RESPONSE')) {
|
|
if (this.currentLLMCall) {
|
|
this.currentLLMCall.endTime = Date.now();
|
|
this.currentLLMCall.testContext = this.currentTestName || 'unknown';
|
|
this.capturingResponse = true;
|
|
}
|
|
}
|
|
|
|
// Capturer le contenu de la réponse depuis le JSON log
|
|
if (this.capturingResponse && this.currentLLMCall && str.includes('"msg"')) {
|
|
try {
|
|
const logLine = JSON.parse(str.trim());
|
|
if (logLine.msg && !logLine.msg.includes('📥') && !logLine.msg.includes('✅') &&
|
|
!logLine.msg.includes('OPENAI') && !logLine.msg.includes('GEMINI') && !logLine.msg.includes('CLAUDE')) {
|
|
// C'est probablement le contenu de la réponse
|
|
if (!this.currentLLMCall.response) {
|
|
this.currentLLMCall.response = logLine.msg;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Pas du JSON, ignorer
|
|
}
|
|
}
|
|
|
|
// Finaliser la capture LLM quand on voit le message de fin
|
|
if ((str.includes('✅ OPENAI') || str.includes('✅ GEMINI') || str.includes('✅ CLAUDE')) && this.capturingResponse) {
|
|
this.capturingResponse = false;
|
|
// Nettoyer les réponses
|
|
if (this.currentLLMCall.prompt) {
|
|
this.currentLLMCall.prompt = this.currentLLMCall.prompt.trim();
|
|
}
|
|
if (this.currentLLMCall.response) {
|
|
this.currentLLMCall.response = this.currentLLMCall.response.trim();
|
|
}
|
|
}
|
|
|
|
return originalWrite.call(process.stdout, chunk, encoding, callback);
|
|
};
|
|
|
|
// Also hook stderr in case Node.js test runner outputs to stderr
|
|
process.stderr.write = (chunk, encoding, callback) => {
|
|
const str = chunk.toString();
|
|
processTestLine(str);
|
|
|
|
return originalError.call(process.stderr, chunk, encoding, callback);
|
|
};
|
|
}
|
|
|
|
onTestStart(testName) {
|
|
this.currentTestName = testName;
|
|
this.testStartTime = Date.now();
|
|
this.updateActivity();
|
|
}
|
|
|
|
setTestContext(testName) {
|
|
this.currentTestName = testName;
|
|
this.updateActivity();
|
|
}
|
|
|
|
generateReport() {
|
|
const totalDuration = Date.now() - this.globalStartTime;
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const reportFile = path.join(this.reportDir, `auto-report-${timestamp}.html`);
|
|
|
|
// Recalculer le nombre de LLM calls par test avec logique améliorée
|
|
this.testResults.forEach(test => {
|
|
test.llmCallsCount = this.llmCalls.filter(call => {
|
|
if (!call.testContext) return false;
|
|
|
|
// Recherche exacte ou inclusion
|
|
return call.testContext === test.name ||
|
|
call.testContext.includes(test.name) ||
|
|
test.name.includes(call.testContext) ||
|
|
(call.testContext !== 'unknown' && test.name.toLowerCase().includes('pipeline'));
|
|
}).length;
|
|
});
|
|
|
|
const stats = {
|
|
total: this.testResults.length,
|
|
passed: this.testResults.filter(r => r.status === 'passed').length,
|
|
failed: this.testResults.filter(r => r.status === 'failed').length,
|
|
totalDuration,
|
|
totalLLMCalls: this.llmCalls.length
|
|
};
|
|
|
|
const html = this.generateHTML(stats);
|
|
fs.writeFileSync(reportFile, html);
|
|
|
|
const jsonFile = path.join(this.reportDir, `auto-report-${timestamp}.json`);
|
|
fs.writeFileSync(jsonFile, JSON.stringify({
|
|
timestamp: new Date().toISOString(),
|
|
stats,
|
|
testResults: this.testResults,
|
|
llmCalls: this.llmCalls,
|
|
phases: this.phases // Ajout des phases dans le JSON
|
|
}, null, 2));
|
|
|
|
console.log(`\n📊 RAPPORT AUTO-GÉNÉRÉ:`);
|
|
console.log(` HTML: ${reportFile}`);
|
|
console.log(` JSON: ${jsonFile}`);
|
|
console.log(` 📈 ${stats.passed}/${stats.total} tests | ${stats.totalLLMCalls} LLM calls | ${Math.round(totalDuration/1000)}s`);
|
|
|
|
return reportFile;
|
|
}
|
|
|
|
generateHTML(stats) {
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Auto-Rapport TI - ${new Date().toLocaleString()}</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
|
.container { max-width: 1000px; margin: 0 auto; }
|
|
.header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 20px; }
|
|
.stat-card { background: white; padding: 15px; border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.stat-value { font-size: 1.8em; font-weight: bold; color: #2196F3; }
|
|
.stat-label { color: #666; margin-top: 5px; font-size: 14px; }
|
|
.test-list { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.test-item { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: background-color 0.2s; }
|
|
.test-item:hover { background: #f8f9fa; }
|
|
.test-item.clickable { border-left: 4px solid #4CAF50; }
|
|
.test-status { width: 12px; height: 12px; border-radius: 50%; margin-right: 10px; }
|
|
.status-passed { background: #4CAF50; }
|
|
.status-failed { background: #f44336; }
|
|
.llm-section { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 20px; }
|
|
.llm-call { background: #f9f9f9; padding: 10px; margin: 5px 0; border-radius: 4px; display: flex; justify-content: space-between; cursor: pointer; transition: background-color 0.2s; }
|
|
.llm-call:hover { background: #e8f4f8; }
|
|
.llm-call.clickable { border-left: 4px solid #2196F3; }
|
|
.phases-section { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 20px; }
|
|
.phase-timeline { display: flex; flex-direction: column; gap: 10px; }
|
|
.phase-item { background: #f8f9fa; padding: 12px; border-radius: 6px; border-left: 4px solid #9C27B0; display: flex; justify-content: space-between; align-items: center; }
|
|
.phase-item.completed { border-left-color: #4CAF50; background: #f1f8e9; }
|
|
.phase-item.started { border-left-color: #FF9800; background: #fff3e0; }
|
|
.phase-number { width: 30px; height: 30px; border-radius: 50%; background: #9C27B0; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 12px; }
|
|
.phase-number.completed { background: #4CAF50; }
|
|
.phase-number.started { background: #FF9800; }
|
|
|
|
/* Modal styles */
|
|
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
|
|
#llmModal { z-index: 1100; } /* LLM modal au-dessus du test modal */
|
|
.modal-content { background-color: white; margin: 5% auto; padding: 20px; border-radius: 8px; width: 90%; max-width: 800px; max-height: 80vh; overflow-y: auto; }
|
|
.close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
|
|
.close:hover { color: black; }
|
|
.prompt-section, .response-section { margin: 15px 0; }
|
|
.prompt-section h4, .response-section h4 { color: #333; border-bottom: 2px solid #2196F3; padding-bottom: 5px; }
|
|
.prompt-content, .response-content { background: #f5f5f5; padding: 15px; border-radius: 4px; white-space: pre-wrap; font-family: monospace; font-size: 14px; max-height: 300px; overflow-y: auto; }
|
|
.response-content { background: #f0f8ff; }
|
|
|
|
/* Test details modal styles */
|
|
.test-details-section { margin: 15px 0; }
|
|
.test-details-section h4 { color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 5px; }
|
|
.test-llm-list { background: #f8f9fa; padding: 10px; border-radius: 4px; margin-top: 10px; }
|
|
.test-llm-item { background: white; margin: 5px 0; padding: 8px; border-radius: 3px; display: flex; justify-content: space-between; cursor: pointer; border: 1px solid #ddd; }
|
|
.test-llm-item:hover { background: #e8f4f8; }
|
|
.test-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin: 10px 0; }
|
|
.test-stat { background: #f0f0f0; padding: 8px; border-radius: 4px; text-align: center; }
|
|
.test-stat-value { font-weight: bold; color: #2196F3; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>📊 Auto-Rapport Tests d'Intégration</h1>
|
|
<p>Généré automatiquement le ${new Date().toLocaleString()}</p>
|
|
</div>
|
|
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<div class="stat-value">${stats.total}</div>
|
|
<div class="stat-label">Tests</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" style="color: #4CAF50">${stats.passed}</div>
|
|
<div class="stat-label">Réussis</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" style="color: #f44336">${stats.failed}</div>
|
|
<div class="stat-label">Échoués</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">${stats.totalLLMCalls}</div>
|
|
<div class="stat-label">LLM Calls</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">${Math.round(stats.totalDuration/1000)}s</div>
|
|
<div class="stat-label">Durée</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-list">
|
|
<h3>Résultats des Tests</h3>
|
|
${this.testResults.map((test, index) => {
|
|
const testLLMCalls = this.llmCalls.filter(call => call.testContext === test.name);
|
|
const totalDuration = testLLMCalls.reduce((sum, call) => sum + call.duration, 0);
|
|
return `
|
|
<div class="test-item clickable" onclick="openTestModal(${index})">
|
|
<div style="display: flex; align-items: center;">
|
|
<span class="test-status status-${test.status}"></span>
|
|
<div>
|
|
<div><strong>${test.name}</strong></div>
|
|
<div style="font-size: 12px; color: #666;">
|
|
${test.llmCallsCount} LLM calls • ${Math.round(totalDuration/1000)}s durée LLM
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="text-align: right;">
|
|
<div style="color: ${test.status === 'passed' ? '#4CAF50' : '#f44336'}; font-weight: bold;">
|
|
${test.status === 'passed' ? '✓ RÉUSSI' : '✗ ÉCHOUÉ'}
|
|
</div>
|
|
<div style="font-size: 10px; color: #2196F3; margin-top: 2px;">
|
|
🔍 Cliquer pour détails
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
|
|
${this.phases.length > 0 ? `
|
|
<div class="phases-section">
|
|
<h3>🔄 Pipeline Phases (${this.phases.length})</h3>
|
|
<div class="phase-timeline">
|
|
${this.phases.map((phase, index) => `
|
|
<div class="phase-item ${phase.status} clickable" onclick="openPhaseModal(${index})">
|
|
<div style="display: flex; align-items: center; flex: 1;">
|
|
<div class="phase-number ${phase.status}">${phase.number}</div>
|
|
<div style="flex: 1;">
|
|
<div><strong>Phase ${phase.number}/4: ${phase.name}</strong></div>
|
|
<div style="font-size: 12px; color: #666; margin-top: 2px;">
|
|
${phase.status === 'completed' ? '✅ Terminé' : '🔄 En cours'} • ${new Date(phase.timestamp).toLocaleTimeString()}
|
|
${phase.endTimestamp ? ` → ${new Date(phase.endTimestamp).toLocaleTimeString()}` : ''}
|
|
${phase.duration ? ` (${Math.round(phase.duration)}ms)` : ''}
|
|
</div>
|
|
${Object.keys(phase.metrics || {}).length > 0 ? `
|
|
<div style="font-size: 11px; color: #888; margin-top: 2px;">
|
|
${phase.metrics.totalElements ? `📊 ${phase.metrics.totalElements} éléments` : ''}
|
|
${phase.metrics.modifications ? ` • ✏️ ${phase.metrics.modifications} modifs` : ''}
|
|
${phase.metrics.processed && phase.metrics.total ? ` • 🎯 ${phase.metrics.processed}/${phase.metrics.total} traités` : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
<div style="text-align: right;">
|
|
<div style="color: ${phase.status === 'completed' ? '#4CAF50' : '#FF9800'}; font-weight: bold;">
|
|
${phase.status === 'completed' ? '✓ COMPLÉTÉ' : '⏳ EN COURS'}
|
|
</div>
|
|
<div style="font-size: 10px; color: #2196F3; margin-top: 2px;">
|
|
🔍 Cliquer pour détails
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="llm-section">
|
|
<h3>Appels LLM (${this.llmCalls.length})</h3>
|
|
${this.llmCalls.map((call, index) => {
|
|
const hasContent = call.prompt || call.response;
|
|
return `
|
|
<div class="llm-call ${hasContent ? 'clickable' : ''}" ${hasContent ? `onclick="openModal(${index})"` : ''}>
|
|
<div>
|
|
<strong>${call.provider}</strong> (${call.model})
|
|
<div style="font-size: 12px; color: #666; margin-top: 2px;">
|
|
Test: ${call.testContext}
|
|
</div>
|
|
</div>
|
|
<div style="text-align: right;">
|
|
<div>${call.duration}ms</div>
|
|
<div style="font-size: 12px; color: #666;">${call.tokens?.prompt || 0}→${call.tokens?.response || 0} tokens</div>
|
|
${hasContent ? '<div style="font-size: 10px; color: #2196F3; margin-top: 2px;">📋 Cliquer pour voir détails</div>' : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
|
|
<!-- Modal pour afficher les détails LLM -->
|
|
<div id="llmModal" class="modal">
|
|
<div class="modal-content">
|
|
<span class="close" onclick="closeModal()">×</span>
|
|
<h2 id="modalTitle">Détails de l'appel LLM</h2>
|
|
|
|
<div class="prompt-section">
|
|
<h4>🔍 Prompt envoyé</h4>
|
|
<div id="promptContent" class="prompt-content">Aucun prompt capturé</div>
|
|
</div>
|
|
|
|
<div class="response-section">
|
|
<h4>📥 Réponse reçue</h4>
|
|
<div id="responseContent" class="response-content">Aucune réponse capturée</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal pour afficher les détails de test -->
|
|
<div id="testModal" class="modal">
|
|
<div class="modal-content">
|
|
<span class="close" onclick="closeTestModal()">×</span>
|
|
<h2 id="testModalTitle">Détails du test</h2>
|
|
|
|
<div class="test-details-section">
|
|
<h4>📊 Statistiques</h4>
|
|
<div id="testStats" class="test-stats">
|
|
<!-- Stats will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-details-section">
|
|
<h4>🚀 Appels LLM associés</h4>
|
|
<div id="testLLMCalls" class="test-llm-list">
|
|
<!-- LLM calls will be listed here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal pour afficher les détails de phase -->
|
|
<div id="phaseModal" class="modal">
|
|
<div class="modal-content">
|
|
<span class="close" onclick="closePhaseModal()">×</span>
|
|
<h2 id="phaseModalTitle">Détails de la phase</h2>
|
|
|
|
<div class="test-details-section">
|
|
<h4>📊 Métriques de la phase</h4>
|
|
<div id="phaseMetrics" class="test-stats">
|
|
<!-- Metrics will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-details-section">
|
|
<h4>🚀 Appels LLM de cette phase</h4>
|
|
<div id="phaseLLMCalls" class="test-llm-list">
|
|
<!-- Phase LLM calls will be listed here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="test-details-section">
|
|
<h4>⏱️ Timeline</h4>
|
|
<div id="phaseTimeline" style="background: #f8f9fa; padding: 10px; border-radius: 4px;">
|
|
<!-- Timeline will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const llmCalls = ${JSON.stringify(this.llmCalls)};
|
|
const testResults = ${JSON.stringify(this.testResults)};
|
|
const phases = ${JSON.stringify(this.phases)};
|
|
|
|
function openModal(index) {
|
|
const call = llmCalls[index];
|
|
if (!call) return;
|
|
|
|
const modal = document.getElementById('llmModal');
|
|
const title = document.getElementById('modalTitle');
|
|
const promptContent = document.getElementById('promptContent');
|
|
const responseContent = document.getElementById('responseContent');
|
|
|
|
title.textContent = \`\${call.provider.toUpperCase()} (\${call.model}) - \${call.testContext}\`;
|
|
|
|
promptContent.textContent = call.prompt || 'Aucun prompt capturé';
|
|
responseContent.textContent = call.response || 'Aucune réponse capturée';
|
|
|
|
modal.style.display = 'block';
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('llmModal').style.display = 'none';
|
|
}
|
|
|
|
function openTestModal(index) {
|
|
const test = testResults[index];
|
|
if (!test) return;
|
|
|
|
const modal = document.getElementById('testModal');
|
|
const title = document.getElementById('testModalTitle');
|
|
const statsDiv = document.getElementById('testStats');
|
|
const llmCallsDiv = document.getElementById('testLLMCalls');
|
|
|
|
title.textContent = \`Test: \${test.name}\`;
|
|
|
|
// Get LLM calls for this test
|
|
const testLLMCalls = llmCalls.filter(call => call.testContext === test.name);
|
|
const totalDuration = testLLMCalls.reduce((sum, call) => sum + call.duration, 0);
|
|
const totalTokensIn = testLLMCalls.reduce((sum, call) => sum + (call.tokens?.prompt || 0), 0);
|
|
const totalTokensOut = testLLMCalls.reduce((sum, call) => sum + (call.tokens?.response || 0), 0);
|
|
|
|
// Populate stats
|
|
statsDiv.innerHTML = \`
|
|
<div class="test-stat">
|
|
<div class="test-stat-value">\${test.status === 'passed' ? '✓' : '✗'}</div>
|
|
<div>Statut</div>
|
|
</div>
|
|
<div class="test-stat">
|
|
<div class="test-stat-value">\${testLLMCalls.length}</div>
|
|
<div>LLM Calls</div>
|
|
</div>
|
|
<div class="test-stat">
|
|
<div class="test-stat-value">\${Math.round(totalDuration/1000)}s</div>
|
|
<div>Durée LLM</div>
|
|
</div>
|
|
<div class="test-stat">
|
|
<div class="test-stat-value">\${totalTokensIn}</div>
|
|
<div>Tokens Input</div>
|
|
</div>
|
|
<div class="test-stat">
|
|
<div class="test-stat-value">\${totalTokensOut}</div>
|
|
<div>Tokens Output</div>
|
|
</div>
|
|
\`;
|
|
|
|
// Populate LLM calls
|
|
if (testLLMCalls.length > 0) {
|
|
llmCallsDiv.innerHTML = testLLMCalls.map((call, callIndex) => {
|
|
const globalIndex = llmCalls.findIndex(c => c === call);
|
|
return \`
|
|
<div class="test-llm-item" onclick="openModal(\${globalIndex})">
|
|
<div>
|
|
<strong>\${call.provider.toUpperCase()}</strong> (\${call.model})
|
|
<div style="font-size: 11px; color: #666; margin-top: 2px;">
|
|
\${new Date(call.timestamp).toLocaleTimeString()}
|
|
</div>
|
|
</div>
|
|
<div style="text-align: right;">
|
|
<div>\${call.duration}ms</div>
|
|
<div style="font-size: 11px; color: #666;">
|
|
\${call.tokens?.prompt || 0}→\${call.tokens?.response || 0} tokens
|
|
</div>
|
|
</div>
|
|
</div>
|
|
\`;
|
|
}).join('');
|
|
} else {
|
|
llmCallsDiv.innerHTML = '<div style="color: #666; text-align: center; padding: 20px;">Aucun appel LLM pour ce test</div>';
|
|
}
|
|
|
|
modal.style.display = 'block';
|
|
}
|
|
|
|
function closeTestModal() {
|
|
document.getElementById('testModal').style.display = 'none';
|
|
}
|
|
|
|
function openPhaseModal(index) {
|
|
const phase = phases[index];
|
|
if (!phase) return;
|
|
|
|
const modal = document.getElementById('phaseModal');
|
|
const title = document.getElementById('phaseModalTitle');
|
|
const metricsDiv = document.getElementById('phaseMetrics');
|
|
const llmCallsDiv = document.getElementById('phaseLLMCalls');
|
|
const timelineDiv = document.getElementById('phaseTimeline');
|
|
|
|
title.textContent = \`Phase \${phase.number}/4: \${phase.name}\`;
|
|
|
|
// Populate metrics
|
|
const metrics = phase.metrics || {};
|
|
metricsDiv.innerHTML = \`
|
|
<div class="test-stat">
|
|
<div class="test-stat-value">\${phase.status === 'completed' ? '✓' : '⏳'}</div>
|
|
<div>Statut</div>
|
|
</div>
|
|
<div class="test-stat">
|
|
<div class="test-stat-value">\${metrics.totalElements || 0}</div>
|
|
<div>Éléments</div>
|
|
</div>
|
|
<div class="test-stat">
|
|
<div class="test-stat-value">\${metrics.modifications || 0}</div>
|
|
<div>Modifications</div>
|
|
</div>
|
|
<div class="test-stat">
|
|
<div class="test-stat-value">\${metrics.processed || 0}/\${metrics.total || 0}</div>
|
|
<div>Traités</div>
|
|
</div>
|
|
<div class="test-stat">
|
|
<div class="test-stat-value">\${phase.duration ? Math.round(phase.duration) + 'ms' : 'N/A'}</div>
|
|
<div>Durée</div>
|
|
</div>
|
|
\`;
|
|
|
|
// Get LLM calls for this phase (by timestamp range)
|
|
const phaseStart = new Date(phase.timestamp);
|
|
const phaseEnd = phase.endTimestamp ? new Date(phase.endTimestamp) : new Date();
|
|
const phaseLLMCalls = llmCalls.filter(call => {
|
|
const callTime = new Date(call.timestamp);
|
|
return callTime >= phaseStart && callTime <= phaseEnd;
|
|
});
|
|
|
|
// Populate LLM calls
|
|
if (phaseLLMCalls.length > 0) {
|
|
llmCallsDiv.innerHTML = phaseLLMCalls.map((call, callIndex) => {
|
|
const globalIndex = llmCalls.findIndex(c => c === call);
|
|
return \`
|
|
<div class="test-llm-item" onclick="openModal(\${globalIndex})">
|
|
<div>
|
|
<strong>\${call.provider.toUpperCase()}</strong> (\${call.model})
|
|
<div style="font-size: 11px; color: #666; margin-top: 2px;">
|
|
\${new Date(call.timestamp).toLocaleTimeString()}
|
|
</div>
|
|
</div>
|
|
<div style="text-align: right;">
|
|
<div>\${call.duration}ms</div>
|
|
<div style="font-size: 11px; color: #666;">
|
|
\${call.promptTokens || 0}→\${call.responseTokens || 0} tokens
|
|
</div>
|
|
</div>
|
|
</div>
|
|
\`;
|
|
}).join('');
|
|
} else {
|
|
llmCallsDiv.innerHTML = '<div style="color: #666; text-align: center; padding: 20px;">Aucun appel LLM pour cette phase</div>';
|
|
}
|
|
|
|
// Populate timeline
|
|
timelineDiv.innerHTML = \`
|
|
<div style="margin-bottom: 10px;">
|
|
<strong>🚀 Début:</strong> \${new Date(phase.timestamp).toLocaleTimeString()}
|
|
</div>
|
|
\${phase.endTimestamp ? \`
|
|
<div style="margin-bottom: 10px;">
|
|
<strong>✅ Fin:</strong> \${new Date(phase.endTimestamp).toLocaleTimeString()}
|
|
</div>
|
|
<div style="margin-bottom: 10px;">
|
|
<strong>⏱️ Durée:</strong> \${phase.duration ? Math.round(phase.duration) + 'ms' : 'N/A'}
|
|
</div>
|
|
\` : '<div style="color: #FF9800;"><strong>⏳ En cours...</strong></div>'}
|
|
<div>
|
|
<strong>🧪 Test:</strong> \${phase.testContext || 'unknown'}
|
|
</div>
|
|
\`;
|
|
|
|
modal.style.display = 'block';
|
|
}
|
|
|
|
function closePhaseModal() {
|
|
document.getElementById('phaseModal').style.display = 'none';
|
|
}
|
|
|
|
// Fermer modal en cliquant à l'extérieur
|
|
window.onclick = function(event) {
|
|
const llmModal = document.getElementById('llmModal');
|
|
const testModal = document.getElementById('testModal');
|
|
const phaseModal = document.getElementById('phaseModal');
|
|
if (event.target === llmModal) {
|
|
closeModal();
|
|
}
|
|
if (event.target === testModal) {
|
|
closeTestModal();
|
|
}
|
|
if (event.target === phaseModal) {
|
|
closePhaseModal();
|
|
}
|
|
}
|
|
|
|
// Fermer modal avec Escape
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.key === 'Escape') {
|
|
closeModal();
|
|
closeTestModal();
|
|
closePhaseModal();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
}
|
|
|
|
export { AutoReporter }; |