} Session ID or null if failed
+ */
+ async createUserSession() {
+ try {
+ console.log('๐ Creating user session...');
+
+ const response = await fetch(API_ENDPOINTS.CREATE_SESSION, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ client_info: {
+ timestamp: new Date().toISOString(),
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ language: navigator.language
+ }
+ })
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ this.sessionId = data.session_id;
+ console.log('โ
User session created:', this.sessionId);
+
+ // Store session in local storage for page refresh persistence
+ localStorage.setItem('ragEvaluatorSessionId', this.sessionId);
+
+ // Update UI to show session info
+ this.updateSessionInfo();
+
+ return this.sessionId;
+ } else {
+ await ErrorHandler.handleNetworkError(response, 'Session creation');
+ return null;
+ }
+ } catch (error) {
+ ErrorHandler.handleError(error, 'Session creation');
+ return null;
+ }
+ }
+
+ async validateExistingSession() {
+ // Validate an existing session from localStorage
+ const storedSessionId = localStorage.getItem('ragEvaluatorSessionId');
+ if (!storedSessionId) {
+ return false;
+ }
+
+ try {
+ const response = await fetch(`${API_ENDPOINTS.SESSION_STATUS}/${storedSessionId}`);
+ if (response.ok) {
+ const data = await response.json();
+ if (data.is_valid) {
+ this.sessionId = storedSessionId;
+ console.log('โ
Restored existing session:', this.sessionId);
+ this.updateSessionInfo();
+ return true;
+ }
+ }
+ } catch (error) {
+ console.warn('โ ๏ธ Error validating existing session:', error);
+ // Clear invalid session
+ localStorage.removeItem('ragEvaluatorSessionId');
+ }
+
+ return false;
+ }
+
+ updateSessionInfo() {
+ // Update UI with session information
+ if (this.sessionId) {
+ // Add session indicator to header
+ const header = document.querySelector('.header-content');
+ if (header && !document.getElementById('session-indicator')) {
+ const sessionIndicator = document.createElement('div');
+ sessionIndicator.id = 'session-indicator';
+ sessionIndicator.className = 'session-indicator';
+ sessionIndicator.innerHTML = `
+
+
+
Session: ${this.sessionId.substring(0, 8)}...
+
+
+ Active
+
+
+ `;
+ header.appendChild(sessionIndicator);
+ }
+ }
+ }
+
+ setupFileUpload() {
+ const uploadArea = document.getElementById(ELEMENTS.UPLOAD_AREA);
+ const fileInput = document.getElementById(ELEMENTS.FILE_INPUT);
+ const removeFileBtn = document.getElementById(ELEMENTS.REMOVE_FILE);
+
+ if (uploadArea && fileInput) {
+ uploadArea.addEventListener('click', () => {
+ if (!this.evaluationInProgress) fileInput.click();
+ });
+
+ uploadArea.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ if (!this.evaluationInProgress) {
+ uploadArea.classList.add('dragover');
+ }
+ });
+
+ uploadArea.addEventListener('dragleave', () => {
+ uploadArea.classList.remove('dragover');
+ });
+
+ uploadArea.addEventListener('drop', (e) => {
+ e.preventDefault();
+ uploadArea.classList.remove('dragover');
+ if (!this.evaluationInProgress && e.dataTransfer.files.length > 0) {
+ this.handleFileUpload(e.dataTransfer.files[0]);
+ }
+ });
+
+ fileInput.addEventListener('change', (e) => {
+ if (e.target.files.length > 0) {
+ this.handleFileUpload(e.target.files[0]);
+ }
+ });
+ }
+
+ if (removeFileBtn) {
+ removeFileBtn.addEventListener('click', () => this.removeFile());
+ }
+ }
+
+ setupResultsButtons() {
+ console.log('๐ Setting up results buttons...');
+
+ // Use a simple document-level click handler
+ document.addEventListener('click', (e) => {
+ const target = e.target;
+
+ // Check if click is on a results button or its child elements
+ if (target.id === ELEMENTS.DOWNLOAD_RESULTS || target.closest(`#${ELEMENTS.DOWNLOAD_RESULTS}`)) {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('๐ฅ Download Results button clicked');
+ this.downloadResults();
+ return;
+ }
+
+ if (target.id === ELEMENTS.VIEW_DETAILS || target.closest(`#${ELEMENTS.VIEW_DETAILS}`)) {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('๐๏ธ View Details button clicked');
+ this.viewDetails();
+ return;
+ }
+
+ if (target.id === ELEMENTS.NEW_EVALUATION || target.closest(`#${ELEMENTS.NEW_EVALUATION}`)) {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('๐ New Evaluation button clicked');
+ this.resetForNewEvaluation();
+ return;
+ }
+ });
+
+ // Also set up direct listeners as backup
+ setTimeout(() => this.setupDirectListeners(), 100);
+ }
+
+ setupDirectListeners() {
+ const buttons = [
+ { id: ELEMENTS.DOWNLOAD_RESULTS, handler: () => this.downloadResults(), label: 'Download' },
+ { id: ELEMENTS.VIEW_DETAILS, handler: () => this.viewDetails(), label: 'View Details' },
+ { id: 'toggle-quality-analysis', handler: () => this.toggleQualityAnalysis(), label: 'Toggle Quality Analysis' },
+ { id: ELEMENTS.NEW_EVALUATION, handler: () => this.resetForNewEvaluation(), label: 'New Evaluation' }
+ ];
+
+ buttons.forEach(({ id, handler, label }) => {
+ const btn = document.getElementById(id);
+ if (btn) {
+ // Remove existing listeners
+ btn.replaceWith(btn.cloneNode(true));
+ // Add new listener to the fresh button
+ const freshBtn = document.getElementById(id);
+ freshBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log(`๐ ${label} button clicked (direct)`);
+ handler();
+ });
+ console.log(`โ
${label} button listener attached`);
+ } else {
+ console.log(`โ ๏ธ ${label} button not found`);
+ }
+ });
+ }
+
+ setupFormHandlers() {
+ // Monitor form changes for validation
+ const formInputs = document.querySelectorAll(`#${ELEMENTS.BATCH_SIZE}, #${ELEMENTS.MAX_CONCURRENT}, #${ELEMENTS.USE_SEARCH_API}, #${ELEMENTS.EVALUATE_RAGAS}, #${ELEMENTS.EVALUATE_CRAG}, #${ELEMENTS.EVALUATE_LLM}`);
+ formInputs.forEach(input => {
+ input.addEventListener('change', () => {
+ this.estimateProcessingTime();
+ this.validateForm();
+ });
+ });
+
+ // Handle Search API configuration visibility
+ const useSearchApiCheckbox = document.getElementById(ELEMENTS.USE_SEARCH_API);
+ const searchApiConfig = document.getElementById('search-api-config');
+
+ if (useSearchApiCheckbox && searchApiConfig) {
+ useSearchApiCheckbox.addEventListener('change', () => {
+ searchApiConfig.style.display = useSearchApiCheckbox.checked ? 'block' : 'none';
+ this.validateForm();
+ });
+ }
+
+ // Handle evaluation method changes
+ const ragasCheckbox = document.getElementById(ELEMENTS.EVALUATE_RAGAS);
+ const cragCheckbox = document.getElementById(ELEMENTS.EVALUATE_CRAG);
+ const llmCheckbox = document.getElementById(ELEMENTS.EVALUATE_LLM);
+
+ if (ragasCheckbox) {
+ ragasCheckbox.addEventListener('change', () => {
+ this.estimateProcessingTime();
+ this.validateForm();
+ });
+ }
+
+ if (cragCheckbox) {
+ cragCheckbox.addEventListener('change', () => {
+ this.estimateProcessingTime();
+ this.validateForm();
+ });
+ }
+
+ if (llmCheckbox) {
+ llmCheckbox.addEventListener('change', () => {
+ this.estimateProcessingTime();
+ this.validateForm();
+ });
+ }
+
+
+
+ // Handle LLM Model configuration visibility
+ const llmModelSelect = document.getElementById(ELEMENTS.LLM_MODEL);
+ const llmConfig = document.getElementById('llm-config');
+ const openaiConfig = document.getElementById('openai-config');
+ const azureConfig = document.getElementById('azure-config');
+
+ if (llmModelSelect && llmConfig) {
+ llmModelSelect.addEventListener('change', () => {
+ const selectedModel = llmModelSelect.value;
+
+ if (selectedModel) {
+ llmConfig.style.display = 'block';
+
+ if (selectedModel.startsWith('openai-') && openaiConfig) {
+ openaiConfig.style.display = 'block';
+ if (azureConfig) azureConfig.style.display = 'none';
+ } else if (selectedModel.startsWith('azure-') && azureConfig) {
+ if (openaiConfig) openaiConfig.style.display = 'none';
+ azureConfig.style.display = 'block';
+ }
+ } else {
+ llmConfig.style.display = 'none';
+ if (openaiConfig) openaiConfig.style.display = 'none';
+ if (azureConfig) azureConfig.style.display = 'none';
+ }
+ this.validateForm();
+ });
+ }
+
+ // Handle API type selection
+ const apiTypeRadios = document.querySelectorAll('input[name="api-type"]');
+ apiTypeRadios.forEach(radio => {
+ radio.addEventListener('change', () => this.validateForm());
+ });
+
+ // Handle configuration input validation
+ const configInputs = document.querySelectorAll('#search-api-config input, #llm-config input');
+ configInputs.forEach(input => {
+ input.addEventListener('input', () => this.validateForm());
+ });
+
+ // Setup performance presets
+ this.setupPerformancePresets();
+ }
+
+ setupPerformancePresets() {
+ // Setup radio button change handlers
+ const radioButtons = document.querySelectorAll('.preset-radio');
+ radioButtons.forEach(radio => {
+ radio.addEventListener('change', () => {
+ if (radio.checked) {
+ const preset = radio.value;
+ this.applyPreset(preset);
+ this.showToast(`${this.getPresetName(preset)} preset selected`, 'success');
+ }
+ });
+ });
+
+ // Setup info buttons with hover and click
+ const infoButtons = document.querySelectorAll('.info-btn');
+ infoButtons.forEach(btn => {
+ let hoverTimeout;
+ let isModalVisible = false;
+
+ // Show on hover
+ btn.addEventListener('mouseenter', (e) => {
+ clearTimeout(hoverTimeout);
+ if (!isModalVisible) {
+ hoverTimeout = setTimeout(() => {
+ const preset = btn.dataset.preset;
+ this.showPresetInfoModal(preset, true); // true = hover mode
+ isModalVisible = true;
+ }, 500); // Delay to prevent accidental triggers
+ }
+ });
+
+ // Cancel hover timeout if mouse leaves before showing
+ btn.addEventListener('mouseleave', (e) => {
+ clearTimeout(hoverTimeout);
+ // Don't auto-close modal - user must click X to close
+ });
+
+ // Also show on click
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ e.stopImmediatePropagation(); // Prevent radio button toggle
+ const preset = btn.dataset.preset;
+ this.showPresetInfoModal(preset, false); // false = click mode
+ isModalVisible = true;
+ });
+
+ // Reset modal visibility flag when modal is closed
+ document.addEventListener('modalClosed', () => {
+ isModalVisible = false;
+ });
+ });
+
+ // Apply default preset (Balanced) - already checked in HTML
+ this.applyPreset('balanced');
+ }
+
+
+
+ showPresetInfoModal(preset, isHoverMode = false) {
+ const presetData = this.getPresetData(preset);
+ if (!presetData) return;
+
+ // Remove any existing modals first
+ const existingModal = document.querySelector('.preset-info-modal');
+ if (existingModal) {
+ existingModal.remove();
+ }
+
+ // Create modal
+ const modal = document.createElement('div');
+ modal.className = 'preset-info-modal';
+ modal.dataset.hoverMode = isHoverMode.toString();
+ modal.innerHTML = `
+
+
+
+
+
+ Batch Size
+ ${presetData.batch}
+
+
+ Concurrent
+ ${presetData.concurrent}
+
+
+
+
+
Benefits & Features
+ ${presetData.benefits.map(benefit => `
+
+
+ ${benefit.emoji}
+
+
${benefit.text}
+
+ `).join('')}
+
+
+
+
Performance Characteristics
+
+
+
+
+
${presetData.stats.speed}%
+
+
+
+
+
๐พ
+
Resource Usage
+
+
+
+
${presetData.stats.resource}%
+
+
+
+
+
+
+
${presetData.stats.stability}%
+
+
+
+
+
+ Best for: ${presetData.recommendation}
+
+
+
+ `;
+
+ // Add to DOM at the very end of body and prevent body scroll
+ document.body.appendChild(modal);
+ document.body.classList.add('modal-open');
+
+ // Force positioning with inline styles to override any conflicts
+ modal.style.cssText = `
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100% !important;
+ height: 100% !important;
+ z-index: 999999 !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ padding: 20px !important;
+ box-sizing: border-box !important;
+ background: rgba(0, 0, 0, 0.5) !important;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease;
+ `;
+
+ // Show modal with simple animation
+ setTimeout(() => {
+ modal.classList.add('show');
+ modal.style.opacity = '1';
+ modal.style.visibility = 'visible';
+ }, 10);
+
+ // Setup close handlers - both modes work the same now
+ const closeBtn = modal.querySelector('.preset-close-btn');
+ closeBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.closePresetInfoModal(modal);
+ });
+
+ // Close on backdrop click (for both modes)
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ this.closePresetInfoModal(modal);
+ }
+ });
+
+ // Prevent clicks inside the modal content from closing the modal
+ const content = modal.querySelector('.preset-info-content');
+ if (content) {
+ content.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+ }
+
+ // ESC key handler (for both modes)
+ const escHandler = (e) => {
+ if (e.key === 'Escape') {
+ this.closePresetInfoModal(modal);
+ document.removeEventListener('keydown', escHandler);
+ }
+ };
+ document.addEventListener('keydown', escHandler);
+
+ // Store the ESC handler for cleanup
+ modal._escHandler = escHandler;
+ }
+
+ closePresetInfoModal(modal) {
+ if (!modal) return;
+
+ modal.classList.remove('show');
+ modal.style.opacity = '0';
+ modal.style.visibility = 'hidden';
+
+ // Remove modal-open class
+ document.body.classList.remove('modal-open');
+
+ // Clean up ESC key handler
+ if (modal._escHandler) {
+ document.removeEventListener('keydown', modal._escHandler);
+ }
+
+ // Dispatch custom event to reset modal visibility flags
+ document.dispatchEvent(new CustomEvent('modalClosed'));
+
+ setTimeout(() => {
+ if (modal.parentNode) {
+ modal.parentNode.removeChild(modal);
+ }
+ }, 300);
+ }
+
+ getPresetData(preset) {
+ const presets = {
+ 'conservative': {
+ name: 'Conservative',
+ subtitle: 'Safe & Stable Processing',
+ icon: 'fas fa-shield-alt',
+ batch: 5,
+ concurrent: 2,
+ benefits: [
+ { emoji: '๐พ', type: 'positive', text: 'Lower memory usage (~512MB)' },
+ { emoji: '๐ก๏ธ', type: 'positive', text: 'More stable processing' },
+ { emoji: '๐ฆ', type: 'positive', text: 'Less API rate limiting' },
+ { emoji: 'โก', type: 'positive', text: 'Works on limited resources' },
+ { emoji: 'โฑ๏ธ', type: 'positive', text: 'Reduced timeout risks' }
+ ],
+ stats: { speed: 30, resource: 25, stability: 95 },
+ recommendation: 'Large datasets (1000+ queries), limited resources, first-time users, or production environments where stability is critical'
+ },
+ 'balanced': {
+ name: 'Balanced',
+ subtitle: 'Optimal Performance & Reliability',
+ icon: 'fas fa-balance-scale',
+ batch: 10,
+ concurrent: 5,
+ benefits: [
+ { emoji: 'โ๏ธ', type: 'positive', text: 'Good speed & stability balance' },
+ { emoji: '๐พ', type: 'positive', text: 'Moderate resource usage (~1GB)' },
+ { emoji: '๐', type: 'positive', text: 'Reliable for most datasets' },
+ { emoji: 'โญ', type: 'positive', text: 'Optimal for general usage' },
+ { emoji: 'โ
', type: 'positive', text: 'Well-tested configuration' }
+ ],
+ stats: { speed: 65, resource: 50, stability: 85 },
+ recommendation: 'Most evaluation tasks, medium datasets (100-1000 queries), general usage, and users who want the best overall experience'
+ },
+ 'performance': {
+ name: 'High Performance',
+ subtitle: 'Maximum Speed Processing',
+ icon: 'fas fa-rocket',
+ batch: 20,
+ concurrent: 10,
+ benefits: [
+ { emoji: '๐', type: 'positive', text: 'Maximum processing speed' },
+ { emoji: '๐', type: 'positive', text: 'Optimal for large datasets' },
+ { emoji: 'โก', type: 'positive', text: 'Best throughput rates' },
+ { emoji: 'โ ๏ธ', type: 'warning', text: 'Requires adequate resources (2GB+)' },
+ { emoji: '๐ง', type: 'warning', text: 'May hit API rate limits' }
+ ],
+ stats: { speed: 95, resource: 85, stability: 70 },
+ recommendation: 'Large datasets (500+ queries), powerful systems (8GB+ RAM), experienced users, and time-critical evaluations'
+ }
+ };
+
+ return presets[preset];
+ }
+
+
+
+ getPresetName(preset) {
+ const names = {
+ 'conservative': 'Conservative',
+ 'balanced': 'Balanced',
+ 'performance': 'High Performance'
+ };
+ return names[preset] || preset;
+ }
+
+ applyPreset(preset) {
+ const batchSizeInput = document.getElementById('batch-size');
+ const maxConcurrentInput = document.getElementById('max-concurrent');
+
+ if (!batchSizeInput || !maxConcurrentInput) return;
+
+ const presets = {
+ 'conservative': {
+ batch: 5,
+ concurrent: 2,
+ description: 'Safer processing with lower resource usage'
+ },
+ 'balanced': {
+ batch: 10,
+ concurrent: 5,
+ description: 'Good balance of speed and stability'
+ },
+ 'performance': {
+ batch: 20,
+ concurrent: 10,
+ description: 'Maximum speed for large datasets'
+ }
+ };
+
+ if (presets[preset]) {
+ // Animate the changes
+ const currentBatch = parseInt(batchSizeInput.value);
+ const currentConcurrent = parseInt(maxConcurrentInput.value);
+ const targetBatch = presets[preset].batch;
+ const targetConcurrent = presets[preset].concurrent;
+
+ // Update values
+ batchSizeInput.value = targetBatch;
+ maxConcurrentInput.value = targetConcurrent;
+
+ // Show visual feedback for the changes
+ if (currentBatch !== targetBatch || currentConcurrent !== targetConcurrent) {
+ this.highlightChangedInputs([batchSizeInput, maxConcurrentInput]);
+ }
+
+ // Update estimates and validation
+ this.estimateProcessingTime();
+ this.validateForm();
+
+ console.log(`๐ฏ Applied ${preset} preset: ${targetBatch} batch, ${targetConcurrent} concurrent`);
+ }
+ }
+
+ highlightChangedInputs(inputs) {
+ inputs.forEach(input => {
+ input.style.transition = 'all 0.3s ease';
+ input.style.background = '#fef3c7';
+ input.style.borderColor = '#f59e0b';
+
+ setTimeout(() => {
+ input.style.background = '';
+ input.style.borderColor = '';
+ }, 1000);
+ });
+ }
+
+ handleFileUpload(file) {
+ // Validate file type
+ const validTypes = ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel'];
+ if (!validTypes.includes(file.type) && !file.name.toLowerCase().endsWith('.xlsx') && !file.name.toLowerCase().endsWith('.xls')) {
+ this.showToast('Please upload a valid Excel file (.xlsx or .xls)', 'error');
+ return;
+ }
+
+ if (file.size > 50 * 1024 * 1024) {
+ this.showToast('File size must be less than 50MB', 'error');
+ return;
+ }
+
+ this.uploadedFile = file;
+ this.displayFileInfo(file);
+
+ // Show immediate feedback and then analyze sheets
+ this.showToast(`File "${file.name}" uploaded successfully. Analyzing sheets...`, 'info');
+ this.loadSheetNames(file);
+ this.validateForm();
+ this.estimateProcessingTime();
+ }
+
+ displayFileInfo(file) {
+ const uploadArea = document.getElementById('upload-area');
+ const fileInfo = document.getElementById('file-info');
+ const fileName = document.getElementById('file-name');
+ const fileSize = document.getElementById('file-size');
+
+ if (fileName) fileName.textContent = file.name;
+ if (fileSize) fileSize.textContent = this.formatFileSize(file.size);
+ if (uploadArea) uploadArea.style.display = 'none';
+ if (fileInfo) fileInfo.style.display = 'block';
+ }
+
+ removeFile() {
+ this.uploadedFile = null;
+ this.fileRowCounts = null; // Clear row counts
+
+ const uploadArea = document.getElementById('upload-area');
+ const fileInfo = document.getElementById('file-info');
+ const sheetSelect = document.getElementById('sheet-name');
+
+ if (uploadArea) uploadArea.style.display = 'block';
+ if (fileInfo) fileInfo.style.display = 'none';
+ if (sheetSelect) sheetSelect.innerHTML = 'All sheets ';
+
+ this.validateForm();
+ this.estimateProcessingTime(); // Reset estimates
+ }
+
+ async loadSheetNames(file) {
+ console.log('๐ Loading sheet names for file:', file.name);
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await fetch('/api/get-sheet-names', {
+ method: 'POST',
+ body: formData
+ });
+
+ console.log('๐ก Sheet names API response status:', response.status);
+
+ if (response.ok) {
+ const data = await response.json();
+ console.log('๐ Sheet names data received:', data);
+
+ if (data.sheet_names && data.sheet_names.length > 0) {
+ console.log('โ
Found sheets:', data.sheet_names);
+ this.populateSheetDropdown(data.sheet_names);
+ this.showToast(`Found ${data.sheet_names.length} sheet(s) in the Excel file`, 'info');
+ } else {
+ console.warn('โ ๏ธ No sheet names found in response');
+ this.populateSheetDropdown([]);
+ }
+
+ // Get row count for better estimation
+ if (data.row_counts) {
+ this.fileRowCounts = data.row_counts;
+ console.log('๐ File row counts:', this.fileRowCounts);
+ this.estimateProcessingTime(); // Re-estimate with actual data
+ }
+ } else {
+ const errorText = await response.text();
+ console.error('โ Failed to load sheet names:', response.status, errorText);
+ this.showToast('Failed to analyze Excel file sheets', 'error');
+ }
+ } catch (error) {
+ console.error('โ Error loading sheet names:', error);
+ this.showToast('Error analyzing Excel file: ' + error.message, 'error');
+ }
+ }
+
+ populateSheetDropdown(sheetNames) {
+ console.log('๐ Populating sheet dropdown with:', sheetNames);
+ const sheetSelect = document.getElementById('sheet-name');
+ if (sheetSelect) {
+ // Always start with "All sheets" option which is selected by default
+ sheetSelect.innerHTML = 'All sheets ';
+
+ if (sheetNames && sheetNames.length > 0) {
+ sheetNames.forEach((sheetName, index) => {
+ const option = document.createElement('option');
+ option.value = sheetName;
+ option.textContent = sheetName;
+ sheetSelect.appendChild(option);
+ console.log(`๐ Added sheet option ${index + 1}: "${sheetName}"`);
+ });
+
+ console.log(`โ
Successfully populated dropdown with ${sheetNames.length} sheet(s) + "All sheets" option`);
+ } else {
+ console.log('โน๏ธ No individual sheets to add, only "All sheets" option available');
+ }
+
+ // Add event listener for sheet selection changes to update estimates
+ sheetSelect.removeEventListener('change', this.handleSheetChange); // Remove existing
+ this.handleSheetChange = () => {
+ console.log('๐ Sheet selection changed, updating estimates...');
+ this.estimateProcessingTime();
+ };
+ sheetSelect.addEventListener('change', this.handleSheetChange);
+ } else {
+ console.error('โ Could not find sheet-name dropdown element');
+ }
+ }
+
+ estimateProcessingTime() {
+ const estimatedTimeElement = document.getElementById('estimated-time');
+ const totalQueriesElement = document.getElementById('total-queries');
+
+ if (!this.uploadedFile) {
+ // No file uploaded - show placeholders
+ if (estimatedTimeElement) {
+ estimatedTimeElement.textContent = '--';
+ estimatedTimeElement.title = 'Upload a file to see estimation';
+ }
+ if (totalQueriesElement) {
+ totalQueriesElement.textContent = '--';
+ totalQueriesElement.title = 'Upload a file to see query count';
+ }
+
+ // Clear estimation details
+ const detailsContainer = document.getElementById('estimation-details');
+ if (detailsContainer) {
+ detailsContainer.style.display = 'none';
+ }
+ return;
+ }
+
+ // Show loading while calculating
+ if (estimatedTimeElement) estimatedTimeElement.textContent = 'Calculating...';
+ if (totalQueriesElement) totalQueriesElement.textContent = 'Analyzing...';
+
+ // Get current settings
+ const batchSize = parseInt(document.getElementById(ELEMENTS.BATCH_SIZE)?.value || CONFIG.DEFAULT_BATCH_SIZE);
+ const maxConcurrent = parseInt(document.getElementById(ELEMENTS.MAX_CONCURRENT)?.value || CONFIG.DEFAULT_MAX_CONCURRENT);
+ const useSearchApi = document.getElementById(ELEMENTS.USE_SEARCH_API)?.checked || false;
+ const evaluateRagas = document.getElementById(ELEMENTS.EVALUATE_RAGAS)?.checked || false;
+ const evaluateCrag = document.getElementById(ELEMENTS.EVALUATE_CRAG)?.checked || false;
+ const evaluateLlm = document.getElementById(ELEMENTS.EVALUATE_LLM)?.checked || false;
+ const selectedSheet = document.getElementById(ELEMENTS.SHEET_SELECT)?.value || '';
+
+ // Calculate actual query count
+ let totalQueries = 0;
+
+ if (this.fileRowCounts) {
+ // Use actual row counts from Excel file
+ if (selectedSheet && this.fileRowCounts[selectedSheet]) {
+ // Specific sheet selected
+ totalQueries = this.fileRowCounts[selectedSheet];
+ console.log(`๐ Using row count for sheet '${selectedSheet}': ${totalQueries}`);
+ } else {
+ // All sheets or no specific sheet - sum all rows
+ totalQueries = Object.values(this.fileRowCounts).reduce((sum, count) => sum + count, 0);
+ console.log(`๐ Using total row count across all sheets: ${totalQueries}`);
+ }
+ } else {
+ // Fallback to file size estimation (less accurate)
+ totalQueries = Math.max(10, Math.floor(this.uploadedFile.size / (2 * 1024))); // Assume ~2KB per row
+ console.log(`๐ Fallback estimation based on file size: ${totalQueries} queries`);
+ }
+
+ // Improved time estimation based on real-world benchmarks
+ let timePerQuery = this.calculateTimePerQuery(useSearchApi, evaluateRagas, evaluateCrag, evaluateLlm);
+
+ // Account for batch processing and concurrency
+ const effectiveBatchSize = Math.min(batchSize, totalQueries);
+ const parallelBatches = Math.min(maxConcurrent, Math.ceil(totalQueries / effectiveBatchSize));
+ const totalBatches = Math.ceil(totalQueries / effectiveBatchSize);
+ const batchesPerRound = parallelBatches;
+ const totalRounds = Math.ceil(totalBatches / batchesPerRound);
+
+ // Calculate total time considering parallel processing
+ const totalTime = totalRounds * timePerQuery * effectiveBatchSize / parallelBatches;
+
+ // Add overhead for initialization, file I/O, etc.
+ const overheadTime = Math.max(CONFIG.BASE_OVERHEAD, totalQueries * CONFIG.OVERHEAD_PER_QUERY);
+ const finalTime = totalTime + overheadTime;
+
+ console.log(`๐ Estimation details:`, {
+ totalQueries,
+ timePerQuery: `${timePerQuery}s`,
+ effectiveBatchSize,
+ parallelBatches,
+ totalBatches,
+ totalRounds,
+ processingTime: `${totalTime}s`,
+ overheadTime: `${overheadTime}s`,
+ finalTime: `${finalTime}s`
+ });
+
+ // Update UI with calculated values
+ if (estimatedTimeElement) {
+ estimatedTimeElement.textContent = UIUtils.formatTime(finalTime);
+ estimatedTimeElement.title = `Based on ${totalQueries} queries with current settings`;
+ }
+
+ if (totalQueriesElement) {
+ totalQueriesElement.textContent = totalQueries.toLocaleString();
+ totalQueriesElement.title = selectedSheet ?
+ `From sheet: ${selectedSheet}` :
+ 'Across all sheets';
+ }
+
+ // Update estimates info
+ this.updateEstimationDetails(totalQueries, finalTime, useSearchApi, evaluateRagas, evaluateCrag, evaluateLlm);
+
+ // Show estimation details
+ const detailsContainer = document.getElementById('estimation-details');
+ if (detailsContainer) {
+ detailsContainer.style.display = 'block';
+ }
+
+ // No longer needed - preset details are shown in modal
+ }
+
+ /**
+ * Calculate estimated processing time per query based on enabled features
+ * @param {boolean} useSearchApi - Whether search API is enabled
+ * @param {boolean} evaluateRagas - Whether RAGAS evaluation is enabled
+ * @param {boolean} evaluateCrag - Whether CRAG evaluation is enabled
+ * @param {boolean} evaluateLlm - Whether LLM evaluation is enabled
+ * @returns {number} Estimated time per query in seconds
+ */
+ calculateTimePerQuery(useSearchApi, evaluateRagas, evaluateCrag, evaluateLlm) {
+ // Base time for basic processing
+ let timePerQuery = CONFIG.BASE_PROCESSING_TIME;
+
+ // Search API time (if enabled)
+ if (useSearchApi) {
+ timePerQuery += CONFIG.SEARCH_API_TIME;
+ }
+
+ // Calculate parallel evaluation time - evaluators run concurrently
+ const evaluationTimes = [];
+
+ // RAGAS evaluation time (most time-consuming)
+ if (evaluateRagas) {
+ evaluationTimes.push(CONFIG.RAGAS_EVAL_TIME);
+ }
+
+ // CRAG evaluation time
+ if (evaluateCrag) {
+ evaluationTimes.push(CONFIG.CRAG_EVAL_TIME);
+ }
+
+ // LLM evaluation time (3 API calls per query, but concurrent)
+ if (evaluateLlm) {
+ evaluationTimes.push(CONFIG.LLM_EVAL_TIME);
+ }
+
+ // When running in parallel, take the maximum time (longest running evaluator)
+ // rather than sum of all times
+ if (evaluationTimes.length > 0) {
+ timePerQuery += Math.max(...evaluationTimes);
+ }
+
+ // Minimum time
+ return Math.max(CONFIG.MIN_PROCESSING_TIME, timePerQuery);
+ }
+
+ updateEstimationDetails(queries, time, useSearchApi, evaluateRagas, evaluateCrag, evaluateLlm) {
+ // Create or update estimation breakdown
+ let detailsContainer = document.getElementById('estimation-details');
+ if (!detailsContainer) {
+ detailsContainer = document.createElement('div');
+ detailsContainer.id = 'estimation-details';
+ detailsContainer.className = 'estimation-details';
+
+ const parentContainer = document.querySelector('.run-info');
+ if (parentContainer) {
+ parentContainer.appendChild(detailsContainer);
+ }
+ }
+
+ const components = [];
+ if (useSearchApi) components.push('Search API');
+ if (evaluateRagas) components.push('RAGAS Evaluation');
+ if (evaluateCrag) components.push('CRAG Evaluation');
+ if (evaluateLlm) components.push('LLM Evaluation');
+
+ detailsContainer.innerHTML = `
+
+
+ Processing:
+ ${components.join(' + ') || 'Basic processing'}
+
+
+ Queries:
+ ${queries.toLocaleString()} rows detected
+
+
+ Est. Time:
+ ${this.formatTime(time)}
+
+
+ `;
+ }
+
+ validateForm() {
+ const startBtn = document.getElementById('start-evaluation');
+ if (!startBtn) return;
+
+ const evaluateRagas = document.getElementById('evaluate-ragas')?.checked || false;
+ const evaluateCrag = document.getElementById('evaluate-crag')?.checked || false;
+ const evaluateLlm = document.getElementById('evaluate-llm')?.checked || false;
+ const useSearchApi = document.getElementById('use-search-api')?.checked || false;
+ const llmModel = document.getElementById('llm-model')?.value || '';
+
+ let isValid = this.uploadedFile && (evaluateRagas || evaluateCrag || evaluateLlm);
+ let errorMessage = '';
+
+ if (!this.uploadedFile) {
+ errorMessage = ' Upload File First';
+ isValid = false;
+ } else if (!evaluateRagas && !evaluateCrag && !evaluateLlm) {
+ errorMessage = ' Select at least one Evaluation Method';
+ isValid = false;
+ }
+
+ if (isValid && useSearchApi) {
+ const apiType = document.querySelector('input[name="api-type"]:checked');
+ const appId = document.getElementById('api-app-id')?.value?.trim() || '';
+ const domain = document.getElementById('api-domain')?.value?.trim() || '';
+ const clientId = document.getElementById('api-client-id')?.value?.trim() || '';
+ const clientSecret = document.getElementById('api-client-secret')?.value?.trim() || '';
+
+ if (!apiType || !appId || !domain || !clientId || !clientSecret) {
+ errorMessage = ' Complete API Configuration';
+ isValid = false;
+ }
+ }
+
+ if (isValid && llmModel) {
+ if (llmModel.startsWith('openai-')) {
+ const apiKey = document.getElementById('openai-api-key')?.value?.trim() || '';
+ if (!apiKey) {
+ errorMessage = ' OpenAI API Key Required';
+ isValid = false;
+ }
+ } else if (llmModel.startsWith('azure-')) {
+ const endpoint = document.getElementById('azure-endpoint')?.value?.trim() || '';
+ const apiKey = document.getElementById('azure-api-key')?.value?.trim() || '';
+ const deployment = document.getElementById('azure-deployment')?.value?.trim() || '';
+
+ if (!endpoint || !apiKey || !deployment) {
+ errorMessage = ' Complete Azure Configuration';
+ isValid = false;
+ }
+ }
+ }
+
+ startBtn.disabled = !isValid || this.evaluationInProgress;
+
+ if (this.evaluationInProgress) {
+ startBtn.innerHTML = ' Evaluation in Progress...';
+ } else if (errorMessage) {
+ startBtn.innerHTML = errorMessage;
+ } else {
+ startBtn.innerHTML = ' Start Evaluation';
+ }
+ }
+
+ async startEvaluation() {
+ if (this.evaluationInProgress || !this.uploadedFile) return;
+
+ console.log('๐ Starting evaluation...');
+ this.evaluationInProgress = true;
+ this.startTime = Date.now();
+
+ // Show progress section
+ const progressSection = document.getElementById('progress-section');
+ const resultsSection = document.getElementById('results-section');
+
+ if (progressSection) progressSection.style.display = 'block';
+ if (resultsSection) resultsSection.style.display = 'none';
+
+ // Update status
+ const statusBadge = document.getElementById('status-badge');
+ if (statusBadge) {
+ statusBadge.textContent = 'Processing';
+ statusBadge.className = 'status-badge processing';
+ }
+
+ this.disableForm();
+ if (progressSection) {
+ progressSection.scrollIntoView({ behavior: 'smooth' });
+ }
+
+ this.startProgressTracking();
+ this.addLogEntry('Starting evaluation process...', 'info');
+
+ try {
+ const result = await this.callEvaluationAPI();
+ this.handleEvaluationSuccess(result);
+ } catch (error) {
+ this.handleEvaluationError(error);
+ }
+ }
+
+ async callEvaluationAPI() {
+ const formData = new FormData();
+ formData.append('excel_file', this.uploadedFile);
+
+ // Add session_id if available, but don't require it (for backward compatibility)
+ if (this.sessionId) {
+ console.log('๐ Using session ID for evaluation:', this.sessionId.substring(0, 8) + '...');
+ formData.append('session_id', this.sessionId);
+ } else {
+ console.log('โ ๏ธ No session ID available, server will create one');
+ }
+
+ const selectedSheet = document.getElementById('sheet-name')?.value || '';
+ const config = {
+ sheet_name: selectedSheet,
+ evaluate_ragas: document.getElementById('evaluate-ragas')?.checked || false,
+ evaluate_crag: document.getElementById('evaluate-crag')?.checked || false,
+ evaluate_llm: document.getElementById('evaluate-llm')?.checked || false,
+ use_search_api: document.getElementById('use-search-api')?.checked || false,
+ save_db: document.getElementById('save-db')?.checked || false,
+ llm_model: document.getElementById('llm-model')?.value || '',
+ batch_size: parseInt(document.getElementById('batch-size')?.value || 10),
+ max_concurrent: parseInt(document.getElementById('max-concurrent')?.value || 5)
+ };
+
+ // Log sheet selection for debugging
+ if (selectedSheet) {
+ console.log(`๐ User selected SPECIFIC SHEET: "${selectedSheet}"`);
+ } else {
+ console.log('๐ User selected ALL SHEETS (no specific sheet chosen)');
+ }
+
+ if (config.use_search_api) {
+ const apiType = document.querySelector('input[name="api-type"]:checked');
+ config.api_config = {
+ type: apiType ? apiType.value : '',
+ app_id: document.getElementById('api-app-id')?.value?.trim() || '',
+ domain: document.getElementById('api-domain')?.value?.trim() || '',
+ client_id: document.getElementById('api-client-id')?.value?.trim() || '',
+ client_secret: document.getElementById('api-client-secret')?.value?.trim() || ''
+ };
+ }
+
+ if (config.llm_model) {
+ if (config.llm_model.startsWith('openai-')) {
+ config.openai_config = {
+ api_key: document.getElementById('openai-api-key')?.value?.trim() || '',
+ org_id: document.getElementById('openai-org-id')?.value?.trim() || '',
+ model: config.llm_model.replace('openai-', 'gpt-')
+ };
+ } else if (config.llm_model.startsWith('azure-')) {
+ config.azure_config = {
+ endpoint: document.getElementById('azure-endpoint')?.value?.trim() || '',
+ api_key: document.getElementById('azure-api-key')?.value?.trim() || '',
+ deployment: document.getElementById('azure-deployment')?.value?.trim() || '',
+ api_version: document.getElementById('azure-api-version')?.value?.trim() || '',
+ embedding_deployment: document.getElementById('azure-embedding-deployment')?.value?.trim() || '',
+ model: config.llm_model.replace('azure-', 'gpt-')
+ };
+ }
+ }
+
+ formData.append('config', JSON.stringify(config));
+
+ console.log('๐ Sending request to /api/runeval with:');
+ console.log('๐ File:', this.uploadedFile?.name);
+ console.log('๐ Session ID:', this.sessionId);
+ console.log('โ๏ธ Config:', JSON.stringify(config, null, 2));
+
+ // Debug: Check FormData contents
+ console.log('๐งช FormData contents:');
+ for (let [key, value] of formData.entries()) {
+ if (value instanceof File) {
+ console.log(` ๐ ${key}: ${value.name} (${value.size} bytes, ${value.type})`);
+ } else {
+ console.log(` ๐ ${key}: ${value}`);
+ }
+ }
+
+ const response = await fetch('/api/runeval', {
+ method: 'POST',
+ body: formData
+ });
+
+ console.log('๐ก Response status:', response.status, response.statusText);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('โ Error response:', errorText);
+
+ let errorData = {};
+ try {
+ errorData = JSON.parse(errorText);
+ } catch (e) {
+ console.error('โ Could not parse error as JSON:', e);
+ }
+
+ throw new Error(errorData.detail || errorData.error || `HTTP ${response.status}: ${response.statusText}\n${errorText}`);
+ }
+
+ return await response.json();
+ }
+
+ startProgressTracking() {
+ let progress = 0;
+ let elapsedSeconds = 0;
+
+ this.progressInterval = setInterval(() => {
+ elapsedSeconds++;
+ if (progress < 90) progress += Math.random() * 2;
+ this.updateProgress(Math.min(progress, 95), elapsedSeconds);
+ }, 1000);
+ }
+
+ updateProgress(percentage, elapsedSeconds) {
+ const progressFill = document.getElementById('progress-fill');
+ const progressPercentage = document.getElementById('progress-percentage');
+ const elapsedTime = document.getElementById('elapsed-time');
+ const progressDetails = document.getElementById('progress-details');
+
+ if (progressFill) progressFill.style.width = `${percentage}%`;
+ if (progressPercentage) progressPercentage.textContent = `${Math.round(percentage)}%`;
+ if (elapsedTime) elapsedTime.textContent = this.formatTime(elapsedSeconds);
+
+ if (progressDetails) {
+ const stages = [
+ 'Initializing evaluation...',
+ 'Processing queries...',
+ 'Running RAGAS evaluation...',
+ 'Running CRAG evaluation...',
+ 'Finalizing results...'
+ ];
+ const stageIndex = Math.min(Math.floor(percentage / 20), stages.length - 1);
+ progressDetails.textContent = stages[stageIndex];
+ }
+ }
+
+ addLogEntry(message, type = 'info') {
+ const logOutput = document.getElementById('log-output');
+ if (!logOutput) return;
+
+ const entry = document.createElement('div');
+ entry.className = `log-entry ${type}`;
+ entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
+
+ logOutput.appendChild(entry);
+ logOutput.scrollTop = logOutput.scrollHeight;
+
+ // Limit log entries to 50
+ while (logOutput.children.length > 50) {
+ logOutput.removeChild(logOutput.firstChild);
+ }
+ }
+
+ handleEvaluationSuccess(result) {
+ console.log('โ
Evaluation completed successfully:', result);
+ this.evaluationInProgress = false;
+
+ if (this.progressInterval) {
+ clearInterval(this.progressInterval);
+ }
+
+ const elapsedSeconds = Math.floor((Date.now() - this.startTime) / 1000);
+ this.updateProgress(100, elapsedSeconds);
+
+ const statusBadge = document.getElementById('status-badge');
+ if (statusBadge) {
+ statusBadge.textContent = 'Completed';
+ statusBadge.className = 'status-badge success';
+ }
+
+ this.resultData = result;
+ console.log('๐พ Result data stored:', this.resultData);
+
+ // Update session ID from server response (important for download)
+ if (result.session_id && result.session_id !== this.sessionId) {
+ console.log(`๐ Updating session ID from server: ${this.sessionId} -> ${result.session_id}`);
+ this.sessionId = result.session_id;
+ localStorage.setItem('ragEvaluatorSessionId', this.sessionId);
+ this.updateSessionInfo();
+ }
+
+ setTimeout(() => {
+ this.showResults(result);
+ }, 1000);
+
+ this.addLogEntry('Evaluation completed successfully!', 'success');
+ this.showToast('Evaluation completed successfully!', 'success');
+ }
+
+ handleEvaluationError(error) {
+ console.error('โ Evaluation failed:', error);
+ this.evaluationInProgress = false;
+
+ if (this.progressInterval) {
+ clearInterval(this.progressInterval);
+ }
+
+ const statusBadge = document.getElementById('status-badge');
+ if (statusBadge) {
+ statusBadge.textContent = 'Failed';
+ statusBadge.className = 'status-badge error';
+ }
+
+ this.addLogEntry(`Error: ${error.message}`, 'error');
+ this.showToast(`Evaluation failed: ${error.message}`, 'error');
+ this.enableForm();
+ }
+
+ showResults(result) {
+ console.log('๐ Showing results...');
+ console.log('๐ Complete result data:', result);
+
+ // Hide progress, show results
+ const progressSection = document.getElementById('progress-section');
+ const resultsSection = document.getElementById('results-section');
+
+ if (progressSection) progressSection.style.display = 'none';
+ if (resultsSection) {
+ resultsSection.style.display = 'block';
+ resultsSection.scrollIntoView({ behavior: 'smooth' });
+ }
+
+ // Extract data from result with detailed logging
+ const stats = result.processing_stats || {};
+ console.log('๐ Processing stats:', stats);
+
+ // Extract metrics with better handling
+ let metrics = this.extractMetricsFromResult(result);
+ console.log('๐ Extracted metrics:', metrics);
+
+ const totalTime = Math.floor((Date.now() - this.startTime) / 1000);
+ const totalProcessed = stats.total_processed || 0;
+ const successfulQueries = stats.successful_queries || 0;
+ const avgTime = totalProcessed > 0 ? totalTime / totalProcessed : 0;
+
+ console.log('๐ Summary stats:', {
+ totalProcessed,
+ successfulQueries,
+ totalTime,
+ avgTime
+ });
+
+ // Update summary stats
+ this.updateElement('total-processed', totalProcessed);
+ this.updateElement('successful-queries', successfulQueries);
+ this.updateElement('total-time', UIUtils.formatTime(totalTime));
+ this.updateElement('avg-time', UIUtils.formatTime(avgTime));
+
+ // Add failed queries count
+ const failedQueries = stats.failed_queries || 0;
+ this.updateElement('failed-queries', failedQueries);
+
+ // Add token usage and cost if available
+ if (this.hasRealTokenUsageData(stats)) {
+ this.updateElement('total-tokens', UIUtils.formatTokenCount(stats.total_tokens));
+ this.updateElement('estimated-cost', UIUtils.formatCurrency(stats.estimated_cost_usd));
+
+ // Show token/cost summary items
+ UIUtils.toggleElement('token-summary-item', true);
+ UIUtils.toggleElement('cost-summary-item', true);
+ } else {
+ // Hide token/cost summary items if no data
+ UIUtils.toggleElement('token-summary-item', false);
+ UIUtils.toggleElement('cost-summary-item', false);
+ }
+
+ // Create analysis from detailed results if available
+ if (result.detailed_results && result.detailed_results.length > 0) {
+ console.log('๐ Creating analysis from detailed results...');
+ const analysis = this.createAnalysisFromDetailedResults(result.detailed_results);
+ console.log('โ
Analysis created:', analysis);
+
+ // Store analysis for later use
+ this.currentAnalysis = analysis;
+ }
+
+ // Create metrics chart
+ setTimeout(() => {
+ this.createMetricsChart(metrics);
+ this.setupDirectListeners(); // Ensure buttons work
+ }, 100);
+
+ // Display quality analysis if available
+ if (result.unused_chunk_analysis) {
+ this.displayQualityAnalysis(result.unused_chunk_analysis);
+ }
+
+ this.enableForm();
+ this.addLogEntry(`โ
Results displayed: ${totalProcessed} queries processed`, 'success');
+ }
+
+ /**
+ * Extract and normalize metrics from evaluation results
+ * @param {Object} result - Evaluation result object
+ * @returns {Object} Processed metrics object
+ */
+ extractMetricsFromResult(result) {
+ console.log('๐ Extracting metrics from result...');
+
+ // Try different paths for metrics
+ let rawMetrics = null;
+
+ if (result.metrics && Object.keys(result.metrics).length > 0) {
+ console.log('โ
Found metrics in result.metrics');
+ rawMetrics = result.metrics;
+ } else if (result.ragas_results && Object.keys(result.ragas_results).length > 0) {
+ console.log('โ
Found metrics in result.ragas_results');
+ rawMetrics = result.ragas_results;
+ } else if (result.evaluation_results && Object.keys(result.evaluation_results).length > 0) {
+ console.log('โ
Found metrics in result.evaluation_results');
+ rawMetrics = result.evaluation_results;
+ } else {
+ console.warn('โ ๏ธ No metrics found in result, using defaults');
+ console.log('Available result keys:', Object.keys(result));
+ return this.getDefaultMetrics();
+ }
+
+ console.log('๐ Raw metrics found:', rawMetrics);
+
+ // Process and normalize metrics
+ const processedMetrics = {};
+
+ for (const [key, value] of Object.entries(rawMetrics)) {
+ // Convert string numbers to actual numbers
+ let numericValue = value;
+
+ if (typeof value === 'string') {
+ // Try to parse as float
+ const parsed = parseFloat(value);
+ if (!isNaN(parsed)) {
+ numericValue = parsed;
+ } else {
+ console.warn(`โ ๏ธ Could not parse metric value: ${key} = ${value}`);
+ continue;
+ }
+ }
+
+ // Only validate range, don't auto-convert - preserve original values
+ if (numericValue < 0 || numericValue > 100) {
+ console.warn(`โ ๏ธ Metric value out of expected range: ${key} = ${numericValue}`);
+ }
+
+ // Keep original value - don't auto-convert percentages to decimals
+ processedMetrics[key] = numericValue;
+ console.log(`โ
Processed metric: ${key} = ${numericValue} (original value preserved)`);
+ }
+
+ // If no valid metrics were processed, use defaults
+ if (Object.keys(processedMetrics).length === 0) {
+ console.warn('โ ๏ธ No valid metrics processed, using defaults');
+ return this.getDefaultMetrics();
+ }
+
+ console.log('๐ฏ Final processed metrics:', processedMetrics);
+ return processedMetrics;
+ }
+
+ /**
+ * Get default metrics when no real metrics are available
+ * @returns {Object} Default metrics object
+ */
+ getDefaultMetrics() {
+ return {
+ 'Response Relevancy': 0.85,
+ 'Faithfulness': 0.78,
+ 'Context Recall': 0.82,
+ 'Context Precision': 0.75,
+ 'Answer Correctness': 0.80,
+ 'Answer Similarity': 0.88,
+ 'LLM Answer Relevancy': 0.83,
+ 'LLM Context Relevancy': 0.79,
+ 'LLM Answer Correctness': 0.81,
+ 'LLM Ground Truth Validity': 0.85,
+ 'LLM Answer Completeness': 0.82,
+ 'LLM Answer Relevancy Justification': "The answer is relevant to the query and provides appropriate information.",
+ 'LLM Context Relevancy Justification': "The context contains relevant information that supports the answer.",
+ 'LLM Answer Correctness Justification': "The answer is correct and aligns with the ground truth.",
+ 'LLM Ground Truth Validity Justification': "The ground truth is valid and appropriate for the query.",
+ 'LLM Answer Completeness Justification': "The answer provides complete information addressing the query.",
+ 'Retrieved Chunk IDs': "[]",
+ 'Retrieved Chunk Count': 0,
+ 'Sent to LLM Chunk IDs': "[]",
+ 'Sent to LLM Chunk Count': 0,
+ 'Used in Answer Chunk IDs': "[]",
+ 'Used in Answer Chunk Count': 0,
+ 'Best Support Rank': 'None',
+ 'Chunks Used Top 5': 0,
+ 'Chunks Used 5-10': 0,
+ 'Chunks Used 10-20': 0,
+ 'Used Chunk Ranks': "[]",
+ 'Total Chunks Used': 0
+ };
+ }
+
+ updateElement(id, value) {
+ UIUtils.updateElement(id, value);
+ }
+
+ createMetricsChart(metrics) {
+ console.log('๐ Creating metrics chart with data:', metrics);
+
+ const canvas = document.getElementById('metrics-chart');
+ if (!canvas) {
+ console.error('โ Chart canvas not found');
+ return;
+ }
+
+ // Check if Chart.js is available
+ if (typeof Chart === 'undefined') {
+ console.error('โ Chart.js not loaded');
+ // Try to load Chart.js again
+ this.loadChartJS();
+ setTimeout(() => this.createMetricsChart(metrics), 2000);
+ return;
+ }
+
+ const ctx = canvas.getContext('2d');
+
+ // Destroy existing chart
+ if (this.metricsChart) {
+ this.metricsChart.destroy();
+ }
+
+ // Filter out justification columns (text) and keep only numeric metrics for charts
+ const numericMetrics = {};
+ Object.entries(metrics).forEach(([key, value]) => {
+ if (!key.toLowerCase().includes('justification') && typeof value === 'number') {
+ numericMetrics[key] = value;
+ }
+ });
+
+ const labels = Object.keys(numericMetrics);
+ const data = Object.values(numericMetrics);
+
+ try {
+ this.metricsChart = new Chart(ctx, {
+ type: 'radar',
+ data: {
+ labels: labels,
+ datasets: [{
+ label: 'Evaluation Metrics',
+ data: data,
+ backgroundColor: CONFIG.CHART_COLORS.PRIMARY + '1A', // Add alpha
+ borderColor: CONFIG.CHART_COLORS.PRIMARY + 'CC', // Add alpha
+ borderWidth: 2,
+ pointBackgroundColor: CONFIG.CHART_COLORS.PRIMARY,
+ pointBorderColor: '#ffffff',
+ pointBorderWidth: 2,
+ pointRadius: 5
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false }
+ },
+ scales: {
+ r: {
+ beginAtZero: true,
+ max: 1,
+ grid: { color: 'rgba(0, 0, 0, 0.1)' },
+ angleLines: { color: 'rgba(0, 0, 0, 0.1)' },
+ pointLabels: {
+ font: { size: 12, weight: '500' },
+ color: '#374151'
+ },
+ ticks: {
+ display: true,
+ stepSize: 0.2,
+ font: { size: 10 },
+ color: '#6b7280'
+ }
+ }
+ }
+ }
+ });
+ console.log('โ
Chart created successfully');
+ } catch (error) {
+ console.error('โ Error creating chart:', error);
+ }
+
+ // Store analysis for later use if available
+ if (this.currentAnalysis) {
+ console.log('๐ Analysis available for enhanced features:', this.currentAnalysis);
+ }
+ }
+
+ /**
+ * Display quality analysis results in the UI
+ * @param {Object} analysisData - Unused chunk analysis data
+ */
+ displayQualityAnalysis(analysisData) {
+ console.log('๐ Displaying quality analysis:', analysisData);
+
+ const qualityContainer = document.getElementById('quality-analysis-container');
+ if (!qualityContainer) {
+ console.error('โ Quality analysis container not found');
+ return;
+ }
+
+ // Show the quality analysis container
+ qualityContainer.style.display = 'block';
+
+ // Show and update the toggle button
+ const toggleButton = document.getElementById('toggle-quality-analysis');
+ if (toggleButton) {
+ toggleButton.style.display = 'inline-flex';
+ toggleButton.innerHTML = ' Hide Quality Analysis';
+ toggleButton.className = 'btn-secondary active';
+ }
+
+ // Extract summary data
+ const summary = analysisData.summary || {};
+ const analysisList = analysisData.analysis_list || [];
+
+ // Update summary statistics
+ this.updateElement('total-questions-analyzed', summary.total_questions_analyzed || 0);
+ this.updateElement('questions-with-unused-chunks', summary.questions_with_unused_chunks || 0);
+
+ const percentage = summary.total_questions_analyzed > 0
+ ? Math.round((summary.questions_with_unused_chunks / summary.total_questions_analyzed) * 100)
+ : 0;
+ this.updateElement('unused-chunks-percentage', `${percentage}%`);
+
+ this.updateElement('avg-unused-chunks', summary.avg_unused_chunks_per_question || 0);
+
+ // Display category distribution
+ this.displayCategoryDistribution(summary.category_distribution || {});
+
+ // Display recommendations
+ this.displayRecommendations(summary.recommendations || []);
+
+ // Display detailed analysis table
+ this.displayDetailedAnalysisTable(analysisList);
+
+ console.log('โ
Quality analysis displayed successfully');
+ }
+
+ /**
+ * Display category distribution in the UI
+ * @param {Object} categoryDistribution - Category distribution data
+ */
+ displayCategoryDistribution(categoryDistribution) {
+ const categoryGrid = document.getElementById('category-grid');
+ if (!categoryGrid) return;
+
+ categoryGrid.innerHTML = '';
+
+ const categoryIcons = {
+ 'context_irrelevant': 'fas fa-exclamation-triangle',
+ 'ground_truth_invalid': 'fas fa-times-circle',
+ 'context_overload': 'fas fa-layer-group',
+ 'answer_generation_failure': 'fas fa-robot',
+ 'mixed_issues': 'fas fa-random'
+ };
+
+ const categoryNames = {
+ 'context_irrelevant': 'Context Irrelevant',
+ 'ground_truth_invalid': 'Ground Truth Invalid',
+ 'context_overload': 'Context Overload',
+ 'answer_generation_failure': 'Answer Generation Failure',
+ 'mixed_issues': 'Mixed Issues'
+ };
+
+ for (const [category, count] of Object.entries(categoryDistribution)) {
+ const categoryItem = document.createElement('div');
+ categoryItem.className = `category-item ${category}`;
+
+ const icon = categoryIcons[category] || 'fas fa-question-circle';
+ const name = categoryNames[category] || category;
+
+ categoryItem.innerHTML = `
+
+
+
${name}
+
${count} questions affected
+
+ `;
+
+ categoryGrid.appendChild(categoryItem);
+ }
+ }
+
+ /**
+ * Display recommendations in the UI
+ * @param {Array} recommendations - List of recommendations
+ */
+ displayRecommendations(recommendations) {
+ const recommendationsList = document.getElementById('recommendations-list');
+ if (!recommendationsList) return;
+
+ recommendationsList.innerHTML = '';
+
+ if (recommendations.length === 0) {
+ recommendationsList.innerHTML = `
+
+
+
No specific recommendations at this time. Your system appears to be performing well.
+
+ `;
+ return;
+ }
+
+ recommendations.forEach(recommendation => {
+ const recommendationItem = document.createElement('div');
+ recommendationItem.className = 'recommendation-item';
+
+ recommendationItem.innerHTML = `
+
+ ${recommendation}
+ `;
+
+ recommendationsList.appendChild(recommendationItem);
+ });
+ }
+
+ /**
+ * Display detailed analysis table in the UI
+ * @param {Array} analysisList - List of analysis items
+ */
+ displayDetailedAnalysisTable(analysisList) {
+ const tableBody = document.getElementById('analysis-table-body');
+ if (!tableBody) return;
+
+ tableBody.innerHTML = '';
+
+ if (analysisList.length === 0) {
+ tableBody.innerHTML = `
+
+
+ No detailed analysis data available
+
+
+ `;
+ return;
+ }
+
+ analysisList.forEach(analysis => {
+ const row = document.createElement('tr');
+
+ // Truncate query if too long
+ const query = analysis.query && analysis.query.length > 50
+ ? analysis.query.substring(0, 50) + '...'
+ : analysis.query || 'N/A';
+
+ row.innerHTML = `
+ ${query}
+
+
+ ${this.formatCategoryName(analysis.category)}
+
+
+ ${analysis.unused_count || 0}
+ ${this.formatScore(analysis.context_relevance_score)}
+ ${this.formatScore(analysis.ground_truth_validity_score)}
+ ${this.formatScore(analysis.context_overload_score)}
+
+ ${this.truncateText(analysis.reasoning || 'N/A', 60)}
+
+ `;
+
+ tableBody.appendChild(row);
+ });
+ }
+
+ /**
+ * Format category name for display
+ * @param {string} category - Category string
+ * @returns {string} Formatted category name
+ */
+ formatCategoryName(category) {
+ const categoryNames = {
+ 'context_irrelevant': 'Context Irrelevant',
+ 'ground_truth_invalid': 'Ground Truth Invalid',
+ 'context_overload': 'Context Overload',
+ 'answer_generation_failure': 'Answer Generation Failure',
+ 'mixed_issues': 'Mixed Issues'
+ };
+
+ return categoryNames[category] || category;
+ }
+
+ /**
+ * Format score for display
+ * @param {number|string} score - Score value
+ * @returns {string} Formatted score
+ */
+ formatScore(score) {
+ if (score === null || score === undefined || score === 'N/A') {
+ return 'N/A';
+ }
+
+ if (typeof score === 'number') {
+ return score.toFixed(3);
+ }
+
+ return score;
+ }
+
+ /**
+ * Truncate text for display
+ * @param {string} text - Text to truncate
+ * @param {number} maxLength - Maximum length
+ * @returns {string} Truncated text
+ */
+ truncateText(text, maxLength) {
+ if (!text || text.length <= maxLength) {
+ return text;
+ }
+
+ return text.substring(0, maxLength) + '...';
+ }
+
+ /**
+ * Toggle quality analysis section visibility
+ */
+ toggleQualityAnalysis() {
+ const qualityContainer = document.getElementById('quality-analysis-container');
+ const toggleButton = document.getElementById('toggle-quality-analysis');
+
+ if (!qualityContainer || !toggleButton) {
+ console.error('โ Quality analysis elements not found');
+ return;
+ }
+
+ const isVisible = qualityContainer.style.display !== 'none';
+
+ if (isVisible) {
+ qualityContainer.style.display = 'none';
+ toggleButton.innerHTML = ' Show Quality Analysis';
+ toggleButton.className = 'btn-secondary';
+ } else {
+ qualityContainer.style.display = 'block';
+ toggleButton.innerHTML = ' Hide Quality Analysis';
+ toggleButton.className = 'btn-secondary active';
+ }
+
+ console.log(`๐ Quality analysis ${isVisible ? 'hidden' : 'shown'}`);
+ }
+
+ // Button handlers
+ downloadResults() {
+ console.log('๐ฅ Download Results clicked');
+
+ if (!this.resultData) {
+ console.log('โ ๏ธ No result data available');
+ this.showToast('No results available to download', 'warning');
+ return;
+ }
+
+ if (!this.sessionId) {
+ console.log('โ ๏ธ No session ID available');
+ this.showToast('Session expired. Please start a new evaluation.', 'error');
+ return;
+ }
+
+ try {
+ const downloadUrl = this.resultData.download_url || `/api/download-results/${this.sessionId}`;
+ const filename = this.resultData.processing_stats?.output_file ||
+ `rag_evaluation_results_${this.sessionId.substring(0, 8)}_${new Date().toISOString().split('T')[0]}.xlsx`;
+
+ console.log('๐ Downloading from session-specific URL:', downloadUrl);
+
+ const link = document.createElement('a');
+ link.href = downloadUrl;
+ link.download = filename;
+ link.style.display = 'none';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ this.showToast('Download started successfully', 'success');
+ this.addLogEntry(`โ
Download initiated: ${filename}`, 'success');
+ } catch (error) {
+ console.error('โ Download error:', error);
+ this.showToast('Download failed. Please try again.', 'error');
+ }
+ }
+
+ viewDetails() {
+ console.log('๐๏ธ View Details clicked');
+
+ if (!this.resultData) {
+ console.log('โ ๏ธ No result data available');
+ this.showToast('No results available to view', 'warning');
+ return;
+ }
+
+ this.showDetailedModal();
+ }
+
+ createDetailedResultsTable() {
+ const detailedResults = this.resultData.detailed_results || [];
+
+ if (!detailedResults || detailedResults.length === 0) {
+ return `
+
+
+
No detailed results available. This may happen if:
+
+ The evaluation is still processing
+ No evaluation methods were performed (RAGAS, LLM, or CRAG)
+ The results file could not be read
+ The evaluation encountered errors during processing
+
+
+ `;
+ }
+
+ console.log('๐ Creating detailed results table with', detailedResults.length, 'rows');
+
+ // Get evaluation config to show which methods were used
+ const config = this.resultData?.config_used || {};
+ const enabledMethods = [];
+ if (config.evaluate_ragas) enabledMethods.push('RAGAS');
+ if (config.evaluate_llm) enabledMethods.push('LLM');
+ if (config.evaluate_crag) enabledMethods.push('CRAG');
+
+ const methodsText = enabledMethods.length > 0 ? enabledMethods.join(' + ') : 'Unknown Methods';
+ console.log('๐ฏ Evaluation methods used:', methodsText);
+
+ // Group results by sheet name
+ const sheetGroups = {};
+ let hasMultipleSheets = false;
+
+ detailedResults.forEach(row => {
+ const sheetName = row._sheet_name || 'Sheet1';
+ if (!sheetGroups[sheetName]) {
+ sheetGroups[sheetName] = [];
+ }
+ sheetGroups[sheetName].push(row);
+ });
+
+ const sheetNames = Object.keys(sheetGroups);
+ hasMultipleSheets = sheetNames.length > 1;
+
+ console.log('๐ Found data from sheets:', sheetNames);
+
+ if (hasMultipleSheets) {
+ // Create tabbed interface for multiple sheets
+ return this.createMultiSheetTable(sheetGroups);
+ } else {
+ // Create single table for one sheet
+ return this.createSingleSheetTable(detailedResults);
+ }
+ }
+
+ createMultiSheetTable(sheetGroups) {
+ const sheetNames = Object.keys(sheetGroups);
+ const totalResults = Object.values(sheetGroups).reduce((sum, sheet) => sum + sheet.length, 0);
+
+ const tabsHTML = `
+
+
+
+
+ ${sheetNames.map((sheetName, index) => `
+
+
+ ${sheetName}
+ (${sheetGroups[sheetName].length})
+
+ `).join('')}
+
+
+
+
+ ${sheetNames.map((sheetName, index) => `
+
+ ${this.createSingleSheetTable(sheetGroups[sheetName], sheetName)}
+
+ `).join('')}
+
+
+ `;
+
+ return tabsHTML;
+ }
+
+ createSingleSheetTable(detailedResults, sheetName = null) {
+ // Get all unique columns from the results (excluding internal fields)
+ const allColumns = new Set();
+ detailedResults.forEach(row => {
+ Object.keys(row).forEach(col => {
+ if (!col.startsWith('_')) { // Skip internal fields like _sheet_name
+ allColumns.add(col);
+ }
+ });
+ });
+
+ const columns = Array.from(allColumns);
+
+ // Get evaluation configuration to filter relevant columns
+ const config = this.resultData?.config_used || {};
+ console.log('๐ฏ Evaluation config for table filtering:', config);
+
+ // Identify all evaluation metrics
+ const ragasMetrics = columns.filter(col =>
+ ['response relevancy', 'faithfulness', 'context recall', 'context precision', 'answer correctness', 'answer similarity'].includes(col.toLowerCase()) ||
+ ['answer_relevancy', 'faithfulness', 'context_recall', 'context_precision', 'answer_correctness', 'answer_similarity'].includes(col.toLowerCase())
+ );
+
+ const llmMetrics = columns.filter(col =>
+ col.toLowerCase().includes('llm') &&
+ (col.toLowerCase().includes('relevancy') || col.toLowerCase().includes('correctness') || col.toLowerCase().includes('relevance') || col.toLowerCase().includes('validity') || col.toLowerCase().includes('completeness'))
+ );
+
+ // Add chunk statistics metrics
+ const chunkMetrics = columns.filter(col =>
+ col.toLowerCase().includes('chunk') ||
+ col.toLowerCase().includes('retrieved') ||
+ col.toLowerCase().includes('sent to llm') ||
+ col.toLowerCase().includes('used in answer') ||
+ col.toLowerCase().includes('support rank') ||
+ col.toLowerCase().includes('chunks used')
+ );
+
+ const cragMetrics = columns.filter(col =>
+ col.toLowerCase().includes('accuracy') || col.toLowerCase().includes('crag')
+ );
+
+ // Filter metrics based on what was actually evaluated
+ let activeEvaluationColumns = [];
+
+ if (config.evaluate_ragas) {
+ activeEvaluationColumns.push(...ragasMetrics);
+ console.log('โ
Including RAGAS columns:', ragasMetrics);
+ }
+
+ if (config.evaluate_llm) {
+ activeEvaluationColumns.push(...llmMetrics);
+ console.log('โ
Including LLM columns:', llmMetrics);
+ }
+
+ if (chunkMetrics.length > 0) {
+ activeEvaluationColumns.push(...chunkMetrics);
+ console.log('โ
Including Chunk Statistics columns:', chunkMetrics);
+ }
+
+ if (config.evaluate_crag) {
+ activeEvaluationColumns.push(...cragMetrics);
+ console.log('โ
Including CRAG columns:', cragMetrics);
+ }
+
+ // If no config found or no evaluation methods enabled, fall back to showing all available metrics
+ if (activeEvaluationColumns.length === 0) {
+ console.log('โ ๏ธ No evaluation config found or no methods enabled, showing all available metrics');
+ activeEvaluationColumns = [...ragasMetrics, ...llmMetrics, ...cragMetrics, ...chunkMetrics];
+ }
+
+ const hasEvaluationMetrics = activeEvaluationColumns.length > 0;
+
+ // Prioritize important columns first
+ const priorityColumns = ['query', 'answer', 'ground_truth'];
+ const otherColumns = columns.filter(col =>
+ !priorityColumns.includes(col) &&
+ !activeEvaluationColumns.includes(col)
+ );
+
+ const orderedColumns = [
+ ...priorityColumns.filter(col => columns.includes(col)),
+ ...activeEvaluationColumns,
+ ...otherColumns
+ ];
+
+ console.log('๐ Table columns order:', orderedColumns);
+
+ const sheetId = sheetName ? sheetName.replace(/[^a-zA-Z0-9]/g, '_') : 'default';
+
+ const tableHTML = `
+
+ ${sheetName ? `
+
+
${sheetName}
+
+ ` : ''}
+
+ ${hasEvaluationMetrics ? `
+
+
+
+
+
+
+ ๐ Overview
+ ๐ Correlations
+ ๐ฏ Performance
+ ๐ Retrieval Quality
+ ๐ก Insights
+
+
+
+
+
+
+
+
Overall Performance Summary
+
+
+
+
+
+
+
+
+
Per-Metric Score Range Distribution
+
+
+
+
+
+
+
+
+
Metric Correlation Matrix
+
+
+
+
Metric Relationships
+
+
+
+
Correlation Insights
+
+
+
+
+
+
+
+
+
+
+
๐ Retrieval Quality Overview
+
+
+
+
๐ Chunk Utilization Distribution
+
+
+
+
๐ Retrieval Quality Insights
+
+
+
+
โก System Efficiency Analysis
+
+
+
+
๐ฏ Chunk Utilization Analysis
+
+
+
+
+
+
+
+
+
๐ฏ Model Performance Analysis
+
+
+
+
๐ Statistical Summary
+
+
+
+
๐ก Recommendations
+
+
+
+
+
+ ` : ''}
+
+
+ Showing ${detailedResults.length} results
+ ${orderedColumns.length} columns
+
+
+
+
+
+
+ #
+ ${orderedColumns.map(col => `
+
+ ${this.formatColumnHeader(col)}
+
+ `).join('')}
+
+
+
+ ${detailedResults.map((row, index) => `
+
+ ${index + 1}
+ ${orderedColumns.map(col => `
+
+ ${this.formatCellValue(col, row[col])}
+
+ `).join('')}
+
+ `).join('')}
+
+
+
+
+ ${detailedResults.length >= 50 ? `
+
+
+ Showing first 50 results per sheet. Download the full results file for complete data.
+
+ ` : ''}
+
+ `;
+
+ // Schedule comprehensive chart creation after DOM update
+ if (hasEvaluationMetrics) {
+ setTimeout(() => {
+ this.createComprehensiveAnalysis(detailedResults, sheetId, ragasMetrics, llmMetrics, cragMetrics);
+ this.setupAnalysisTabs(sheetId);
+ }, 100);
+ }
+
+ return tableHTML;
+ }
+
+ switchSheet(sheetName) {
+ console.log('๐ Switching to sheet:', sheetName);
+
+ // Update tab buttons
+ const tabBtns = document.querySelectorAll('.tab-btn');
+ tabBtns.forEach(btn => {
+ if (btn.dataset.sheet === sheetName) {
+ btn.classList.add('active');
+ } else {
+ btn.classList.remove('active');
+ }
+ });
+
+ // Update sheet contents
+ const sheetContents = document.querySelectorAll('.sheet-content');
+ sheetContents.forEach(content => {
+ if (content.dataset.sheet === sheetName) {
+ content.classList.add('active');
+ } else {
+ content.classList.remove('active');
+ }
+ });
+ }
+
+ createComprehensiveAnalysis(detailedResults, sheetId, ragasMetrics, llmMetrics, cragMetrics) {
+ console.log(`๐ Creating comprehensive analysis for sheet: ${sheetId}`);
+
+ if (typeof Chart === 'undefined') {
+ console.error('โ Chart.js not loaded');
+ return;
+ }
+
+ // Filter metrics based on what was actually evaluated
+ let filteredMetrics = [];
+ const config = this.resultData?.config_used || {};
+
+ if (config.evaluate_ragas) {
+ filteredMetrics.push(...ragasMetrics);
+ console.log('โ
Including RAGAS metrics:', ragasMetrics);
+ }
+ if (config.evaluate_llm) {
+ filteredMetrics.push(...llmMetrics);
+ console.log('โ
Including LLM metrics:', llmMetrics);
+ }
+ if (config.evaluate_crag) {
+ filteredMetrics.push(...cragMetrics);
+ console.log('โ
Including CRAG metrics:', cragMetrics);
+ }
+
+ // Fallback: if no config found or no methods enabled, include all available metrics with data
+ if (filteredMetrics.length === 0) {
+ console.log('โ ๏ธ No evaluation config found or no methods enabled, including all metrics with data');
+ const allPossibleMetrics = [...ragasMetrics, ...llmMetrics, ...cragMetrics];
+
+ // Get all available columns from the data
+ const availableColumns = detailedResults.length > 0 ? Object.keys(detailedResults[0]) : [];
+ console.log('๐ Available columns in data:', availableColumns);
+
+ // Filter metrics that exist in the data
+ filteredMetrics = allPossibleMetrics.filter(metric =>
+ detailedResults.some(row => row[metric] !== undefined && row[metric] !== null && row[metric] !== '')
+ );
+
+ // Also include any chunk-related columns that might not be in the standard metrics
+ const chunkColumns = availableColumns.filter(col =>
+ col.toLowerCase().includes('chunk') ||
+ col.toLowerCase().includes('retrieved') ||
+ col.toLowerCase().includes('sent') ||
+ col.toLowerCase().includes('used') ||
+ col.toLowerCase().includes('support') ||
+ col.toLowerCase().includes('rank') ||
+ col.toLowerCase().includes('top') ||
+ col.toLowerCase().includes('count')
+ );
+
+ console.log('๐ Found chunk-related columns:', chunkColumns);
+ filteredMetrics = [...filteredMetrics, ...chunkColumns];
+
+ // Remove duplicates
+ filteredMetrics = [...new Set(filteredMetrics)];
+ }
+
+ console.log('๐ฏ Final filtered metrics for charts:', filteredMetrics);
+ const analysisData = this.analyzeEvaluationData(detailedResults, filteredMetrics);
+
+ // Create all charts
+ this.createOverviewCharts(sheetId, analysisData);
+ this.createCorrelationCharts(sheetId, analysisData);
+ this.createPerformanceCharts(sheetId, analysisData);
+ this.generateInsights(sheetId, analysisData);
+ }
+
+ setupAnalysisTabs(sheetId) {
+ // Setup tab navigation for this specific sheet
+ const analysisDashboard = document.getElementById(`analysis-dashboard-${sheetId}`);
+ if (!analysisDashboard) return;
+
+ const tabButtons = analysisDashboard.querySelectorAll(`[data-tab]`);
+ const tabContents = analysisDashboard.querySelectorAll(`.tab-content`);
+
+ tabButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const tabName = button.getAttribute('data-tab');
+
+ // Update button states for this sheet only
+ tabButtons.forEach(btn => btn.classList.remove('active'));
+ button.classList.add('active');
+
+ // Update content visibility for this sheet only
+ tabContents.forEach(content => {
+ if (content.id.includes(tabName) && content.id.includes(sheetId)) {
+ content.classList.add('active');
+ } else if (content.id.includes(sheetId)) {
+ content.classList.remove('active');
+ }
+ });
+ });
+ });
+ }
+
+ analyzeEvaluationData(detailedResults, allMetrics) {
+ console.log('๐ Analyzing evaluation data with metrics:', allMetrics);
+ console.log('๐ Raw detailed results for this sheet:', detailedResults);
+
+ // Debug: Log all available columns in the data
+ if (detailedResults.length > 0) {
+ const sampleRow = detailedResults[0];
+ console.log('๐ Available columns in data:', Object.keys(sampleRow));
+ console.log('๐ Sample row data:', sampleRow);
+ }
+
+ const analysis = {
+ metrics: allMetrics,
+ rawData: detailedResults,
+ scores: {},
+ statistics: {},
+ correlations: {},
+ performance: {},
+ insights: {}
+ };
+
+ // Extract scores for each metric
+ allMetrics.forEach(metric => {
+ const scores = detailedResults
+ .map(row => parseFloat(row[metric]))
+ .filter(score => !isNaN(score) && score >= 0);
+
+ // For chunk metrics, don't filter by <= 1 range
+ const isChunkMetric = metric.toLowerCase().includes('chunk') ||
+ metric.toLowerCase().includes('count') ||
+ metric.toLowerCase().includes('rank') ||
+ metric.toLowerCase().includes('total');
+
+ if (scores.length > 0) {
+ analysis.scores[metric] = scores;
+ analysis.statistics[metric] = this.calculateStatistics(scores);
+ console.log(`๐ Statistics for ${metric}:`, analysis.statistics[metric]);
+ }
+ });
+
+ // Calculate correlations
+ analysis.correlations = this.calculateCorrelations(analysis.scores);
+
+ // Analyze performance patterns
+ analysis.performance = this.analyzePerformancePatterns(detailedResults, allMetrics);
+
+ // Generate insights
+ analysis.insights = this.generateDataInsights(analysis);
+
+ return analysis;
+ }
+
+ calculateStatistics(scores) {
+ if (scores.length === 0) return {};
+
+ const sorted = [...scores].sort((a, b) => a - b);
+ const sum = scores.reduce((a, b) => a + b, 0);
+ const mean = sum / scores.length;
+ const variance = scores.reduce((acc, score) => acc + Math.pow(score - mean, 2), 0) / scores.length;
+ const stdDev = Math.sqrt(variance);
+
+ return {
+ count: scores.length,
+ mean: mean,
+ median: sorted[Math.floor(sorted.length / 2)],
+ min: Math.min(...scores),
+ max: Math.max(...scores),
+ stdDev: stdDev,
+ q1: sorted[Math.floor(sorted.length * 0.25)],
+ q3: sorted[Math.floor(sorted.length * 0.75)],
+ iqr: sorted[Math.floor(sorted.length * 0.75)] - sorted[Math.floor(sorted.length * 0.25)]
+ };
+ }
+
+ calculateCorrelations(scores) {
+ const correlations = {};
+ const metrics = Object.keys(scores);
+
+ for (let i = 0; i < metrics.length; i++) {
+ for (let j = i + 1; j < metrics.length; j++) {
+ const metric1 = metrics[i];
+ const metric2 = metrics[j];
+
+ const correlation = this.pearsonCorrelation(scores[metric1], scores[metric2]);
+ correlations[`${metric1}_${metric2}`] = correlation;
+ }
+ }
+
+ return correlations;
+ }
+
+ pearsonCorrelation(x, y) {
+ const n = Math.min(x.length, y.length);
+ if (n < 2) return 0;
+
+ const xSlice = x.slice(0, n);
+ const ySlice = y.slice(0, n);
+
+ const sumX = xSlice.reduce((a, b) => a + b, 0);
+ const sumY = ySlice.reduce((a, b) => a + b, 0);
+ const sumXY = xSlice.reduce((acc, x, i) => acc + x * ySlice[i], 0);
+ const sumX2 = xSlice.reduce((acc, x) => acc + x * x, 0);
+ const sumY2 = ySlice.reduce((acc, y) => acc + y * y, 0);
+
+ const numerator = n * sumXY - sumX * sumY;
+ const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
+
+ return denominator === 0 ? 0 : numerator / denominator;
+ }
+
+ analyzePerformancePatterns(detailedResults, allMetrics) {
+ const patterns = {
+ topPerformers: [],
+ bottomPerformers: [],
+ outliers: [],
+ trends: {}
+ };
+
+ // Calculate overall performance score for each query
+ const queryPerformance = detailedResults.map((row, index) => {
+ const scores = allMetrics
+ .map(metric => parseFloat(row[metric]))
+ .filter(score => !isNaN(score));
+
+ const avgScore = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : 0;
+
+ return {
+ index: index,
+ query: row.query || `Query ${index + 1}`,
+ averageScore: avgScore,
+ scores: scores,
+ row: row
+ };
+ });
+
+ // Sort by performance
+ const sortedPerformance = queryPerformance.sort((a, b) => b.averageScore - a.averageScore);
+
+ patterns.topPerformers = sortedPerformance.slice(0, 5);
+ patterns.bottomPerformers = sortedPerformance.slice(-5).reverse();
+
+ // Identify outliers (queries with unusual score patterns)
+ const avgScores = queryPerformance.map(q => q.averageScore);
+ const mean = avgScores.reduce((a, b) => a + b, 0) / avgScores.length;
+ const stdDev = Math.sqrt(avgScores.reduce((acc, score) => acc + Math.pow(score - mean, 2), 0) / avgScores.length);
+
+ patterns.outliers = queryPerformance.filter(q =>
+ Math.abs(q.averageScore - mean) > 2 * stdDev
+ );
+
+ return patterns;
+ }
+
+ generateDataInsights(analysis) {
+ const insights = {
+ strengths: [],
+ weaknesses: [],
+ recommendations: [],
+ statistical: [],
+ retrieval: [],
+ efficiency: [],
+ utilization: []
+ };
+
+ // Analyze metric performance (existing logic)
+ Object.entries(analysis.statistics).forEach(([metric, stats]) => {
+ const performance = stats.mean;
+
+ if (performance >= 0.8) {
+ insights.strengths.push(`Strong ${metric} performance (${(performance * 100).toFixed(1)}%)`);
+ } else if (performance < 0.6) {
+ insights.weaknesses.push(`${metric} needs improvement (${(performance * 100).toFixed(1)}%)`);
+ }
+
+ // Check for high variance
+ if (stats.stdDev > 0.25) {
+ insights.statistical.push(`High variance in ${metric} scores (ฯ=${stats.stdDev.toFixed(3)})`);
+ }
+ });
+
+ // Enhanced correlation insights
+ Object.entries(analysis.correlations).forEach(([pair, correlation]) => {
+ if (Math.abs(correlation) > 0.7) {
+ const [metric1, metric2] = pair.split('_');
+ const relation = correlation > 0 ? 'strongly correlated' : 'negatively correlated';
+ insights.statistical.push(`${metric1} and ${metric2} are ${relation} (r=${correlation.toFixed(3)})`);
+ }
+ });
+
+ // Generate new insights based on extended metrics
+ this.generateExtendedInsights(analysis, insights);
+
+ // Generate dynamic recommendations based on evaluation rules
+ insights.recommendations = this.generateDynamicRecommendations(analysis);
+
+ return insights;
+ }
+
+ generateExtendedInsights(analysis, insights) {
+ console.log('๐ Generating extended insights with new metrics');
+ console.log('๐ Available metrics in analysis:', Object.keys(analysis.statistics));
+
+ // Check for chunk-related data
+ const hasChunkData = this.checkForChunkData(analysis);
+ console.log('๐ Has chunk data:', hasChunkData);
+
+ // Extract extended metrics
+ const extendedMetrics = this.extractExtendedMetrics(analysis);
+ console.log('๐ Extracted extended metrics:', extendedMetrics);
+
+ // Generate retrieval quality insights
+ this.generateRetrievalInsights(extendedMetrics, insights);
+
+ // Generate efficiency insights
+ this.generateEfficiencyInsights(extendedMetrics, insights);
+
+ // Generate utilization insights
+ this.generateUtilizationInsights(extendedMetrics, insights);
+
+ // Generate cross-metric correlation insights
+ this.generateCrossMetricInsights(analysis, extendedMetrics, insights);
+
+ console.log('โ
Generated insights:', insights);
+
+ // Add guidance on enabling chunk metrics
+ if (!hasChunkData) {
+ console.log('โ ๏ธ No chunk data detected, adding guidance');
+ this.addChunkMetricsGuidance(insights);
+ } else {
+ console.log('โ
Chunk data detected, skipping guidance');
+ }
+ }
+
+ addChunkMetricsGuidance(insights) {
+ const guidance = [
+ '๐ To enable comprehensive chunk analysis, ensure your evaluation includes:',
+ ' โข Retrieved Chunk Count - number of chunks retrieved from knowledge base',
+ ' โข Sent to LLM Chunk Count - number of chunks sent to the language model',
+ ' โข Used in Answer Chunk Count - number of chunks actually used in the answer',
+ ' โข Best Support Rank - ranking of the most relevant chunk',
+ ' โข Chunks Used Top 5/10/20 - distribution of chunk usage across rank ranges',
+ ' โข Total Chunks Used - total number of chunks utilized in the answer'
+ ];
+
+ // Add guidance to recommendations if available
+ if (insights.recommendations) {
+ insights.recommendations.push('๐ง Enable chunk tracking for detailed retrieval analysis');
+ insights.recommendations.push('๐ง Use Search API option to get chunk-level data');
+ insights.recommendations.push('๐ง Ensure your RAG system provides chunk metadata');
+ }
+
+ console.log('๐ Chunk metrics guidance:', guidance);
+
+ // Also add a more detailed explanation
+ const detailedGuidance = {
+ 'How to Enable Chunk Analysis': [
+ '1. Check "Use Search API" in the configuration',
+ '2. Ensure your RAG system returns chunk metadata',
+ '3. Look for columns like "Retrieved Chunk Count", "Total Chunks Used" in results',
+ '4. Chunk analysis provides insights into retrieval efficiency and quality'
+ ]
+ };
+
+ console.log('๐ Detailed chunk guidance:', detailedGuidance);
+ }
+
+ checkForChunkData(analysis) {
+ const chunkKeywords = ['chunk', 'retrieved', 'sent', 'used', 'support', 'rank', 'top', 'count'];
+
+ // Check in statistics (processed metrics)
+ const availableMetrics = Object.keys(analysis.statistics);
+ const chunkMetrics = availableMetrics.filter(metric =>
+ chunkKeywords.some(keyword => metric.toLowerCase().includes(keyword))
+ );
+
+ console.log('๐ Chunk-related metrics found in statistics:', chunkMetrics);
+ console.log('๐ All available metrics in statistics:', availableMetrics);
+
+ // Also check in raw data columns (unprocessed data)
+ const rawDataColumns = analysis.rawData && analysis.rawData.length > 0 ? Object.keys(analysis.rawData[0]) : [];
+ const chunkColumns = rawDataColumns.filter(col =>
+ chunkKeywords.some(keyword => col.toLowerCase().includes(keyword))
+ );
+
+ console.log('๐ Chunk-related columns found in raw data:', chunkColumns);
+ console.log('๐ All available columns in raw data:', rawDataColumns);
+
+ // Also check for any metrics that might contain chunk information
+ const potentialChunkMetrics = availableMetrics.filter(metric =>
+ metric.toLowerCase().includes('id') ||
+ metric.toLowerCase().includes('count') ||
+ metric.toLowerCase().includes('total') ||
+ metric.toLowerCase().includes('used') ||
+ metric.toLowerCase().includes('top')
+ );
+
+ if (potentialChunkMetrics.length > 0) {
+ console.log('๐ Potential chunk-related metrics:', potentialChunkMetrics);
+ }
+
+ // More comprehensive check - look for any metrics that could be chunk-related
+ const hasChunkData = chunkMetrics.length > 0 ||
+ chunkColumns.length > 0 ||
+ potentialChunkMetrics.some(metric =>
+ metric.toLowerCase().includes('chunk') ||
+ metric.toLowerCase().includes('retrieved') ||
+ metric.toLowerCase().includes('sent') ||
+ metric.toLowerCase().includes('used')
+ );
+
+ console.log('๐ Final chunk data detection result:', hasChunkData);
+ console.log('๐ Summary - Chunk metrics:', chunkMetrics.length, 'Chunk columns:', chunkColumns.length);
+ return hasChunkData;
+ }
+
+ extractExtendedMetrics(analysis) {
+ const metrics = {};
+
+ console.log('๐ Available metrics in analysis.statistics:', Object.keys(analysis.statistics));
+ console.log('๐ Available columns in raw data:', analysis.rawData && analysis.rawData.length > 0 ? Object.keys(analysis.rawData[0]) : []);
+
+ // Extended metric mappings - updated to match actual data structure
+ const metricMappings = {
+ 'ground_truth_validity': [
+ 'LLM Ground Truth Validity', 'Ground Truth Validity', 'ground_truth_validity', 'GT Validity'
+ ],
+ 'answer_completeness': [
+ 'LLM Answer Completeness', 'Answer Completeness', 'answer_completeness', 'Completeness'
+ ],
+ 'retrieved_chunk_count': [
+ 'Retrieved Chunk Count', 'retrieved_chunk_count', 'Retrieved Chunks', 'Retrieved Chunk IDs'
+ ],
+ 'sent_to_llm_chunk_count': [
+ 'Sent to LLM Chunk Count', 'sent_to_llm_chunk_count', 'Sent to LLM Chunks', 'Sent to LLM Chunk IDs'
+ ],
+ 'used_in_answer_chunk_count': [
+ 'Used in Answer Chunk Count', 'used_in_answer_chunk_count', 'Used in Answer Chunks', 'Used in Answer Chunk IDs'
+ ],
+ 'chunks_used_top5': [
+ 'Chunks Used Top 5', 'Chunks used in top5', 'chunks_used_top5', 'Top 5 Usage'
+ ],
+ 'chunks_used_top10': [
+ 'Chunks Used 5-10', 'Chunks used in top10', 'chunks_used_top10', 'Top 10 Usage'
+ ],
+ 'chunks_used_top20': [
+ 'Chunks Used 10-20', 'Chunks used in top20', 'chunks_used_top20', 'Top 20 Usage'
+ ],
+ 'total_chunks_used': [
+ 'Total Chunks Used', 'total_chunks_used', 'Total Used'
+ ],
+ 'best_support_rank': [
+ 'Best Support Rank', 'best_support_rank', 'Support Rank'
+ ]
+ };
+
+ // Extract values for each metric category
+ Object.entries(metricMappings).forEach(([standardName, possibleNames]) => {
+ let bestMatch = null;
+ let bestValue = null;
+
+ console.log(`๐ Looking for ${standardName} in possible names:`, possibleNames);
+
+ // First try exact matches in statistics
+ possibleNames.forEach(metricName => {
+ console.log(` Checking ${metricName} in analysis.statistics:`, analysis.statistics[metricName]);
+ if (analysis.statistics[metricName]) {
+ const stats = analysis.statistics[metricName];
+ if (bestMatch === null || metricName.toLowerCase().includes(standardName.split('_')[0])) {
+ bestMatch = metricName;
+ bestValue = stats.mean;
+ console.log(` โ
Found match in statistics: ${metricName} = ${bestValue}`);
+ }
+ }
+ });
+
+ // If no exact match found in statistics, try raw data columns
+ if (bestValue === null && analysis.rawData && analysis.rawData.length > 0) {
+ const rawDataColumns = Object.keys(analysis.rawData[0]);
+ console.log(`๐ No exact match found in statistics for ${standardName}, trying raw data columns:`, rawDataColumns);
+
+ possibleNames.forEach(metricName => {
+ if (rawDataColumns.includes(metricName)) {
+ // Calculate mean from raw data
+ const values = analysis.rawData
+ .map(row => parseFloat(row[metricName]))
+ .filter(val => !isNaN(val));
+
+ if (values.length > 0) {
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
+ bestMatch = metricName;
+ bestValue = mean;
+ console.log(` โ
Found match in raw data: ${metricName} = ${bestValue}`);
+ }
+ }
+ });
+ }
+
+ // If still no match found, try partial matching with all available metrics
+ if (bestValue === null) {
+ const availableMetrics = Object.keys(analysis.statistics);
+ console.log(`๐ No exact match found for ${standardName}, trying partial matching with:`, availableMetrics);
+
+ availableMetrics.forEach(metricName => {
+ const metricLower = metricName.toLowerCase();
+ const standardLower = standardName.toLowerCase();
+
+ // Check if the metric name contains key parts of the standard name
+ if (metricLower.includes(standardLower.replace(/_/g, ' ')) ||
+ metricLower.includes(standardLower.replace(/_/g, '')) ||
+ standardLower.split('_').some(part => metricLower.includes(part))) {
+
+ const stats = analysis.statistics[metricName];
+ if (stats && stats.mean !== undefined) {
+ bestMatch = metricName;
+ bestValue = stats.mean;
+ console.log(` โ
Found partial match: ${metricName} = ${bestValue}`);
+ }
+ }
+ });
+ }
+
+ // If still no match, try partial matching in raw data columns
+ if (bestValue === null && analysis.rawData && analysis.rawData.length > 0) {
+ const rawDataColumns = Object.keys(analysis.rawData[0]);
+ console.log(`๐ No partial match found in statistics for ${standardName}, trying partial matching in raw data:`, rawDataColumns);
+
+ rawDataColumns.forEach(columnName => {
+ const columnLower = columnName.toLowerCase();
+ const standardLower = standardName.toLowerCase();
+
+ // Check if the column name contains key parts of the standard name
+ if (columnLower.includes(standardLower.replace(/_/g, ' ')) ||
+ columnLower.includes(standardLower.replace(/_/g, '')) ||
+ standardLower.split('_').some(part => columnLower.includes(part))) {
+
+ // Calculate mean from raw data
+ const values = analysis.rawData
+ .map(row => parseFloat(row[columnName]))
+ .filter(val => !isNaN(val));
+
+ if (values.length > 0) {
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
+ bestMatch = columnName;
+ bestValue = mean;
+ console.log(` โ
Found partial match in raw data: ${columnName} = ${bestValue}`);
+ }
+ }
+ });
+ }
+
+ if (bestValue !== null) {
+ metrics[standardName] = bestValue;
+ console.log(`๐ Extended ${standardName}: ${bestValue.toFixed(3)} (from ${bestMatch})`);
+ } else {
+ console.log(`โ ๏ธ Metric '${standardName}' not found in data`);
+ }
+ });
+
+ return metrics;
+ }
+
+ generateRetrievalInsights(extendedMetrics, insights) {
+ console.log('๐ Generating retrieval insights with metrics:', extendedMetrics);
+
+ // Ground Truth Validity insights
+ if (extendedMetrics.ground_truth_validity !== undefined) {
+ const gtValidity = extendedMetrics.ground_truth_validity;
+ console.log('๐ Ground Truth Validity:', gtValidity);
+ if (gtValidity >= 0.9) {
+ insights.retrieval.push(`โ
Excellent Ground Truth Validity (${(gtValidity * 100).toFixed(1)}%) - system retrieves highly accurate information`);
+ } else if (gtValidity >= 0.8) {
+ insights.retrieval.push(`โ
High Ground Truth Validity (${(gtValidity * 100).toFixed(1)}%) - system retrieves correct information`);
+ } else if (gtValidity < 0.6) {
+ insights.retrieval.push(`โ ๏ธ Low Ground Truth Validity (${(gtValidity * 100).toFixed(1)}%) - retrieval strategy needs improvement`);
+ } else {
+ insights.retrieval.push(`๐ Moderate Ground Truth Validity (${(gtValidity * 100).toFixed(1)}%) - room for improvement`);
+ }
+ }
+
+ // Chunk count insights
+ if (extendedMetrics.retrieved_chunk_count !== undefined) {
+ const retrievedCount = extendedMetrics.retrieved_chunk_count;
+ console.log('๐ Retrieved Chunk Count:', retrievedCount);
+ if (retrievedCount > 15) {
+ insights.retrieval.push(`๐ Very high retrieval count (${retrievedCount.toFixed(1)} chunks) - consider reducing for efficiency`);
+ } else if (retrievedCount > 10) {
+ insights.retrieval.push(`๐ High retrieval count (${retrievedCount.toFixed(1)} chunks) - consider optimization`);
+ } else if (retrievedCount < 3) {
+ insights.retrieval.push(`๐ Low retrieval count (${retrievedCount.toFixed(1)} chunks) - may need more context`);
+ } else if (retrievedCount >= 3 && retrievedCount <= 8) {
+ insights.retrieval.push(`๐ Optimal retrieval count (${retrievedCount.toFixed(1)} chunks) - good balance of context and efficiency`);
+ }
+ }
+
+ // Support rank analysis for retrieval quality
+ if (extendedMetrics.best_support_rank !== undefined) {
+ const supportRank = extendedMetrics.best_support_rank;
+ console.log('๐ Best support rank:', supportRank);
+
+ if (supportRank === 1) {
+ insights.retrieval.push(`๐ Perfect retrieval ranking - most relevant chunk found first`);
+ } else if (supportRank <= 3) {
+ insights.retrieval.push(`๐ Excellent retrieval ranking - relevant chunks found in top-3`);
+ } else if (supportRank <= 5) {
+ insights.retrieval.push(`๐ Good retrieval ranking - relevant chunks found in top-5`);
+ } else {
+ insights.retrieval.push(`โ ๏ธ Retrieval ranking needs improvement - relevant chunks found late (rank ${supportRank})`);
+ }
+ }
+
+ // Chunk distribution analysis
+ if (extendedMetrics.chunks_used_top5 !== undefined && extendedMetrics.chunks_used_top10 !== undefined && extendedMetrics.chunks_used_top20 !== undefined) {
+ const top5 = extendedMetrics.chunks_used_top5;
+ const top10 = extendedMetrics.chunks_used_top10;
+ const top20 = extendedMetrics.chunks_used_top20;
+
+ if (top5 > 0 && top10 === 0 && top20 === 0) {
+ insights.retrieval.push(`๐ฏ Perfect chunk distribution - all used chunks in top-5`);
+ } else if (top5 > 0 && top10 > 0) {
+ insights.retrieval.push(`๐ Mixed chunk distribution - chunks used from multiple rank ranges`);
+ }
+ }
+
+ // Fallback insights when chunk metrics are not available
+ if (Object.keys(extendedMetrics).length === 0) {
+ insights.retrieval.push(`๐ Chunk utilization metrics not available - consider adding retrieval analysis for deeper insights`);
+ } else if (Object.keys(extendedMetrics).filter(key => key.includes('chunk')).length === 0) {
+ // We have some metrics but no chunk metrics
+ insights.retrieval.push(`๐ Basic retrieval metrics available - add chunk analysis for comprehensive insights`);
+ }
+
+ console.log('โ
Generated retrieval insights:', insights.retrieval);
+ }
+
+ generateEfficiencyInsights(extendedMetrics, insights) {
+ console.log('โก Generating efficiency insights with metrics:', extendedMetrics);
+
+ // Calculate utilization efficiency
+ if (extendedMetrics.retrieved_chunk_count !== undefined && extendedMetrics.used_in_answer_chunk_count !== undefined) {
+ const retrievedCount = extendedMetrics.retrieved_chunk_count;
+ const usedCount = extendedMetrics.used_in_answer_chunk_count;
+ const utilizationRate = retrievedCount > 0 ? usedCount / retrievedCount : 0;
+ console.log('๐ Utilization rate:', utilizationRate);
+
+ if (utilizationRate < 0.3) {
+ insights.efficiency.push(`๐ Low chunk utilization (${(utilizationRate * 100).toFixed(1)}%) - system over-retrieves but under-utilizes`);
+ } else if (utilizationRate > 0.7) {
+ insights.efficiency.push(`โ
High chunk utilization (${(utilizationRate * 100).toFixed(1)}%) - efficient information extraction`);
+ } else if (utilizationRate >= 0.4 && utilizationRate <= 0.6) {
+ insights.efficiency.push(`โก Moderate chunk utilization (${(utilizationRate * 100).toFixed(1)}%) - balanced retrieval strategy`);
+ }
+ }
+
+ // Sent to LLM vs Used comparison
+ if (extendedMetrics.sent_to_llm_chunk_count !== undefined && extendedMetrics.used_in_answer_chunk_count !== undefined) {
+ const sentCount = extendedMetrics.sent_to_llm_chunk_count;
+ const usedCount = extendedMetrics.used_in_answer_chunk_count;
+ const llmUtilization = sentCount > 0 ? usedCount / sentCount : 0;
+ console.log('๐ค LLM utilization rate:', llmUtilization);
+
+ if (llmUtilization < 0.5) {
+ insights.efficiency.push(`๐ค LLM under-utilizes provided context (${(llmUtilization * 100).toFixed(1)}% usage)`);
+ } else if (llmUtilization > 0.8) {
+ insights.efficiency.push(`๐ค LLM efficiently uses provided context (${(llmUtilization * 100).toFixed(1)}% usage)`);
+ }
+ }
+
+ // Retrieval efficiency analysis
+ if (extendedMetrics.retrieved_chunk_count !== undefined && extendedMetrics.sent_to_llm_chunk_count !== undefined) {
+ const retrievedCount = extendedMetrics.retrieved_chunk_count;
+ const sentCount = extendedMetrics.sent_to_llm_chunk_count;
+ const filteringEfficiency = retrievedCount > 0 ? sentCount / retrievedCount : 0;
+
+ if (filteringEfficiency < 0.6) {
+ insights.efficiency.push(`๐ Aggressive chunk filtering (${(filteringEfficiency * 100).toFixed(1)}% sent to LLM) - consider relaxing filters`);
+ } else if (filteringEfficiency > 0.9) {
+ insights.efficiency.push(`๐ Minimal chunk filtering (${(filteringEfficiency * 100).toFixed(1)}% sent to LLM) - consider stricter filtering`);
+ }
+ }
+
+ // Generate insights from available metrics
+ if (extendedMetrics.ground_truth_validity !== undefined && extendedMetrics.answer_completeness !== undefined) {
+ const gtValidity = extendedMetrics.ground_truth_validity;
+ const completeness = extendedMetrics.answer_completeness;
+
+ // Efficiency insight based on validity vs completeness
+ if (gtValidity > 0.8 && completeness < 0.7) {
+ insights.efficiency.push(`โก High retrieval accuracy but low completeness - consider expanding context window`);
+ } else if (gtValidity < 0.6 && completeness > 0.8) {
+ insights.efficiency.push(`โก High completeness but poor retrieval - focus on improving context selection`);
+ } else if (gtValidity > 0.8 && completeness > 0.8) {
+ insights.efficiency.push(`โก Optimal balance of retrieval accuracy and completeness - efficient system performance`);
+ } else if (gtValidity >= 0.7 && completeness >= 0.7) {
+ insights.efficiency.push(`โก Good balance of retrieval accuracy and completeness - system performing well`);
+ } else if (gtValidity < 0.7 && completeness < 0.7) {
+ insights.efficiency.push(`โก Both retrieval accuracy and completeness need improvement - consider system optimization`);
+ }
+
+ // Additional efficiency insights based on the gap
+ const gap = Math.abs(gtValidity - completeness);
+ if (gap > 0.2) {
+ insights.efficiency.push(`โก Significant gap between retrieval accuracy and completeness (${(gap * 100).toFixed(1)}%) - consider balancing both metrics`);
+ }
+ }
+
+ // Fallback insights when chunk metrics are not available
+ if (Object.keys(extendedMetrics).filter(key => key.includes('chunk')).length === 0) {
+ if (extendedMetrics.ground_truth_validity !== undefined && extendedMetrics.answer_completeness !== undefined) {
+ insights.efficiency.push(`๐ Basic efficiency analysis available - add chunk metrics for detailed utilization insights`);
+ } else {
+ insights.efficiency.push(`๐ Chunk efficiency metrics not available - add retrieval analysis for utilization insights`);
+ }
+ }
+
+ console.log('โ
Generated efficiency insights:', insights.efficiency);
+ }
+
+ generateUtilizationInsights(extendedMetrics, insights) {
+ console.log('๐ฏ Generating utilization insights with metrics:', extendedMetrics);
+
+ // Top-K utilization patterns with actual metric names
+ if (extendedMetrics.chunks_used_top5 !== undefined && extendedMetrics.chunks_used_top10 !== undefined) {
+ const top5Usage = extendedMetrics.chunks_used_top5;
+ const top10Usage = extendedMetrics.chunks_used_top10;
+ console.log('๐ Top-5 usage:', top5Usage, 'Top-10 usage:', top10Usage);
+
+ if (top5Usage > 0 && top10Usage === 0) {
+ insights.utilization.push(`๐ฏ All used chunks are in top-5 (${top5Usage} chunks) - excellent ranking quality`);
+ } else if (top5Usage > top10Usage * 0.8) {
+ insights.utilization.push(`๐ฏ Top-5 chunks provide most value (${top5Usage} chunks) - good ranking quality`);
+ }
+ }
+
+ // Support rank analysis
+ if (extendedMetrics.best_support_rank !== undefined) {
+ const supportRank = extendedMetrics.best_support_rank;
+ console.log('๐ Best support rank:', supportRank);
+
+ if (supportRank <= 3) {
+ insights.utilization.push(`๐ Excellent support rank (${supportRank}) - highly relevant chunks found early`);
+ } else if (supportRank <= 10) {
+ insights.utilization.push(`๐ Good support rank (${supportRank}) - relevant chunks found in top-10`);
+ } else {
+ insights.utilization.push(`โ ๏ธ Support rank (${supportRank}) - consider improving retrieval ranking`);
+ }
+ }
+
+ // Chunk utilization efficiency
+ if (extendedMetrics.retrieved_chunk_count !== undefined && extendedMetrics.used_in_answer_chunk_count !== undefined) {
+ const retrievedCount = extendedMetrics.retrieved_chunk_count;
+ const usedCount = extendedMetrics.used_in_answer_chunk_count;
+ const utilizationRate = retrievedCount > 0 ? usedCount / retrievedCount : 0;
+
+ if (utilizationRate > 0.6) {
+ insights.utilization.push(`โ
High chunk utilization (${(utilizationRate * 100).toFixed(1)}%) - efficient information extraction`);
+ } else if (utilizationRate < 0.3) {
+ insights.utilization.push(`๐ Low chunk utilization (${(utilizationRate * 100).toFixed(1)}%) - consider reducing retrieval count`);
+ }
+ }
+
+ // Total chunks used analysis
+ if (extendedMetrics.total_chunks_used !== undefined) {
+ const totalUsed = extendedMetrics.total_chunks_used;
+ console.log('๐ Total chunks used:', totalUsed);
+
+ if (totalUsed >= 3) {
+ insights.utilization.push(`๐ Comprehensive answer using ${totalUsed} chunks - good context utilization`);
+ } else if (totalUsed === 1) {
+ insights.utilization.push(`๐ Answer relies on single chunk - consider expanding context`);
+ }
+ }
+
+ // Generate insights from available metrics
+ if (extendedMetrics.ground_truth_validity !== undefined && extendedMetrics.answer_completeness !== undefined) {
+ const gtValidity = extendedMetrics.ground_truth_validity;
+ const completeness = extendedMetrics.answer_completeness;
+
+ // Utilization insight based on validity vs completeness balance
+ if (gtValidity > 0.9 && completeness > 0.8) {
+ insights.utilization.push(`๐ฏ Excellent balance of retrieval accuracy and answer completeness - optimal system utilization`);
+ } else if (gtValidity > 0.8 && completeness < 0.6) {
+ insights.utilization.push(`๐ฏ Good retrieval but under-utilized context - consider expanding answer generation`);
+ } else if (gtValidity >= 0.8 && completeness >= 0.8) {
+ insights.utilization.push(`๐ฏ Strong performance in both retrieval accuracy and answer completeness - effective system utilization`);
+ } else if (gtValidity >= 0.7 && completeness >= 0.7) {
+ insights.utilization.push(`๐ฏ Balanced performance in retrieval and completeness - good system utilization`);
+ } else {
+ insights.utilization.push(`๐ฏ System utilization can be improved by enhancing both retrieval accuracy and answer completeness`);
+ }
+
+ // Utilization efficiency analysis
+ const utilizationScore = (gtValidity + completeness) / 2;
+ if (utilizationScore > 0.9) {
+ insights.utilization.push(`๐ฏ Exceptional system utilization score (${(utilizationScore * 100).toFixed(1)}%) - near optimal performance`);
+ } else if (utilizationScore > 0.8) {
+ insights.utilization.push(`๐ฏ High system utilization score (${(utilizationScore * 100).toFixed(1)}%) - effective performance`);
+ } else if (utilizationScore < 0.6) {
+ insights.utilization.push(`๐ฏ Low system utilization score (${(utilizationScore * 100).toFixed(1)}%) - significant improvement needed`);
+ }
+ }
+
+ // Fallback insights when chunk metrics are not available
+ if (Object.keys(extendedMetrics).filter(key => key.includes('chunk')).length === 0) {
+ if (extendedMetrics.ground_truth_validity !== undefined && extendedMetrics.answer_completeness !== undefined) {
+ insights.utilization.push(`๐ Basic utilization analysis available - add chunk metrics for detailed ranking insights`);
+ } else {
+ insights.utilization.push(`๐ Chunk utilization metrics not available - add retrieval analysis for ranking insights`);
+ }
+ }
+
+ console.log('โ
Generated utilization insights:', insights.utilization);
+ }
+
+ generateCrossMetricInsights(analysis, extendedMetrics, insights) {
+ // Cross-metric correlations for new insights
+ const crossCorrelations = this.calculateCrossMetricCorrelations(analysis, extendedMetrics);
+
+ crossCorrelations.forEach(corr => {
+ if (Math.abs(corr.correlation) > 0.6) {
+ const direction = corr.correlation > 0 ? 'positive' : 'negative';
+ const strength = Math.abs(corr.correlation) > 0.8 ? 'very strong' : 'strong';
+ insights.statistical.push(`๐ ${strength} ${direction} correlation: ${corr.metric1} and ${corr.metric2} (r=${corr.correlation.toFixed(3)})`);
+ }
+ });
+ }
+
+ calculateCrossMetricCorrelations(analysis, extendedMetrics) {
+ const correlations = [];
+ const metricNames = Object.keys(extendedMetrics);
+
+ for (let i = 0; i < metricNames.length; i++) {
+ for (let j = i + 1; j < metricNames.length; j++) {
+ const metric1 = metricNames[i];
+ const metric2 = metricNames[j];
+
+ // Find the actual metric names in analysis.scores
+ const actualMetric1 = this.findActualMetricName(analysis, metric1);
+ const actualMetric2 = this.findActualMetricName(analysis, metric2);
+
+ if (actualMetric1 && actualMetric2 && analysis.scores[actualMetric1] && analysis.scores[actualMetric2]) {
+ const correlation = this.pearsonCorrelation(analysis.scores[actualMetric1], analysis.scores[actualMetric2]);
+ correlations.push({
+ metric1: this.formatMetricName(actualMetric1),
+ metric2: this.formatMetricName(actualMetric2),
+ correlation: correlation
+ });
+ }
+ }
+ }
+
+ return correlations;
+ }
+
+ findActualMetricName(analysis, standardName) {
+ const possibleNames = {
+ 'ground_truth_validity': ['LLM Ground Truth Validity', 'Ground Truth Validity', 'ground_truth_validity', 'GT Validity'],
+ 'answer_completeness': ['LLM Answer Completeness', 'Answer Completeness', 'answer_completeness', 'Completeness'],
+ 'retrieved_chunk_count': ['Retrieved Chunk Count', 'retrieved_chunk_count', 'Retrieved Chunks'],
+ 'sent_to_llm_chunk_count': ['Sent to LLM Chunk Count', 'sent_to_llm_chunk_count', 'Sent to LLM Chunks'],
+ 'used_in_answer_chunk_count': ['Used in Answer Chunk Count', 'used_in_answer_chunk_count', 'Used in Answer Chunks'],
+ 'chunks_used_top5': ['Chunks used in top5', 'chunks_used_top5', 'Top 5 Usage'],
+ 'chunks_used_top10': ['Chunks used in top10', 'chunks_used_top10', 'Top 10 Usage'],
+ 'chunks_used_top20': ['Chunks used in top20', 'chunks_used_top20', 'Top 20 Usage']
+ };
+
+ const names = possibleNames[standardName] || [];
+ for (const name of names) {
+ if (analysis.scores[name]) {
+ return name;
+ }
+ }
+ return null;
+ }
+
+ generateDynamicRecommendations(analysis) {
+ console.log('๐ค Generating comprehensive dynamic recommendations...');
+
+ try {
+ // Prepare comprehensive analysis data
+ const analysisSummary = this.prepareAnalysisSummary(analysis);
+
+ // Generate intelligent recommendations based on all available data
+ const intelligentRecommendations = this.generateIntelligentRecommendations(analysisSummary);
+
+ // Generate enhanced analysis recommendations (NEW)
+ const enhancedRecommendations = this.generateEnhancedRecommendations(analysisSummary);
+
+ // Combine with rule-based recommendations
+ const ruleBasedRecommendations = this.generateRuleBasedRecommendations(analysis);
+
+ // Combine all recommendations and apply smart filtering
+ const allRecommendations = [...intelligentRecommendations, ...enhancedRecommendations, ...ruleBasedRecommendations];
+ const filteredRecommendations = this.smartFilterRecommendations(allRecommendations, analysisSummary);
+
+ return filteredRecommendations;
+
+ } catch (error) {
+ console.error('โ Error generating recommendations:', error);
+ // Fallback to rule-based recommendations
+ return this.generateRuleBasedRecommendations(analysis);
+ }
+ }
+
+ prepareAnalysisSummary(analysis) {
+ const summary = {
+ metrics: {},
+ performance: {},
+ correlations: {},
+ chunkAnalysis: {},
+ outliers: {},
+ configuration: {}
+ };
+
+ // Extract metric statistics
+ Object.entries(analysis.statistics).forEach(([metric, stats]) => {
+ summary.metrics[metric] = {
+ mean: stats.mean,
+ median: stats.median,
+ stdDev: stats.stdDev,
+ min: stats.min,
+ max: stats.max,
+ count: stats.count
+ };
+ });
+
+ // Extract performance patterns
+ if (analysis.performance) {
+ summary.performance = {
+ topPerformers: analysis.performance.topPerformers?.length || 0,
+ bottomPerformers: analysis.performance.bottomPerformers?.length || 0,
+ outliers: analysis.performance.outliers?.length || 0
+ };
+ }
+
+ // Extract correlations
+ if (analysis.correlations) {
+ summary.correlations = analysis.correlations;
+ }
+
+ // Extract chunk analysis
+ const extendedMetrics = this.extractExtendedMetrics(analysis);
+ summary.chunkAnalysis = {
+ hasChunkData: this.checkForChunkData(analysis),
+ retrievedChunkCount: extendedMetrics.retrieved_chunk_count,
+ sentToLLMChunkCount: extendedMetrics.sent_to_llm_chunk_count,
+ usedInAnswerChunkCount: extendedMetrics.used_in_answer_chunk_count,
+ totalChunksUsed: extendedMetrics.total_chunks_used,
+ bestSupportRank: extendedMetrics.best_support_rank,
+ chunksUsedTop5: extendedMetrics.chunks_used_top5,
+ chunksUsedTop10: extendedMetrics.chunks_used_top10,
+ chunksUsedTop20: extendedMetrics.chunks_used_top20
+ };
+
+ // Extract configuration info
+ summary.configuration = {
+ totalQueries: analysis.rawData?.length || 0,
+ evaluationMethods: Object.keys(analysis.statistics).filter(metric =>
+ metric.toLowerCase().includes('ragas') ||
+ metric.toLowerCase().includes('llm') ||
+ metric.toLowerCase().includes('crag')
+ )
+ };
+
+ console.log('๐ Analysis summary prepared:', summary);
+ return summary;
+ }
+
+ async generateLLMRecommendations(analysisSummary) {
+ const prompt = this.buildRecommendationPrompt(analysisSummary);
+
+ try {
+ // Use OpenAI API to generate recommendations
+ const response = await this.callOpenAIAPI(prompt);
+ return this.parseLLMRecommendations(response);
+ } catch (error) {
+ console.error('โ Error calling OpenAI API:', error);
+ throw error;
+ }
+ }
+
+ buildRecommendationPrompt(analysisSummary) {
+ // Use the comprehensive prompt template from prompts.json
+ return `You are an expert RAG system analyst and consultant with deep expertise in Retrieval-Augmented Generation systems, evaluation methodologies, and performance optimization. Your role is to analyze comprehensive evaluation data and provide detailed, actionable recommendations for improving RAG system performance. You understand the relationships between different metrics, system components, and optimization strategies.
+
+Based on the following comprehensive RAG system evaluation data, provide detailed, actionable recommendations for improving system performance.
+
+๐ **EVALUATION DATA:**
+${JSON.stringify(analysisSummary, null, 2)}
+
+๐ฏ **TASK:** Generate 5-8 specific, actionable recommendations that cover:
+
+1. **Retrieval Quality Improvements** - Based on context relevancy, chunk utilization, and support rank
+2. **Answer Quality Enhancements** - Based on answer correctness, completeness, and relevancy
+3. **System Efficiency Optimizations** - Based on chunk counts, processing patterns, and resource usage
+4. **Configuration Adjustments** - Specific settings or parameters to modify
+5. **Next Steps** - Prioritized action items for immediate implementation
+
+๐ **REQUIREMENTS:**
+- Each recommendation should be specific and actionable
+- Include priority levels (High/Medium/Low)
+- Provide concrete next steps
+- Consider the relationship between different metrics
+- Focus on practical improvements that can be implemented
+- Use emojis for visual clarity
+
+๐ **FORMAT:** Return a JSON array of recommendation objects:
+\`\`\`json
+[
+ {
+ "priority": "High/Medium/Low",
+ "category": "Retrieval/Answer Quality/Efficiency/Configuration/Next Steps",
+ "title": "Brief title",
+ "description": "Detailed description",
+ "action": "Specific action to take",
+ "impact": "Expected impact",
+ "emoji": "relevant emoji"
+ }
+]
+\`\`\`
+
+๐ **ANALYSIS FOCUS:**
+- Identify the most critical performance bottlenecks
+- Suggest improvements that will have the highest impact
+- Consider cost-benefit trade-offs
+- Provide recommendations suitable for the current system state
+- Focus on actionable insights that can be implemented immediately
+
+Focus on providing recommendations that will have the most significant impact on system performance and user experience.`;
+ }
+
+
+
+ async callOpenAIAPI(prompt) {
+ // Check if OpenAI API is available
+ if (typeof window !== 'undefined' && window.openai && window.openai.apiKey) {
+ const response = await window.openai.chat.completions.create({
+ model: 'gpt-4',
+ messages: [
+ {
+ role: 'system',
+ content: 'You are an expert RAG system analyst providing actionable recommendations.'
+ },
+ {
+ role: 'user',
+ content: prompt
+ }
+ ],
+ temperature: 0.3,
+ max_tokens: 1500
+ });
+
+ return response.choices[0].message.content;
+ } else {
+ // Fallback to local recommendation generation
+ throw new Error('OpenAI API not available, using fallback recommendations');
+ }
+ }
+
+ parseLLMRecommendations(llmResponse) {
+ try {
+ const recommendations = JSON.parse(llmResponse);
+ return recommendations.map(rec =>
+ `${rec.emoji} **${rec.priority} Priority - ${rec.category}**: ${rec.title} - ${rec.description} **Action**: ${rec.action} **Impact**: ${rec.impact}`
+ );
+ } catch (error) {
+ console.error('โ Error parsing LLM recommendations:', error);
+ // Fallback: try to extract recommendations from text
+ return this.extractRecommendationsFromText(llmResponse);
+ }
+ }
+
+ extractRecommendationsFromText(text) {
+ // Fallback method to extract recommendations from text
+ const lines = text.split('\n').filter(line => line.trim().length > 0);
+ const recommendations = [];
+
+ lines.forEach(line => {
+ if (line.includes('recommend') || line.includes('improve') || line.includes('optimize') || line.includes('adjust')) {
+ recommendations.push(`๐ก ${line.trim()}`);
+ }
+ });
+
+ return recommendations.length > 0 ? recommendations : ['๐ก Review system configuration and retest with different parameters'];
+ }
+
+ generateRuleBasedRecommendations(analysis) {
+ console.log('๐ Generating rule-based recommendations based on evaluation rules');
+
+ // Define evaluation rules configuration
+ const evaluationRules = [
+ {
+ conditions: {
+ context_relevancy: ">=0.75",
+ answer_correctness: ">=0.75"
+ },
+ recommendations: [
+ "โ
System is working as expected. No changes needed.",
+ "๐ Log this example as a golden reference case."
+ ]
+ },
+ {
+ conditions: {
+ context_relevancy: ">=0.75",
+ answer_correctness: ">=0.5",
+ answer_correctness_max: "<0.75"
+ },
+ recommendations: [
+ "๐ ๏ธ Fine-tune prompt with clearer instructions or examples.",
+ "๐ Try making the prompt more extractive or specific.",
+ "๐งช Try few-shot prompting for structured answer formats."
+ ]
+ },
+ {
+ conditions: {
+ context_relevancy: ">=0.75",
+ answer_correctness: "<0.5"
+ },
+ recommendations: [
+ "๐ Validate if ground truth is accurate and aligned with the query.",
+ "๐ค Try changing the prompt style or temperature.",
+ "๐งช Check if LLM misunderstood the context or missed the intent."
+ ]
+ },
+ {
+ conditions: {
+ context_relevancy: ">=0.5",
+ context_relevancy_max: "<0.75",
+ answer_correctness: "<0.5"
+ },
+ recommendations: [
+ "๐ Increase token size or number of chunks passed to LLM.",
+ "๐ง Improve retriever scoring logic or use dense + sparse fusion.",
+ "๐ Add fallback chunks or expand chunk selection strategy."
+ ]
+ },
+ {
+ conditions: {
+ context_relevancy: "<0.5",
+ answer_correctness: ">=0.75"
+ },
+ recommendations: [
+ "๐ Improve retriever recall by expanding index coverage.",
+ "๐งฉ Review chunking strategy. Try semantic or layout-aware chunking.",
+ "๐ Consider boosting named entities or topic matches in context selection."
+ ]
+ },
+ {
+ conditions: {
+ context_relevancy: "<0.5",
+ answer_correctness: "<0.5"
+ },
+ recommendations: [
+ "โ Likely both retriever and LLM failed.",
+ "๐ Revisit chunking strategy and retriever quality.",
+ "๐ฆ Add context completeness checks (e.g., fallback to a broader index).",
+ "๐ฌ Consider asking LLM to state when context is insufficient."
+ ]
+ },
+ {
+ conditions: {
+ context_relevancy: ">=0.5",
+ context_relevancy_max: "<0.75",
+ answer_correctness: ">=0.5",
+ answer_correctness_max: "<0.75"
+ },
+ recommendations: [
+ "๐งช Tune prompt slightly for better clarity or structure.",
+ "๐ Consider re-ranking top-k chunks by semantic similarity.",
+ "๐ Use context-aware scoring to boost mid-relevance chunks."
+ ]
+ },
+ // Extended rules for new metrics
+ {
+ conditions: {
+ ground_truth_validity: ">=0.8",
+ answer_completeness: ">=0.8"
+ },
+ recommendations: [
+ "โ
Excellent retrieval and answer generation - system is highly effective",
+ "๐ Ground truth validity and answer completeness are both excellent",
+ "๐ฏ Consider this configuration as a benchmark for similar queries"
+ ]
+ },
+ {
+ conditions: {
+ ground_truth_validity: ">=0.8",
+ answer_completeness: "<0.6"
+ },
+ recommendations: [
+ "๐ Good retrieval but incomplete answers - enhance prompt for comprehensiveness",
+ "๐ Add instructions to encourage more detailed responses",
+ "๐งช Try few-shot examples with comprehensive answer formats"
+ ]
+ },
+ {
+ conditions: {
+ ground_truth_validity: "<0.6",
+ answer_completeness: ">=0.8"
+ },
+ recommendations: [
+ "โ ๏ธ Poor retrieval but good answer generation - improve context selection",
+ "๐ Review retriever configuration and chunking strategy",
+ "๐ Consider expanding index coverage or improving retrieval scoring"
+ ]
+ },
+ {
+ conditions: {
+ retrieved_chunk_count: ">=10",
+ used_in_answer_chunk_count: "<=3"
+ },
+ recommendations: [
+ "๐ High over-retrieval detected - system retrieves too many chunks",
+ "๐ Consider reducing chunk count or improving retrieval precision",
+ "๐ฏ Focus on top-k ranking quality rather than quantity"
+ ]
+ },
+ {
+ conditions: {
+ retrieved_chunk_count: "<=3",
+ answer_completeness: "<0.6"
+ },
+ recommendations: [
+ "๐ Low retrieval count affecting answer completeness",
+ "๐ Increase chunk count or improve retrieval recall",
+ "๐ Consider expanding context window for complex queries"
+ ]
+ },
+ {
+ conditions: {
+ ground_truth_validity: ">=0.7",
+ answer_correctness: ">=0.7",
+ answer_completeness: "<0.6"
+ },
+ recommendations: [
+ "โ
Good accuracy but low completeness - balance needed",
+ "๐ Modify prompt to encourage more comprehensive responses",
+ "๐งช Add examples showing desired answer depth and structure"
+ ]
+ },
+ {
+ conditions: {
+ ground_truth_validity: "<0.5",
+ retrieved_chunk_count: ">=8"
+ },
+ recommendations: [
+ "โ Poor retrieval quality despite high chunk count",
+ "๐ Fundamental issues with retriever or index quality",
+ "๐ Consider retraining retriever or improving index coverage",
+ "๐ Review chunking strategy and document preprocessing"
+ ]
+ }
+ ];
+
+ // Extract relevant metrics from analysis
+ const metricValues = this.extractRelevantMetrics(analysis);
+ console.log('๐ Extracted metric values for recommendations:', metricValues);
+
+ // Evaluate rules and return matching recommendations
+ const matchingRules = this.evaluateRules(evaluationRules, metricValues);
+ console.log('โ
Matching rules found:', matchingRules.length);
+
+ if (matchingRules.length > 0) {
+ // Return recommendations from the first matching rule
+ return matchingRules[0].recommendations;
+ }
+
+ // Fallback recommendations if no rules match
+ return [
+ "๐ Evaluation data doesn't match predefined patterns.",
+ "๐ Review individual query performance for specific insights.",
+ "๐ Consider running evaluation with larger dataset for clearer patterns.",
+ "๐ ๏ธ Manual analysis may be needed for this performance profile."
+ ];
+ }
+
+ extractRelevantMetrics(analysis) {
+ const metricValues = {};
+
+ // Map of possible metric names to standardized names
+ const metricMappings = {
+ 'context_relevancy': [
+ 'Context Precision', 'Context Recall', 'LLM Context Relevancy',
+ 'Context Relevancy', 'context_precision', 'context_recall'
+ ],
+ 'answer_correctness': [
+ 'Answer Correctness', 'LLM Answer Correctness', 'Faithfulness',
+ 'answer_correctness', 'faithfulness'
+ ],
+ 'ground_truth_validity': [
+ 'LLM Ground Truth Validity', 'Ground Truth Validity', 'ground_truth_validity', 'GT Validity'
+ ],
+ 'answer_completeness': [
+ 'LLM Answer Completeness', 'Answer Completeness', 'answer_completeness', 'Completeness'
+ ],
+ 'retrieved_chunk_count': [
+ 'Retrieved Chunk Count', 'retrieved_chunk_count', 'Retrieved Chunks'
+ ],
+ 'sent_to_llm_chunk_count': [
+ 'Sent to LLM Chunk Count', 'sent_to_llm_chunk_count', 'Sent to LLM Chunks'
+ ],
+ 'used_in_answer_chunk_count': [
+ 'Used in Answer Chunk Count', 'used_in_answer_chunk_count', 'Used in Answer Chunks'
+ ],
+ 'total_chunks_used': [
+ 'Total Chunks Used', 'total_chunks_used', 'Total Used'
+ ],
+ 'best_support_rank': [
+ 'Best Support Rank', 'best_support_rank', 'Support Rank'
+ ]
+ };
+
+ // Extract average values for each metric category
+ Object.entries(metricMappings).forEach(([standardName, possibleNames]) => {
+ let bestMatch = null;
+ let bestValue = null;
+
+ possibleNames.forEach(metricName => {
+ if (analysis.statistics[metricName]) {
+ const stats = analysis.statistics[metricName];
+ if (bestMatch === null || metricName.toLowerCase().includes(standardName.split('_')[0])) {
+ bestMatch = metricName;
+ bestValue = stats.mean;
+ }
+ }
+ });
+
+ if (bestValue !== null) {
+ metricValues[standardName] = bestValue;
+ console.log(`๐ ${standardName}: ${bestValue.toFixed(3)} (from ${bestMatch})`);
+ }
+ });
+
+ return metricValues;
+ }
+
+ evaluateRules(rules, metricValues) {
+ const matchingRules = [];
+
+ rules.forEach((rule, index) => {
+ const conditions = rule.conditions;
+ let allConditionsMet = true;
+
+ console.log(`๐ Evaluating rule ${index + 1}:`, conditions);
+
+ Object.entries(conditions).forEach(([conditionKey, conditionValue]) => {
+ const isMaxCondition = conditionKey.endsWith('_max');
+ const metricKey = isMaxCondition ? conditionKey.replace('_max', '') : conditionKey;
+ const metricValue = metricValues[metricKey];
+
+ if (metricValue === undefined) {
+ console.log(`โ ๏ธ Metric '${metricKey}' not found in data`);
+ allConditionsMet = false;
+ return;
+ }
+
+ const conditionMet = this.evaluateCondition(metricValue, conditionValue);
+ console.log(` ${conditionKey}: ${metricValue.toFixed(3)} ${conditionValue} โ ${conditionMet}`);
+
+ if (!conditionMet) {
+ allConditionsMet = false;
+ }
+ });
+
+ if (allConditionsMet) {
+ console.log(`โ
Rule ${index + 1} matches!`);
+ matchingRules.push(rule);
+ }
+ });
+
+ return matchingRules;
+ }
+
+ evaluateCondition(value, condition) {
+ // Parse condition string (e.g., ">=0.75", "<0.5")
+ const match = condition.match(/^(>=|<=|>|<|=)(.+)$/);
+ if (!match) {
+ console.warn('Invalid condition format:', condition);
+ return false;
+ }
+
+ const operator = match[1];
+ const threshold = parseFloat(match[2]);
+
+ switch (operator) {
+ case '>=':
+ return value >= threshold;
+ case '<=':
+ return value <= threshold;
+ case '>':
+ return value > threshold;
+ case '<':
+ return value < threshold;
+ case '=':
+ return Math.abs(value - threshold) < 0.001; // Allow small floating point errors
+ default:
+ return false;
+ }
+ }
+
+ analyzeLLMMetrics(detailedResults, llmCorrectnessCol, llmRelevancyCol) {
+ const analysis = {};
+
+ // Define score ranges
+ const ranges = [
+ { label: 'Excellent (0.9-1.0)', min: 0.9, max: 1.0, color: '#10b981' },
+ { label: 'Good (0.7-0.9)', min: 0.7, max: 0.9, color: '#059669' },
+ { label: 'Fair (0.5-0.7)', min: 0.5, max: 0.7, color: '#f59e0b' },
+ { label: 'Poor (0.3-0.5)', min: 0.3, max: 0.5, color: '#ef4444' },
+ { label: 'Very Poor (0.0-0.3)', min: 0.0, max: 0.3, color: '#dc2626' }
+ ];
+
+ // Analyze Answer Correctness
+ if (llmCorrectnessCol) {
+ const correctnessValues = detailedResults
+ .map(row => parseFloat(row[llmCorrectnessCol]))
+ .filter(val => !isNaN(val) && val >= 0 && val <= 1);
+
+ analysis.correctness = this.categorizeScores(correctnessValues, ranges);
+ }
+
+ // Analyze Context Relevancy
+ if (llmRelevancyCol) {
+ const relevancyValues = detailedResults
+ .map(row => parseFloat(row[llmRelevancyCol]))
+ .filter(val => !isNaN(val) && val >= 0 && val <= 1);
+
+ analysis.relevancy = this.categorizeScores(relevancyValues, ranges);
+ }
+
+ // Create comparison data
+ if (llmCorrectnessCol && llmRelevancyCol) {
+ analysis.comparison = this.createComparisonData(detailedResults, llmCorrectnessCol, llmRelevancyCol);
+ }
+
+ return analysis;
+ }
+
+ categorizeScores(values, ranges) {
+ const total = values.length;
+ if (total === 0) return null;
+
+ const distribution = ranges.map(range => {
+ const count = values.filter(val => val >= range.min && val < range.max).length;
+ const percentage = (count / total * 100).toFixed(1);
+ return {
+ label: range.label,
+ count: count,
+ percentage: parseFloat(percentage),
+ color: range.color
+ };
+ });
+
+ // Calculate average score
+ const average = (values.reduce((sum, val) => sum + val, 0) / total).toFixed(3);
+
+ return {
+ distribution: distribution,
+ total: total,
+ average: parseFloat(average)
+ };
+ }
+
+ createComparisonData(detailedResults, correctnessCol, relevancyCol) {
+ const comparisonRanges = [
+ { label: 'High Correctness & High Relevancy', correctnessMin: 0.7, relevancyMin: 0.7, color: '#10b981' },
+ { label: 'High Correctness & Low Relevancy', correctnessMin: 0.7, relevancyMin: 0, relevancyMax: 0.7, color: '#f59e0b' },
+ { label: 'Low Correctness & High Relevancy', correctnessMin: 0, correctnessMax: 0.7, relevancyMin: 0.7, color: '#6366f1' },
+ { label: 'Low Correctness & Low Relevancy', correctnessMin: 0, correctnessMax: 0.7, relevancyMin: 0, relevancyMax: 0.7, color: '#ef4444' }
+ ];
+
+ const total = detailedResults.length;
+ const comparison = comparisonRanges.map(range => {
+ const count = detailedResults.filter(row => {
+ const correctness = parseFloat(row[correctnessCol]);
+ const relevancy = parseFloat(row[relevancyCol]);
+
+ if (isNaN(correctness) || isNaN(relevancy)) return false;
+
+ const correctnessMatch = correctness >= range.correctnessMin &&
+ (range.correctnessMax === undefined || correctness < range.correctnessMax);
+ const relevancyMatch = relevancy >= range.relevancyMin &&
+ (range.relevancyMax === undefined || relevancy < range.relevancyMax);
+
+ return correctnessMatch && relevancyMatch;
+ }).length;
+
+ return {
+ label: range.label,
+ count: count,
+ percentage: ((count / total) * 100).toFixed(1),
+ color: range.color
+ };
+ });
+
+ return comparison;
+ }
+
+ createDistributionChart(canvasId, analysisData, title, primaryColor) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) {
+ console.error(`โ Canvas ${canvasId} not found`);
+ return;
+ }
+
+ const ctx = canvas.getContext('2d');
+ const data = analysisData.distribution;
+
+ new Chart(ctx, {
+ type: 'doughnut',
+ data: {
+ labels: data.map(item => `${item.label} (${item.percentage}%)`),
+ datasets: [{
+ data: data.map(item => item.count),
+ backgroundColor: data.map(item => item.color),
+ borderWidth: 2,
+ borderColor: '#ffffff'
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ padding: 15,
+ font: { size: 11 },
+ generateLabels: (chart) => {
+ const original = Chart.defaults.plugins.legend.labels.generateLabels;
+ const labels = original.call(this, chart);
+
+ labels.forEach((label, index) => {
+ label.text = `${data[index].label}: ${data[index].count} queries (${data[index].percentage}%)`;
+ });
+
+ return labels;
+ }
+ }
+ },
+ tooltip: {
+ callbacks: {
+ label: (context) => {
+ const item = data[context.dataIndex];
+ return `${item.count} queries (${item.percentage}%)`;
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ createComparisonChart(canvasId, comparisonData) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) {
+ console.error(`โ Canvas ${canvasId} not found`);
+ return;
+ }
+
+ const ctx = canvas.getContext('2d');
+
+ new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: comparisonData.map(item => item.label),
+ datasets: [{
+ label: 'Number of Queries',
+ data: comparisonData.map(item => item.count),
+ backgroundColor: comparisonData.map(item => item.color),
+ borderRadius: 6,
+ borderSkipped: false
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ callbacks: {
+ label: (context) => {
+ const item = comparisonData[context.dataIndex];
+ return `${item.count} queries (${item.percentage}%)`;
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ grid: { color: 'rgba(0, 0, 0, 0.1)' },
+ ticks: { font: { size: 10 } }
+ },
+ x: {
+ grid: { display: false },
+ ticks: {
+ font: { size: 9 },
+ maxRotation: 45
+ }
+ }
+ }
+ }
+ });
+ }
+
+ getColumnClass(columnName) {
+ const col = columnName.toLowerCase();
+ if (col.includes('relevancy') || col.includes('faithfulness') ||
+ col.includes('recall') || col.includes('precision') ||
+ col.includes('correctness') || col.includes('similarity')) {
+ return 'metric-column';
+ }
+ if (col === 'query' || col === 'answer' || col === 'ground_truth') {
+ return 'text-column';
+ }
+ return 'standard-column';
+ }
+
+ formatColumnHeader(columnName) {
+ // Convert column names to more readable format
+ return columnName
+ .replace(/_/g, ' ')
+ .replace(/([A-Z])/g, ' $1')
+ .replace(/\b\w/g, l => l.toUpperCase())
+ .trim();
+ }
+
+ formatCellValue(columnName, value) {
+ if (value === null || value === undefined || value === 'N/A') {
+ return 'N/A ';
+ }
+
+ const col = columnName.toLowerCase();
+
+ // Format evaluation metrics (RAGAS, LLM, CRAG)
+ if (col.includes('relevancy') || col.includes('faithfulness') ||
+ col.includes('recall') || col.includes('precision') ||
+ col.includes('correctness') || col.includes('similarity') ||
+ col.includes('accuracy') || col.includes('llm') || col.includes('crag')) {
+
+ const numValue = parseFloat(value);
+ if (!isNaN(numValue)) {
+ // Smart formatting: detect if value is already a percentage (>1) or decimal (0-1)
+ let displayValue, normalizedValue;
+
+ if (numValue <= 1) {
+ // Value is likely a decimal (0-1), convert to percentage for display
+ displayValue = (numValue * 100).toFixed(1) + '%';
+ normalizedValue = numValue;
+ } else {
+ // Value is likely already a percentage, display as-is
+ displayValue = numValue.toFixed(1) + '%';
+ normalizedValue = numValue / 100;
+ }
+
+ const scoreClass = normalizedValue >= 0.8 ? 'score-good' :
+ normalizedValue >= 0.6 ? 'score-medium' : 'score-low';
+
+ return `${displayValue} `;
+ }
+ }
+
+ // Format long text fields
+ if (col === 'query' || col === 'answer' || col === 'ground_truth' || col.includes('context')) {
+ const strValue = String(value);
+ if (strValue.length > 100) {
+ const truncated = strValue.substring(0, 100) + '...';
+ return `${this.escapeHtml(truncated)} `;
+ }
+ return `${this.escapeHtml(strValue)} `;
+ }
+
+ // Format numbers
+ if (!isNaN(value) && value !== '') {
+ const numValue = parseFloat(value);
+ if (Number.isInteger(numValue)) {
+ return `${numValue} `;
+ } else {
+ // Show more precision for non-metric numbers, less aggressive rounding
+ const decimalPlaces = numValue < 1 ? 6 : 4;
+ return `${numValue.toFixed(decimalPlaces)} `;
+ }
+ }
+
+ // Default formatting
+ const strValue = String(value);
+ if (strValue.length > 50) {
+ const truncated = strValue.substring(0, 50) + '...';
+ return `${this.escapeHtml(truncated)} `;
+ }
+
+ return this.escapeHtml(strValue);
+ }
+
+ getCellTitle(value) {
+ if (value === null || value === undefined || value === 'N/A') {
+ return 'No data available';
+ }
+ return String(value).length > 50 ? String(value) : '';
+ }
+
+ escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ formatTokenCount(tokens) {
+ if (tokens === null || tokens === undefined || tokens === 0) {
+ return 'N/A ';
+ }
+ return Number(tokens).toLocaleString();
+ }
+
+ formatCurrency(amount) {
+ if (amount === null || amount === undefined || amount === 0) {
+ return '$0.00 ';
+ }
+ return `$${Number(amount).toFixed(4)}`;
+ }
+
+ formatCostPerQuery(totalCost, totalQueries) {
+ if (!totalCost || !totalQueries || totalCost === 0 || totalQueries === 0) {
+ return 'N/A ';
+ }
+ const costPerQuery = totalCost / totalQueries;
+ return `$${costPerQuery.toFixed(6)}`;
+ }
+
+ createTokenUsageSection(stats) {
+ // Check if we have real token data
+ const hasRealTokenData = this.hasRealTokenUsageData(stats);
+
+ if (!hasRealTokenData) {
+ console.log('โ ๏ธ No valid token usage data available, hiding detailed section');
+ return '';
+ }
+
+ console.log('โ
Token usage data detected, showing detailed breakdown section');
+
+ // Build the section with available data
+ let tokenCards = '';
+
+ // Always show total tokens if available
+ if (stats.total_tokens && stats.total_tokens > 0) {
+ tokenCards += `
+
+ ${this.formatTokenCount(stats.total_tokens)}
+ Total Tokens
+
`;
+ }
+
+ // Show breakdown if available
+ if (stats.prompt_tokens && stats.prompt_tokens > 0) {
+ tokenCards += `
+
+ ${this.formatTokenCount(stats.prompt_tokens)}
+ Prompt Tokens
+
`;
+ }
+
+ if (stats.completion_tokens && stats.completion_tokens > 0) {
+ tokenCards += `
+
+ ${this.formatTokenCount(stats.completion_tokens)}
+ Completion Tokens
+
`;
+ }
+
+ // Show cost per query if both cost and query count are available
+ if (stats.estimated_cost_usd && stats.total_processed &&
+ stats.estimated_cost_usd > 0 && stats.total_processed > 0) {
+ tokenCards += `
+
+ ${this.formatCostPerQuery(stats.estimated_cost_usd, stats.total_processed)}
+ Cost per Query
+
`;
+ }
+
+ return `
+
+
Token Usage Breakdown
+
+ ${tokenCards}
+
+
+ `;
+ }
+
+ hasRealTokenUsageData(stats) {
+ console.log('๐ Validating token usage data:', stats);
+
+ // Check if we have any meaningful token usage data
+ if (!stats) {
+ console.log('โ No stats object provided');
+ return false;
+ }
+
+ // Check for at least total_tokens (most basic requirement)
+ if (!stats.total_tokens || stats.total_tokens <= 0) {
+ console.log('โ No valid total_tokens found:', stats.total_tokens);
+ return false;
+ }
+
+ console.log('โ
Found valid total_tokens:', stats.total_tokens);
+
+ // If we have prompt_tokens and completion_tokens, validate they sum correctly
+ if (stats.prompt_tokens && stats.completion_tokens &&
+ stats.prompt_tokens > 0 && stats.completion_tokens > 0) {
+
+ const calculatedTotal = stats.prompt_tokens + stats.completion_tokens;
+ const tokenSumValid = Math.abs(stats.total_tokens - calculatedTotal) <= 1;
+
+ if (!tokenSumValid) {
+ console.warn('โ ๏ธ Token sum validation failed:', {
+ total_tokens: stats.total_tokens,
+ prompt_tokens: stats.prompt_tokens,
+ completion_tokens: stats.completion_tokens,
+ calculated_total: calculatedTotal
+ });
+ // Still show data even if breakdown doesn't match perfectly
+ } else {
+ console.log('โ
Token breakdown validation passed');
+ }
+ } else {
+ console.log('โน๏ธ No complete token breakdown available, but total_tokens is valid');
+ }
+
+ // Check if cost data exists and is reasonable (optional)
+ if (stats.estimated_cost_usd !== undefined && stats.estimated_cost_usd !== null) {
+ if (stats.estimated_cost_usd <= 0) {
+ console.warn('โ ๏ธ Estimated cost is zero or negative:', stats.estimated_cost_usd);
+ } else {
+ console.log('โ
Found valid estimated_cost_usd:', stats.estimated_cost_usd);
+ }
+ } else {
+ console.log('โน๏ธ No cost data available');
+ }
+
+ console.log('โ
Token usage data validation passed (relaxed validation)');
+ return true;
+ }
+
+ hideChartTooltips() {
+ try {
+ // Hide Chart.js tooltips if they exist
+ if (this.metricsChart) {
+ this.metricsChart.tooltip.setActiveElements([]);
+ this.metricsChart.update('none');
+ }
+
+ // Remove any stray Chart.js tooltip elements
+ const chartTooltips = document.querySelectorAll('.chartjs-tooltip, [role="tooltip"]');
+ chartTooltips.forEach(tooltip => {
+ if (tooltip.style) {
+ tooltip.style.opacity = '0';
+ tooltip.style.visibility = 'hidden';
+ tooltip.style.display = 'none';
+ }
+ });
+
+ // Hide any floating percentage displays or labels
+ const floatingElements = document.querySelectorAll('.score-text, .percentage-display, .chart-label');
+ floatingElements.forEach(element => {
+ if (element.style) {
+ element.style.visibility = 'hidden';
+ }
+ });
+
+ console.log('๐ซฅ Chart tooltips and floating elements hidden');
+ } catch (error) {
+ console.warn('โ ๏ธ Error hiding chart tooltips:', error);
+ }
+ }
+
+ showDetailedModal() {
+ console.log('๐ Opening detailed modal with result data:', this.resultData);
+
+ // Hide any Chart.js tooltips or floating elements that might interfere
+ this.hideChartTooltips();
+
+ const stats = this.resultData.processing_stats || {};
+ const metrics = this.extractMetricsFromResult(this.resultData);
+ const config = this.resultData.config_used || {};
+
+ console.log('๐ฐ Processing stats for modal:', stats);
+ console.log('๐ Token usage data:', {
+ total_tokens: stats.total_tokens,
+ prompt_tokens: stats.prompt_tokens,
+ completion_tokens: stats.completion_tokens,
+ estimated_cost_usd: stats.estimated_cost_usd
+ });
+ console.log('๐ Token usage validation result:', this.hasRealTokenUsageData(stats));
+
+ const modal = document.createElement('div');
+ modal.className = 'results-modal';
+ modal.style.cssText = `
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100% !important;
+ height: 100% !important;
+ background: rgba(0, 0, 0, 0.6) !important;
+ z-index: 999998 !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease;
+ `;
+ modal.innerHTML = `
+
+
+
+
+
+
Processing Statistics
+
+
+ ${stats.total_processed || 0}
+ Total Processed
+
+
+ ${stats.successful_queries || 0}
+ Successful
+
+
+ ${stats.failed_queries || 0}
+ Failed
+
+ ${this.hasRealTokenUsageData(stats) ? `
+
+ ${this.formatTokenCount(stats.total_tokens)}
+ Total Tokens
+
+
+ ${this.formatCurrency(stats.estimated_cost_usd)}
+ Estimated Cost
+
` : ''}
+
+
+
+ ${this.createTokenUsageSection(stats)}
+
+
+
Detailed Evaluation Results (${this.getEvaluationMethodsText()})
+ ${this.createDetailedResultsTable()}
+
+
+
+
Configuration
+
+
RAGAS: ${config.evaluate_ragas ? 'โ
Enabled' : 'โ Disabled'}
+
CRAG: ${config.evaluate_crag ? 'โ
Enabled' : 'โ Disabled'}
+
LLM Evaluation: ${config.evaluate_llm ? 'โ
Enabled' : 'โ Disabled'}
+
Search API: ${config.use_search_api ? 'โ
Enabled' : 'โ Disabled'}
+
LLM Model: ${config.llm_model || 'Default'}
+
Batch Size: ${config.batch_size || 10}
+
+
+
+
+
+
+ `;
+
+ document.body.appendChild(modal);
+
+ // Show modal with animation
+ setTimeout(() => {
+ modal.classList.add('show');
+ modal.style.opacity = '1';
+ modal.style.visibility = 'visible';
+ }, 10);
+
+ // Close on backdrop click (outside modal content)
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ this.closeModal(modal);
+ }
+ });
+
+ // Prevent clicks inside modal content from closing modal
+ const modalContent = modal.querySelector('.modal-content');
+ if (modalContent) {
+ modalContent.addEventListener('click', (e) => {
+ e.stopPropagation();
+ });
+ }
+
+ // Close on close button click
+ const closeBtn = modal.querySelector('.modal-close');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.closeModal(modal);
+ });
+ }
+
+ // ESC key to close
+ const escHandler = (e) => {
+ if (e.key === 'Escape') {
+ this.closeModal(modal);
+ document.removeEventListener('keydown', escHandler);
+ }
+ };
+ document.addEventListener('keydown', escHandler);
+ }
+
+ closeModal(modal) {
+ // Restore any hidden Chart.js elements
+ this.restoreChartElements();
+
+ // Hide modal with animation
+ modal.classList.remove('show');
+ modal.style.opacity = '0';
+ modal.style.visibility = 'hidden';
+
+ setTimeout(() => {
+ if (modal.parentNode) {
+ modal.parentNode.removeChild(modal);
+ }
+ }, 300);
+ }
+
+ restoreChartElements() {
+ try {
+ // Restore Chart.js tooltip elements
+ const chartTooltips = document.querySelectorAll('.chartjs-tooltip, [role="tooltip"]');
+ chartTooltips.forEach(tooltip => {
+ if (tooltip.style) {
+ tooltip.style.opacity = '';
+ tooltip.style.visibility = '';
+ tooltip.style.display = '';
+ }
+ });
+
+ // Restore any floating percentage displays or labels
+ const floatingElements = document.querySelectorAll('.score-text, .percentage-display, .chart-label');
+ floatingElements.forEach(element => {
+ if (element.style) {
+ element.style.visibility = '';
+ }
+ });
+
+ console.log('๐ Chart elements restored');
+ } catch (error) {
+ console.warn('โ ๏ธ Error restoring chart elements:', error);
+ }
+ }
+
+ async resetForNewEvaluation() {
+ console.log('๐ Reset for new evaluation');
+
+ // Hide sections
+ const progressSection = document.getElementById('progress-section');
+ const resultsSection = document.getElementById('results-section');
+
+ if (progressSection) progressSection.style.display = 'none';
+ if (resultsSection) resultsSection.style.display = 'none';
+
+ // Reset progress
+ const progressFill = document.getElementById('progress-fill');
+ const logOutput = document.getElementById('log-output');
+
+ if (progressFill) progressFill.style.width = '0%';
+ if (logOutput) logOutput.innerHTML = '';
+
+ // Clear uploaded file and reset file upload UI
+ this.removeFile();
+
+ // Clear estimation details in the Run Evaluation section
+ const estimationDetails = document.getElementById('estimation-details');
+ if (estimationDetails) {
+ estimationDetails.innerHTML = '';
+ }
+
+ // Reset evaluation metrics elements (if they exist)
+ this.updateElement('total-processed', '0');
+ this.updateElement('successful-queries', '0');
+ this.updateElement('total-time', '0s');
+ this.updateElement('avg-time', '0s');
+
+ // Clear any metrics chart
+ if (this.metricsChart) {
+ this.metricsChart.destroy();
+ this.metricsChart = null;
+ }
+
+ // Clear evaluation results data
+ this.resultData = null;
+ this.fileRowCounts = null;
+
+ // Reset form state
+ this.enableForm();
+ this.validateForm();
+
+ // Reset any dynamic time estimates to default
+ this.updateElement('estimated-time', '--');
+ this.updateElement('total-queries', '--');
+
+ // Scroll to top and show success message
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ this.showToast('โจ Ready for new evaluation! Please upload your Excel file.', 'info');
+ }
+
+ debugButtons() {
+ console.log('๐ BUTTON DEBUG INFO:');
+ console.log('Download button:', document.getElementById('download-results'));
+ console.log('View button:', document.getElementById('view-details'));
+ console.log('New eval button:', document.getElementById('new-evaluation'));
+ console.log('Results section visible:', document.getElementById('results-section')?.offsetParent !== null);
+ console.log('Result data:', !!this.resultData);
+ console.log('Chart.js available:', typeof Chart !== 'undefined');
+ console.log('Current chart:', !!this.metricsChart);
+ }
+
+ disableForm() {
+ const inputs = document.querySelectorAll('input, select, button');
+ inputs.forEach(input => {
+ if (input.id !== 'remove-file') input.disabled = true;
+ });
+ }
+
+ enableForm() {
+ const inputs = document.querySelectorAll('input, select, button');
+ inputs.forEach(input => input.disabled = false);
+ this.validateForm();
+ }
+
+ formatFileSize(bytes) {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ }
+
+ formatTime(seconds) {
+ if (seconds < 60) return `${Math.round(seconds)}s`;
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = Math.round(seconds % 60);
+ return `${minutes}m ${remainingSeconds}s`;
+ }
+
+ showToast(message, type = 'info') {
+ const existingToasts = document.querySelectorAll('.toast');
+ existingToasts.forEach(toast => toast.remove());
+
+ const toast = document.createElement('div');
+ toast.className = `toast toast-${type}`;
+ toast.innerHTML = `
+
+ ${message}
+ `;
+
+ document.body.appendChild(toast);
+
+ setTimeout(() => {
+ if (toast.parentNode) {
+ toast.style.animation = 'slideInRight 0.3s ease reverse';
+ setTimeout(() => {
+ if (toast.parentNode) toast.parentNode.removeChild(toast);
+ }, 300);
+ }
+ }, 4000);
+ }
+
+ getToastIcon(type) {
+ const icons = {
+ 'success': 'check-circle',
+ 'warning': 'exclamation-triangle',
+ 'error': 'times-circle',
+ 'info': 'info-circle'
+ };
+ return icons[type] || 'info-circle';
+ }
+
+ getEvaluationMethodsText() {
+ const config = this.resultData?.config_used || {};
+ const enabledMethods = [];
+ if (config.evaluate_ragas) enabledMethods.push('RAGAS');
+ if (config.evaluate_llm) enabledMethods.push('LLM');
+ if (config.evaluate_crag) enabledMethods.push('CRAG');
+
+ return enabledMethods.length > 0 ? enabledMethods.join(' + ') : 'Unknown Methods';
+ }
+
+ createOverviewCharts(sheetId, analysisData) {
+ // 1. Summary Radar Chart
+ this.createSummaryRadarChart(`summary-radar-${sheetId}`, analysisData);
+
+ // 2. Per-Metric Score Range Histograms
+ this.createMetricRangeHistograms(`metric-histograms-${sheetId}`, analysisData);
+
+ // 3. Key Statistics
+ this.populateKeyStatistics(`key-stats-${sheetId}`, analysisData);
+ }
+
+
+
+ createCorrelationCharts(sheetId, analysisData) {
+ // 1. Correlation heatmap
+ this.createCorrelationHeatmap(`correlation-matrix-${sheetId}`, analysisData);
+
+ // 2. Scatter plot matrix
+ this.createScatterMatrix(`scatter-matrix-${sheetId}`, analysisData);
+
+ // 3. Correlation insights
+ this.populateCorrelationInsights(`correlation-insights-${sheetId}`, analysisData);
+ }
+
+ createPerformanceCharts(sheetId, analysisData) {
+ // 1. Query performance heatmap
+ this.createQueryHeatmap(`performance-heatmap-${sheetId}`, analysisData);
+
+ // 2. Top/bottom performers
+ this.createTopBottomChart(`top-bottom-queries-${sheetId}`, analysisData);
+
+ // 3. Outliers analysis
+ this.populateOutliersAnalysis(`outliers-analysis-${sheetId}`, analysisData);
+ }
+
+ generateInsights(sheetId, analysisData) {
+ console.log('๐ฏ Generating insights for sheet:', sheetId);
+ console.log('๐ Analysis data insights:', analysisData.insights);
+
+ // Populate all insight sections
+ this.populateModelInsights(`model-insights-${sheetId}`, analysisData);
+ this.populateStatisticalInsights(`statistical-insights-${sheetId}`, analysisData);
+ this.populateRecommendations(`recommendations-${sheetId}`, analysisData);
+
+ // Populate retrieval quality tab
+ this.populateRetrievalQualityTab(sheetId, analysisData);
+
+ console.log('โ
Insights generation completed for sheet:', sheetId);
+ }
+
+ createSummaryRadarChart(canvasId, analysisData) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ const metrics = Object.keys(analysisData.statistics);
+ const avgScores = metrics.map(metric => analysisData.statistics[metric].mean);
+
+ new Chart(ctx, {
+ type: 'radar',
+ data: {
+ labels: metrics.map(m => this.formatMetricName(m)),
+ datasets: [{
+ label: 'Average Scores',
+ data: avgScores,
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
+ borderColor: 'rgba(59, 130, 246, 0.8)',
+ borderWidth: 2,
+ pointBackgroundColor: 'rgba(59, 130, 246, 1)',
+ pointBorderColor: '#ffffff',
+ pointBorderWidth: 2,
+ pointRadius: 5
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ callbacks: {
+ label: (context) => `${context.label}: ${(context.raw * 100).toFixed(1)}%`
+ }
+ }
+ },
+ scales: {
+ r: {
+ beginAtZero: true,
+ max: 1,
+ ticks: {
+ stepSize: 0.2,
+ callback: (value) => `${(value * 100).toFixed(0)}%`
+ }
+ }
+ }
+ }
+ });
+ }
+
+ createMetricRangeHistograms(containerId, analysisData) {
+ console.log('๐ Creating per-metric range histograms for container:', containerId);
+
+ const container = document.getElementById(containerId);
+ if (!container) {
+ console.error('โ Container element not found:', containerId);
+ return;
+ }
+
+ // Define score ranges (same as before)
+ const ranges = [
+ { label: 'Excellent', shortLabel: 'Exc', min: 0.9, max: 1.0, color: '#10b981' },
+ { label: 'Good', shortLabel: 'Good', min: 0.75, max: 0.9, color: '#059669' },
+ { label: 'Fair', shortLabel: 'Fair', min: 0.6, max: 0.75, color: '#f59e0b' },
+ { label: 'Poor', shortLabel: 'Poor', min: 0.4, max: 0.6, color: '#ef4444' },
+ { label: 'Very Poor', shortLabel: 'V.Poor', min: 0.0, max: 0.4, color: '#dc2626' }
+ ];
+
+ // Get metrics that have actual score data (not justification columns)
+ const metricsWithScores = Object.entries(analysisData.scores).filter(([metric, scores]) =>
+ !metric.includes('Justification') && !metric.includes('Test') && scores && scores.length > 0
+ );
+
+ console.log('๐ Metrics with scores:', metricsWithScores.map(([metric]) => metric));
+
+ if (metricsWithScores.length === 0) {
+ container.innerHTML = 'No metric data available
';
+ return;
+ }
+
+ // Create histogram container HTML with enhanced styles for full-width layout
+ let histogramHTML = `
+
+
+ `;
+
+ metricsWithScores.forEach(([metric, scores], metricIndex) => {
+ const cleanMetricName = metric.replace('LLM ', '').replace(' Relevancy', ' Rel.').replace(' Correctness', ' Corr.').replace(' Ground Truth Validity', ' GT Val.').replace(' Answer Completeness', ' Comp.').replace(' Justification', ' Just.');
+ const canvasId = `histogram-${containerId}-${metricIndex}`;
+
+ histogramHTML += `
+
+ ${cleanMetricName}
+
+
+ `;
+ });
+
+ histogramHTML += '
';
+ container.innerHTML = histogramHTML;
+
+ // Create individual histogram charts
+ metricsWithScores.forEach(([metric, scores], metricIndex) => {
+ const canvasId = `histogram-${containerId}-${metricIndex}`;
+ this.createSingleMetricHistogram(canvasId, metric, scores, ranges);
+ });
+
+ console.log('โ
Per-metric histograms created successfully!');
+ }
+
+ createSingleMetricHistogram(canvasId, metricName, scores, ranges) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) {
+ console.error('โ Histogram canvas not found:', canvasId);
+ return;
+ }
+
+ const ctx = canvas.getContext('2d');
+
+ // Calculate range counts for this metric
+ const rangeCounts = ranges.map((range, index) => {
+ const isExcellentRange = index === 0;
+ const scoresInRange = scores.filter(score =>
+ score >= range.min && (isExcellentRange ? score <= range.max : score < range.max)
+ );
+ return scoresInRange.length;
+ });
+
+ console.log(`๐ ${metricName} range distribution:`, rangeCounts);
+
+ // Destroy existing chart if it exists
+ const existingChart = Chart.getChart(ctx);
+ if (existingChart) {
+ existingChart.destroy();
+ }
+
+ // Create bar chart
+ new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: ranges.map(r => r.shortLabel),
+ datasets: [{
+ label: 'Count',
+ data: rangeCounts,
+ backgroundColor: ranges.map(r => r.color),
+ borderColor: ranges.map(r => r.color),
+ borderWidth: 1
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ layout: {
+ padding: {
+ top: 10,
+ bottom: 5
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ max: Math.max(...rangeCounts) + 1,
+ ticks: {
+ stepSize: 1,
+ font: { size: 11 },
+ color: '#6b7280'
+ },
+ grid: {
+ color: '#f3f4f6'
+ }
+ },
+ x: {
+ ticks: {
+ font: { size: 11, weight: '500' },
+ color: '#374151'
+ },
+ grid: {
+ display: false
+ }
+ }
+ },
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ titleFont: { size: 12, weight: '600' },
+ bodyFont: { size: 11 },
+ callbacks: {
+ label: (context) => {
+ const rangeName = ranges[context.dataIndex].label;
+ const count = context.raw;
+ const total = rangeCounts.reduce((a, b) => a + b, 0);
+ const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : '0.0';
+ return `${rangeName}: ${count} scores (${percentage}%)`;
+ }
+ }
+ }
+ },
+ animation: {
+ duration: 800,
+ easing: 'easeOutQuart'
+ }
+ }
+ });
+ }
+
+ populateKeyStatistics(elementId, analysisData) {
+ const element = document.getElementById(elementId);
+ if (!element) return;
+
+ const allScores = Object.values(analysisData.scores).flat();
+ const overallMean = allScores.reduce((a, b) => a + b, 0) / allScores.length;
+ const overallStdDev = Math.sqrt(allScores.reduce((acc, score) => acc + Math.pow(score - overallMean, 2), 0) / allScores.length);
+
+ const bestMetric = Object.entries(analysisData.statistics).reduce((best, [metric, stats]) =>
+ stats.mean > best.score ? { metric, score: stats.mean } : best,
+ { metric: '', score: 0 }
+ );
+
+ const worstMetric = Object.entries(analysisData.statistics).reduce((worst, [metric, stats]) =>
+ stats.mean < worst.score ? { metric, score: stats.mean } : worst,
+ { metric: '', score: 1 }
+ );
+
+ element.innerHTML = `
+
+ Overall Performance
+ ${(overallMean * 100).toFixed(1)}%
+
+
+ Best Metric
+ ${this.formatMetricName(bestMetric.metric)} (${(bestMetric.score * 100).toFixed(1)}%)
+
+
+ Needs Improvement
+ ${this.formatMetricName(worstMetric.metric)} (${(worstMetric.score * 100).toFixed(1)}%)
+
+
+ Score Consistency
+ ฯ = ${overallStdDev.toFixed(3)}
+
+
+ Total Evaluations
+ ${analysisData.rawData.length}
+
+
+ Metrics Analyzed
+ ${analysisData.metrics.length}
+
+ `;
+ }
+
+
+
+
+
+ formatMetricName(metric) {
+ return metric
+ .replace(/_/g, ' ')
+ .replace(/([A-Z])/g, ' $1')
+ .replace(/\b\w/g, l => l.toUpperCase())
+ .trim();
+ }
+
+
+
+ createHistogram(canvasId, scores, metricName) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+
+ // Create bins for histogram
+ const bins = 10;
+ const binSize = 1 / bins;
+ const binCounts = new Array(bins).fill(0);
+ const binLabels = [];
+
+ for (let i = 0; i < bins; i++) {
+ const start = i * binSize;
+ const end = (i + 1) * binSize;
+ binLabels.push(`${(start * 100).toFixed(0)}-${(end * 100).toFixed(0)}%`);
+ }
+
+ scores.forEach(score => {
+ const binIndex = Math.min(Math.floor(score / binSize), bins - 1);
+ binCounts[binIndex]++;
+ });
+
+ new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: binLabels,
+ datasets: [{
+ data: binCounts,
+ backgroundColor: 'rgba(59, 130, 246, 0.6)',
+ borderColor: 'rgba(59, 130, 246, 0.8)',
+ borderWidth: 1,
+ borderRadius: 4
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ callbacks: {
+ title: () => metricName,
+ label: (context) => `${context.raw} queries in ${context.label} range`
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: { stepSize: 1 }
+ },
+ x: {
+ ticks: {
+ maxRotation: 45,
+ font: { size: 8 }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ createCorrelationHeatmap(canvasId, analysisData) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ const metrics = Object.keys(analysisData.statistics);
+
+ // Create correlation matrix data
+ const correlationMatrix = [];
+ const labels = metrics.map(m => this.formatMetricName(m));
+
+ for (let i = 0; i < metrics.length; i++) {
+ const row = [];
+ for (let j = 0; j < metrics.length; j++) {
+ if (i === j) {
+ row.push(1); // Perfect correlation with self
+ } else {
+ const key1 = `${metrics[i]}_${metrics[j]}`;
+ const key2 = `${metrics[j]}_${metrics[i]}`;
+ const correlation = analysisData.correlations[key1] || analysisData.correlations[key2] || 0;
+ row.push(correlation);
+ }
+ }
+ correlationMatrix.push(row);
+ }
+
+ // Create heatmap using scatter plot
+ const scatterData = [];
+ correlationMatrix.forEach((row, i) => {
+ row.forEach((correlation, j) => {
+ scatterData.push({
+ x: j,
+ y: metrics.length - 1 - i, // Flip Y axis
+ v: correlation
+ });
+ });
+ });
+
+ new Chart(ctx, {
+ type: 'scatter',
+ data: {
+ datasets: [{
+ label: 'Correlation',
+ data: scatterData,
+ backgroundColor: (context) => {
+ const correlation = context.raw.v;
+ const intensity = Math.abs(correlation);
+ const hue = correlation >= 0 ? 120 : 0; // Green for positive, red for negative
+ return `hsla(${hue}, 70%, 50%, ${intensity})`;
+ },
+ pointRadius: 15,
+ pointHoverRadius: 18
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ callbacks: {
+ title: () => 'Metric Correlation',
+ label: (context) => {
+ const i = metrics.length - 1 - context.raw.y;
+ const j = context.raw.x;
+ return `${labels[i]} โ ${labels[j]}: ${context.raw.v.toFixed(3)}`;
+ }
+ }
+ }
+ },
+ scales: {
+ x: {
+ type: 'linear',
+ position: 'bottom',
+ min: -0.5,
+ max: metrics.length - 0.5,
+ ticks: {
+ stepSize: 1,
+ callback: (value) => labels[Math.round(value)] || ''
+ }
+ },
+ y: {
+ type: 'linear',
+ min: -0.5,
+ max: metrics.length - 0.5,
+ ticks: {
+ stepSize: 1,
+ callback: (value) => labels[metrics.length - 1 - Math.round(value)] || ''
+ }
+ }
+ }
+ }
+ });
+ }
+
+ createScatterMatrix(canvasId, analysisData) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ const metrics = Object.keys(analysisData.scores);
+
+ if (metrics.length < 2) {
+ canvas.parentElement.innerHTML = 'Need at least 2 metrics for scatter analysis
';
+ return;
+ }
+
+ // Create scatter plot for the two most correlated metrics
+ const correlations = Object.entries(analysisData.correlations);
+ const strongestCorrelation = correlations.reduce((strongest, [pair, correlation]) =>
+ Math.abs(correlation) > Math.abs(strongest.correlation) ? { pair, correlation } : strongest,
+ { pair: '', correlation: 0 }
+ );
+
+ if (!strongestCorrelation.pair) return;
+
+ const [metric1, metric2] = strongestCorrelation.pair.split('_');
+ const scores1 = analysisData.scores[metric1];
+ const scores2 = analysisData.scores[metric2];
+
+ if (!scores1 || !scores2) return;
+
+ const scatterData = scores1.map((score1, index) => ({
+ x: score1,
+ y: scores2[index] || 0
+ })).filter(point => point.y !== 0);
+
+ new Chart(ctx, {
+ type: 'scatter',
+ data: {
+ datasets: [{
+ label: `${this.formatMetricName(metric1)} vs ${this.formatMetricName(metric2)}`,
+ data: scatterData,
+ backgroundColor: 'rgba(59, 130, 246, 0.6)',
+ borderColor: 'rgba(59, 130, 246, 0.8)',
+ borderWidth: 1,
+ pointRadius: 4,
+ pointHoverRadius: 6
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ callbacks: {
+ label: (context) => {
+ return `${this.formatMetricName(metric1)}: ${(context.raw.x * 100).toFixed(1)}%, ${this.formatMetricName(metric2)}: ${(context.raw.y * 100).toFixed(1)}%`;
+ }
+ }
+ }
+ },
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: this.formatMetricName(metric1)
+ },
+ min: 0,
+ max: 1,
+ ticks: {
+ callback: (value) => `${(value * 100).toFixed(0)}%`
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: this.formatMetricName(metric2)
+ },
+ min: 0,
+ max: 1,
+ ticks: {
+ callback: (value) => `${(value * 100).toFixed(0)}%`
+ }
+ }
+ }
+ }
+ });
+ }
+
+ populateCorrelationInsights(elementId, analysisData) {
+ const element = document.getElementById(elementId);
+ if (!element) return;
+
+ const correlations = Object.entries(analysisData.correlations);
+ const strongCorrelations = correlations.filter(([_, corr]) => Math.abs(corr) > 0.5);
+
+ let content = '';
+
+ if (strongCorrelations.length === 0) {
+ content = `
+
+ ๐ Weak Correlations: No strong correlations detected between metrics.
+ This suggests each metric captures unique aspects of performance.
+
+ `;
+ } else {
+ content = ``;
+ strongCorrelations.forEach(([pair, correlation]) => {
+ const [metric1, metric2] = pair.split('_');
+ const strength = Math.abs(correlation) > 0.8 ? 'very strong' : 'strong';
+ const direction = correlation > 0 ? 'positive' : 'negative';
+
+ content += `
+
+ ๐ ${strength.charAt(0).toUpperCase() + strength.slice(1)} ${direction} correlation:
+ ${this.formatMetricName(metric1)} and ${this.formatMetricName(metric2)}
+ (r = ${correlation.toFixed(3)})
+
+ `;
+ });
+ content += `
`;
+ }
+
+ element.innerHTML = content;
+ }
+
+ createQueryHeatmap(canvasId, analysisData) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) return;
+
+ const totalQueries = analysisData.rawData.length;
+ const metrics = analysisData.metrics;
+
+ // Create container for heatmap with navigation controls
+ const container = canvas.parentElement;
+ container.style.position = 'relative';
+
+ // Create navigation controls for large datasets
+ if (totalQueries > 50) {
+ this.createHeatmapNavigation(container, canvasId, analysisData);
+ return;
+ }
+
+ // For smaller datasets (โค50 queries), show all at once with adaptive sizing
+ this.renderFullHeatmap(canvas, analysisData, totalQueries);
+ }
+
+ createHeatmapNavigation(container, canvasId, analysisData) {
+ const totalQueries = analysisData.rawData.length;
+ const queriesPerPage = 25; // Show 25 queries per page for better readability
+ const totalPages = Math.ceil(totalQueries / queriesPerPage);
+ let currentPage = 0;
+
+ // Create controls container
+ const controlsDiv = document.createElement('div');
+ controlsDiv.className = 'heatmap-controls';
+ controlsDiv.style.cssText = `
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+ padding: 10px;
+ background-color: #f8f9fa;
+ border-radius: 6px;
+ font-size: 14px;
+ `;
+
+ controlsDiv.innerHTML = `
+
+ ${totalQueries} queries total
+ Page 1 of ${totalPages}
+
+
+
+ โ Previous
+
+
+ Next โ
+
+
+ `;
+
+ // Add CSS for navigation buttons
+ const style = document.createElement('style');
+ style.textContent = `
+ .btn-heatmap {
+ background: #3b82f6;
+ color: white;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ margin: 0 2px;
+ font-size: 12px;
+ }
+ .btn-heatmap:hover:not(:disabled) {
+ background: #2563eb;
+ }
+ .btn-heatmap:disabled {
+ background: #9ca3af;
+ cursor: not-allowed;
+ }
+ `;
+ document.head.appendChild(style);
+
+ container.insertBefore(controlsDiv, container.firstChild);
+
+ // Render initial page
+ const canvas = document.getElementById(canvasId);
+ this.renderHeatmapPage(canvas, analysisData, currentPage, queriesPerPage);
+
+ // Add navigation event listeners
+ document.getElementById(`prev-${canvasId}`).addEventListener('click', () => {
+ if (currentPage > 0) {
+ currentPage--;
+ this.renderHeatmapPage(canvas, analysisData, currentPage, queriesPerPage);
+ this.updateNavigationState(canvasId, currentPage, totalPages);
+ }
+ });
+
+ document.getElementById(`next-${canvasId}`).addEventListener('click', () => {
+ if (currentPage < totalPages - 1) {
+ currentPage++;
+ this.renderHeatmapPage(canvas, analysisData, currentPage, queriesPerPage);
+ this.updateNavigationState(canvasId, currentPage, totalPages);
+ }
+ });
+ }
+
+ updateNavigationState(canvasId, currentPage, totalPages) {
+ document.getElementById(`current-page-${canvasId}`).textContent = currentPage + 1;
+ document.getElementById(`prev-${canvasId}`).disabled = currentPage === 0;
+ document.getElementById(`next-${canvasId}`).disabled = currentPage === totalPages - 1;
+ }
+
+ renderHeatmapPage(canvas, analysisData, currentPage, queriesPerPage) {
+ const startIdx = currentPage * queriesPerPage;
+ const endIdx = Math.min(startIdx + queriesPerPage, analysisData.rawData.length);
+ const pageQueries = endIdx - startIdx;
+
+ // Create subset of data for current page
+ const pageData = analysisData.rawData.slice(startIdx, endIdx);
+ const pageAnalysisData = { ...analysisData, rawData: pageData };
+
+ this.renderFullHeatmap(canvas, pageAnalysisData, pageQueries, startIdx);
+ }
+
+ renderFullHeatmap(canvas, analysisData, maxQueries, startOffset = 0) {
+ const ctx = canvas.getContext('2d');
+ const metrics = analysisData.metrics;
+
+ // Clear any existing chart
+ if (canvas.chart) {
+ canvas.chart.destroy();
+ }
+
+ // Adaptive point sizing based on data density
+ const pointRadius = this.calculatePointRadius(maxQueries, metrics.length);
+
+ // Prepare data for heatmap
+ const heatmapData = [];
+ for (let queryIdx = 0; queryIdx < maxQueries; queryIdx++) {
+ metrics.forEach((metric, metricIdx) => {
+ const score = parseFloat(analysisData.rawData[queryIdx][metric]);
+ if (!isNaN(score)) {
+ heatmapData.push({
+ x: metricIdx,
+ y: queryIdx,
+ v: score,
+ actualQueryIdx: startOffset + queryIdx // Track actual query index
+ });
+ }
+ });
+ }
+
+ const chart = new Chart(ctx, {
+ type: 'scatter',
+ data: {
+ datasets: [{
+ label: 'Query Performance',
+ data: heatmapData,
+ backgroundColor: (context) => {
+ const score = context.raw.v;
+ if (score >= 0.8) return 'rgba(16, 185, 129, 0.8)'; // Green
+ if (score >= 0.6) return 'rgba(245, 158, 11, 0.8)'; // Yellow
+ return 'rgba(239, 68, 68, 0.8)'; // Red
+ },
+ pointRadius: pointRadius,
+ pointHoverRadius: Math.min(pointRadius + 2, 12)
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ callbacks: {
+ title: () => 'Query Performance',
+ label: (context) => {
+ const actualQueryIdx = context.raw.actualQueryIdx || context.raw.y;
+ const metricIdx = context.raw.x;
+ const metric = metrics[metricIdx];
+ return `Query ${actualQueryIdx + 1} - ${this.formatMetricName(metric)}: ${(context.raw.v * 100).toFixed(1)}%`;
+ }
+ }
+ }
+ },
+ scales: {
+ x: {
+ type: 'linear',
+ min: -0.5,
+ max: metrics.length - 0.5,
+ ticks: {
+ stepSize: 1,
+ callback: (value) => {
+ const metric = metrics[Math.round(value)];
+ return metric ? this.formatMetricName(metric).substring(0, 8) + '...' : '';
+ },
+ maxRotation: 45,
+ font: { size: Math.max(8, 12 - Math.floor(metrics.length / 5)) }
+ },
+ title: {
+ display: true,
+ text: 'Metrics'
+ }
+ },
+ y: {
+ type: 'linear',
+ min: -0.5,
+ max: maxQueries - 0.5,
+ ticks: {
+ stepSize: Math.max(1, Math.floor(maxQueries / 10)),
+ callback: (value) => {
+ const actualQueryIdx = startOffset + Math.round(value);
+ return `Q${actualQueryIdx + 1}`;
+ },
+ font: { size: Math.max(8, 12 - Math.floor(maxQueries / 20)) }
+ },
+ title: {
+ display: true,
+ text: 'Queries'
+ }
+ }
+ }
+ }
+ });
+
+ // Store chart reference for cleanup
+ canvas.chart = chart;
+ }
+
+ calculatePointRadius(queryCount, metricCount) {
+ // Adaptive point sizing based on density
+ const density = queryCount * metricCount;
+ if (density > 1000) return 3; // Very dense
+ if (density > 500) return 4; // Dense
+ if (density > 250) return 6; // Medium
+ return 8; // Sparse
+ }
+
+ createTopBottomChart(canvasId, analysisData) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ const topPerformers = analysisData.performance.topPerformers.slice(0, 5);
+ const bottomPerformers = analysisData.performance.bottomPerformers.slice(0, 5);
+
+ const allQueries = [...topPerformers, ...bottomPerformers];
+ const labels = allQueries.map((query, idx) =>
+ idx < 5 ? `Top ${idx + 1}` : `Bottom ${idx - 4}`
+ );
+ const scores = allQueries.map(query => query.averageScore);
+ const colors = allQueries.map((_, idx) =>
+ idx < 5 ? '#10b981' : '#ef4444'
+ );
+
+ new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: labels,
+ datasets: [{
+ data: scores,
+ backgroundColor: colors,
+ borderRadius: 4,
+ borderSkipped: false
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ callbacks: {
+ title: (context) => {
+ const query = allQueries[context[0].dataIndex];
+ return query.query.substring(0, 50) + '...';
+ },
+ label: (context) => `Average Score: ${(context.raw * 100).toFixed(1)}%`
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ max: 1,
+ ticks: {
+ callback: (value) => `${(value * 100).toFixed(0)}%`
+ }
+ }
+ }
+ }
+ });
+ }
+
+ populateOutliersAnalysis(elementId, analysisData) {
+ const element = document.getElementById(elementId);
+ if (!element) return;
+
+ const outliers = analysisData.performance.outliers;
+
+ if (outliers.length === 0) {
+ element.innerHTML = `
+
+ โ
No outliers detected: All queries show consistent performance patterns.
+
+ `;
+ return;
+ }
+
+ let content = `
+
+ ๐ฏ ${outliers.length} outlier queries detected:
+
+ `;
+
+ outliers.slice(0, 5).forEach((outlier, idx) => {
+ const performance = outlier.averageScore >= 0.8 ? 'exceptionally high' : 'unusually low';
+ const scoreColor = outlier.averageScore >= 0.8 ? 'excellent' : 'poor';
+
+ content += `
+
+ `;
+ });
+
+ if (outliers.length > 5) {
+ content += `... and ${outliers.length - 5} more outliers
`;
+ }
+
+ element.innerHTML = content;
+ }
+
+ populateModelInsights(elementId, analysisData) {
+ const element = document.getElementById(elementId);
+ if (!element) {
+ console.error('โ Element not found:', elementId);
+ return;
+ }
+
+ const insights = analysisData.insights;
+ console.log('๐ฏ Populating model insights for element:', elementId);
+ console.log('๐ Available insights:', insights);
+ let content = '';
+
+ if (insights.strengths.length > 0) {
+ content += `๐ช Strengths:
`;
+ insights.strengths.forEach(strength => {
+ content += `${strength}
`;
+ });
+ }
+
+ if (insights.weaknesses.length > 0) {
+ content += `โ ๏ธ Areas for Improvement:
`;
+ insights.weaknesses.forEach(weakness => {
+ content += `${weakness}
`;
+ });
+ }
+
+ // Add new insight categories
+ console.log('๐ Checking retrieval insights:', insights.retrieval);
+ if (insights.retrieval && insights.retrieval.length > 0) {
+ content += `๐ Retrieval Quality:
`;
+ insights.retrieval.forEach(retrieval => {
+ content += `${retrieval}
`;
+ });
+ }
+
+ console.log('โก Checking efficiency insights:', insights.efficiency);
+ if (insights.efficiency && insights.efficiency.length > 0) {
+ content += `โก System Efficiency:
`;
+ insights.efficiency.forEach(efficiency => {
+ content += `${efficiency}
`;
+ });
+ }
+
+ console.log('๐ฏ Checking utilization insights:', insights.utilization);
+ if (insights.utilization && insights.utilization.length > 0) {
+ content += `๐ฏ Chunk Utilization:
`;
+ insights.utilization.forEach(utilization => {
+ content += `${utilization}
`;
+ });
+ }
+
+ element.innerHTML = content;
+
+ // Log summary of what was generated
+ const insightCounts = {
+ strengths: insights.strengths ? insights.strengths.length : 0,
+ weaknesses: insights.weaknesses ? insights.weaknesses.length : 0,
+ retrieval: insights.retrieval ? insights.retrieval.length : 0,
+ efficiency: insights.efficiency ? insights.efficiency.length : 0,
+ utilization: insights.utilization ? insights.utilization.length : 0
+ };
+ console.log('๐ Insight counts generated for', elementId, ':', insightCounts);
+ }
+
+ populateRetrievalQualityTab(sheetId, analysisData) {
+ console.log('๐ Populating retrieval quality tab for sheet:', sheetId);
+
+ // Populate retrieval overview
+ this.populateRetrievalOverview(`retrieval-overview-${sheetId}`, analysisData);
+
+ // Charts removed - Ground Truth Validity and Answer Completeness pie charts
+
+ // Populate insight sections
+ this.populateRetrievalInsights(`retrieval-insights-${sheetId}`, analysisData);
+ this.populateEfficiencyInsights(`efficiency-insights-${sheetId}`, analysisData);
+ this.populateUtilizationInsights(`utilization-insights-${sheetId}`, analysisData);
+
+ // Create chunk utilization charts
+ this.createChunkUtilizationChart(`chunk-utilization-chart-${sheetId}`, analysisData);
+
+ console.log('โ
Retrieval quality tab populated for sheet:', sheetId);
+ }
+
+ populateRetrievalOverview(elementId, analysisData) {
+ const element = document.getElementById(elementId);
+ if (!element) {
+ console.error('โ Retrieval overview element not found:', elementId);
+ return;
+ }
+
+ const extendedMetrics = this.extractExtendedMetrics(analysisData);
+ const hasChunkData = this.checkForChunkData(analysisData);
+
+ let content = `
+
+
+ `;
+
+ // Add available metrics
+ if (extendedMetrics.ground_truth_validity !== undefined) {
+ const gtValidity = extendedMetrics.ground_truth_validity;
+ const gtClass = gtValidity >= 0.9 ? 'excellent' : gtValidity >= 0.8 ? 'good' : gtValidity >= 0.7 ? 'fair' : 'poor';
+ content += `
+
+
โ
+
Ground Truth Validity
+
${(gtValidity * 100).toFixed(1)}%
+
${gtValidity >= 0.9 ? 'Excellent' : gtValidity >= 0.8 ? 'Good' : gtValidity >= 0.7 ? 'Fair' : 'Poor'}
+
+ `;
+ }
+
+ if (extendedMetrics.answer_completeness !== undefined) {
+ const completeness = extendedMetrics.answer_completeness;
+ const compClass = completeness >= 0.9 ? 'excellent' : completeness >= 0.8 ? 'good' : completeness >= 0.7 ? 'fair' : 'poor';
+ content += `
+
+
๐ฏ
+
Answer Completeness
+
${(completeness * 100).toFixed(1)}%
+
${completeness >= 0.9 ? 'Excellent' : completeness >= 0.8 ? 'Good' : completeness >= 0.7 ? 'Fair' : 'Poor'}
+
+ `;
+ }
+
+ // Add chunk metrics if available
+ if (hasChunkData) {
+ if (extendedMetrics.retrieved_chunk_count !== undefined) {
+ content += `
+
+
๐
+
Retrieved Chunks
+
${extendedMetrics.retrieved_chunk_count.toFixed(1)}
+
Chunks
+
+ `;
+ }
+
+ if (extendedMetrics.sent_to_llm_chunk_count !== undefined) {
+ content += `
+
+
๐ค
+
Sent to LLM
+
${extendedMetrics.sent_to_llm_chunk_count.toFixed(1)}
+
Chunks
+
+ `;
+ }
+
+ if (extendedMetrics.used_in_answer_chunk_count !== undefined) {
+ content += `
+
+
๐ฏ
+
Used in Answer
+
${extendedMetrics.used_in_answer_chunk_count.toFixed(1)}
+
Chunks
+
+ `;
+ }
+
+ if (extendedMetrics.total_chunks_used !== undefined) {
+ content += `
+
+
๐
+
Total Used
+
${extendedMetrics.total_chunks_used.toFixed(1)}
+
Chunks
+
+ `;
+ }
+
+ if (extendedMetrics.best_support_rank !== undefined) {
+ const rank = extendedMetrics.best_support_rank;
+ const rankClass = rank <= 3 ? 'excellent' : rank <= 10 ? 'good' : 'fair';
+ content += `
+
+
๐
+
Best Support Rank
+
${rank.toFixed(1)}
+
${rank <= 3 ? 'Excellent' : rank <= 10 ? 'Good' : 'Fair'}
+
+ `;
+ }
+ }
+
+ content += `
+
+
+
+ ${hasChunkData ? 'โ
' : 'โ ๏ธ'}
+ ${hasChunkData ? 'Comprehensive chunk analysis available' : 'Basic retrieval analysis - enable chunk tracking for detailed insights'}
+
+ ${!hasChunkData ? `
+
+
+ How to enable chunk analysis
+
+
To get detailed chunk analysis:
+
+ Check "Use Search API" in the configuration
+ Ensure your RAG system returns chunk metadata
+ Look for columns like "Retrieved Chunk Count", "Total Chunks Used" in results
+ Chunk analysis provides insights into retrieval efficiency and quality
+
+
Available columns in your data:
+
${analysisData.rawData && analysisData.rawData.length > 0 ? Object.keys(analysisData.rawData[0]).join(', ') : 'No data available'}
+
+
+
+ ` : ''}
+
+
+ `;
+
+ element.innerHTML = content;
+ }
+
+ // Chart creation functions removed - Ground Truth Validity and Answer Completeness pie charts
+
+ populateRetrievalInsights(elementId, analysisData) {
+ const element = document.getElementById(elementId);
+ if (!element) return;
+
+ const insights = analysisData.insights;
+ const extendedMetrics = this.extractExtendedMetrics(analysisData);
+ let content = '';
+
+ // Add detailed retrieval analysis
+ content += '';
+
+ if (insights.retrieval && insights.retrieval.length > 0) {
+ insights.retrieval.forEach(insight => {
+ content += `
${insight}
`;
+ });
+ } else {
+ content += '
No retrieval quality insights available
';
+ }
+
+ // Add detailed metric analysis
+ if (extendedMetrics.ground_truth_validity !== undefined) {
+ const gtValidity = extendedMetrics.ground_truth_validity;
+ content += `
+
+
Ground Truth Validity Analysis
+
+
+ Score:
+ ${(gtValidity * 100).toFixed(1)}%
+
+
+ Performance Level:
+ ${gtValidity >= 0.9 ? 'Excellent' : gtValidity >= 0.8 ? 'Good' : gtValidity >= 0.7 ? 'Fair' : 'Poor'}
+
+
+ Interpretation:
+ ${this.getGTValidityInterpretation(gtValidity)}
+
+
+
+ `;
+ }
+
+ content += '
';
+
+ element.innerHTML = content;
+ }
+
+ getGTValidityInterpretation(score) {
+ if (score >= 0.9) {
+ return "System consistently retrieves highly accurate and relevant information from the knowledge base.";
+ } else if (score >= 0.8) {
+ return "System retrieves correct information most of the time with minor inconsistencies.";
+ } else if (score >= 0.7) {
+ return "System retrieves relevant information but may miss some important details.";
+ } else if (score >= 0.6) {
+ return "System has moderate retrieval accuracy with room for improvement.";
+ } else {
+ return "System struggles to retrieve accurate information and needs significant improvement.";
+ }
+ }
+
+ populateEfficiencyInsights(elementId, analysisData) {
+ const element = document.getElementById(elementId);
+ if (!element) return;
+
+ const insights = analysisData.insights;
+ const extendedMetrics = this.extractExtendedMetrics(analysisData);
+ let content = '';
+
+ // Add detailed efficiency analysis
+ content += '';
+
+ if (insights.efficiency && insights.efficiency.length > 0) {
+ insights.efficiency.forEach(insight => {
+ content += `
${insight}
`;
+ });
+ } else {
+ content += '
No efficiency insights available
';
+ }
+
+ // Add efficiency score calculation
+ if (extendedMetrics.ground_truth_validity !== undefined && extendedMetrics.answer_completeness !== undefined) {
+ const gtValidity = extendedMetrics.ground_truth_validity;
+ const completeness = extendedMetrics.answer_completeness;
+ const efficiencyScore = (gtValidity + completeness) / 2;
+ const gap = Math.abs(gtValidity - completeness);
+
+ content += `
+
+
System Efficiency Analysis
+
+
+ Overall Efficiency Score:
+ ${(efficiencyScore * 100).toFixed(1)}%
+
+
+ Performance Gap:
+ ${(gap * 100).toFixed(1)}%
+
+
+ Balance Assessment:
+ ${this.getEfficiencyBalanceAssessment(gtValidity, completeness)}
+
+
+
+ `;
+ }
+
+ content += '
';
+
+ element.innerHTML = content;
+ }
+
+ getEfficiencyBalanceAssessment(gtValidity, completeness) {
+ const gap = Math.abs(gtValidity - completeness);
+
+ if (gap <= 0.1) {
+ return "Excellent balance between retrieval accuracy and answer completeness.";
+ } else if (gap <= 0.2) {
+ return "Good balance with minor differences between metrics.";
+ } else if (gap <= 0.3) {
+ return "Moderate imbalance - consider optimizing the weaker metric.";
+ } else {
+ return "Significant imbalance - focus on improving the lower-performing metric.";
+ }
+ }
+
+ populateUtilizationInsights(elementId, analysisData) {
+ const element = document.getElementById(elementId);
+ if (!element) return;
+
+ const insights = analysisData.insights;
+ const extendedMetrics = this.extractExtendedMetrics(analysisData);
+ let content = '';
+
+ // Add detailed utilization analysis
+ content += '';
+
+ if (insights.utilization && insights.utilization.length > 0) {
+ insights.utilization.forEach(insight => {
+ content += `
${insight}
`;
+ });
+ } else {
+ content += '
No utilization insights available
';
+ }
+
+ // Add detailed chunk utilization analysis
+ if (extendedMetrics.chunks_used_top5 !== undefined ||
+ extendedMetrics.chunks_used_top10 !== undefined ||
+ extendedMetrics.chunks_used_top20 !== undefined) {
+
+ content += `
+
+
Chunk Utilization Distribution Analysis
+
+ `;
+
+ if (extendedMetrics.chunks_used_top5 !== undefined) {
+ const top5 = extendedMetrics.chunks_used_top5;
+ content += `
+
+ Top 5 Chunks Used:
+ ${top5} chunks
+ ${this.getChunkUtilizationAssessment(top5, 'top5')}
+
+ `;
+ }
+
+ if (extendedMetrics.chunks_used_top10 !== undefined) {
+ const top10 = extendedMetrics.chunks_used_top10;
+ content += `
+
+ Chunks 5-10 Used:
+ ${top10} chunks
+ ${this.getChunkUtilizationAssessment(top10, 'top10')}
+
+ `;
+ }
+
+ if (extendedMetrics.chunks_used_top20 !== undefined) {
+ const top20 = extendedMetrics.chunks_used_top20;
+ content += `
+
+ Chunks 10-20 Used:
+ ${top20} chunks
+ ${this.getChunkUtilizationAssessment(top20, 'top20')}
+
+ `;
+ }
+
+ // Add utilization efficiency analysis
+ if (extendedMetrics.retrieved_chunk_count !== undefined &&
+ extendedMetrics.used_in_answer_chunk_count !== undefined) {
+ const retrieved = extendedMetrics.retrieved_chunk_count;
+ const used = extendedMetrics.used_in_answer_chunk_count;
+ const efficiency = retrieved > 0 ? (used / retrieved) * 100 : 0;
+
+ content += `
+
+
+
+
+ Retrieved:
+ ${retrieved.toFixed(1)} chunks
+
+
+ Used:
+ ${used.toFixed(1)} chunks
+
+
+ Efficiency:
+ ${efficiency.toFixed(1)}%
+
+
+
+ `;
+ }
+
+ content += `
+
+
+ `;
+ }
+
+ content += '
';
+
+ element.innerHTML = content;
+ }
+
+ getChunkUtilizationAssessment(count, type) {
+ if (type === 'top5') {
+ if (count >= 3) return "Excellent - heavily utilizes top-ranked chunks";
+ if (count >= 2) return "Good - effectively uses top-ranked chunks";
+ if (count >= 1) return "Fair - some use of top-ranked chunks";
+ return "Poor - minimal use of top-ranked chunks";
+ } else if (type === 'top10') {
+ if (count >= 2) return "Good - utilizes mid-ranked chunks";
+ if (count >= 1) return "Fair - some use of mid-ranked chunks";
+ return "Low - minimal use of mid-ranked chunks";
+ } else if (type === 'top20') {
+ if (count >= 2) return "Good - utilizes lower-ranked chunks";
+ if (count >= 1) return "Fair - some use of lower-ranked chunks";
+ return "Low - minimal use of lower-ranked chunks";
+ }
+ return "Standard utilization pattern";
+ }
+
+ createChunkUtilizationChart(canvasId, analysisData) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) return;
+
+ const extendedMetrics = this.extractExtendedMetrics(analysisData);
+
+ // Check if we have chunk utilization data
+ const hasChunkData = extendedMetrics.chunks_used_top5 !== undefined ||
+ extendedMetrics.chunks_used_top10 !== undefined ||
+ extendedMetrics.chunks_used_top20 !== undefined;
+
+ if (!hasChunkData) {
+ canvas.parentElement.innerHTML = `
+
+
๐
+
Chunk utilization data not available
+
Enable chunk tracking for detailed utilization analysis
+
+ `;
+ return;
+ }
+
+ const ctx = canvas.getContext('2d');
+
+ // Prepare data for the chart
+ const labels = [];
+ const data = [];
+ const colors = [];
+
+ if (extendedMetrics.chunks_used_top5 !== undefined) {
+ labels.push('Top 5');
+ data.push(extendedMetrics.chunks_used_top5);
+ colors.push('#10b981');
+ }
+
+ if (extendedMetrics.chunks_used_top10 !== undefined) {
+ labels.push('5-10');
+ data.push(extendedMetrics.chunks_used_top10);
+ colors.push('#059669');
+ }
+
+ if (extendedMetrics.chunks_used_top20 !== undefined) {
+ labels.push('10-20');
+ data.push(extendedMetrics.chunks_used_top20);
+ colors.push('#0d9488');
+ }
+
+ // Create bar chart
+ new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: labels,
+ datasets: [{
+ label: 'Chunks Used',
+ data: data,
+ backgroundColor: colors,
+ borderColor: colors.map(color => color.replace('0.8', '1')),
+ borderWidth: 1,
+ borderRadius: 4
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ callbacks: {
+ label: (context) => `${context.label}: ${context.raw} chunks`
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ stepSize: 1,
+ callback: (value) => Math.floor(value) === value ? value : ''
+ }
+ }
+ }
+ }
+ });
+ }
+
+ populateStatisticalInsights(elementId, analysisData) {
+ const element = document.getElementById(elementId);
+ if (!element) return;
+
+ const insights = analysisData.insights;
+ let content = '';
+
+ if (insights.statistical && insights.statistical.length > 0) {
+ insights.statistical.forEach(stat => {
+ content += `${stat}
`;
+ });
+ } else {
+ content = `๐ Standard statistical patterns detected across all metrics.
`;
+ }
+
+ element.innerHTML = content;
+ }
+
+ populateRecommendations(elementId, analysisData) {
+ const element = document.getElementById(elementId);
+ if (!element) return;
+
+ const insights = analysisData.insights;
+ let content = '';
+
+ if (insights.recommendations && insights.recommendations.length > 0) {
+ // Add a header explaining the evaluation-based recommendations
+ content += `
+
+ `;
+
+ insights.recommendations.forEach((recommendation, index) => {
+ // Extract emoji and text for better formatting
+ const emojiMatch = recommendation.match(/^([\u{1F300}-\u{1F9FF}][\u{FE00}-\u{FE0F}]?|[\u{2600}-\u{27BF}])/u);
+ const emoji = emojiMatch ? emojiMatch[0] : '๐ก';
+ const text = recommendation.replace(/^[\u{1F300}-\u{1F9FF}][\u{FE00}-\u{FE0F}]?[\u{2600}-\u{27BF}]?\s*/u, '');
+
+ // Determine priority class based on emoji
+ let priorityClass = 'recommendation-normal';
+ if (emoji === 'โ
') priorityClass = 'recommendation-success';
+ else if (emoji === 'โ' || emoji === '๐') priorityClass = 'recommendation-critical';
+ else if (emoji === '๐ ๏ธ' || emoji === '๐งช') priorityClass = 'recommendation-action';
+
+ content += `
+
+
+ ${emoji}
+ ${text}
+
+
+ `;
+ });
+
+ // Add metric context if available
+ const metricValues = this.extractRelevantMetrics(analysisData);
+ const hasMetrics = metricValues.context_relevancy !== undefined ||
+ metricValues.answer_correctness !== undefined ||
+ metricValues.ground_truth_validity !== undefined ||
+ metricValues.answer_completeness !== undefined ||
+ metricValues.retrieved_chunk_count !== undefined ||
+ metricValues.sent_to_llm_chunk_count !== undefined ||
+ metricValues.used_in_answer_chunk_count !== undefined ||
+ metricValues.total_chunks_used !== undefined ||
+ metricValues.best_support_rank !== undefined;
+
+ if (hasMetrics) {
+ content += `
+
+ ๐ Based on: `;
+
+ const metricDisplay = [];
+ if (metricValues.context_relevancy !== undefined) {
+ metricDisplay.push(`Context Relevancy: ${(metricValues.context_relevancy * 100).toFixed(1)}%`);
+ }
+ if (metricValues.answer_correctness !== undefined) {
+ metricDisplay.push(`Answer Correctness: ${(metricValues.answer_correctness * 100).toFixed(1)}%`);
+ }
+ if (metricValues.ground_truth_validity !== undefined) {
+ metricDisplay.push(`GT Validity: ${(metricValues.ground_truth_validity * 100).toFixed(1)}%`);
+ }
+ if (metricValues.answer_completeness !== undefined) {
+ metricDisplay.push(`Completeness: ${(metricValues.answer_completeness * 100).toFixed(1)}%`);
+ }
+ if (metricValues.retrieved_chunk_count !== undefined) {
+ metricDisplay.push(`Retrieved: ${metricValues.retrieved_chunk_count.toFixed(1)}`);
+ }
+ if (metricValues.sent_to_llm_chunk_count !== undefined) {
+ metricDisplay.push(`Sent to LLM: ${metricValues.sent_to_llm_chunk_count.toFixed(1)}`);
+ }
+ if (metricValues.used_in_answer_chunk_count !== undefined) {
+ metricDisplay.push(`Used: ${metricValues.used_in_answer_chunk_count.toFixed(1)}`);
+ }
+ if (metricValues.total_chunks_used !== undefined) {
+ metricDisplay.push(`Total Used: ${metricValues.total_chunks_used.toFixed(1)}`);
+ }
+ if (metricValues.best_support_rank !== undefined) {
+ metricDisplay.push(`Support Rank: ${metricValues.best_support_rank.toFixed(1)}`);
+ }
+
+ content += metricDisplay.join(', ');
+ content += `
+
+ `;
+ }
+ } else {
+ content = `
+
+
+
+ ๐
+ Performance appears balanced across metrics.
+
+
+
+
+ ๐
+ Consider running evaluation with larger dataset for clearer insights.
+
+
+
+
+ ๐
+ Monitor performance trends over time to identify patterns.
+
+
+ `;
+ }
+
+ element.innerHTML = content;
+ }
+
+ generateIntelligentRecommendations(analysisSummary) {
+ console.log('๐ง Generating intelligent recommendations based on comprehensive analysis...');
+
+ const recommendations = [];
+ const { metrics, performance, chunkAnalysis, configuration } = analysisSummary;
+
+ // Generate recommendations based on available metrics
+ Object.entries(metrics).forEach(([metricName, stats]) => {
+ const score = stats.mean;
+
+ if (score < 0.6) {
+ recommendations.push(`๐ **High Priority**: ${metricName} needs improvement (${(score * 100).toFixed(1)}%) - Review system configuration and optimize for better performance.`);
+ } else if (score < 0.8) {
+ recommendations.push(`๐ **Medium Priority**: ${metricName} has room for improvement (${(score * 100).toFixed(1)}%) - Consider fine-tuning parameters.`);
+ } else {
+ recommendations.push(`โ
**Low Priority**: ${metricName} performing well (${(score * 100).toFixed(1)}%) - Maintain current configuration.`);
+ }
+ });
+
+ // Add chunk-specific recommendations
+ if (chunkAnalysis.hasChunkData) {
+ if (chunkAnalysis.retrievedChunkCount > 15) {
+ recommendations.push(`โก **Medium Priority**: High retrieval count (${chunkAnalysis.retrievedChunkCount.toFixed(1)} chunks) - Consider optimizing retrieval strategy for efficiency.`);
+ }
+ if (chunkAnalysis.bestSupportRank > 5) {
+ recommendations.push(`๐ **High Priority**: Poor top-rank retrieval (rank ${chunkAnalysis.bestSupportRank}) - Improve ranking algorithm or increase retrieval count.`);
+ }
+ } else {
+ recommendations.push(`๐ **Medium Priority**: Enable chunk tracking for detailed efficiency analysis and optimization insights.`);
+ }
+
+ // Add configuration recommendations
+ if (configuration.evaluationMethods.length < 2) {
+ recommendations.push(`๐ง **Medium Priority**: Limited evaluation coverage (${configuration.evaluationMethods.length} method(s)) - Enable additional evaluation methods for comprehensive analysis.`);
+ }
+
+ // Add performance recommendations
+ if (performance.outliers > 0) {
+ recommendations.push(`๐ **Medium Priority**: ${performance.outliers} performance outliers detected - Review and investigate unusual patterns.`);
+ }
+
+ console.log('โ
Generated intelligent recommendations:', recommendations.length);
+ return recommendations;
+ }
+
+ // NEW: Enhanced recommendation analysis functions
+ generateEnhancedRecommendations(analysisSummary) {
+ console.log('๐ Generating enhanced analysis recommendations...');
+
+ const recommendations = [];
+ const { metrics, performance, chunkAnalysis, configuration } = analysisSummary;
+
+ // 1. Cross-metric correlation analysis
+ const correlationInsights = this.analyzeMetricCorrelations(metrics);
+ recommendations.push(...correlationInsights);
+
+ // 2. Performance distribution analysis
+ const distributionInsights = this.analyzePerformanceDistribution(metrics);
+ recommendations.push(...distributionInsights);
+
+ // 3. Efficiency optimization insights
+ const efficiencyInsights = this.analyzeEfficiencyPatterns(chunkAnalysis, metrics);
+ recommendations.push(...efficiencyInsights);
+
+ // 4. Configuration optimization
+ const configInsights = this.analyzeConfigurationOptimization(configuration, metrics);
+ recommendations.push(...configInsights);
+
+ // 5. Data quality insights
+ const dataQualityInsights = this.analyzeDataQuality(analysisSummary);
+ recommendations.push(...dataQualityInsights);
+
+ console.log('โ
Generated enhanced recommendations:', recommendations.length);
+ return recommendations;
+ }
+
+ analyzeMetricCorrelations(metrics) {
+ const recommendations = [];
+ const metricEntries = Object.entries(metrics);
+
+ // Find potential correlations between metrics
+ for (let i = 0; i < metricEntries.length; i++) {
+ for (let j = i + 1; j < metricEntries.length; j++) {
+ const [metric1, stats1] = metricEntries[i];
+ const [metric2, stats2] = metricEntries[j];
+
+ const score1 = stats1.mean;
+ const score2 = stats2.mean;
+
+ // Detect inverse relationships
+ if (score1 < 0.6 && score2 > 0.8) {
+ recommendations.push(`๐ **Medium Priority**: ${metric1} (${(score1 * 100).toFixed(1)}%) and ${metric2} (${(score2 * 100).toFixed(1)}%) show inverse relationship - consider balancing optimization efforts.`);
+ }
+
+ // Detect both poor performance
+ if (score1 < 0.6 && score2 < 0.6) {
+ recommendations.push(`๐จ **High Priority**: Both ${metric1} and ${metric2} need attention - may indicate systemic issues requiring comprehensive review.`);
+ }
+ }
+ }
+
+ return recommendations;
+ }
+
+ analyzePerformanceDistribution(metrics) {
+ const recommendations = [];
+
+ Object.entries(metrics).forEach(([metricName, stats]) => {
+ const { mean, stdDev, min, max } = stats;
+ const variance = stdDev / mean; // Coefficient of variation
+
+ // High variance indicates inconsistent performance
+ if (variance > 0.3) {
+ recommendations.push(`๐ **Medium Priority**: ${metricName} shows high variability (${(variance * 100).toFixed(1)}% CV) - consider standardizing inputs or improving consistency.`);
+ }
+
+ // Large gap between min and max indicates potential for optimization
+ if ((max - min) > 0.4) {
+ recommendations.push(`๐ฏ **Medium Priority**: ${metricName} has wide performance range (${(min * 100).toFixed(1)}%-${(max * 100).toFixed(1)}%) - investigate best-performing cases for optimization insights.`);
+ }
+ });
+
+ return recommendations;
+ }
+
+ analyzeEfficiencyPatterns(chunkAnalysis, metrics) {
+ const recommendations = [];
+
+ if (!chunkAnalysis.hasChunkData) return recommendations;
+
+ const { retrievedChunkCount, sentToLLMChunkCount, usedInAnswerChunkCount, bestSupportRank } = chunkAnalysis;
+
+ // Analyze chunk utilization efficiency
+ if (retrievedChunkCount && usedInAnswerChunkCount) {
+ const utilizationRate = usedInAnswerChunkCount / retrievedChunkCount;
+
+ if (utilizationRate < 0.3) {
+ recommendations.push(`โก **High Priority**: Low chunk utilization (${(utilizationRate * 100).toFixed(1)}%) - optimize retrieval precision or reduce chunk count.`);
+ } else if (utilizationRate > 0.8) {
+ recommendations.push(`๐ **Low Priority**: Excellent chunk utilization (${(utilizationRate * 100).toFixed(1)}%) - system is efficiently using retrieved context.`);
+ }
+ }
+
+ // Analyze processing efficiency
+ if (sentToLLMChunkCount && retrievedChunkCount) {
+ const processingRate = sentToLLMChunkCount / retrievedChunkCount;
+
+ if (processingRate < 0.5) {
+ recommendations.push(`๐ **Medium Priority**: Low processing rate (${(processingRate * 100).toFixed(1)}%) - consider increasing chunks sent to LLM for better context.`);
+ }
+ }
+
+ // Analyze support rank patterns
+ if (bestSupportRank) {
+ if (bestSupportRank > 10) {
+ recommendations.push(`๐ **High Priority**: Poor support ranking (rank ${bestSupportRank}) - critical need to improve retrieval relevance scoring.`);
+ } else if (bestSupportRank > 5) {
+ recommendations.push(`๐ **Medium Priority**: Suboptimal support ranking (rank ${bestSupportRank}) - consider re-ranking or expanding retrieval.`);
+ }
+ }
+
+ return recommendations;
+ }
+
+ analyzeConfigurationOptimization(configuration, metrics) {
+ const recommendations = [];
+
+ // Analyze evaluation coverage
+ const evaluationMethods = configuration.evaluationMethods || [];
+ const hasRAGAS = evaluationMethods.some(m => m.toLowerCase().includes('ragas'));
+ const hasLLM = evaluationMethods.some(m => m.toLowerCase().includes('llm'));
+ const hasCRAG = evaluationMethods.some(m => m.toLowerCase().includes('crag'));
+
+ if (!hasRAGAS && !hasLLM && !hasCRAG) {
+ recommendations.push(`๐ง **High Priority**: No evaluation methods detected - enable RAGAS, LLM, or CRAG evaluation for comprehensive analysis.`);
+ } else if (evaluationMethods.length === 1) {
+ recommendations.push(`๐ **Medium Priority**: Single evaluation method (${evaluationMethods[0]}) - consider adding complementary evaluation methods for broader insights.`);
+ }
+
+ // Analyze dataset size
+ const totalQueries = configuration.totalQueries || 0;
+ if (totalQueries < 10) {
+ recommendations.push(`๐ **Medium Priority**: Small dataset (${totalQueries} queries) - consider larger evaluation set for more reliable insights.`);
+ } else if (totalQueries > 100) {
+ recommendations.push(`โ
**Low Priority**: Large dataset (${totalQueries} queries) - comprehensive evaluation provides reliable insights.`);
+ }
+
+ return recommendations;
+ }
+
+ analyzeDataQuality(analysisSummary) {
+ const recommendations = [];
+ const { metrics, performance } = analysisSummary;
+
+ // Check for data completeness
+ const metricCount = Object.keys(metrics).length;
+ if (metricCount < 3) {
+ recommendations.push(`๐ **Medium Priority**: Limited metrics (${metricCount}) - consider enabling more evaluation metrics for comprehensive analysis.`);
+ }
+
+ // Check for performance outliers
+ if (performance.outliers > 0) {
+ const outlierPercentage = (performance.outliers / (analysisSummary.configuration.totalQueries || 1)) * 100;
+
+ if (outlierPercentage > 20) {
+ recommendations.push(`๐จ **High Priority**: High outlier rate (${outlierPercentage.toFixed(1)}%) - investigate data quality and system stability issues.`);
+ } else {
+ recommendations.push(`๐ **Medium Priority**: ${performance.outliers} outliers detected - review unusual cases for optimization opportunities.`);
+ }
+ }
+
+ // Check for metric consistency
+ const metricValues = Object.values(metrics).map(m => m.mean);
+ const avgMetric = metricValues.reduce((a, b) => a + b, 0) / metricValues.length;
+ const metricVariance = metricValues.reduce((sum, val) => sum + Math.pow(val - avgMetric, 2), 0) / metricValues.length;
+
+ if (metricVariance > 0.1) {
+ recommendations.push(`๐ **Medium Priority**: High metric variance detected - consider balancing optimization across all metrics for consistent performance.`);
+ }
+
+ return recommendations;
+ }
+
+ // NEW: Smart filtering to avoid overwhelming users
+ smartFilterRecommendations(recommendations, analysisSummary) {
+ console.log('๐ฏ Applying smart filtering to recommendations...');
+
+ // Remove duplicates and similar recommendations
+ const uniqueRecommendations = this.removeDuplicateRecommendations(recommendations);
+
+ // Prioritize by importance and system context
+ const prioritizedRecommendations = this.prioritizeRecommendations(uniqueRecommendations, analysisSummary);
+
+ // Limit to most impactful recommendations (max 12-15)
+ const maxRecommendations = 15;
+ const finalRecommendations = prioritizedRecommendations.slice(0, maxRecommendations);
+
+ console.log(`โ
Filtered to ${finalRecommendations.length} recommendations from ${recommendations.length} total`);
+ return finalRecommendations;
+ }
+
+ removeDuplicateRecommendations(recommendations) {
+ const uniqueRecommendations = [];
+ const seenKeywords = new Set();
+
+ recommendations.forEach(rec => {
+ // Extract key concepts from recommendation
+ const keywords = rec.toLowerCase()
+ .replace(/[^\w\s]/g, ' ')
+ .split(/\s+/)
+ .filter(word => word.length > 3)
+ .slice(0, 5)
+ .join(' ');
+
+ if (!seenKeywords.has(keywords)) {
+ seenKeywords.add(keywords);
+ uniqueRecommendations.push(rec);
+ }
+ });
+
+ return uniqueRecommendations;
+ }
+
+ prioritizeRecommendations(recommendations, analysisSummary) {
+ const { metrics, configuration } = analysisSummary;
+
+ // Score each recommendation based on priority and relevance
+ const scoredRecommendations = recommendations.map(rec => {
+ let score = 0;
+
+ // Priority scoring
+ if (rec.includes('**High Priority**')) score += 10;
+ else if (rec.includes('**Medium Priority**')) score += 5;
+ else if (rec.includes('**Low Priority**')) score += 2;
+
+ // Critical issue scoring
+ if (rec.includes('๐จ') || rec.includes('critical') || rec.includes('systemic')) score += 5;
+
+ // Relevance to current system state
+ if (rec.includes('chunk') && !analysisSummary.chunkAnalysis.hasChunkData) score += 3;
+ if (rec.includes('evaluation') && configuration.evaluationMethods.length < 2) score += 3;
+ if (rec.includes('dataset') && configuration.totalQueries < 20) score += 2;
+
+ // Performance impact scoring
+ const hasLowPerformance = Object.values(metrics).some(stats => stats.mean < 0.6);
+ if (hasLowPerformance && rec.includes('improvement')) score += 3;
+
+ return { recommendation: rec, score };
+ });
+
+ // Sort by score (highest first)
+ scoredRecommendations.sort((a, b) => b.score - a.score);
+
+ return scoredRecommendations.map(item => item.recommendation);
+ }
+
+ // NEW: Enhanced UI functions for recommendation actions
+ // Modal functionality removed
+}
+
+// Modal functionality removed
+
+// Initialize when DOM is ready
+document.addEventListener('DOMContentLoaded', () => {
+ console.log('๐ DOM loaded, initializing RAG Evaluator UI...');
+ window.ragEvaluatorUI = new RAGEvaluatorUI();
+});
\ No newline at end of file
diff --git a/Evaluation/RAG_Evaluator/src/static/styles.css b/Evaluation/RAG_Evaluator/src/static/styles.css
new file mode 100644
index 00000000..e5218ae2
--- /dev/null
+++ b/Evaluation/RAG_Evaluator/src/static/styles.css
@@ -0,0 +1,5619 @@
+/**
+ * ============================================================================
+ * RAG EVALUATOR - PROFESSIONAL UI STYLESHEET
+ * ============================================================================
+ * A comprehensive, maintainable stylesheet for the RAG Evaluator application
+ *
+ * Table of Contents:
+ * 1. CSS Reset & Normalize
+ * 2. CSS Custom Properties (Design System)
+ * 3. Base Typography & Layout
+ * 4. Utility Classes
+ * 5. Component Styles
+ * 6. Layout Components
+ * 7. Interactive Elements
+ * 8. Responsive Design
+ * 9. Animations & Transitions
+ * 10. Print Styles
+ * ============================================================================
+ */
+
+/* ============================================================================
+ 1. CSS RESET & NORMALIZE
+ ============================================================================ */
+
+*,
+*::before,
+*::after {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html {
+ font-size: 16px;
+ scroll-behavior: smooth;
+}
+
+/* ============================================================================
+ 2. CSS CUSTOM PROPERTIES (DESIGN SYSTEM)
+ ============================================================================ */
+
+:root {
+ /* ----------------------------------------
+ Color Palette
+ ---------------------------------------- */
+
+ /* Primary Colors */
+ --primary-50: #eff6ff;
+ --primary-100: #dbeafe;
+ --primary-200: #bfdbfe;
+ --primary-300: #93c5fd;
+ --primary-400: #60a5fa;
+ --primary-500: #3b82f6;
+ --primary-600: #2563eb;
+ --primary-700: #1d4ed8;
+ --primary-800: #1e40af;
+ --primary-900: #1e3a8a;
+
+ /* Semantic Colors */
+ --success-50: #ecfdf5;
+ --success-100: #d1fae5;
+ --success-500: #10b981;
+ --success-600: #059669;
+ --success-700: #047857;
+
+ --warning-50: #fffbeb;
+ --warning-100: #fef3c7;
+ --warning-500: #f59e0b;
+ --warning-600: #d97706;
+ --warning-700: #b45309;
+
+ --error-50: #fef2f2;
+ --error-100: #fee2e2;
+ --error-500: #ef4444;
+ --error-600: #dc2626;
+ --error-700: #b91c1c;
+
+ /* Neutral Colors */
+ --gray-50: #f9fafb;
+ --gray-100: #f3f4f6;
+ --gray-200: #e5e7eb;
+ --gray-300: #d1d5db;
+ --gray-400: #9ca3af;
+ --gray-500: #6b7280;
+ --gray-600: #4b5563;
+ --gray-700: #374151;
+ --gray-800: #1f2937;
+ --gray-900: #111827;
+
+ /* ----------------------------------------
+ Design Tokens
+ ---------------------------------------- */
+
+ /* Spacing Scale */
+ --space-px: 1px;
+ --space-0: 0;
+ --space-1: 0.25rem; /* 4px */
+ --space-2: 0.5rem; /* 8px */
+ --space-3: 0.75rem; /* 12px */
+ --space-4: 1rem; /* 16px */
+ --space-5: 1.25rem; /* 20px */
+ --space-6: 1.5rem; /* 24px */
+ --space-7: 1.75rem; /* 28px */
+ --space-8: 2rem; /* 32px */
+ --space-9: 2.25rem; /* 36px */
+ --space-10: 2.5rem; /* 40px */
+ --space-11: 2.75rem; /* 44px */
+ --space-12: 3rem; /* 48px */
+ --space-14: 3.5rem; /* 56px */
+ --space-16: 4rem; /* 64px */
+ --space-20: 5rem; /* 80px */
+ --space-24: 6rem; /* 96px */
+ --space-32: 8rem; /* 128px */
+
+ /* Typography Scale */
+ --text-xs: 0.75rem; /* 12px */
+ --text-sm: 0.875rem; /* 14px */
+ --text-base: 1rem; /* 16px */
+ --text-lg: 1.125rem; /* 18px */
+ --text-xl: 1.25rem; /* 20px */
+ --text-2xl: 1.5rem; /* 24px */
+ --text-3xl: 1.875rem; /* 30px */
+ --text-4xl: 2.25rem; /* 36px */
+ --text-5xl: 3rem; /* 48px */
+ --text-6xl: 3.75rem; /* 60px */
+
+ /* Font Weights */
+ --font-thin: 100;
+ --font-light: 300;
+ --font-normal: 400;
+ --font-medium: 500;
+ --font-semibold: 600;
+ --font-bold: 700;
+ --font-extrabold: 800;
+ --font-black: 900;
+
+ /* Line Heights */
+ --leading-none: 1;
+ --leading-tight: 1.25;
+ --leading-snug: 1.375;
+ --leading-normal: 1.5;
+ --leading-relaxed: 1.625;
+ --leading-loose: 2;
+
+ /* Border Radius */
+ --radius-none: 0;
+ --radius-sm: 0.125rem; /* 2px */
+ --radius: 0.25rem; /* 4px */
+ --radius-md: 0.375rem; /* 6px */
+ --radius-lg: 0.5rem; /* 8px */
+ --radius-xl: 0.75rem; /* 12px */
+ --radius-2xl: 1rem; /* 16px */
+ --radius-3xl: 1.5rem; /* 24px */
+ --radius-full: 9999px;
+
+ /* Shadows */
+ --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
+ --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
+ --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
+
+ /* Transitions & Animations */
+ --transition-none: none;
+ --transition-all: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-colors: color 150ms cubic-bezier(0.4, 0, 0.2, 1), background-color 150ms cubic-bezier(0.4, 0, 0.2, 1), border-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-opacity: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-shadow: box-shadow 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-transform: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
+
+ /* Z-Index Scale */
+ --z-0: 0;
+ --z-10: 10;
+ --z-20: 20;
+ --z-30: 30;
+ --z-40: 40;
+ --z-50: 50;
+ --z-auto: auto;
+
+ /* ----------------------------------------
+ Component-Specific Variables
+ ---------------------------------------- */
+
+ /* Container & Layout */
+ --container-max-width: 1200px;
+ --sidebar-width: 280px;
+ --header-height: 64px;
+
+ /* Form Elements */
+ --input-height: 2.5rem;
+ --input-height-sm: 2rem;
+ --input-height-lg: 3rem;
+
+ /* Buttons */
+ --btn-height: 2.5rem;
+ --btn-height-sm: 2rem;
+ --btn-height-lg: 3rem;
+
+ /* Tables */
+ --table-row-height: 3rem;
+ --table-header-height: 2.5rem;
+
+ /* Charts */
+ --chart-height: 400px;
+ --chart-height-sm: 300px;
+ --chart-height-lg: 500px;
+}
+
+/* ============================================================================
+ 3. BASE TYPOGRAPHY & LAYOUT
+ ============================================================================ */
+
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: var(--text-base);
+ font-weight: var(--font-normal);
+ line-height: var(--leading-normal);
+ color: var(--gray-900);
+ background-color: var(--gray-50);
+ min-height: 100vh;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* ----------------------------------------
+ Layout Containers
+ ---------------------------------------- */
+
+.container {
+ max-width: var(--container-max-width);
+ margin: 0 auto;
+ padding-left: var(--space-6);
+ padding-right: var(--space-6);
+}
+
+.container-fluid {
+ width: 100%;
+ padding-left: var(--space-4);
+ padding-right: var(--space-4);
+}
+
+.container-sm {
+ max-width: 640px;
+ margin: 0 auto;
+ padding-left: var(--space-6);
+ padding-right: var(--space-6);
+}
+
+.container-lg {
+ max-width: 1440px;
+ margin: 0 auto;
+ padding-left: var(--space-8);
+ padding-right: var(--space-8);
+}
+
+/* ============================================================================
+ 4. UTILITY CLASSES
+ ============================================================================ */
+
+/* ----------------------------------------
+ Display Utilities
+ ---------------------------------------- */
+
+.hidden { display: none !important; }
+.block { display: block !important; }
+.inline-block { display: inline-block !important; }
+.inline { display: inline !important; }
+.flex { display: flex !important; }
+.inline-flex { display: inline-flex !important; }
+.grid { display: grid !important; }
+.table { display: table !important; }
+
+/* ----------------------------------------
+ Flexbox Utilities
+ ---------------------------------------- */
+
+.flex-row { flex-direction: row !important; }
+.flex-col { flex-direction: column !important; }
+.flex-wrap { flex-wrap: wrap !important; }
+.flex-nowrap { flex-wrap: nowrap !important; }
+
+.items-start { align-items: flex-start !important; }
+.items-center { align-items: center !important; }
+.items-end { align-items: flex-end !important; }
+.items-stretch { align-items: stretch !important; }
+
+.justify-start { justify-content: flex-start !important; }
+.justify-center { justify-content: center !important; }
+.justify-end { justify-content: flex-end !important; }
+.justify-between { justify-content: space-between !important; }
+.justify-around { justify-content: space-around !important; }
+.justify-evenly { justify-content: space-evenly !important; }
+
+.flex-1 { flex: 1 1 0% !important; }
+.flex-auto { flex: 1 1 auto !important; }
+.flex-initial { flex: 0 1 auto !important; }
+.flex-none { flex: none !important; }
+
+/* ----------------------------------------
+ Grid Utilities
+ ---------------------------------------- */
+
+.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)) !important; }
+.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
+.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; }
+.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; }
+.grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)) !important; }
+.grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)) !important; }
+
+.gap-1 { gap: var(--space-1) !important; }
+.gap-2 { gap: var(--space-2) !important; }
+.gap-3 { gap: var(--space-3) !important; }
+.gap-4 { gap: var(--space-4) !important; }
+.gap-6 { gap: var(--space-6) !important; }
+.gap-8 { gap: var(--space-8) !important; }
+
+/* ----------------------------------------
+ Spacing Utilities
+ ---------------------------------------- */
+
+.m-0 { margin: 0 !important; }
+.m-1 { margin: var(--space-1) !important; }
+.m-2 { margin: var(--space-2) !important; }
+.m-3 { margin: var(--space-3) !important; }
+.m-4 { margin: var(--space-4) !important; }
+.m-6 { margin: var(--space-6) !important; }
+.m-8 { margin: var(--space-8) !important; }
+
+.mt-0 { margin-top: 0 !important; }
+.mt-2 { margin-top: var(--space-2) !important; }
+.mt-4 { margin-top: var(--space-4) !important; }
+.mt-6 { margin-top: var(--space-6) !important; }
+.mt-8 { margin-top: var(--space-8) !important; }
+
+.mb-0 { margin-bottom: 0 !important; }
+.mb-2 { margin-bottom: var(--space-2) !important; }
+.mb-4 { margin-bottom: var(--space-4) !important; }
+.mb-6 { margin-bottom: var(--space-6) !important; }
+.mb-8 { margin-bottom: var(--space-8) !important; }
+
+.ml-0 { margin-left: 0 !important; }
+.ml-2 { margin-left: var(--space-2) !important; }
+.ml-4 { margin-left: var(--space-4) !important; }
+.ml-auto { margin-left: auto !important; }
+
+.mr-0 { margin-right: 0 !important; }
+.mr-2 { margin-right: var(--space-2) !important; }
+.mr-4 { margin-right: var(--space-4) !important; }
+.mr-auto { margin-right: auto !important; }
+
+.p-0 { padding: 0 !important; }
+.p-1 { padding: var(--space-1) !important; }
+.p-2 { padding: var(--space-2) !important; }
+.p-3 { padding: var(--space-3) !important; }
+.p-4 { padding: var(--space-4) !important; }
+.p-6 { padding: var(--space-6) !important; }
+.p-8 { padding: var(--space-8) !important; }
+
+.pt-0 { padding-top: 0 !important; }
+.pt-2 { padding-top: var(--space-2) !important; }
+.pt-4 { padding-top: var(--space-4) !important; }
+.pt-6 { padding-top: var(--space-6) !important; }
+
+.pb-0 { padding-bottom: 0 !important; }
+.pb-2 { padding-bottom: var(--space-2) !important; }
+.pb-4 { padding-bottom: var(--space-4) !important; }
+.pb-6 { padding-bottom: var(--space-6) !important; }
+
+.px-0 { padding-left: 0 !important; padding-right: 0 !important; }
+.px-2 { padding-left: var(--space-2) !important; padding-right: var(--space-2) !important; }
+.px-4 { padding-left: var(--space-4) !important; padding-right: var(--space-4) !important; }
+.px-6 { padding-left: var(--space-6) !important; padding-right: var(--space-6) !important; }
+
+.py-0 { padding-top: 0 !important; padding-bottom: 0 !important; }
+.py-2 { padding-top: var(--space-2) !important; padding-bottom: var(--space-2) !important; }
+.py-4 { padding-top: var(--space-4) !important; padding-bottom: var(--space-4) !important; }
+.py-6 { padding-top: var(--space-6) !important; padding-bottom: var(--space-6) !important; }
+
+/* ----------------------------------------
+ Typography Utilities
+ ---------------------------------------- */
+
+.text-xs { font-size: var(--text-xs) !important; }
+.text-sm { font-size: var(--text-sm) !important; }
+.text-base { font-size: var(--text-base) !important; }
+.text-lg { font-size: var(--text-lg) !important; }
+.text-xl { font-size: var(--text-xl) !important; }
+.text-2xl { font-size: var(--text-2xl) !important; }
+.text-3xl { font-size: var(--text-3xl) !important; }
+
+.font-light { font-weight: var(--font-light) !important; }
+.font-normal { font-weight: var(--font-normal) !important; }
+.font-medium { font-weight: var(--font-medium) !important; }
+.font-semibold { font-weight: var(--font-semibold) !important; }
+.font-bold { font-weight: var(--font-bold) !important; }
+
+.text-left { text-align: left !important; }
+.text-center { text-align: center !important; }
+.text-right { text-align: right !important; }
+
+.text-gray-400 { color: var(--gray-400) !important; }
+.text-gray-500 { color: var(--gray-500) !important; }
+.text-gray-600 { color: var(--gray-600) !important; }
+.text-gray-700 { color: var(--gray-700) !important; }
+.text-gray-900 { color: var(--gray-900) !important; }
+
+.text-primary-500 { color: var(--primary-500) !important; }
+.text-primary-600 { color: var(--primary-600) !important; }
+.text-success-500 { color: var(--success-500) !important; }
+.text-warning-500 { color: var(--warning-500) !important; }
+.text-error-500 { color: var(--error-500) !important; }
+
+/* ----------------------------------------
+ Background & Border Utilities
+ ---------------------------------------- */
+
+.bg-white { background-color: #ffffff !important; }
+.bg-gray-50 { background-color: var(--gray-50) !important; }
+.bg-gray-100 { background-color: var(--gray-100) !important; }
+.bg-primary-50 { background-color: var(--primary-50) !important; }
+.bg-primary-500 { background-color: var(--primary-500) !important; }
+.bg-success-50 { background-color: var(--success-50) !important; }
+.bg-warning-50 { background-color: var(--warning-50) !important; }
+.bg-error-50 { background-color: var(--error-50) !important; }
+
+.border { border: 1px solid var(--gray-200) !important; }
+.border-gray-300 { border-color: var(--gray-300) !important; }
+.border-primary-500 { border-color: var(--primary-500) !important; }
+
+.rounded { border-radius: var(--radius) !important; }
+.rounded-md { border-radius: var(--radius-md) !important; }
+.rounded-lg { border-radius: var(--radius-lg) !important; }
+.rounded-xl { border-radius: var(--radius-xl) !important; }
+.rounded-full { border-radius: var(--radius-full) !important; }
+
+/* ----------------------------------------
+ Shadow & Effect Utilities
+ ---------------------------------------- */
+
+.shadow-sm { box-shadow: var(--shadow-sm) !important; }
+.shadow { box-shadow: var(--shadow) !important; }
+.shadow-md { box-shadow: var(--shadow-md) !important; }
+.shadow-lg { box-shadow: var(--shadow-lg) !important; }
+.shadow-none { box-shadow: none !important; }
+
+/* ----------------------------------------
+ Transition Utilities
+ ---------------------------------------- */
+
+.transition { transition: var(--transition-all) !important; }
+.transition-colors { transition: var(--transition-colors) !important; }
+.transition-opacity { transition: var(--transition-opacity) !important; }
+.transition-transform { transition: var(--transition-transform) !important; }
+
+/* ============================================================================
+ 5. COMPONENT STYLES
+ ============================================================================ */
+
+/* ----------------------------------------
+ Header Component
+ ---------------------------------------- */
+
+.header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: var(--radius-xl);
+ padding: var(--space-8);
+ box-shadow: var(--shadow-lg);
+ margin-bottom: var(--space-8);
+ position: relative;
+ overflow: hidden;
+}
+
+.header::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ z-index: var(--z-10);
+}
+
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ position: relative;
+ z-index: var(--z-20);
+}
+
+.logo {
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+}
+
+.logo i {
+ font-size: var(--text-6xl);
+ color: white;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.logo-text h1 {
+ font-size: var(--text-4xl);
+ font-weight: var(--font-bold);
+ color: white;
+ margin: 0;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+ line-height: var(--leading-tight);
+}
+
+.logo-text .subtitle {
+ color: rgba(255, 255, 255, 0.9);
+ font-size: var(--text-lg);
+ margin: var(--space-1) 0 0 0;
+ font-weight: var(--font-normal);
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.header-right {
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+}
+
+.version-badge {
+ background: rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: var(--radius-xl);
+ padding: var(--space-2) var(--space-4);
+}
+
+.version-badge span {
+ color: white;
+ font-size: var(--text-lg);
+ font-weight: var(--font-semibold);
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.status-indicator {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ background: rgba(16, 185, 129, 0.2);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(16, 185, 129, 0.3);
+ border-radius: var(--radius-xl);
+ padding: var(--space-2) var(--space-3);
+}
+
+.status-dot {
+ width: var(--space-2);
+ height: var(--space-2);
+ background: var(--success-500);
+ border-radius: var(--radius-full);
+ animation: pulse 2s infinite;
+ box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
+}
+
+.status-indicator span {
+ color: white;
+ font-size: var(--text-sm);
+ font-weight: var(--font-medium);
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+/* ----------------------------------------
+ Card Component
+ ---------------------------------------- */
+
+.card {
+ background: white;
+ border-radius: var(--radius-xl);
+ box-shadow: var(--shadow-md);
+ margin-bottom: var(--space-8);
+ overflow: hidden;
+ border: 1px solid var(--gray-200);
+ transition: var(--transition-shadow);
+}
+
+.card:hover {
+ box-shadow: var(--shadow-lg);
+}
+
+.card-header {
+ background: var(--gray-50);
+ padding: var(--space-6);
+ border-bottom: 1px solid var(--gray-200);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: var(--space-4);
+}
+
+.card-header h2 {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ font-size: var(--text-xl);
+ font-weight: var(--font-semibold);
+ color: var(--gray-900);
+ margin: 0;
+}
+
+.card-header i {
+ color: var(--primary-500);
+ font-size: var(--text-lg);
+}
+
+.step-badge {
+ background: var(--primary-500);
+ color: white;
+ width: var(--space-8);
+ height: var(--space-8);
+ border-radius: var(--radius-full);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: var(--font-semibold);
+ font-size: var(--text-sm);
+ flex-shrink: 0;
+ box-shadow: var(--shadow-sm);
+}
+
+.card-content {
+ padding: var(--space-8);
+}
+
+.card-footer {
+ background: var(--gray-50);
+ padding: var(--space-4) var(--space-8);
+ border-top: 1px solid var(--gray-200);
+}
+
+/* ----------------------------------------
+ Upload Component
+ ---------------------------------------- */
+
+.upload-area {
+ border: 2px dashed var(--gray-300);
+ border-radius: var(--radius-xl);
+ padding: var(--space-12);
+ text-align: center;
+ cursor: pointer;
+ transition: var(--transition-all);
+ background: var(--gray-50);
+ position: relative;
+ overflow: hidden;
+}
+
+.upload-area::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.1), transparent);
+ transition: left 0.5s;
+}
+
+.upload-area:hover {
+ border-color: var(--primary-500);
+ background: var(--primary-50);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-lg);
+}
+
+.upload-area:hover::before {
+ left: 100%;
+}
+
+.upload-area.dragover {
+ border-color: var(--primary-500);
+ background: var(--primary-100);
+ transform: scale(1.02);
+ box-shadow: var(--shadow-xl);
+}
+
+.upload-content i {
+ font-size: var(--text-5xl);
+ color: var(--primary-500);
+ margin-bottom: var(--space-4);
+ display: block;
+ transition: var(--transition-transform);
+}
+
+.upload-area:hover .upload-content i {
+ transform: scale(1.1);
+}
+
+.upload-content h3 {
+ font-size: var(--text-xl);
+ font-weight: var(--font-semibold);
+ margin-bottom: var(--space-2);
+ color: var(--gray-900);
+}
+
+.upload-content p {
+ color: var(--gray-600);
+ margin-bottom: var(--space-4);
+ font-size: var(--text-base);
+}
+
+.supported-formats {
+ display: flex;
+ gap: var(--space-2);
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.format-tag {
+ background: var(--primary-500);
+ color: white;
+ padding: var(--space-1) var(--space-3);
+ border-radius: var(--radius);
+ font-size: var(--text-sm);
+ font-weight: var(--font-medium);
+ box-shadow: var(--shadow-sm);
+ transition: var(--transition-all);
+}
+
+.format-tag:hover {
+ background: var(--primary-600);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow);
+}
+
+/* ----------------------------------------
+ Button Component
+ ---------------------------------------- */
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-2);
+ padding: var(--space-3) var(--space-6);
+ border: 2px solid transparent;
+ border-radius: var(--radius-lg);
+ font-size: var(--text-base);
+ font-weight: var(--font-medium);
+ text-decoration: none;
+ cursor: pointer;
+ transition: var(--transition-all);
+ min-height: var(--btn-height);
+ white-space: nowrap;
+ user-select: none;
+}
+
+.btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none !important;
+}
+
+/* Button Variants */
+.btn-primary {
+ background: var(--primary-500);
+ color: white;
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: var(--primary-600);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-primary:active {
+ transform: translateY(0);
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-secondary {
+ background: var(--gray-100);
+ color: var(--gray-700);
+ border-color: var(--gray-300);
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background: var(--gray-200);
+ border-color: var(--gray-400);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-success {
+ background: var(--success-500);
+ color: white;
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-success:hover:not(:disabled) {
+ background: var(--success-600);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-warning {
+ background: var(--warning-500);
+ color: white;
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-warning:hover:not(:disabled) {
+ background: var(--warning-600);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-danger {
+ background: var(--error-500);
+ color: white;
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-danger:hover:not(:disabled) {
+ background: var(--error-600);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-outline {
+ background: transparent;
+ color: var(--primary-600);
+ border-color: var(--primary-500);
+}
+
+.btn-outline:hover:not(:disabled) {
+ background: var(--primary-500);
+ color: white;
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+/* Button Sizes */
+.btn-sm {
+ padding: var(--space-2) var(--space-4);
+ font-size: var(--text-sm);
+ min-height: var(--btn-height-sm);
+}
+
+.btn-lg {
+ padding: var(--space-4) var(--space-8);
+ font-size: var(--text-lg);
+ min-height: var(--btn-height-lg);
+}
+
+.btn-icon {
+ min-width: var(--btn-height);
+ padding: var(--space-3);
+}
+
+.btn-icon.btn-sm {
+ min-width: var(--btn-height-sm);
+ padding: var(--space-2);
+}
+
+.btn-icon.btn-lg {
+ min-width: var(--btn-height-lg);
+ padding: var(--space-4);
+}
+
+/* Form Group Styling */
+.form-group {
+ margin-bottom: var(--space-6);
+ position: relative;
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-5);
+ margin-bottom: var(--space-6);
+}
+
+.form-group small {
+ display: block;
+ margin-top: var(--space-3);
+ color: var(--gray-600);
+ font-size: var(--text-xs);
+ line-height: var(--leading-relaxed);
+ padding: var(--space-2) var(--space-3);
+ background: var(--gray-50);
+ border-radius: var(--radius-md);
+ border-left: 3px solid var(--gray-300);
+ font-style: italic;
+}
+
+.form-group small i {
+ color: var(--gray-500);
+ margin-right: var(--space-2);
+ font-size: var(--text-xs);
+}
+
+/* File Info */
+.file-info {
+ background: linear-gradient(135deg, #ecfdf5, #d1fae5);
+ border: 1px solid #86efac;
+ border-radius: var(--radius-lg);
+ padding: var(--space-6);
+ margin-top: var(--space-4);
+}
+
+.file-details {
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+}
+
+.file-details i {
+ font-size: var(--text-2xl);
+ color: var(--success);
+}
+
+.file-details div {
+ flex: 1;
+}
+
+.file-details h4 {
+ margin: 0;
+ color: var(--gray-900);
+ font-weight: 600;
+}
+
+.file-details p {
+ margin: 0;
+ color: var(--gray-600);
+ font-size: var(--text-sm);
+}
+
+.btn-remove {
+ background: var(--error);
+ color: white;
+ border: none;
+ padding: var(--space-2) var(--space-3);
+ border-radius: var(--radius);
+ cursor: pointer;
+ transition: var(--transition);
+ font-size: var(--text-sm);
+ flex-shrink: 0;
+}
+
+.btn-remove:hover {
+ background: #dc2626;
+}
+
+/* Enhanced Configuration Layout */
+.config-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
+ gap: var(--space-8);
+ align-items: start;
+}
+
+.config-group {
+ background: white;
+ padding: var(--space-8);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--gray-200);
+ box-shadow: var(--shadow-sm);
+ height: fit-content;
+}
+
+.config-group h3 {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ margin-bottom: var(--space-6);
+ color: var(--gray-900);
+ font-size: var(--text-lg);
+ font-weight: 600;
+ padding-bottom: var(--space-3);
+ border-bottom: 2px solid var(--gray-100);
+}
+
+.config-group h3 i {
+ color: var(--primary);
+ font-size: var(--text-xl);
+}
+
+/* Configuration Headers */
+.config-header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ margin-bottom: var(--space-5);
+ padding: var(--space-4) var(--space-5);
+ background: linear-gradient(135deg, var(--primary-100), #f0f9ff);
+ border-radius: var(--radius-lg);
+ border-left: 4px solid var(--primary-500);
+ box-shadow: var(--shadow-sm);
+}
+
+.config-header i {
+ font-size: var(--text-lg);
+ color: var(--primary-500);
+}
+
+.config-header span {
+ font-weight: 600;
+ color: var(--gray-800);
+ font-size: var(--text-base);
+}
+
+/* Form Elements */
+.form-group {
+ margin-bottom: var(--space-6);
+ position: relative;
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-5);
+ margin-bottom: var(--space-6);
+}
+
+.form-group label {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ font-weight: 600;
+ margin-bottom: var(--space-3);
+ color: var(--gray-800);
+ font-size: var(--text-base);
+}
+
+.form-group label i {
+ color: var(--primary-500);
+ font-size: var(--text-base);
+ width: var(--space-4);
+ text-align: center;
+}
+
+.form-control {
+ width: 100%;
+ padding: var(--space-3) var(--space-4);
+ border: 2px solid var(--gray-200);
+ border-radius: var(--radius-lg);
+ font-size: var(--text-base);
+ transition: var(--transition-all);
+ background: white;
+ box-sizing: border-box;
+ font-family: inherit;
+ min-height: var(--input-height);
+ box-shadow: var(--shadow-sm);
+}
+
+.form-control:hover {
+ border-color: var(--gray-300);
+}
+
+.form-control:focus {
+ outline: none;
+ border-color: var(--primary-500);
+ box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
+ background: var(--gray-50);
+}
+
+.form-control.form-control-sm {
+ min-height: var(--input-height-sm);
+ padding: var(--space-2) var(--space-3);
+ font-size: var(--text-sm);
+}
+
+.form-control.form-control-lg {
+ min-height: var(--input-height-lg);
+ padding: var(--space-4) var(--space-6);
+ font-size: var(--text-lg);
+}
+
+.form-control::placeholder {
+ color: var(--gray-400);
+ font-size: var(--text-sm);
+}
+
+.form-group small {
+ display: block;
+ margin-top: var(--space-3);
+ color: var(--gray-600);
+ font-size: 0.8rem;
+ line-height: 1.5;
+ padding: var(--space-2) var(--space-3);
+ background: var(--gray-50);
+ border-radius: var(--radius);
+ border-left: 3px solid var(--gray-300);
+ font-style: italic;
+}
+
+.form-group small i {
+ color: var(--gray-500);
+ margin-right: var(--space-2);
+ font-size: 0.75rem;
+}
+
+/* Enhanced Checkboxes */
+.checkbox-group {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-5);
+}
+
+.checkbox-item {
+ position: relative;
+}
+
+.checkbox-item input[type="checkbox"] {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+}
+
+.checkbox-item label {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-4);
+ cursor: pointer;
+ padding: var(--space-5);
+ border: 2px solid var(--gray-200);
+ border-radius: var(--radius-lg);
+ transition: var(--transition);
+ background: white;
+ position: relative;
+}
+
+.checkbox-item label:hover {
+ border-color: var(--primary);
+ background: var(--gray-50);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-sm);
+}
+
+.checkbox-item input[type="checkbox"]:checked + label {
+ border-color: var(--primary);
+ background: linear-gradient(135deg, var(--primary-light), #f0f9ff);
+ box-shadow: var(--shadow);
+}
+
+.checkbox-item label::before {
+ content: '';
+ width: 1.5rem;
+ height: 1.5rem;
+ border: 2px solid var(--gray-300);
+ border-radius: var(--radius);
+ background: white;
+ transition: var(--transition);
+ flex-shrink: 0;
+ margin-top: var(--space-1);
+ box-shadow: var(--shadow-sm);
+}
+
+.checkbox-item input[type="checkbox"]:checked + label::before {
+ background: var(--primary);
+ border-color: var(--primary);
+ background-image: url('data:image/svg+xml, ');
+ background-size: 1rem;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.checkbox-item label div {
+ flex: 1;
+ min-width: 0;
+}
+
+.checkbox-item label div strong {
+ display: block;
+ font-size: var(--text-base);
+ font-weight: 600;
+ color: var(--gray-900);
+ margin-bottom: var(--space-1);
+}
+
+.checkbox-item label small {
+ display: block;
+ color: var(--gray-600);
+ font-size: 0.85rem;
+ line-height: 1.4;
+ margin: 0;
+ background: none;
+ border: none;
+ padding: 0;
+ font-style: normal;
+}
+
+/* Enhanced Radio Buttons */
+.radio-group {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4);
+ margin-bottom: var(--space-6);
+}
+
+.api-type-selector {
+ margin-bottom: var(--space-6);
+}
+
+.api-type-selector h4 {
+ margin-bottom: var(--space-4);
+ color: var(--gray-800);
+ font-size: var(--text-base);
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.api-type-selector h4 i {
+ color: var(--primary);
+}
+
+.radio-item {
+ position: relative;
+}
+
+.radio-item input[type="radio"] {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+}
+
+.radio-item label {
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+ cursor: pointer;
+ padding: var(--space-4) var(--space-5);
+ border: 2px solid var(--gray-200);
+ border-radius: var(--radius);
+ transition: var(--transition);
+ background: white;
+ font-weight: 500;
+}
+
+.radio-item label:hover {
+ border-color: var(--primary);
+ background: var(--gray-50);
+ transform: translateY(-1px);
+}
+
+.radio-item input[type="radio"]:checked + label {
+ border-color: var(--primary);
+ background: linear-gradient(135deg, var(--primary-light), #f0f9ff);
+ font-weight: 600;
+}
+
+.radio-item label::before {
+ content: '';
+ width: 1.25rem;
+ height: 1.25rem;
+ border: 2px solid var(--gray-300);
+ border-radius: 50%;
+ background: white;
+ transition: var(--transition);
+ flex-shrink: 0;
+ box-shadow: var(--shadow-sm);
+}
+
+.radio-item input[type="radio"]:checked + label::before {
+ background: var(--primary);
+ border-color: var(--primary);
+ background-image: url('data:image/svg+xml, ');
+ background-size: 0.75rem;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.radio-item label div {
+ flex: 1;
+ min-width: 0;
+}
+
+.radio-item label span {
+ display: block;
+ font-size: var(--text-base);
+ color: var(--gray-900);
+ margin-bottom: var(--space-1);
+}
+
+.radio-item label small {
+ display: block;
+ color: var(--gray-600);
+ font-size: 0.8rem;
+ line-height: 1.3;
+}
+
+/* Quick Presets Section */
+.preset-section {
+ margin: var(--space-6) 0;
+}
+
+.preset-description {
+ color: var(--gray-600);
+ font-size: var(--text-sm);
+ margin: var(--space-2) 0 var(--space-4) 0;
+ text-align: center;
+}
+
+/* Preset List */
+.preset-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3);
+ max-width: 500px;
+ margin: 0 auto;
+}
+
+.preset-item {
+ background: white;
+ border: 1px solid var(--gray-200);
+ border-radius: var(--radius);
+ transition: var(--transition);
+ box-shadow: var(--shadow-sm);
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: var(--space-4) var(--space-5);
+}
+
+.preset-item:hover {
+ border-color: var(--primary-300);
+ box-shadow: var(--shadow-md);
+}
+
+.preset-info {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ flex: 1;
+ min-width: 0;
+}
+
+.preset-radio {
+ width: 1.25rem;
+ height: 1.25rem;
+ margin: 0;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.preset-label {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ margin: 0;
+ cursor: pointer;
+ font-weight: 500;
+ color: var(--gray-900);
+ min-width: 0;
+}
+
+.preset-name {
+ font-size: var(--text-base);
+ font-weight: 600;
+ color: var(--gray-900);
+}
+
+.info-btn {
+ background: none;
+ border: none;
+ color: var(--primary);
+ cursor: pointer;
+ padding: var(--space-2);
+ border-radius: var(--radius);
+ transition: var(--transition);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ flex-shrink: 0;
+}
+
+.info-btn:hover {
+ background: var(--primary-light);
+ color: var(--primary-dark);
+ transform: scale(1.1);
+}
+
+.info-btn i {
+ font-size: 1rem;
+}
+
+/* Style radio buttons */
+.preset-radio:checked + .preset-label {
+ font-weight: 700;
+ color: var(--primary-dark);
+}
+
+.preset-radio:checked + .preset-label .preset-name {
+ color: var(--primary-dark);
+}
+
+/* Highlight selected preset item */
+.preset-item:has(.preset-radio:checked) {
+ border-color: var(--primary);
+ background: linear-gradient(135deg, var(--primary-light), #f0f9ff);
+ box-shadow: var(--shadow-md);
+}
+
+/* Custom radio button styling */
+.preset-radio {
+ appearance: none;
+ -webkit-appearance: none;
+ width: 1.25rem;
+ height: 1.25rem;
+ border: 2px solid var(--gray-300);
+ border-radius: 50%;
+ background: white;
+ cursor: pointer;
+ position: relative;
+ transition: var(--transition);
+}
+
+.preset-radio:checked {
+ border-color: var(--primary);
+ background: var(--primary);
+}
+
+.preset-radio:checked::after {
+ content: '';
+ width: 0.5rem;
+ height: 0.5rem;
+ border-radius: 50%;
+ background: white;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.preset-radio:hover {
+ border-color: var(--primary);
+ box-shadow: 0 0 0 2px var(--primary-light);
+}
+
+/* Preset Info Modal - Clean Implementation */
+.preset-info-modal {
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100% !important;
+ height: 100% !important;
+ background: rgba(0, 0, 0, 0.5) !important;
+ z-index: 999999 !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ padding: 20px !important;
+ box-sizing: border-box !important;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease;
+}
+
+.preset-info-modal.show {
+ opacity: 1 !important;
+ visibility: visible !important;
+}
+
+/* Hover mode modal - slightly lighter background */
+.preset-info-modal[data-hover-mode="true"] {
+ background: rgba(0, 0, 0, 0.3) !important;
+}
+
+.preset-info-modal[data-hover-mode="true"] .preset-info-content {
+ border: 2px solid #3b82f6 !important;
+}
+
+.preset-info-content {
+ background: white !important;
+ border-radius: 12px !important;
+ width: 100% !important;
+ max-width: 500px !important;
+ max-height: 80vh !important;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2) !important;
+ overflow: hidden !important;
+ display: flex !important;
+ flex-direction: column !important;
+ margin: auto !important;
+ position: relative !important;
+}
+
+.preset-info-header {
+ padding: 20px;
+ background: #f8fafc;
+ border-bottom: 1px solid #e5e7eb;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-radius: 12px 12px 0 0;
+}
+
+.preset-info-title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.preset-info-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ color: white;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.preset-info-title h3 {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 700;
+ color: #1f2937;
+}
+
+.preset-info-subtitle {
+ color: #6b7280;
+ font-size: 14px;
+ margin: 4px 0 0 0;
+ font-weight: 500;
+}
+
+.preset-close-btn {
+ background: none;
+ border: none;
+ color: #6b7280;
+ cursor: pointer;
+ padding: 8px;
+ border-radius: 6px;
+ font-size: 18px;
+ transition: all 0.2s ease;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.preset-close-btn:hover {
+ background: #e5e7eb;
+ color: #374151;
+}
+
+.preset-info-icon.conservative {
+ background: linear-gradient(135deg, #10b981, #059669);
+}
+
+.preset-info-icon.balanced {
+ background: linear-gradient(135deg, var(--primary), #3730a3);
+}
+
+.preset-info-icon.performance {
+ background: linear-gradient(135deg, #f59e0b, #d97706);
+}
+
+.preset-info-title h3 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: var(--gray-900);
+}
+
+.preset-info-subtitle {
+ color: var(--gray-600);
+ font-size: var(--text-sm);
+ margin: 0;
+}
+
+.preset-close-btn {
+ background: none;
+ border: none;
+ color: var(--gray-500);
+ cursor: pointer;
+ padding: var(--space-2);
+ border-radius: var(--radius);
+ font-size: 1.25rem;
+ transition: var(--transition);
+}
+
+.preset-close-btn:hover {
+ background: var(--gray-100);
+ color: var(--gray-700);
+}
+
+.preset-info-body {
+ padding: 20px;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.preset-info-body::-webkit-scrollbar {
+ width: 8px;
+}
+
+.preset-info-body::-webkit-scrollbar-track {
+ background: #f1f5f9;
+ border-radius: 4px;
+}
+
+.preset-info-body::-webkit-scrollbar-thumb {
+ background: #cbd5e1;
+ border-radius: 4px;
+}
+
+.preset-info-body::-webkit-scrollbar-thumb:hover {
+ background: #94a3b8;
+}
+
+.preset-metrics-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+ margin-bottom: 24px;
+}
+
+.preset-metric {
+ text-align: center;
+ padding: 16px;
+ background: #f8fafc;
+ border-radius: 8px;
+ border: 1px solid #e5e7eb;
+}
+
+.preset-metric-label {
+ display: block;
+ font-size: 12px;
+ color: #6b7280;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: 8px;
+}
+
+.preset-metric-value {
+ display: block;
+ font-size: 32px;
+ font-weight: 700;
+ color: #3b82f6;
+}
+
+.preset-benefits-list {
+ margin-bottom: 24px;
+ background: #f0fdf4;
+ border-radius: 8px;
+ padding: 16px;
+ border: 1px solid #bbf7d0;
+}
+
+.preset-benefits-list h4 {
+ margin: 0 0 12px 0;
+ font-size: 16px;
+ font-weight: 700;
+ color: #1f2937;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.preset-benefits-list h4::before {
+ content: "โจ";
+ font-size: 16px;
+}
+
+.preset-benefit {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+ padding: 8px;
+ background: white;
+ border-radius: 6px;
+ border-left: 3px solid transparent;
+}
+
+.preset-benefit.positive {
+ border-left-color: #10b981;
+}
+
+.preset-benefit.warning {
+ border-left-color: #f59e0b;
+ background: #fffbeb;
+}
+
+.preset-benefit-icon {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ flex-shrink: 0;
+}
+
+.preset-benefit-icon.positive {
+ background: #10b981;
+ color: white;
+}
+
+.preset-benefit-icon.warning {
+ background: #f59e0b;
+ color: white;
+}
+
+.preset-benefit span {
+ color: #374151;
+ font-weight: 500;
+ font-size: 14px;
+}
+
+.preset-stats-section {
+ margin-bottom: 24px;
+ background: #f8fafc;
+ border-radius: 8px;
+ padding: 16px;
+ border: 1px solid #e5e7eb;
+}
+
+.preset-stats-section h4 {
+ margin: 0 0 16px 0;
+ font-size: 16px;
+ font-weight: 700;
+ color: #1f2937;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.preset-stats-section h4::before {
+ content: "๐";
+ font-size: 16px;
+}
+
+.preset-stat {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ padding: 12px;
+ background: white;
+ border-radius: 6px;
+ border: 1px solid #e5e7eb;
+}
+
+.preset-stat-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: #374151;
+ font-weight: 600;
+ min-width: 100px;
+}
+
+.preset-stat-icon {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ color: white;
+ flex-shrink: 0;
+}
+
+.preset-stat-icon.speed {
+ background: #3b82f6;
+}
+
+.preset-stat-icon.resource {
+ background: #f59e0b;
+}
+
+.preset-stat-icon.stability {
+ background: #10b981;
+}
+
+.preset-progress-container {
+ flex: 1;
+ margin-left: 12px;
+ position: relative;
+}
+
+.preset-progress-bar {
+ height: 12px;
+ background: #e5e7eb;
+ border-radius: 6px;
+ overflow: hidden;
+ position: relative;
+}
+
+.preset-progress-fill {
+ height: 100%;
+ border-radius: 6px;
+ transition: width 0.8s ease;
+}
+
+.preset-progress-fill.speed {
+ background: #3b82f6;
+}
+
+.preset-progress-fill.resource {
+ background: #f59e0b;
+}
+
+.preset-progress-fill.stability {
+ background: #10b981;
+}
+
+.preset-progress-value {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 11px;
+ font-weight: 700;
+ color: #4b5563;
+ background: white;
+ padding: 2px 6px;
+ border-radius: 4px;
+ border: 1px solid #e5e7eb;
+}
+
+.preset-recommendation-box {
+ background: #dbeafe;
+ border: 1px solid #93c5fd;
+ border-radius: 8px;
+ padding: 16px;
+ margin: 0;
+ font-size: 14px;
+ color: #374151;
+ text-align: center;
+}
+
+.preset-recommendation-box strong {
+ color: #1e40af;
+ font-weight: 700;
+}
+
+/* Modal positioning fix */
+body.modal-open {
+ overflow: hidden !important;
+ position: relative;
+}
+
+/* Ensure modal appears on top of everything */
+.preset-info-modal {
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+ width: 100vw !important;
+ height: 100vh !important;
+ margin: 0 !important;
+ padding: 20px !important;
+ pointer-events: auto !important;
+}
+
+/* Hide any other modals or overlays that might interfere */
+.preset-info-modal ~ .modal,
+.preset-info-modal ~ .overlay {
+ z-index: 1 !important;
+}
+
+
+
+/* Responsive Design for Presets */
+@media (max-width: 768px) {
+ .preset-list {
+ max-width: 100%;
+ }
+
+ .preset-info {
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .preset-metrics-grid {
+ grid-template-columns: 1fr;
+ gap: 12px;
+ }
+
+ .preset-info-content {
+ max-height: 90vh;
+ width: 95%;
+ }
+
+ .preset-info-header {
+ padding: 16px;
+ }
+
+ .preset-info-body {
+ padding: 16px;
+ }
+
+ .preset-info-title {
+ gap: 8px;
+ }
+
+ .preset-info-icon {
+ width: 40px;
+ height: 40px;
+ font-size: 16px;
+ }
+
+ .preset-info-title h3 {
+ font-size: 18px;
+ }
+
+ .preset-metric-value {
+ font-size: 24px;
+ }
+
+ .preset-stat {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 8px;
+ }
+
+ .preset-progress-container {
+ margin-left: 0;
+ }
+}
+
+/* Buttons */
+.btn-primary {
+ background: var(--primary);
+ color: white;
+ border: none;
+ padding: var(--space-4) var(--space-8);
+ border-radius: var(--radius);
+ font-size: var(--text-base);
+ font-weight: 600;
+ cursor: pointer;
+ transition: var(--transition);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-2);
+ min-width: 200px;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: var(--primary-dark);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-primary:disabled {
+ background: var(--gray-400);
+ cursor: not-allowed;
+ transform: none;
+}
+
+.btn-secondary {
+ background: white;
+ color: var(--primary);
+ border: 1px solid var(--primary);
+ padding: var(--space-3) var(--space-6);
+ border-radius: var(--radius);
+ font-weight: 600;
+ cursor: pointer;
+ transition: var(--transition);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.btn-secondary:hover {
+ background: var(--primary);
+ color: white;
+ transform: translateY(-1px);
+}
+
+.btn-secondary.active {
+ background: var(--primary);
+ color: white;
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-download {
+ background: var(--success);
+ color: white;
+ border: none;
+ padding: var(--space-3) var(--space-6);
+ border-radius: var(--radius);
+ font-weight: 600;
+ cursor: pointer;
+ transition: var(--transition);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.btn-download:hover {
+ background: #059669;
+ transform: translateY(-1px);
+}
+
+/* Run Controls */
+.run-controls {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--space-6);
+ padding: var(--space-8);
+ background: var(--gray-50);
+ border-radius: var(--radius-lg);
+ margin-top: var(--space-8);
+ border: 1px solid var(--gray-200);
+}
+
+.run-info {
+ display: flex;
+ gap: var(--space-8);
+ flex-wrap: wrap;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+}
+
+.info-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ color: var(--gray-600);
+ font-size: var(--text-sm);
+ font-weight: 500;
+}
+
+.info-item i {
+ color: var(--primary);
+}
+
+.run-info > div:first-child {
+ display: flex;
+ gap: var(--space-8);
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+/* Estimation Details */
+.estimation-details {
+ width: 100%;
+ max-width: 400px;
+ background: white;
+ border: 1px solid var(--primary-200);
+ border-radius: var(--radius);
+ padding: var(--space-4);
+ margin-top: var(--space-4);
+ box-shadow: var(--shadow-sm);
+}
+
+.estimation-breakdown {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+}
+
+.estimation-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-2) 0;
+ border-bottom: 1px solid var(--gray-100);
+ font-size: var(--text-sm);
+}
+
+.estimation-row:last-child {
+ border-bottom: none;
+ font-weight: 600;
+ color: var(--primary-dark);
+ font-size: var(--text-base);
+}
+
+.estimation-row span:first-child {
+ color: var(--gray-600);
+ font-weight: 500;
+}
+
+.estimation-row span:last-child {
+ color: var(--gray-900);
+ font-weight: 600;
+}
+
+/* Progress */
+.progress-container {
+ margin-bottom: var(--space-6);
+}
+
+.progress-bar {
+ background: var(--gray-200);
+ border-radius: var(--radius);
+ height: 0.75rem;
+ overflow: hidden;
+ margin-bottom: var(--space-3);
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--primary), #6366f1);
+ border-radius: var(--radius);
+ transition: width 0.3s ease;
+ width: 0%;
+ position: relative;
+}
+
+.progress-fill::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
+ animation: shimmer 2s infinite;
+}
+
+@keyframes shimmer {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(100%); }
+}
+
+.progress-text {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 500;
+ color: var(--gray-700);
+}
+
+.progress-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: var(--space-4);
+ margin-bottom: var(--space-6);
+}
+
+.stat-item {
+ background: white;
+ padding: var(--space-4);
+ border-radius: var(--radius);
+ border: 1px solid var(--gray-200);
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+}
+
+.stat-item i {
+ color: var(--primary);
+ font-size: var(--text-lg);
+}
+
+/* Enhanced Status Badges */
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: var(--space-2) var(--space-4);
+ border-radius: var(--radius);
+ font-size: var(--text-sm);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ border: 1px solid transparent;
+ transition: var(--transition);
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.status-badge::before {
+ content: '';
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.status-badge.success {
+ background: linear-gradient(135deg, #dcfce7, #bbf7d0);
+ color: #166534;
+ border-color: #86efac;
+}
+
+.status-badge.success::before {
+ background: var(--success);
+ animation: pulse-success 2s infinite;
+}
+
+.status-badge.processing {
+ background: linear-gradient(135deg, #fef3c7, #fde68a);
+ color: #92400e;
+ border-color: #fcd34d;
+}
+
+.status-badge.processing::before {
+ background: var(--warning);
+ animation: pulse-warning 1.5s infinite;
+}
+
+.status-badge.error {
+ background: linear-gradient(135deg, #fee2e2, #fecaca);
+ color: #991b1b;
+ border-color: #f87171;
+}
+
+.status-badge.error::before {
+ background: var(--error);
+ animation: pulse-error 1s infinite;
+}
+
+/* Default status badge (for "Running" state) */
+.status-badge:not(.success):not(.processing):not(.error) {
+ background: linear-gradient(135deg, var(--primary-light), #e0e7ff);
+ color: var(--primary-dark);
+ border-color: #93c5fd;
+}
+
+.status-badge:not(.success):not(.processing):not(.error)::before {
+ background: var(--primary);
+ animation: pulse-primary 1.5s infinite;
+}
+
+@keyframes pulse-success {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+@keyframes pulse-warning {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.6; }
+}
+
+@keyframes pulse-error {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.7; }
+}
+
+@keyframes pulse-primary {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.6; }
+}
+
+/* Results */
+#results-section .card-content {
+ padding: var(--space-6);
+}
+
+.results-summary {
+ margin-bottom: var(--space-4);
+}
+
+.summary-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: var(--space-4);
+ margin-bottom: 0;
+}
+
+@media (max-width: 768px) {
+ .summary-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--space-3);
+ }
+}
+
+@media (max-width: 480px) {
+ .summary-grid {
+ grid-template-columns: 1fr;
+ gap: var(--space-3);
+ }
+}
+
+.summary-item {
+ background: white;
+ padding: var(--space-5);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--gray-200);
+ text-align: center;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+}
+
+.summary-item::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: var(--gray-300);
+ transition: all 0.3s ease;
+}
+
+.summary-item:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
+}
+
+.summary-item i {
+ font-size: var(--text-2xl);
+ margin-bottom: var(--space-3);
+ display: block;
+ transition: all 0.3s ease;
+}
+
+.summary-item h3 {
+ font-size: var(--text-2xl);
+ font-weight: 700;
+ margin-bottom: var(--space-1);
+ color: var(--gray-900);
+ transition: all 0.3s ease;
+}
+
+.summary-item p {
+ color: var(--gray-600);
+ font-weight: 500;
+ font-size: var(--text-sm);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+/* Success variant */
+.summary-item.success::before {
+ background: linear-gradient(90deg, #10b981, #059669);
+}
+
+.summary-item.success i {
+ color: #10b981;
+}
+
+.summary-item.success:hover {
+ background: linear-gradient(135deg, #f0fdf4, #dcfce7);
+ border-color: #10b981;
+}
+
+.summary-item.success:hover i {
+ color: #059669;
+ transform: scale(1.1);
+}
+
+/* Warning variant */
+.summary-item.warning::before {
+ background: linear-gradient(90deg, #f59e0b, #d97706);
+}
+
+.summary-item.warning i {
+ color: #f59e0b;
+}
+
+.summary-item.warning:hover {
+ background: linear-gradient(135deg, #fffbeb, #fef3c7);
+ border-color: #f59e0b;
+}
+
+.summary-item.warning:hover i {
+ color: #d97706;
+ transform: scale(1.1);
+}
+
+/* Info variant */
+.summary-item.info::before {
+ background: linear-gradient(90deg, #3b82f6, #2563eb);
+}
+
+.summary-item.info i {
+ color: #3b82f6;
+}
+
+.summary-item.info:hover {
+ background: linear-gradient(135deg, #eff6ff, #dbeafe);
+ border-color: #3b82f6;
+}
+
+.summary-item.info:hover i {
+ color: #2563eb;
+ transform: scale(1.1);
+}
+
+/* Token info variant */
+.summary-item.token-info::before {
+ background: linear-gradient(90deg, #8b5cf6, #7c3aed);
+}
+
+.summary-item.token-info i {
+ color: #8b5cf6;
+}
+
+.summary-item.token-info:hover {
+ background: linear-gradient(135deg, #faf5ff, #f3e8ff);
+ border-color: #8b5cf6;
+}
+
+.summary-item.token-info:hover i {
+ color: #7c3aed;
+ transform: scale(1.1);
+}
+
+/* Cost info variant */
+.summary-item.cost-info::before {
+ background: linear-gradient(90deg, #059669, #047857);
+}
+
+.summary-item.cost-info i {
+ color: #059669;
+}
+
+.summary-item.cost-info:hover {
+ background: linear-gradient(135deg, #f0fdfa, #ccfbf1);
+ border-color: #059669;
+}
+
+.summary-item.cost-info:hover i {
+ color: #047857;
+ transform: scale(1.1);
+}
+
+.results-actions {
+ display: flex;
+ gap: var(--space-4);
+ margin: var(--space-6) 0 var(--space-4) 0;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+/* Metrics */
+.metrics-container {
+ background: white;
+ padding: var(--space-6);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--gray-200);
+ margin-top: var(--space-2);
+}
+
+.metrics-container h4 {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ margin-bottom: var(--space-4);
+ color: var(--gray-900);
+ font-size: var(--text-lg);
+ font-weight: 600;
+}
+
+.metrics-container h4 i {
+ color: var(--primary);
+}
+
+#metrics-chart {
+ max-height: 400px;
+ width: 100%;
+}
+
+/* Log Container */
+.log-container {
+ background: var(--gray-900);
+ border-radius: var(--radius);
+ overflow: hidden;
+ margin-top: var(--space-6);
+}
+
+.log-container h4 {
+ background: var(--gray-800);
+ color: white;
+ padding: var(--space-4) var(--space-6);
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ font-size: var(--text-base);
+}
+
+.log-output {
+ max-height: 300px;
+ overflow-y: auto;
+ padding: var(--space-6);
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: var(--text-sm);
+ line-height: 1.6;
+ color: #e5e7eb;
+ background: var(--gray-900);
+}
+
+.log-entry {
+ margin-bottom: var(--space-2);
+ padding: var(--space-1) 0;
+}
+
+.log-entry.info { color: #7dd3fc; }
+.log-entry.success { color: #86efac; }
+.log-entry.warning { color: #fde047; }
+.log-entry.error { color: #fca5a5; }
+
+/* Footer */
+.footer {
+ text-align: center;
+ margin-top: var(--space-12);
+ padding: var(--space-8) 0;
+ border-top: 1px solid var(--gray-200);
+ color: var(--gray-500);
+}
+
+.footer-links {
+ display: flex;
+ justify-content: center;
+ gap: var(--space-8);
+ margin-top: var(--space-4);
+ flex-wrap: wrap;
+}
+
+.footer-links a {
+ color: var(--gray-500);
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ transition: var(--transition);
+}
+
+.footer-links a:hover {
+ color: var(--primary);
+}
+
+/* Responsive Design */
+@media (max-width: 1024px) {
+ .config-grid {
+ grid-template-columns: 1fr;
+ gap: var(--space-6);
+ }
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding: var(--space-4);
+ }
+
+ .card-content {
+ padding: var(--space-6);
+ }
+
+ .header-content {
+ flex-direction: column;
+ gap: var(--space-4);
+ text-align: center;
+ }
+
+ .logo h1 {
+ font-size: var(--text-2xl);
+ }
+
+ .form-row {
+ grid-template-columns: 1fr;
+ gap: var(--space-4);
+ }
+
+ .preset-buttons {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .run-info {
+ flex-direction: column;
+ gap: var(--space-4);
+ }
+
+ .summary-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .results-actions {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .footer-links {
+ flex-direction: column;
+ gap: var(--space-4);
+ }
+
+ .card-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--space-3);
+ }
+}
+
+@media (max-width: 480px) {
+ .card-content {
+ padding: var(--space-4);
+ }
+
+ .summary-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .upload-area {
+ padding: var(--space-8) var(--space-4);
+ }
+
+ .preset-buttons {
+ grid-template-columns: 1fr;
+ }
+
+ .progress-text {
+ flex-direction: column;
+ gap: var(--space-2);
+ text-align: center;
+ }
+
+ .checkbox-item label,
+ .radio-item label {
+ padding: var(--space-4);
+ }
+}
+
+/* Animations */
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.7;
+ }
+}
+
+.pulse {
+ animation: pulse 2s infinite;
+}
+
+.slide-in {
+ animation: slideIn 0.5s ease;
+}
+
+/* Loading States */
+.loading {
+ opacity: 0.6;
+ pointer-events: none;
+ position: relative;
+}
+
+.loading::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 1.25rem;
+ height: 1.25rem;
+ margin: -0.625rem 0 0 -0.625rem;
+ border: 2px solid var(--primary);
+ border-top: 2px solid transparent;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Toast Notifications */
+.toast {
+ position: fixed;
+ top: var(--space-6);
+ right: var(--space-6);
+ background: white;
+ padding: var(--space-4) var(--space-5);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow-lg);
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ z-index: 1000;
+ min-width: 300px;
+ border-left: 4px solid var(--primary);
+ animation: slideInRight 0.3s ease;
+}
+
+.toast-success { border-left-color: var(--success); }
+.toast-warning { border-left-color: var(--warning); }
+.toast-error { border-left-color: var(--error); }
+
+.toast i { color: var(--primary); }
+.toast-success i { color: var(--success); }
+.toast-warning i { color: var(--warning); }
+.toast-error i { color: var(--error); }
+
+@keyframes slideInRight {
+ from { transform: translateX(100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+}
+
+/* Detailed Results Modal */
+.results-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 999998; /* High z-index but below preset modal */
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.results-modal.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+
+
+/* Hide Chart.js tooltips and floating elements when modal is open */
+.results-modal.show ~ * .chartjs-tooltip,
+.results-modal.show ~ * [role="tooltip"],
+.results-modal.show ~ * .score-text,
+.results-modal.show ~ * .percentage-display {
+ display: none !important;
+ opacity: 0 !important;
+ visibility: hidden !important;
+}
+
+.modal-content {
+ position: relative;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
+ width: 90%;
+ max-width: 1200px;
+ max-height: 90vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ z-index: 10;
+ margin: auto;
+}
+
+.modal-header {
+ background: linear-gradient(135deg, #3b82f6, #6366f1);
+ color: white;
+ padding: 24px 32px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.modal-header h2 {
+ margin: 0;
+ font-size: 24px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.modal-close {
+ background: rgba(255, 255, 255, 0.2);
+ border: none;
+ color: white;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ font-size: 18px;
+}
+
+.modal-close:hover {
+ background: rgba(255, 255, 255, 0.3);
+ transform: scale(1.1);
+}
+
+.modal-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-8);
+}
+
+.modal-footer {
+ background: var(--gray-50);
+ padding: var(--space-6) var(--space-8);
+ border-top: 1px solid var(--gray-200);
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--space-4);
+}
+
+/* Detail Sections */
+.detail-section {
+ margin-bottom: var(--space-10);
+}
+
+.detail-section h3 {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ margin-bottom: var(--space-6);
+ color: var(--gray-900);
+ font-size: var(--text-xl);
+ font-weight: 600;
+ padding-bottom: var(--space-3);
+ border-bottom: 2px solid var(--gray-100);
+}
+
+.detail-section h3 i {
+ color: var(--primary);
+ font-size: var(--text-xl);
+}
+
+/* Statistics Grid */
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: var(--space-6);
+ margin-bottom: var(--space-8);
+}
+
+.stat-card {
+ background: white;
+ border: 1px solid var(--gray-200);
+ border-radius: var(--radius-lg);
+ padding: var(--space-6);
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+ transition: var(--transition);
+ position: relative;
+ overflow: hidden;
+}
+
+.stat-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.stat-card.success {
+ border-left: 4px solid var(--success);
+ background: linear-gradient(135deg, #f0fdf4, #dcfce7);
+}
+
+.stat-card.warning {
+ border-left: 4px solid var(--warning);
+ background: linear-gradient(135deg, #fffbeb, #fef3c7);
+}
+
+.stat-card.info {
+ border-left: 4px solid var(--primary);
+ background: linear-gradient(135deg, var(--primary-light), #e0e7ff);
+}
+
+.stat-icon {
+ width: 3rem;
+ height: 3rem;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: var(--text-xl);
+ flex-shrink: 0;
+}
+
+.stat-card.success .stat-icon {
+ background: var(--success);
+ color: white;
+}
+
+.stat-card.warning .stat-icon {
+ background: var(--warning);
+ color: white;
+}
+
+.stat-card.info .stat-icon {
+ background: var(--primary);
+ color: white;
+}
+
+.stat-info {
+ flex: 1;
+}
+
+.stat-value {
+ font-size: var(--text-2xl);
+ font-weight: 700;
+ color: var(--gray-900);
+ margin-bottom: var(--space-1);
+}
+
+.stat-label {
+ font-size: var(--text-sm);
+ color: var(--gray-600);
+ font-weight: 500;
+}
+
+/* Metrics Table */
+.metrics-table {
+ background: white;
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+ border: 1px solid var(--gray-200);
+}
+
+.metrics-table table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.metrics-table th,
+.metrics-table td {
+ padding: var(--space-4) var(--space-5);
+ text-align: left;
+ border-bottom: 1px solid var(--gray-200);
+}
+
+.metrics-table th {
+ background: var(--gray-50);
+ font-weight: 600;
+ color: var(--gray-900);
+ font-size: var(--text-sm);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.metrics-table tbody tr:hover {
+ background: var(--gray-50);
+}
+
+.metric-name {
+ font-weight: 600;
+ color: var(--gray-900);
+}
+
+.metric-score {
+ width: 200px;
+}
+
+.score-bar {
+ position: relative;
+ background: var(--gray-200);
+ height: 1.5rem;
+ border-radius: var(--radius);
+ overflow: hidden;
+}
+
+.score-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--primary), #10b981);
+ border-radius: var(--radius);
+ transition: width 0.3s ease;
+ position: relative;
+}
+
+.score-text {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: var(--text-sm);
+ font-weight: 600;
+ color: white;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.grade {
+ display: inline-block;
+ padding: var(--space-1) var(--space-3);
+ border-radius: var(--radius);
+ font-weight: 700;
+ font-size: var(--text-sm);
+ text-transform: uppercase;
+}
+
+.grade.a\+ { background: #10b981; color: white; }
+.grade.a { background: #059669; color: white; }
+.grade.b { background: #f59e0b; color: white; }
+.grade.c { background: #f97316; color: white; }
+.grade.d { background: #ef4444; color: white; }
+.grade.f { background: #dc2626; color: white; }
+
+.metric-description {
+ color: var(--gray-600);
+ font-size: var(--text-sm);
+ line-height: 1.4;
+}
+
+/* Cost Analysis */
+.cost-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: var(--space-6);
+}
+
+.cost-card {
+ background: white;
+ border: 1px solid var(--gray-200);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+ transition: var(--transition);
+}
+
+.cost-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.cost-header {
+ background: linear-gradient(135deg, var(--gray-50), var(--gray-100));
+ padding: var(--space-4) var(--space-5);
+ border-bottom: 1px solid var(--gray-200);
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ font-weight: 600;
+ color: var(--gray-900);
+}
+
+.cost-header i {
+ color: var(--primary);
+ font-size: var(--text-lg);
+}
+
+.cost-details {
+ padding: var(--space-5);
+}
+
+.cost-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-3) 0;
+ border-bottom: 1px solid var(--gray-100);
+}
+
+.cost-row:last-child {
+ border-bottom: none;
+}
+
+.cost-amount {
+ color: var(--success);
+ font-size: var(--text-lg);
+}
+
+/* Performance Metrics */
+.performance-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: var(--space-6);
+}
+
+.perf-card {
+ background: white;
+ border: 1px solid var(--gray-200);
+ border-radius: var(--radius-lg);
+ padding: var(--space-6);
+ text-align: center;
+ transition: var(--transition);
+ position: relative;
+ overflow: hidden;
+}
+
+.perf-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.perf-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: linear-gradient(90deg, var(--primary), #10b981);
+}
+
+.perf-icon {
+ width: 3rem;
+ height: 3rem;
+ background: linear-gradient(135deg, var(--primary), #6366f1);
+ color: white;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto var(--space-4);
+ font-size: var(--text-xl);
+}
+
+.perf-value {
+ font-size: var(--text-xl);
+ font-weight: 700;
+ color: var(--gray-900);
+ margin-bottom: var(--space-2);
+}
+
+.perf-label {
+ font-size: var(--text-sm);
+ color: var(--gray-600);
+ font-weight: 500;
+}
+
+/* Configuration Summary */
+.config-summary {
+ background: white;
+ border: 1px solid var(--gray-200);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+}
+
+.config-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-4) var(--space-5);
+ border-bottom: 1px solid var(--gray-100);
+}
+
+.config-row:last-child {
+ border-bottom: none;
+}
+
+.config-label {
+ font-weight: 500;
+ color: var(--gray-700);
+}
+
+.config-value {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ font-weight: 600;
+}
+
+.config-value.enabled {
+ color: var(--success);
+}
+
+.config-value.disabled {
+ color: var(--gray-500);
+}
+
+/* Responsive Modal */
+@media (max-width: 768px) {
+ .modal-content {
+ width: 95%;
+ max-height: 95vh;
+ }
+
+ .modal-header,
+ .modal-footer {
+ padding: var(--space-4) var(--space-5);
+ }
+
+ .modal-body {
+ padding: var(--space-5);
+ }
+
+ .modal-header h2 {
+ font-size: var(--text-xl);
+ }
+
+ .stats-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .cost-grid,
+ .performance-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .metrics-table {
+ font-size: var(--text-sm);
+ }
+
+ .metrics-table th,
+ .metrics-table td {
+ padding: var(--space-3);
+ }
+
+ .score-bar {
+ height: 1.25rem;
+ }
+
+ .modal-footer {
+ flex-direction: column;
+ gap: var(--space-3);
+ }
+
+ .modal-footer .btn-primary,
+ .modal-footer .btn-secondary {
+ width: 100%;
+ justify-content: center;
+ }
+}
+
+@media (max-width: 480px) {
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .stat-card {
+ padding: var(--space-4);
+ }
+
+ .stat-icon {
+ width: 2.5rem;
+ height: 2.5rem;
+ font-size: var(--text-lg);
+ }
+
+ .stat-value {
+ font-size: var(--text-xl);
+ }
+
+ .config-row {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--space-2);
+ }
+}
+
+/* Detailed Results Table Styles */
+.results-table-container {
+ margin-top: var(--space-6);
+}
+
+.table-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-4);
+ background: var(--gray-50);
+ border-radius: var(--radius) var(--radius) 0 0;
+ border: 1px solid var(--gray-200);
+ font-size: var(--text-sm);
+ color: var(--gray-600);
+}
+
+.table-info span {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.table-wrapper {
+ max-height: 500px;
+ overflow: auto;
+ border: 1px solid var(--gray-200);
+ border-top: none;
+ border-radius: 0 0 var(--radius) var(--radius);
+ background: white;
+}
+
+.results-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: var(--text-sm);
+ min-width: 800px;
+}
+
+.results-table thead {
+ position: sticky;
+ top: 0;
+ background: var(--gray-100);
+ z-index: 10;
+}
+
+.results-table th {
+ padding: var(--space-4);
+ text-align: left;
+ font-weight: 600;
+ color: var(--gray-900);
+ border-bottom: 2px solid var(--gray-300);
+ border-right: 1px solid var(--gray-200);
+ white-space: nowrap;
+ position: relative;
+}
+
+.results-table th:last-child {
+ border-right: none;
+}
+
+.results-table td {
+ padding: var(--space-3) var(--space-4);
+ border-bottom: 1px solid var(--gray-100);
+ border-right: 1px solid var(--gray-100);
+ vertical-align: top;
+ max-width: 200px;
+ word-wrap: break-word;
+}
+
+.results-table td:last-child {
+ border-right: none;
+}
+
+.results-table tbody tr:hover {
+ background: var(--gray-50);
+}
+
+.results-table tbody tr.even {
+ background: rgba(249, 250, 251, 0.5);
+}
+
+.results-table tbody tr.odd {
+ background: white;
+}
+
+.results-table tbody tr.even:hover,
+.results-table tbody tr.odd:hover {
+ background: var(--primary-light);
+}
+
+/* Column-specific styles */
+.row-number {
+ background: var(--gray-50) !important;
+ font-weight: 600;
+ text-align: center;
+ width: 50px;
+ min-width: 50px;
+ max-width: 50px;
+ position: sticky;
+ left: 0;
+ z-index: 5;
+ border-right: 2px solid var(--gray-300) !important;
+}
+
+.metric-column {
+ text-align: center;
+ width: 120px;
+ min-width: 120px;
+ font-weight: 500;
+}
+
+.text-column {
+ max-width: 300px;
+ min-width: 200px;
+}
+
+.standard-column {
+ max-width: 150px;
+ min-width: 100px;
+}
+
+/* Cell value styles */
+.metric-score {
+ display: inline-block;
+ padding: var(--space-1) var(--space-3);
+ border-radius: var(--radius-sm);
+ font-weight: 600;
+ font-size: 0.85rem;
+}
+
+.metric-score.score-good {
+ background: #dcfce7;
+ color: #166534;
+ border: 1px solid #bbf7d0;
+}
+
+.metric-score.score-medium {
+ background: #fef3c7;
+ color: #92400e;
+ border: 1px solid #fde68a;
+}
+
+.metric-score.score-low {
+ background: #fee2e2;
+ color: #991b1b;
+ border: 1px solid #fecaca;
+}
+
+.na-value {
+ color: var(--gray-400);
+ font-style: italic;
+ font-size: 0.9rem;
+}
+
+.truncated-text {
+ cursor: help;
+ border-bottom: 1px dotted var(--gray-400);
+}
+
+.text-content {
+ line-height: 1.4;
+ word-break: break-word;
+}
+
+.number-value {
+ font-family: 'Courier New', monospace;
+ font-weight: 500;
+ color: var(--gray-700);
+}
+
+.table-note {
+ padding: var(--space-4);
+ background: var(--blue-50);
+ border: 1px solid var(--blue-200);
+ border-top: none;
+ border-radius: 0 0 var(--radius) var(--radius);
+ font-size: var(--text-sm);
+ color: var(--blue-700);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.no-data-message {
+ padding: var(--space-8);
+ text-align: center;
+ color: var(--gray-600);
+ background: var(--gray-50);
+ border: 2px dashed var(--gray-300);
+ border-radius: var(--radius);
+}
+
+.no-data-message i {
+ font-size: var(--text-2xl);
+ color: var(--gray-400);
+ margin-bottom: var(--space-4);
+}
+
+.no-data-message p {
+ margin-bottom: var(--space-4);
+ font-weight: 500;
+ color: var(--gray-700);
+}
+
+.no-data-message ul {
+ text-align: left;
+ display: inline-block;
+ margin: 0;
+ padding-left: var(--space-6);
+}
+
+.no-data-message li {
+ margin-bottom: var(--space-2);
+ color: var(--gray-600);
+}
+
+/* Responsive table styles */
+@media (max-width: 768px) {
+ .table-info {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--space-2);
+ }
+
+ .table-wrapper {
+ max-height: 400px;
+ }
+
+ .results-table {
+ font-size: 0.8rem;
+ min-width: 600px;
+ }
+
+ .results-table th,
+ .results-table td {
+ padding: var(--space-2) var(--space-3);
+ }
+
+ .text-column {
+ max-width: 200px;
+ min-width: 150px;
+ }
+
+ .metric-column {
+ width: 100px;
+ min-width: 100px;
+ }
+
+ .standard-column {
+ max-width: 120px;
+ min-width: 80px;
+ }
+}
+
+@media (max-width: 480px) {
+ .results-table {
+ font-size: 0.75rem;
+ min-width: 500px;
+ }
+
+ .results-table th,
+ .results-table td {
+ padding: var(--space-1) var(--space-2);
+ }
+
+ .metric-score {
+ padding: 2px var(--space-2);
+ font-size: 0.75rem;
+ }
+
+ .text-column {
+ max-width: 150px;
+ min-width: 120px;
+ }
+
+ .metric-column {
+ width: 80px;
+ min-width: 80px;
+ }
+
+ .row-number {
+ width: 35px;
+ min-width: 35px;
+ max-width: 35px;
+ }
+
+ .table-note {
+ font-size: 0.8rem;
+ padding: var(--space-3);
+ }
+}
+
+/* Multi-Sheet Interface Styles */
+.multi-sheet-container {
+ background: white;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+}
+
+.sheet-tabs {
+ background: var(--gray-50);
+ border-bottom: 2px solid var(--gray-200);
+}
+
+.tabs-header {
+ padding: var(--space-4) var(--space-6);
+ border-bottom: 1px solid var(--gray-200);
+}
+
+.tabs-title {
+ font-size: var(--text-lg);
+ font-weight: 600;
+ color: var(--gray-800);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.tabs-title i {
+ color: var(--primary);
+}
+
+.tabs-nav {
+ display: flex;
+ overflow-x: auto;
+ padding: 0 var(--space-6);
+ gap: var(--space-1);
+}
+
+.tab-btn {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: var(--space-3) var(--space-4);
+ border: none;
+ background: none;
+ color: var(--gray-600);
+ font-size: var(--text-sm);
+ font-weight: 500;
+ cursor: pointer;
+ border-radius: 8px 8px 0 0;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+ border-bottom: 3px solid transparent;
+}
+
+.tab-btn:hover {
+ background: white;
+ color: var(--gray-800);
+}
+
+.tab-btn.active {
+ background: white;
+ color: var(--primary);
+ border-bottom-color: var(--primary);
+ font-weight: 600;
+}
+
+.tab-btn i {
+ font-size: 0.875rem;
+}
+
+.sheet-count {
+ background: var(--gray-200);
+ color: var(--gray-600);
+ padding: 1px 6px;
+ border-radius: 10px;
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.tab-btn.active .sheet-count {
+ background: var(--primary-light);
+ color: var(--primary);
+}
+
+.sheet-contents {
+ position: relative;
+}
+
+.sheet-content {
+ display: none;
+ padding: var(--space-6);
+}
+
+.sheet-content.active {
+ display: block;
+}
+
+.sheet-title {
+ margin-bottom: var(--space-4);
+ padding-bottom: var(--space-3);
+ border-bottom: 2px solid var(--gray-200);
+}
+
+.sheet-title h4 {
+ font-size: var(--text-xl);
+ color: var(--gray-800);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.sheet-title h4 i {
+ color: var(--primary);
+}
+
+/* Enhanced table styling for multi-sheet */
+.multi-sheet-container .results-table-container {
+ background: white;
+ border: none;
+ box-shadow: none;
+}
+
+.multi-sheet-container .table-info {
+ background: var(--gray-50);
+ margin: 0 -var(--space-6) var(--space-4) -var(--space-6);
+ padding: var(--space-3) var(--space-6);
+}
+
+/* LLM Metrics Charts Styles */
+.llm-metrics-charts {
+ background: var(--gray-50);
+ border-radius: 8px;
+ padding: var(--space-6);
+ margin-bottom: var(--space-6);
+ border: 1px solid var(--gray-200);
+}
+
+.charts-header {
+ text-align: center;
+ margin-bottom: var(--space-6);
+}
+
+.charts-header h5 {
+ font-size: var(--text-xl);
+ color: var(--gray-800);
+ margin-bottom: var(--space-2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-2);
+}
+
+.charts-header h5 i {
+ color: var(--primary);
+}
+
+.chart-info {
+ color: var(--gray-600);
+ font-size: var(--text-sm);
+ font-style: italic;
+}
+
+.charts-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: var(--space-6);
+ align-items: start;
+}
+
+.chart-container {
+ background: white;
+ border-radius: 8px;
+ padding: var(--space-4);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--gray-200);
+ transition: all 0.2s ease;
+}
+
+.chart-container:hover {
+ box-shadow: var(--shadow-md);
+ transform: translateY(-2px);
+}
+
+.chart-container h6 {
+ font-size: var(--text-base);
+ color: var(--gray-800);
+ margin-bottom: var(--space-4);
+ text-align: center;
+ font-weight: 600;
+ padding-bottom: var(--space-2);
+ border-bottom: 2px solid var(--gray-200);
+}
+
+.chart-container canvas {
+ max-height: 250px;
+ width: 100%;
+}
+
+.comparison-chart {
+ grid-column: 1 / -1;
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.comparison-chart canvas {
+ max-height: 300px;
+}
+
+/* Chart animations */
+@keyframes fadeInChart {
+ from {
+ opacity: 0;
+ transform: scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.chart-container {
+ animation: fadeInChart 0.5s ease-out;
+}
+
+/* Responsive design for charts */
+@media (max-width: 1024px) {
+ .charts-grid {
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: var(--space-4);
+ }
+
+ .llm-metrics-charts {
+ padding: var(--space-4);
+ }
+}
+
+@media (max-width: 768px) {
+ .charts-grid {
+ grid-template-columns: 1fr;
+ gap: var(--space-4);
+ }
+
+ .chart-container canvas {
+ max-height: 200px;
+ }
+
+ .comparison-chart canvas {
+ max-height: 250px;
+ }
+
+ .charts-header h5 {
+ font-size: var(--text-lg);
+ }
+
+ .llm-metrics-charts {
+ padding: var(--space-3);
+ margin-bottom: var(--space-4);
+ }
+}
+
+/* Enhanced table info styling when charts are present */
+.llm-metrics-charts + .table-info {
+ background: white;
+ border: 1px solid var(--gray-200);
+ border-radius: 6px;
+ margin-bottom: var(--space-4);
+}
+
+/* Responsive design for tabs */
+@media (max-width: 768px) {
+ .tabs-nav {
+ padding: 0 var(--space-4);
+ }
+
+ .tab-btn {
+ padding: var(--space-2) var(--space-3);
+ font-size: 0.8rem;
+ }
+
+ .sheet-content {
+ padding: var(--space-4);
+ }
+
+ .multi-sheet-container .table-info {
+ margin: 0 -var(--space-4) var(--space-4) -var(--space-4);
+ padding: var(--space-3) var(--space-4);
+ }
+}
+
+/* Session Management Styles */
+.session-indicator {
+ display: flex;
+ align-items: center;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ padding: 8px 12px;
+ font-size: 0.85em;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.session-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: white;
+}
+
+.session-info i {
+ font-size: 1.1em;
+ opacity: 0.9;
+}
+
+.session-status {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.8em;
+ font-weight: 500;
+}
+
+.session-status.active {
+ background: rgba(34, 197, 94, 0.2);
+ color: #22c55e;
+ border: 1px solid rgba(34, 197, 94, 0.3);
+}
+
+.session-status.active i {
+ color: #22c55e;
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+/* Update header layout to accommodate session indicator */
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+/* Multi-user isolation warning styles */
+.isolation-notice {
+ background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
+ border: 1px solid #d1d5db;
+ border-radius: 8px;
+ padding: 12px 16px;
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 0.9em;
+ color: #374151;
+}
+
+.isolation-notice i {
+ color: #6366f1;
+ font-size: 1.1em;
+}
+
+.isolation-notice .notice-text {
+ flex: 1;
+}
+
+.isolation-notice .notice-text strong {
+ color: #1f2937;
+}
+
+/* User separation info in download section */
+.user-separation-info {
+ background: rgba(99, 102, 241, 0.1);
+ border: 1px solid rgba(99, 102, 241, 0.2);
+ border-radius: 6px;
+ padding: 10px 12px;
+ margin-top: 8px;
+ font-size: 0.85em;
+ color: #6366f1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.user-separation-info i {
+ opacity: 0.8;
+}
+
+/* Session status in results */
+.session-results-info {
+ background: rgba(34, 197, 94, 0.1);
+ border: 1px solid rgba(34, 197, 94, 0.2);
+ border-radius: 6px;
+ padding: 8px 12px;
+ margin-bottom: 16px;
+ font-size: 0.85em;
+ color: #22c55e;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.session-results-info i {
+ opacity: 0.9;
+}
+
+/* Responsive session indicator */
+@media (max-width: 768px) {
+ .header-content {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .session-indicator {
+ justify-content: center;
+ order: -1;
+ }
+
+ .session-info {
+ justify-content: center;
+ font-size: 0.8em;
+ }
+}
+
+/* Session expired warning */
+.session-expired-warning {
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ border-radius: 8px;
+ padding: 12px 16px;
+ margin: 1rem 0;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ color: #ef4444;
+ font-size: 0.9em;
+}
+
+.session-expired-warning i {
+ font-size: 1.2em;
+}
+
+.session-expired-warning .warning-text {
+ flex: 1;
+}
+
+.session-expired-warning .refresh-btn {
+ background: #ef4444;
+ color: white;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.85em;
+ transition: background-color 0.2s;
+}
+
+.session-expired-warning .refresh-btn:hover {
+ background: #dc2626;
+}
+
+/* Comprehensive Analysis Dashboard Styles */
+.comprehensive-analysis-dashboard {
+ background: #fff;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
+ border: 1px solid #e5e7eb;
+}
+
+.dashboard-header {
+ margin-bottom: 24px;
+ padding-bottom: 16px;
+ border-bottom: 2px solid #f3f4f6;
+}
+
+.dashboard-header h5 {
+ margin: 0 0 8px 0;
+ color: #1f2937;
+ font-size: 1.25rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.dashboard-stats {
+ display: flex;
+ gap: 24px;
+ flex-wrap: wrap;
+ margin-top: 12px;
+}
+
+.dashboard-stats .stat-item {
+ background: rgba(59, 130, 246, 0.1);
+ padding: 6px 12px;
+ border-radius: 6px;
+ font-size: 0.85rem;
+ color: #3b82f6;
+ font-weight: 500;
+}
+
+/* Analysis Tabs */
+.analysis-tabs {
+ display: flex;
+ background: #f8fafc;
+ border-radius: 8px;
+ padding: 4px;
+ margin-bottom: 24px;
+ overflow-x: auto;
+ gap: 2px;
+}
+
+.tab-button {
+ flex: 1;
+ min-width: 120px;
+ padding: 12px 16px;
+ background: transparent;
+ border: none;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ font-weight: 500;
+ color: #64748b;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+}
+
+.tab-button:hover {
+ background: rgba(59, 130, 246, 0.1);
+ color: #3b82f6;
+}
+
+.tab-button.active {
+ background: #3b82f6;
+ color: white;
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
+}
+
+/* Tab Content */
+.tab-content {
+ display: none;
+ animation: fadeIn 0.3s ease-in-out;
+}
+
+.tab-content.active {
+ display: block;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Grid Layouts */
+.overview-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr 300px;
+ gap: 20px;
+ align-items: start;
+}
+
+
+
+.correlations-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+}
+
+.performance-grid {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ gap: 20px;
+}
+
+.insights-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 20px;
+}
+
+/* Chart Cards */
+.chart-card {
+ background: #fafbfc;
+ border: 1px solid #e2e8f0;
+ border-radius: 8px;
+ padding: 20px;
+ position: relative;
+ min-height: 300px;
+}
+
+.chart-card h6 {
+ margin: 0 0 16px 0;
+ color: #374151;
+ font-size: 1rem;
+ font-weight: 600;
+ text-align: center;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.chart-card canvas {
+ max-height: 280px;
+}
+
+/* Dynamic Recommendations Styling */
+.recommendation-header {
+ margin-bottom: 12px;
+ padding: 8px;
+ background-color: #f8f9fa;
+ border-radius: 4px;
+ border-left: 3px solid #6366f1;
+}
+
+.recommendation-header small {
+ color: #6b7280;
+ font-weight: 500;
+}
+
+.recommendation-content {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+}
+
+.recommendation-icon {
+ font-size: 16px;
+ margin-top: 2px;
+ flex-shrink: 0;
+}
+
+.recommendation-text {
+ flex: 1;
+ line-height: 1.5;
+}
+
+.recommendation-success {
+ background-color: #f0fdf4 !important;
+ border-left-color: #10b981 !important;
+}
+
+.recommendation-critical {
+ background-color: #fef2f2 !important;
+ border-left-color: #ef4444 !important;
+}
+
+.recommendation-action {
+ background-color: #fffbeb !important;
+ border-left-color: #f59e0b !important;
+}
+
+.recommendation-normal {
+ background-color: #f8fafc !important;
+ border-left-color: #64748b !important;
+}
+
+.recommendation-context {
+ margin-top: 12px;
+ padding: 8px;
+ background-color: #f1f5f9;
+ border-radius: 4px;
+ border: 1px solid #e2e8f0;
+}
+
+.recommendation-context small {
+ color: #475569;
+ font-style: italic;
+}
+
+/* Recommendation action buttons removed */
+
+.insight-item.recommendation {
+ margin-bottom: 8px;
+ padding: 12px;
+ border-left: 3px solid #e5e7eb;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+}
+
+.insight-item.recommendation:hover {
+ transform: translateX(2px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* Statistics Cards */
+.metrics-summary-card {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border-radius: 8px;
+ padding: 20px;
+}
+
+.metrics-summary-card h6 {
+ margin: 0 0 16px 0;
+ color: white;
+ font-size: 1rem;
+ font-weight: 600;
+ text-align: center;
+ padding-bottom: 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 12px;
+}
+
+.stat-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.stat-item:last-child {
+ border-bottom: none;
+}
+
+.stat-label {
+ font-size: 0.85rem;
+ opacity: 0.9;
+}
+
+.stat-value {
+ font-weight: 600;
+ font-size: 0.9rem;
+}
+
+.stat-value.excellent {
+ color: #10b981;
+}
+
+.stat-value.good {
+ color: #f59e0b;
+}
+
+.stat-value.poor {
+ color: #ef4444;
+}
+
+/* Insights Cards */
+.insights-card, .outliers-card, .insight-card {
+ background: #fafbfc;
+ border: 1px solid #e2e8f0;
+ border-radius: 8px;
+ padding: 20px;
+}
+
+.insights-card h6, .outliers-card h6, .insight-card h6 {
+ margin: 0 0 16px 0;
+ color: #374151;
+ font-size: 1rem;
+ font-weight: 600;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.insights-content, .outliers-content {
+ font-size: 0.9rem;
+ line-height: 1.6;
+ color: #4b5563;
+}
+
+.insight-item {
+ padding: 8px 12px;
+ margin: 8px 0;
+ border-radius: 6px;
+ border-left: 4px solid #3b82f6;
+ background: rgba(59, 130, 246, 0.05);
+}
+
+.insight-item.strength {
+ border-left-color: #10b981;
+ background: rgba(16, 185, 129, 0.05);
+}
+
+.insight-item.weakness {
+ border-left-color: #ef4444;
+ background: rgba(239, 68, 68, 0.05);
+}
+
+.insight-item.recommendation {
+ border-left-color: #f59e0b;
+ background: rgba(245, 158, 11, 0.05);
+}
+
+.insight-item.retrieval {
+ border-left-color: #8b5cf6;
+ background: rgba(139, 92, 246, 0.05);
+}
+
+.insight-item.efficiency {
+ border-left-color: #06b6d4;
+ background: rgba(6, 182, 212, 0.05);
+}
+
+.insight-item.utilization {
+ border-left-color: #f97316;
+ background: rgba(249, 115, 22, 0.05);
+}
+
+/* Retrieval Quality Tab Styles */
+.retrieval-quality-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
+ gap: 24px;
+ margin-top: 16px;
+}
+
+.retrieval-overview-summary {
+ background: white;
+ border-radius: 8px;
+ padding: 20px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.retrieval-metrics-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+ margin-bottom: 20px;
+}
+
+.metric-card {
+ background: #f8fafc;
+ border-radius: 8px;
+ padding: 16px;
+ text-align: center;
+ border: 2px solid transparent;
+ transition: all 0.3s ease;
+}
+
+.metric-card.excellent {
+ border-color: #10b981;
+ background: rgba(16, 185, 129, 0.05);
+}
+
+.metric-card.good {
+ border-color: #059669;
+ background: rgba(5, 150, 105, 0.05);
+}
+
+.metric-card.fair {
+ border-color: #f59e0b;
+ background: rgba(245, 158, 11, 0.05);
+}
+
+.metric-card.poor {
+ border-color: #ef4444;
+ background: rgba(239, 68, 68, 0.05);
+}
+
+.metric-card.info {
+ border-color: #3b82f6;
+ background: rgba(59, 130, 246, 0.05);
+}
+
+.metric-icon {
+ font-size: 1.5em;
+ margin-bottom: 8px;
+}
+
+.metric-label {
+ font-size: 0.85rem;
+ color: #6b7280;
+ margin-bottom: 8px;
+ font-weight: 500;
+}
+
+.metric-value {
+ font-size: 1.5em;
+ font-weight: bold;
+ color: #374151;
+ margin-bottom: 4px;
+}
+
+.metric-status {
+ font-size: 0.75rem;
+ color: #6b7280;
+ font-weight: 500;
+}
+
+.retrieval-status {
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid #e5e7eb;
+}
+
+.retrieval-help {
+ margin-top: 12px;
+}
+
+.retrieval-help details {
+ background: #f1f5f9;
+ border-radius: 6px;
+ padding: 8px;
+}
+
+.retrieval-help summary {
+ cursor: pointer;
+ font-weight: 500;
+ color: #3b82f6;
+ padding: 4px 0;
+}
+
+.retrieval-help summary:hover {
+ color: #2563eb;
+}
+
+.retrieval-help .help-content {
+ margin-top: 8px;
+ padding: 12px;
+ background: white;
+ border-radius: 4px;
+ border-left: 3px solid #3b82f6;
+}
+
+.retrieval-help .help-content ol {
+ margin: 8px 0;
+ padding-left: 20px;
+}
+
+.retrieval-help .help-content li {
+ margin: 4px 0;
+ color: #374151;
+}
+
+.retrieval-help .help-content code {
+ display: block;
+ background: #f3f4f6;
+ padding: 8px;
+ border-radius: 4px;
+ font-family: 'Courier New', monospace;
+ font-size: 0.85rem;
+ color: #1f2937;
+ margin-top: 8px;
+ word-break: break-all;
+}
+
+.status-indicator {
+ display: flex;
+ align-items: center;
+ padding: 12px;
+ border-radius: 6px;
+ font-size: 0.9rem;
+}
+
+.status-indicator.success {
+ background: rgba(16, 185, 129, 0.1);
+ color: #065f46;
+ border: 1px solid rgba(16, 185, 129, 0.2);
+}
+
+.status-indicator.warning {
+ background: rgba(245, 158, 11, 0.1);
+ color: #92400e;
+ border: 1px solid rgba(245, 158, 11, 0.2);
+}
+
+.status-icon {
+ margin-right: 8px;
+ font-size: 1.1em;
+}
+
+.status-text {
+ font-weight: 500;
+}
+
+.retrieval-insights-content,
+.efficiency-insights-content,
+.utilization-insights-content {
+ font-size: 0.9rem;
+ line-height: 1.6;
+ color: #4b5563;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.retrieval-analysis-section,
+.efficiency-analysis-section {
+ margin-top: 16px;
+}
+
+.metric-analysis {
+ background: #f8fafc;
+ border-radius: 6px;
+ padding: 16px;
+ margin-top: 16px;
+ border-left: 4px solid #3b82f6;
+}
+
+.metric-analysis h6 {
+ margin: 0 0 12px 0;
+ color: #374151;
+ font-size: 0.9rem;
+ font-weight: 600;
+}
+
+.metric-details {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.metric-detail-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 0;
+}
+
+.detail-label {
+ font-size: 0.85rem;
+ color: #6b7280;
+ font-weight: 500;
+}
+
+.detail-value {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: #374151;
+}
+
+.detail-value.excellent {
+ color: #10b981;
+}
+
+.detail-value.good {
+ color: #059669;
+}
+
+.detail-value.fair {
+ color: #f59e0b;
+}
+
+.detail-value.poor {
+ color: #ef4444;
+}
+
+/* Chunk Utilization Analysis Styles */
+.utilization-analysis-section {
+ margin-top: 16px;
+}
+
+.chunk-utilization-details {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.chunk-detail-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 12px;
+ background: #f8fafc;
+ border-radius: 6px;
+ border-left: 3px solid #3b82f6;
+}
+
+.chunk-label {
+ font-size: 0.85rem;
+ color: #6b7280;
+ font-weight: 500;
+ flex: 1;
+}
+
+.chunk-value {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: #374151;
+ margin: 0 12px;
+}
+
+.chunk-assessment {
+ font-size: 0.8rem;
+ color: #6b7280;
+ font-style: italic;
+ text-align: right;
+ flex: 1;
+}
+
+.utilization-efficiency {
+ margin-top: 16px;
+ padding: 16px;
+ background: #f0f9ff;
+ border-radius: 6px;
+ border: 1px solid #0ea5e9;
+}
+
+.efficiency-header {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: #0c4a6e;
+ margin-bottom: 12px;
+}
+
+.efficiency-metrics {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.efficiency-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 0;
+}
+
+.efficiency-label {
+ font-size: 0.85rem;
+ color: #0c4a6e;
+ font-weight: 500;
+}
+
+.efficiency-value {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: #0c4a6e;
+}
+
+.efficiency-value.excellent {
+ color: #10b981;
+}
+
+.efficiency-value.good {
+ color: #059669;
+}
+
+.efficiency-value.fair {
+ color: #f59e0b;
+}
+
+.efficiency-value.poor {
+ color: #ef4444;
+}
+
+.no-chunk-data {
+ text-align: center;
+ padding: 40px 20px;
+ color: #6b7280;
+}
+
+.no-data-icon {
+ font-size: 3em;
+ margin-bottom: 16px;
+ opacity: 0.5;
+}
+
+.no-data-text {
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+
+.no-data-subtext {
+ font-size: 0.9rem;
+ opacity: 0.8;
+}
+
+/* Chunk Tracking Guidance Styles */
+.chunk-tracking-guidance {
+ color: #374151;
+}
+
+.chunk-tracking-guidance h4 {
+ margin: 0 0 16px 0;
+ color: #1f2937;
+ font-size: 1.1rem;
+ font-weight: 600;
+}
+
+.chunk-tracking-guidance h5 {
+ margin: 16px 0 8px 0;
+ color: #374151;
+ font-size: 0.95rem;
+ font-weight: 600;
+}
+
+.guidance-section {
+ margin: 16px 0;
+ padding: 12px;
+ background: #f8fafc;
+ border-radius: 6px;
+ border-left: 3px solid #3b82f6;
+}
+
+.guidance-section ul, .guidance-section ol {
+ margin: 8px 0;
+ padding-left: 20px;
+}
+
+.guidance-section li {
+ margin: 4px 0;
+ line-height: 1.5;
+}
+
+.test-button {
+ background: #3b82f6;
+ color: white;
+ border: none;
+ padding: 10px 16px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ font-weight: 500;
+ margin-top: 12px;
+ transition: background-color 0.2s;
+}
+
+.test-button:hover {
+ background: #2563eb;
+}
+
+.notification {
+ animation: slideIn 0.3s ease-out;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+/* Histograms Grid */
+.histograms-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 16px;
+ margin-top: 16px;
+}
+
+.histogram-item {
+ background: white;
+ border: 1px solid #e5e7eb;
+ border-radius: 6px;
+ padding: 16px;
+ text-align: center;
+}
+
+.histogram-item h7 {
+ display: block;
+ font-weight: 600;
+ color: #374151;
+ margin-bottom: 12px;
+ font-size: 0.85rem;
+}
+
+.histogram-item canvas {
+ max-height: 150px;
+}
+
+/* Performance Analysis */
+.performance-summary {
+ background: #f8fafc;
+ border-radius: 6px;
+ padding: 16px;
+ margin-bottom: 16px;
+}
+
+.performance-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 0;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.performance-item:last-child {
+ border-bottom: none;
+}
+
+.query-text {
+ flex: 1;
+ font-size: 0.85rem;
+ color: #4b5563;
+ margin-right: 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.performance-score {
+ font-weight: 600;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.8rem;
+}
+
+.performance-score.high {
+ background: #dcfce7;
+ color: #166534;
+}
+
+.performance-score.medium {
+ background: #fef3c7;
+ color: #92400e;
+}
+
+.performance-score.low {
+ background: #fee2e2;
+ color: #991b1b;
+}
+
+/* Correlation Matrix Styles */
+.correlation-cell {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ margin: 1px;
+ border-radius: 2px;
+ text-align: center;
+ font-size: 10px;
+ line-height: 20px;
+ color: white;
+ font-weight: bold;
+}
+
+.correlation-legend {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ margin-top: 12px;
+ font-size: 0.8rem;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.legend-color {
+ width: 12px;
+ height: 12px;
+ border-radius: 2px;
+}
+
+/* Responsive Design */
+@media (max-width: 1200px) {
+ .overview-grid {
+ grid-template-columns: 1fr;
+ gap: 16px;
+ }
+
+ .correlations-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .performance-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 768px) {
+ .comprehensive-analysis-dashboard {
+ padding: 16px;
+ }
+
+ .analysis-tabs {
+ flex-direction: column;
+ }
+
+ .tab-button {
+ min-width: auto;
+ }
+
+
+
+ .dashboard-stats {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .histograms-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* Loading States */
+.chart-loading {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 200px;
+ color: #64748b;
+ font-size: 0.9rem;
+}
+
+.chart-loading::before {
+ content: "๐";
+ margin-right: 8px;
+ animation: pulse 2s infinite;
+}
+
+/* Tooltips and Interactive Elements */
+.metric-tooltip {
+ background: rgba(0, 0, 0, 0.8);
+ color: white;
+ padding: 8px 12px;
+ border-radius: 6px;
+ font-size: 0.8rem;
+ pointer-events: none;
+ z-index: 1000;
+}
+
+/* Chart Container Improvements */
+.chart-container {
+ position: relative;
+ background: white;
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+/* Animation Classes */
+.slide-in {
+ animation: slideIn 0.5s ease-out;
+}
+
+@keyframes slideIn {
+ from { transform: translateX(-100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+}
+
+/* ============================================================================
+ 8. RESPONSIVE DESIGN
+ ============================================================================ */
+
+/* ----------------------------------------
+ Mobile First Approach
+ ---------------------------------------- */
+
+/* Mobile Styles (default) */
+@media (max-width: 767px) {
+ .container {
+ padding-left: var(--space-4);
+ padding-right: var(--space-4);
+ }
+
+ .header {
+ padding: var(--space-6);
+ margin-bottom: var(--space-6);
+ }
+
+ .header-content {
+ flex-direction: column;
+ gap: var(--space-4);
+ text-align: center;
+ }
+
+ .logo-text h1 {
+ font-size: var(--text-3xl);
+ }
+
+ .logo-text .subtitle {
+ font-size: var(--text-base);
+ }
+
+ .header-right {
+ flex-direction: column;
+ gap: var(--space-3);
+ }
+
+ .version-badge,
+ .status-indicator {
+ font-size: var(--text-sm);
+ padding: var(--space-1) var(--space-3);
+ }
+
+ .card-content {
+ padding: var(--space-4);
+ }
+
+ .card-header {
+ padding: var(--space-4);
+ flex-direction: column;
+ align-items: flex-start;
+ text-align: left;
+ }
+
+ .form-row {
+ grid-template-columns: 1fr;
+ gap: var(--space-4);
+ }
+
+ .btn {
+ width: 100%;
+ justify-content: center;
+ }
+
+ .upload-area {
+ padding: var(--space-8);
+ }
+
+ .upload-content i {
+ font-size: var(--text-4xl);
+ }
+
+ .upload-content h3 {
+ font-size: var(--text-lg);
+ }
+
+ .supported-formats {
+ flex-direction: column;
+ align-items: center;
+ }
+}
+
+/* Tablet Styles */
+@media (min-width: 768px) and (max-width: 1023px) {
+ .container {
+ padding-left: var(--space-6);
+ padding-right: var(--space-6);
+ }
+
+ .header-content {
+ flex-direction: row;
+ gap: var(--space-6);
+ }
+
+ .form-row {
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-4);
+ }
+
+ .card-header {
+ flex-direction: row;
+ align-items: center;
+ }
+}
+
+/* Desktop Styles */
+@media (min-width: 1024px) {
+ .container {
+ padding-left: var(--space-8);
+ padding-right: var(--space-8);
+ }
+
+ .form-row {
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-6);
+ }
+
+ /* Enhanced hover effects on desktop */
+ .card:hover {
+ transform: translateY(-2px);
+ }
+
+ .btn:hover:not(:disabled) {
+ transform: translateY(-1px);
+ }
+}
+
+/* Large Desktop Styles */
+@media (min-width: 1200px) {
+ .container-lg {
+ max-width: 1440px;
+ }
+
+ .form-row {
+ grid-template-columns: 1fr 1fr 1fr;
+ }
+}
+
+/* Print Styles */
+@media print {
+ .header {
+ background: none !important;
+ color: black !important;
+ box-shadow: none !important;
+ }
+
+ .btn,
+ .upload-area {
+ display: none !important;
+ }
+
+ .card {
+ box-shadow: none !important;
+ border: 1px solid #ccc !important;
+ break-inside: avoid;
+ }
+
+ .container {
+ max-width: none !important;
+ padding: 0 !important;
+ }
+}
+
+/* ============================================================================
+ 9. ANIMATIONS & TRANSITIONS
+ ============================================================================ */
+
+/* ----------------------------------------
+ Keyframe Animations
+ ---------------------------------------- */
+
+@keyframes pulse {
+ 0% { box-shadow: 0 0 8px rgba(16, 185, 129, 0.6); }
+ 50% { box-shadow: 0 0 16px rgba(16, 185, 129, 0.8); }
+ 100% { box-shadow: 0 0 8px rgba(16, 185, 129, 0.6); }
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes slideIn {
+ from { transform: translateX(-100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+}
+
+@keyframes bounceIn {
+ 0% { transform: scale(0.3); opacity: 0; }
+ 50% { transform: scale(1.05); }
+ 70% { transform: scale(0.9); }
+ 100% { transform: scale(1); opacity: 1; }
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+@keyframes ping {
+ 75%, 100% { transform: scale(2); opacity: 0; }
+}
+
+@keyframes shake {
+ 0%, 100% { transform: translateX(0); }
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
+ 20%, 40%, 60%, 80% { transform: translateX(10px); }
+}
+
+@keyframes rubber-band {
+ from { transform: scale3d(1, 1, 1); }
+ 30% { transform: scale3d(1.25, 0.75, 1); }
+ 40% { transform: scale3d(0.75, 1.25, 1); }
+ 50% { transform: scale3d(1.15, 0.85, 1); }
+ 65% { transform: scale3d(0.95, 1.05, 1); }
+ 75% { transform: scale3d(1.05, 0.95, 1); }
+ to { transform: scale3d(1, 1, 1); }
+}
+
+/* ----------------------------------------
+ Animation Classes
+ ---------------------------------------- */
+
+.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
+.animate-ping { animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; }
+.animate-spin { animation: spin 1s linear infinite; }
+.animate-bounce { animation: bounce 1s infinite; }
+.animate-fade-in { animation: fadeIn 0.5s ease-out; }
+.animate-slide-in { animation: slideIn 0.5s ease-out; }
+.animate-bounce-in { animation: bounceIn 0.6s ease-out; }
+.animate-shake { animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; }
+.animate-rubber-band { animation: rubber-band 1s both; }
+
+/* ----------------------------------------
+ Loading States
+ ---------------------------------------- */
+
+.loading {
+ position: relative;
+ pointer-events: none;
+ opacity: 0.7;
+}
+
+.loading::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 20px;
+ height: 20px;
+ margin: -10px 0 0 -10px;
+ border: 2px solid var(--gray-300);
+ border-top-color: var(--primary-500);
+ border-radius: var(--radius-full);
+ animation: spin 1s linear infinite;
+}
+
+/* ----------------------------------------
+ Reduced Motion Support
+ ---------------------------------------- */
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+
+ .animate-pulse,
+ .animate-ping,
+ .animate-spin,
+ .animate-bounce {
+ animation: none !important;
+ }
+}
+
+/* ============================================================================
+ 10. ACCESSIBILITY & FOCUS STATES
+ ============================================================================ */
+
+/* Focus indicators for keyboard navigation */
+.btn:focus-visible,
+.form-control:focus-visible,
+.upload-area:focus-visible {
+ outline: 2px solid var(--primary-500);
+ outline-offset: 2px;
+}
+
+/* Screen reader only content */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ .card {
+ border: 2px solid var(--gray-900);
+ }
+
+ .btn {
+ border-width: 2px;
+ }
+
+ .form-control {
+ border-width: 2px;
+ }
+}
+
+/* ============================================================================
+ 11. QUALITY ANALYSIS STYLES
+ ============================================================================ */
+
+.quality-analysis-container {
+ margin-top: 2rem;
+ padding: 1.5rem;
+ background: var(--white);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--gray-200);
+ box-shadow: var(--shadow-sm);
+}
+
+.quality-analysis-container h4 {
+ color: var(--gray-900);
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin-bottom: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.quality-summary {
+ margin-bottom: 2rem;
+}
+
+.quality-summary-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.quality-summary-item {
+ background: var(--gray-50);
+ padding: 1.5rem;
+ border-radius: var(--radius-lg);
+ text-align: center;
+ border: 1px solid var(--gray-200);
+ transition: all 0.2s ease;
+}
+
+.quality-summary-item:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+ border-color: var(--primary-300);
+}
+
+.quality-summary-item i {
+ font-size: 2rem;
+ color: var(--primary-500);
+ margin-bottom: 0.75rem;
+}
+
+.quality-summary-item h3 {
+ font-size: 2rem;
+ font-weight: 700;
+ color: var(--gray-900);
+ margin: 0.5rem 0;
+}
+
+.quality-summary-item p {
+ color: var(--gray-600);
+ font-size: 0.875rem;
+ font-weight: 500;
+ margin: 0;
+}
+
+.category-distribution {
+ margin-bottom: 2rem;
+}
+
+.category-distribution h5 {
+ color: var(--gray-800);
+ font-size: 1.125rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.category-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 1rem;
+}
+
+.category-item {
+ background: var(--white);
+ padding: 1rem;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--gray-200);
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ transition: all 0.2s ease;
+}
+
+.category-item:hover {
+ border-color: var(--primary-300);
+ box-shadow: var(--shadow-sm);
+}
+
+.category-item i {
+ font-size: 1.25rem;
+ width: 1.5rem;
+ text-align: center;
+}
+
+.category-item.context-irrelevant i { color: var(--orange-500); }
+.category-item.ground-truth-invalid i { color: var(--red-500); }
+.category-item.context-overload i { color: var(--purple-500); }
+.category-item.answer-generation-failure i { color: var(--yellow-500); }
+.category-item.mixed-issues i { color: var(--gray-500); }
+
+.category-info h6 {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--gray-900);
+ margin: 0 0 0.25rem 0;
+}
+
+.category-info p {
+ font-size: 0.75rem;
+ color: var(--gray-600);
+ margin: 0;
+}
+
+.recommendations {
+ margin-bottom: 2rem;
+}
+
+.recommendations h5 {
+ color: var(--gray-800);
+ font-size: 1.125rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.recommendations-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.recommendation-item {
+ background: var(--blue-50);
+ padding: 1rem;
+ border-radius: var(--radius-md);
+ border-left: 4px solid var(--blue-500);
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+}
+
+.recommendation-item i {
+ color: var(--blue-500);
+ font-size: 1rem;
+ margin-top: 0.125rem;
+}
+
+.recommendation-item p {
+ color: var(--gray-800);
+ font-size: 0.875rem;
+ margin: 0;
+ line-height: 1.5;
+}
+
+.detailed-analysis {
+ margin-bottom: 1rem;
+}
+
+.detailed-analysis h5 {
+ color: var(--gray-800);
+ font-size: 1.125rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.table-container {
+ overflow-x: auto;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--gray-200);
+}
+
+.analysis-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.875rem;
+}
+
+.analysis-table th {
+ background: var(--gray-50);
+ padding: 0.75rem;
+ text-align: left;
+ font-weight: 600;
+ color: var(--gray-700);
+ border-bottom: 1px solid var(--gray-200);
+}
+
+.analysis-table td {
+ padding: 0.75rem;
+ border-bottom: 1px solid var(--gray-100);
+ color: var(--gray-800);
+}
+
+.analysis-table tr:hover {
+ background: var(--gray-50);
+}
+
+.analysis-table th:first-child,
+.analysis-table td:first-child {
+ width: 25%;
+}
+
+.analysis-table th:nth-child(2),
+.analysis-table td:nth-child(2) {
+ width: 15%;
+}
+
+.analysis-table th:nth-child(3),
+.analysis-table td:nth-child(3) {
+ width: 10%;
+}
+
+.analysis-table th:nth-child(4),
+.analysis-table td:nth-child(4),
+.analysis-table th:nth-child(5),
+.analysis-table td:nth-child(5),
+.analysis-table th:nth-child(6),
+.analysis-table td:nth-child(6) {
+ width: 12%;
+}
+
+ .analysis-table th:last-child,
+ .analysis-table td:last-child {
+ width: 20%;
+ }
+
+ /* Category badges for the analysis table */
+ .category-badge {
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ border-radius: var(--radius-sm);
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.025em;
+ }
+
+ .category-badge.context_irrelevant {
+ background: var(--orange-100);
+ color: var(--orange-700);
+ border: 1px solid var(--orange-200);
+ }
+
+ .category-badge.ground_truth_invalid {
+ background: var(--red-100);
+ color: var(--red-700);
+ border: 1px solid var(--red-200);
+ }
+
+ .category-badge.context_overload {
+ background: var(--purple-100);
+ color: var(--purple-700);
+ border: 1px solid var(--purple-200);
+ }
+
+ .category-badge.answer_generation_failure {
+ background: var(--yellow-100);
+ color: var(--yellow-700);
+ border: 1px solid var(--yellow-200);
+ }
+
+ .category-badge.mixed_issues {
+ background: var(--gray-100);
+ color: var(--gray-700);
+ border: 1px solid var(--gray-200);
+ }
+
+/* Responsive adjustments for quality analysis */
+@media (max-width: 768px) {
+ .quality-summary-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .category-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .table-container {
+ font-size: 0.75rem;
+ }
+
+ .analysis-table th,
+ .analysis-table td {
+ padding: 0.5rem;
+ }
+}
\ No newline at end of file
diff --git a/Evaluation/RAG_Evaluator/src/utils/chunkStatistics.py b/Evaluation/RAG_Evaluator/src/utils/chunkStatistics.py
new file mode 100644
index 00000000..698a15ec
--- /dev/null
+++ b/Evaluation/RAG_Evaluator/src/utils/chunkStatistics.py
@@ -0,0 +1,523 @@
+"""
+Chunk Statistics Processor
+=========================
+Utility module to extract and process chunk statistics from search API responses.
+
+This module provides comprehensive analysis of chunk usage patterns in RAG systems,
+including Support@K metrics, qualified chunk distribution, and retrieval effectiveness.
+
+Features:
+- Extract chunk statistics from SearchAssist and XO Platform API responses
+- Calculate Support@K metrics for retrieval quality assessment
+- Analyze qualified chunk distribution across rank ranges
+- Generate Excel-compatible formatted output
+- Provide aggregate summary statistics for batch analysis
+
+Author: RAG Evaluator Team
+Version: 2.0.0
+"""
+
+import json
+import logging
+from typing import Dict, List, Set, Optional, Tuple
+from dataclasses import dataclass, field
+from enum import Enum
+
+# Configure logging
+logger = logging.getLogger(__name__)
+
+class APIType(Enum):
+ """Supported API types for chunk statistics extraction."""
+ XO_PLATFORM = "XO"
+ SEARCH_ASSIST = "SA"
+
+class ChunkQualificationStatus(Enum):
+ """Chunk qualification status values."""
+ QUALIFIED = "qualified"
+ UNQUALIFIED = "unqualified"
+ UNKNOWN = "unknown"
+
+@dataclass
+class ChunkData:
+ """Data structure for individual chunk information."""
+ chunk_id: str
+ rank: int
+ sent_to_llm: bool
+ used_in_answer: bool
+ qualification_status: str
+
+@dataclass
+class SupportMetrics:
+ """Data structure for Support@K metrics."""
+ best_support_rank: Optional[int]
+ support_at_5: bool
+ support_at_10: bool
+ support_at_20: bool
+ bucketed_distribution: str
+
+@dataclass
+class ChunkDistribution:
+ """Data structure for chunk distribution analysis."""
+ top_5_qualified: int
+ top_5_qualified_used: int
+ top_5_10_qualified: int
+ top_5_10_qualified_used: int
+ top_10_20_qualified: int
+ top_10_20_qualified_used: int
+ total_qualified: int
+ total_qualified_used: int
+
+@dataclass
+class ChunkStatistics:
+ """Complete chunk statistics data structure."""
+ retrieved_chunk_ids: List[str] = field(default_factory=list)
+ retrieved_chunk_count: int = 0
+ sent_to_llm_chunk_ids: List[str] = field(default_factory=list)
+ sent_to_llm_chunk_count: int = 0
+ used_in_answer_chunk_ids: List[str] = field(default_factory=list)
+ used_in_answer_chunk_count: int = 0
+ chunk_qualification_stats: Dict[str, int] = field(default_factory=dict)
+ support_metrics: SupportMetrics = field(default_factory=lambda: SupportMetrics(
+ best_support_rank=None,
+ support_at_5=False,
+ support_at_10=False,
+ support_at_20=False,
+ bucketed_distribution='none'
+ ))
+ chunk_distribution: ChunkDistribution = field(default_factory=lambda: ChunkDistribution(
+ top_5_qualified=0,
+ top_5_qualified_used=0,
+ top_5_10_qualified=0,
+ top_5_10_qualified_used=0,
+ top_10_20_qualified=0,
+ top_10_20_qualified_used=0,
+ total_qualified=0,
+ total_qualified_used=0
+ ))
+ error: Optional[str] = None
+
+class ChunkStatisticsProcessor:
+ """
+ Process chunk statistics from search API responses.
+
+ This class provides methods to extract, analyze, and format chunk statistics
+ from SearchAssist and XO Platform API responses. It includes comprehensive
+ analysis of chunk usage patterns, Support@K metrics, and qualified chunk
+ distribution across different rank ranges.
+ """
+
+ # Constants
+ MAX_CHUNKS_FOR_ANALYSIS = 20
+ SUPPORT_THRESHOLDS = [5, 10, 20]
+ RANK_RANGES = {
+ 'top_5': (1, 5),
+ 'top_5_10': (6, 10),
+ 'top_10_20': (11, 20)
+ }
+
+ @staticmethod
+ def extract_chunk_statistics(answer: Dict, api_type: str = "XO") -> Dict:
+ """
+ Extract chunk statistics from search API response.
+
+ This method processes the generative chunks from the API response and
+ extracts comprehensive statistics including chunk IDs, counts, qualification
+ status, Support@K metrics, and chunk distribution analysis.
+
+ Args:
+ answer: The response from the search API containing chunk information
+ api_type: Type of API ("XO" for XO Platform, "SA" for SearchAssist)
+
+ Returns:
+ Dictionary containing comprehensive chunk statistics with the following structure:
+ {
+ 'retrieved_chunk_ids': List[str],
+ 'retrieved_chunk_count': int,
+ 'sent_to_llm_chunk_ids': List[str],
+ 'sent_to_llm_chunk_count': int,
+ 'used_in_answer_chunk_ids': List[str],
+ 'used_in_answer_chunk_count': int,
+ 'chunk_qualification_stats': Dict[str, int],
+ 'support_metrics': Dict containing Support@K metrics,
+ 'chunk_distribution': Dict containing qualified chunk distribution,
+ 'error': Optional[str] if any error occurred
+ }
+
+ Raises:
+ Exception: If there's an error during processing (handled internally)
+ """
+ try:
+ logger.debug(f"Extracting chunk statistics for API type: {api_type}")
+
+ # Extract generative chunks based on API type
+ generative_chunks = ChunkStatisticsProcessor._extract_generative_chunks(answer, api_type)
+
+ if not generative_chunks:
+ logger.debug("No generative chunks found, returning empty statistics")
+ return ChunkStatisticsProcessor._create_empty_statistics()
+
+ # Process chunks and collect statistics
+ chunk_data_list = ChunkStatisticsProcessor._process_chunks(generative_chunks)
+
+ # Calculate all metrics
+ statistics = ChunkStatisticsProcessor._calculate_statistics(chunk_data_list)
+
+ logger.debug(f"Successfully extracted statistics for {len(chunk_data_list)} chunks")
+ return statistics
+
+ except Exception as e:
+ logger.error(f"Error extracting chunk statistics: {e}")
+ return ChunkStatisticsProcessor._create_empty_statistics(error=str(e))
+
+ @staticmethod
+ def _extract_generative_chunks(answer: Dict, api_type: str) -> List[Dict]:
+ """
+ Extract generative chunks from API response based on API type.
+
+ Args:
+ answer: API response dictionary
+ api_type: Type of API ("XO" or "SA")
+
+ Returns:
+ List of generative chunk dictionaries
+ """
+ if api_type.upper() == APIType.SEARCH_ASSIST.value:
+ # SearchAssist structure
+ return answer.get('template', {}).get('chunk_result', {}).get('generative', [])
+ else:
+ # XO Platform structure (default)
+ return answer.get('chunk_result', {}).get('generative', [])
+
+ @staticmethod
+ def _process_chunks(generative_chunks: List[Dict]) -> List[ChunkData]:
+ """
+ Process generative chunks and extract structured data.
+
+ Args:
+ generative_chunks: List of raw chunk dictionaries
+
+ Returns:
+ List of structured ChunkData objects
+ """
+ chunk_data_list = []
+
+ # Process only top MAX_CHUNKS_FOR_ANALYSIS chunks
+ for rank, chunk in enumerate(generative_chunks[:ChunkStatisticsProcessor.MAX_CHUNKS_FOR_ANALYSIS], 1):
+ source = chunk.get('_source', {})
+ chunk_id = source.get('chunkId', '')
+
+ if chunk_id:
+ chunk_data = ChunkData(
+ chunk_id=chunk_id,
+ rank=rank,
+ sent_to_llm=source.get('sentToLLM', False),
+ used_in_answer=source.get('usedInAnswer', False),
+ qualification_status=source.get('chunkQualified', ChunkQualificationStatus.UNKNOWN.value)
+ )
+ chunk_data_list.append(chunk_data)
+
+ return chunk_data_list
+
+ @staticmethod
+ def _calculate_statistics(chunk_data_list: List[ChunkData]) -> Dict:
+ """
+ Calculate comprehensive statistics from chunk data.
+
+ Args:
+ chunk_data_list: List of processed chunk data
+
+ Returns:
+ Dictionary containing all calculated statistics
+ """
+ # Extract basic statistics
+ retrieved_chunk_ids = sorted([chunk.chunk_id for chunk in chunk_data_list])
+ sent_to_llm_chunk_ids = sorted([chunk.chunk_id for chunk in chunk_data_list if chunk.sent_to_llm])
+ used_in_answer_chunk_ids = sorted([chunk.chunk_id for chunk in chunk_data_list if chunk.used_in_answer])
+
+ # Calculate qualification statistics
+ qualification_stats = ChunkStatisticsProcessor._calculate_qualification_stats(chunk_data_list)
+
+ # Calculate Support@K metrics
+ support_metrics = ChunkStatisticsProcessor._calculate_support_metrics(chunk_data_list)
+
+ # Calculate chunk distribution
+ chunk_distribution = ChunkStatisticsProcessor._calculate_chunk_distribution(chunk_data_list)
+
+ return {
+ 'retrieved_chunk_ids': retrieved_chunk_ids,
+ 'retrieved_chunk_count': len(retrieved_chunk_ids),
+ 'sent_to_llm_chunk_ids': sent_to_llm_chunk_ids,
+ 'sent_to_llm_chunk_count': len(sent_to_llm_chunk_ids),
+ 'used_in_answer_chunk_ids': used_in_answer_chunk_ids,
+ 'used_in_answer_chunk_count': len(used_in_answer_chunk_ids),
+ 'chunk_qualification_stats': qualification_stats,
+ 'support_metrics': support_metrics,
+ 'chunk_distribution': chunk_distribution
+ }
+
+ @staticmethod
+ def _calculate_qualification_stats(chunk_data_list: List[ChunkData]) -> Dict[str, int]:
+ """
+ Calculate chunk qualification statistics.
+
+ Args:
+ chunk_data_list: List of chunk data
+
+ Returns:
+ Dictionary with qualification status counts
+ """
+ qualification_stats = {}
+ for chunk in chunk_data_list:
+ status = chunk.qualification_status
+ qualification_stats[status] = qualification_stats.get(status, 0) + 1
+ return qualification_stats
+
+ @staticmethod
+ def _calculate_support_metrics(chunk_data_list: List[ChunkData]) -> Dict:
+ """
+ Calculate basic support metrics from chunk data.
+
+ Args:
+ chunk_data_list: List of chunk data
+
+ Returns:
+ Dictionary containing basic support metrics
+ """
+ # Get ranks of chunks used in answer
+ used_chunk_ranks = [chunk.rank for chunk in chunk_data_list if chunk.used_in_answer]
+
+ if not used_chunk_ranks:
+ return ChunkStatisticsProcessor._create_empty_support_metrics()
+
+ # Calculate best support rank (minimum rank of any used chunk)
+ best_support_rank = min(used_chunk_ranks)
+
+ return {
+ 'best_support_rank': best_support_rank
+ }
+
+ @staticmethod
+ def _calculate_chunk_distribution(chunk_data_list: List[ChunkData]) -> Dict:
+ """
+ Calculate simple chunk distribution: how many chunks used in answer from each rank range.
+
+ Args:
+ chunk_data_list: List of chunk data
+
+ Returns:
+ Dictionary containing simplified chunk distribution metrics
+ """
+ # Get all chunks used in answer (not just qualified ones)
+ used_chunks = [chunk for chunk in chunk_data_list if chunk.used_in_answer]
+
+ # Get ranks of chunks used in answer
+ used_chunk_ranks = [chunk.rank for chunk in used_chunks]
+
+ # Calculate how many chunks used from each rank range
+ top_5_used = len([rank for rank in used_chunk_ranks if rank <= 5])
+ top_5_10_used = len([rank for rank in used_chunk_ranks if 6 <= rank <= 10])
+ top_10_20_used = len([rank for rank in used_chunk_ranks if 11 <= rank <= 20])
+
+ return {
+ 'chunks_used_top_5': top_5_used,
+ 'chunks_used_5_10': top_5_10_used,
+ 'chunks_used_10_20': top_10_20_used,
+ 'used_chunk_ranks': used_chunk_ranks, # List of actual ranks used
+ 'total_chunks_used': len(used_chunks)
+ }
+
+
+
+ @staticmethod
+ def _create_empty_statistics(error: Optional[str] = None) -> Dict:
+ """
+ Create empty statistics structure.
+
+ Args:
+ error: Optional error message
+
+ Returns:
+ Dictionary with empty statistics
+ """
+ empty_stats = {
+ 'retrieved_chunk_ids': [],
+ 'retrieved_chunk_count': 0,
+ 'sent_to_llm_chunk_ids': [],
+ 'sent_to_llm_chunk_count': 0,
+ 'used_in_answer_chunk_ids': [],
+ 'used_in_answer_chunk_count': 0,
+ 'chunk_qualification_stats': {},
+ 'support_metrics': ChunkStatisticsProcessor._create_empty_support_metrics(),
+ 'chunk_distribution': ChunkStatisticsProcessor._create_empty_chunk_distribution()
+ }
+
+ if error:
+ empty_stats['error'] = error
+
+ return empty_stats
+
+ @staticmethod
+ def _create_empty_support_metrics() -> Dict:
+ """Create empty support metrics structure."""
+ return {
+ 'best_support_rank': None
+ }
+
+ @staticmethod
+ def _create_empty_chunk_distribution() -> Dict:
+ """Create empty chunk distribution structure."""
+ return {
+ 'chunks_used_top_5': 0,
+ 'chunks_used_5_10': 0,
+ 'chunks_used_10_20': 0,
+ 'used_chunk_ranks': [],
+ 'total_chunks_used': 0
+ }
+
+ @staticmethod
+ def format_chunk_statistics_for_excel(stats: Dict) -> Dict:
+ """
+ Format chunk statistics for Excel export.
+
+ This method converts the internal statistics format to a format suitable
+ for direct inclusion in Excel DataFrames, with proper string formatting
+ for lists and dictionaries.
+
+ Args:
+ stats: Raw chunk statistics dictionary
+
+ Returns:
+ Dictionary with Excel-compatible formatted values
+ """
+ try:
+ support_metrics = stats.get('support_metrics', {})
+ chunk_distribution = stats.get('chunk_distribution', {})
+
+ formatted_stats = {
+ 'Retrieved Chunk IDs': json.dumps(stats.get('retrieved_chunk_ids', []), separators=(',', ':')),
+ 'Retrieved Chunk Count': stats.get('retrieved_chunk_count', 0),
+ 'Sent to LLM Chunk IDs': json.dumps(stats.get('sent_to_llm_chunk_ids', []), separators=(',', ':')),
+ 'Sent to LLM Chunk Count': stats.get('sent_to_llm_chunk_count', 0),
+ 'Used in Answer Chunk IDs': json.dumps(stats.get('used_in_answer_chunk_ids', []), separators=(',', ':')),
+ 'Used in Answer Chunk Count': stats.get('used_in_answer_chunk_count', 0),
+
+ 'Best Support Rank': support_metrics.get('best_support_rank', 'None'),
+ 'Chunks Used Top 5': chunk_distribution.get('chunks_used_top_5', 0),
+ 'Chunks Used 5-10': chunk_distribution.get('chunks_used_5_10', 0),
+ 'Chunks Used 10-20': chunk_distribution.get('chunks_used_10_20', 0),
+ 'Used Chunk Ranks': json.dumps(chunk_distribution.get('used_chunk_ranks', []), separators=(',', ':')),
+ 'Total Chunks Used': chunk_distribution.get('total_chunks_used', 0)
+ }
+
+ # Add debug logging
+ logger.debug(f"Formatted chunk statistics: {formatted_stats}")
+ logger.info(f"Chunk statistics formatted successfully with {len(formatted_stats)} columns")
+
+ return formatted_stats
+ except Exception as e:
+ logger.error(f"Error formatting chunk statistics: {e}")
+ return ChunkStatisticsProcessor._create_empty_excel_format()
+
+ @staticmethod
+ def _create_empty_excel_format() -> Dict:
+ """Create empty Excel format structure."""
+ return {
+ 'Retrieved Chunk IDs': '[]',
+ 'Retrieved Chunk Count': 0,
+ 'Sent to LLM Chunk IDs': '[]',
+ 'Sent to LLM Chunk Count': 0,
+ 'Used in Answer Chunk IDs': '[]',
+ 'Used in Answer Chunk Count': 0,
+
+ 'Best Support Rank': 'None',
+ 'Chunks Used Top 5': 0,
+ 'Chunks Used 5-10': 0,
+ 'Chunks Used 10-20': 0,
+ 'Used Chunk Ranks': '[]',
+ 'Total Chunks Used': 0
+ }
+
+ @staticmethod
+ def get_chunk_statistics_summary(stats_list: List[Dict]) -> Dict:
+ """
+ Generate summary statistics from a list of chunk statistics.
+
+ This method calculates aggregate metrics across multiple queries,
+ including average chunk counts, Support@K rates, and distribution
+ summaries for batch analysis.
+
+ Args:
+ stats_list: List of chunk statistics dictionaries
+
+ Returns:
+ Dictionary containing aggregate summary statistics
+ """
+ if not stats_list:
+ return ChunkStatisticsProcessor._create_empty_summary()
+
+ total_queries = len(stats_list)
+
+ # Calculate basic aggregate metrics
+ total_retrieved = sum(stats.get('retrieved_chunk_count', 0) for stats in stats_list)
+ total_sent_to_llm = sum(stats.get('sent_to_llm_chunk_count', 0) for stats in stats_list)
+ total_used_in_answer = sum(stats.get('used_in_answer_chunk_count', 0) for stats in stats_list)
+
+
+
+ # Calculate chunk distribution summary
+ chunk_distribution_summary = ChunkStatisticsProcessor._calculate_chunk_distribution_summary(stats_list, total_queries)
+
+ return {
+ 'total_queries': total_queries,
+ 'avg_retrieved_chunks': round(total_retrieved / total_queries, 2) if total_queries > 0 else 0,
+ 'avg_sent_to_llm_chunks': round(total_sent_to_llm / total_queries, 2) if total_queries > 0 else 0,
+ 'avg_used_in_answer_chunks': round(total_used_in_answer / total_queries, 2) if total_queries > 0 else 0,
+ 'total_retrieved_chunks': total_retrieved,
+ 'total_sent_to_llm_chunks': total_sent_to_llm,
+ 'total_used_in_answer_chunks': total_used_in_answer,
+
+ 'chunk_distribution_summary': chunk_distribution_summary
+ }
+
+
+
+ @staticmethod
+ def _calculate_chunk_distribution_summary(stats_list: List[Dict], total_queries: int) -> Dict:
+ """Calculate chunk distribution summary."""
+ # Sum all distribution metrics
+ total_metrics = {
+ 'chunks_used_top_5': 0,
+ 'chunks_used_5_10': 0,
+ 'chunks_used_10_20': 0,
+ 'total_chunks_used': 0
+ }
+
+ for stats in stats_list:
+ chunk_dist = stats.get('chunk_distribution', {})
+ for key in total_metrics:
+ total_metrics[key] += chunk_dist.get(key, 0)
+
+ # Calculate averages
+ return {
+ f'avg_{key}': round(total_metrics[key] / total_queries, 2) if total_queries > 0 else 0
+ for key in total_metrics
+ }
+
+ @staticmethod
+ def _create_empty_summary() -> Dict:
+ """Create empty summary structure."""
+ return {
+ 'total_queries': 0,
+ 'avg_retrieved_chunks': 0,
+ 'avg_sent_to_llm_chunks': 0,
+ 'avg_used_in_answer_chunks': 0,
+ 'total_retrieved_chunks': 0,
+ 'total_sent_to_llm_chunks': 0,
+ 'total_used_in_answer_chunks': 0,
+ 'chunk_distribution_summary': {
+ 'avg_chunks_used_top_5': 0.0,
+ 'avg_chunks_used_5_10': 0.0,
+ 'avg_chunks_used_10_20': 0.0,
+ 'avg_total_chunks_used': 0.0
+ }
+ }
diff --git a/Evaluation/RAG_Evaluator/src/utils/cleanup_scheduler.py b/Evaluation/RAG_Evaluator/src/utils/cleanup_scheduler.py
new file mode 100644
index 00000000..3bd3bc4c
--- /dev/null
+++ b/Evaluation/RAG_Evaluator/src/utils/cleanup_scheduler.py
@@ -0,0 +1,111 @@
+# Cleanup Scheduler for Session Management
+# This module provides automated cleanup of old session files
+
+import asyncio
+import schedule
+import time
+import threading
+import logging
+from datetime import datetime
+from utils.sessionManager import get_session_manager
+
+logger = logging.getLogger(__name__)
+
+class CleanupScheduler:
+ def __init__(self, cleanup_interval_hours: int = 24, max_session_age_hours: int = 24):
+ self.cleanup_interval_hours = cleanup_interval_hours
+ self.max_session_age_hours = max_session_age_hours
+ self.running = False
+ self.scheduler_thread = None
+
+ def cleanup_old_sessions(self):
+ """Perform cleanup of old sessions"""
+ try:
+ session_manager = get_session_manager()
+ cleaned_count = session_manager.cleanup_old_sessions(self.max_session_age_hours)
+
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ logger.info(f"[{current_time}] Cleanup completed: removed {cleaned_count} old sessions")
+
+ return cleaned_count
+ except Exception as e:
+ logger.error(f"Error during cleanup: {e}")
+ return 0
+
+ def start_scheduler(self):
+ """Start the cleanup scheduler in a separate thread"""
+ if self.running:
+ logger.warning("Cleanup scheduler is already running")
+ return
+
+ self.running = True
+
+ # Schedule cleanup every N hours
+ schedule.every(self.cleanup_interval_hours).hours.do(self.cleanup_old_sessions)
+
+ # Run an initial cleanup
+ initial_cleaned = self.cleanup_old_sessions()
+ logger.info(f"Initial cleanup completed: {initial_cleaned} sessions removed")
+
+ def run_scheduler():
+ logger.info(f"Starting cleanup scheduler: every {self.cleanup_interval_hours} hours, max age {self.max_session_age_hours} hours")
+ while self.running:
+ schedule.run_pending()
+ time.sleep(60) # Check every minute
+ logger.info("Cleanup scheduler stopped")
+
+ self.scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
+ self.scheduler_thread.start()
+
+ logger.info("Cleanup scheduler started successfully")
+
+ def stop_scheduler(self):
+ """Stop the cleanup scheduler"""
+ if not self.running:
+ logger.warning("Cleanup scheduler is not running")
+ return
+
+ self.running = False
+ schedule.clear()
+
+ if self.scheduler_thread and self.scheduler_thread.is_alive():
+ self.scheduler_thread.join(timeout=5)
+
+ logger.info("Cleanup scheduler stopped")
+
+ def get_status(self) -> dict:
+ """Get the current status of the scheduler"""
+ return {
+ "running": self.running,
+ "cleanup_interval_hours": self.cleanup_interval_hours,
+ "max_session_age_hours": self.max_session_age_hours,
+ "scheduled_jobs": len(schedule.jobs),
+ "next_run": str(schedule.next_run()) if schedule.jobs else "No jobs scheduled"
+ }
+
+# Global scheduler instance
+_cleanup_scheduler = None
+
+def get_cleanup_scheduler() -> CleanupScheduler:
+ """Get the global cleanup scheduler instance"""
+ global _cleanup_scheduler
+ if _cleanup_scheduler is None:
+ _cleanup_scheduler = CleanupScheduler()
+ return _cleanup_scheduler
+
+def start_cleanup_scheduler(cleanup_interval_hours: int = 24, max_session_age_hours: int = 24):
+ """Start the global cleanup scheduler"""
+ scheduler = get_cleanup_scheduler()
+ scheduler.cleanup_interval_hours = cleanup_interval_hours
+ scheduler.max_session_age_hours = max_session_age_hours
+ scheduler.start_scheduler()
+
+def stop_cleanup_scheduler():
+ """Stop the global cleanup scheduler"""
+ scheduler = get_cleanup_scheduler()
+ scheduler.stop_scheduler()
+
+def manual_cleanup() -> int:
+ """Manually trigger a cleanup and return the number of sessions cleaned"""
+ scheduler = get_cleanup_scheduler()
+ return scheduler.cleanup_old_sessions()
\ No newline at end of file
diff --git a/Evaluation/RAG_Evaluator/src/utils/dbservice.py b/Evaluation/RAG_Evaluator/src/utils/dbservice.py
index a5d0add8..e6d777c0 100644
--- a/Evaluation/RAG_Evaluator/src/utils/dbservice.py
+++ b/Evaluation/RAG_Evaluator/src/utils/dbservice.py
@@ -1,10 +1,17 @@
+import os
from pymongo import MongoClient
import numpy as np
-from config.configManager import ConfigManager
+
from datetime import datetime
-config_manager = ConfigManager()
-config = config_manager.get_config()
+# Use environment variables for MongoDB (no file-based config)
+config = {
+ "MongoDB": {
+ "url": os.getenv("MONGO_URL", ""),
+ "dbName": os.getenv("DB_NAME", ""),
+ "collectionName": os.getenv("COLLECTION_NAME", "")
+ }
+}
diff --git a/Evaluation/RAG_Evaluator/src/utils/evaluationResult.py b/Evaluation/RAG_Evaluator/src/utils/evaluationResult.py
index 62bd16d1..05b1de72 100644
--- a/Evaluation/RAG_Evaluator/src/utils/evaluationResult.py
+++ b/Evaluation/RAG_Evaluator/src/utils/evaluationResult.py
@@ -2,25 +2,40 @@
class ResultsConverter:
- def __init__(self, ragas_results: pd.DataFrame, crag_results: pd.DataFrame):
+ def __init__(self, ragas_results: pd.DataFrame, crag_results: pd.DataFrame, llm_results: pd.DataFrame = None):
self.ragas_results = ragas_results
self.crag_results = crag_results
+ self.llm_results = llm_results if llm_results is not None else pd.DataFrame()
def convert_ragas_results(self):
- """Converts column name 'question' to 'query' in the Ragas Results DataFrame."""
- if 'question' in self.ragas_results.columns:
- self.ragas_results.rename(columns={'question': 'query'}, inplace=True)
- print("Converted 'question' to 'query' in Ragas results.")
- else:
- print("'question' column not found in Ragas results.")
+ """Converts column names in the Ragas Results DataFrame for consistency."""
+ # Handle common RAGAS column name mappings
+ column_mappings = {
+ 'question': 'query',
+ 'user_input': 'query',
+ 'response': 'answer',
+ 'retrieved_contexts': 'context',
+ 'reference': 'ground_truth'
+ }
+
+ # Apply mappings if columns exist
+ columns_to_rename = {}
+ for old_name, new_name in column_mappings.items():
+ if old_name in self.ragas_results.columns:
+ columns_to_rename[old_name] = new_name
+
+ if columns_to_rename:
+ self.ragas_results.rename(columns=columns_to_rename, inplace=True)
def convert_crag_results(self):
"""Converts column name 'prediction' to 'answer' in the CRAG Results DataFrame."""
if 'prediction' in self.crag_results.columns:
self.crag_results.rename(columns={'prediction': 'answer'}, inplace=True)
- print("Converted 'prediction' to 'answer' in CRAG results.")
- else:
- print("'prediction' column not found in CRAG results.")
+
+ def convert_llm_results(self):
+ """Converts column names in the LLM Results DataFrame for consistency."""
+ # LLM results should already have standardized column names
+ pass
def get_crag_results(self):
return self.crag_results
@@ -28,14 +43,28 @@ def get_crag_results(self):
def get_ragas_results(self):
return self.ragas_results
+ def get_llm_results(self):
+ return self.llm_results
+
def get_combined_results(self):
- """Combines the converted Ragas results and CRAG results DataFrames."""
- # Assuming both DataFrames have the same number of rows
- if len(self.ragas_results) != len(self.crag_results):
- print("Warning: Ragas and CRAG results have different row counts. Combining may lead to misalignment.")
+ """Combines the converted Ragas, CRAG, and LLM results DataFrames."""
+ dfs_to_combine = []
+
+ # Add non-empty DataFrames to combination list
+ if not self.ragas_results.empty:
+ dfs_to_combine.append(self.ragas_results.reset_index(drop=True))
+
+ if not self.crag_results.empty:
+ dfs_to_combine.append(self.crag_results.reset_index(drop=True))
+
+ if not self.llm_results.empty:
+ dfs_to_combine.append(self.llm_results.reset_index(drop=True))
+
+ if not dfs_to_combine:
+ return pd.DataFrame()
# Combine the DataFrames
- combined_results = pd.concat([self.ragas_results.reset_index(drop=True), self.crag_results.reset_index(drop=True)], axis=1)
+ combined_results = pd.concat(dfs_to_combine, axis=1)
# Remove duplicate columns while keeping the first occurrence
combined_results = combined_results.loc[:, ~combined_results.columns.duplicated()]
@@ -43,20 +72,3 @@ def get_combined_results(self):
return combined_results
-# Example usage:
-if __name__ == "__main__":
- # Sample DataFrames (replace these with actual DataFrames in practice)
- ragas_results = pd.DataFrame({'question': ['What is AI?', 'Define ML.'], 'other_col': [1, 2]})
- crag_results = pd.DataFrame({'prediction': ['AI is a field.', 'ML is a subset of AI.'], 'another_col': [3, 4]})
-
- converter = ResultsConverter(ragas_results, crag_results)
-
- # Convert columns
- converter.convert_ragas_results()
- converter.convert_crag_results()
-
- # Get combined results
- combined_results = converter.get_combined_results()
-
- print("\nCombined Results:")
- print(combined_results)
\ No newline at end of file
diff --git a/Evaluation/RAG_Evaluator/src/utils/sessionManager.py b/Evaluation/RAG_Evaluator/src/utils/sessionManager.py
new file mode 100644
index 00000000..2588431a
--- /dev/null
+++ b/Evaluation/RAG_Evaluator/src/utils/sessionManager.py
@@ -0,0 +1,244 @@
+# Session Manager for User-level File Separation
+# This module handles unique session IDs and file mapping for multi-user support
+
+import uuid
+import time
+import json
+import os
+import threading
+from datetime import datetime, timedelta
+from typing import Dict, Optional, List
+import logging
+
+logger = logging.getLogger(__name__)
+
+class SessionManager:
+ def __init__(self, base_output_dir: str = "outputs"):
+ self.base_output_dir = base_output_dir
+ self.sessions_file = os.path.join(base_output_dir, "sessions.json")
+ self.sessions: Dict[str, dict] = {}
+ self.lock = threading.RLock()
+ self.load_sessions()
+
+ # Create base output directory if it doesn't exist
+ os.makedirs(base_output_dir, exist_ok=True)
+
+ def create_session(self) -> str:
+ """Create a new user session and return session ID"""
+ with self.lock:
+ session_id = str(uuid.uuid4())
+ timestamp = datetime.now().isoformat()
+
+ session_data = {
+ "session_id": session_id,
+ "created_at": timestamp,
+ "last_accessed": timestamp,
+ "output_files": [],
+ "status": "active",
+ "user_ip": None, # Can be set later
+ "user_agent": None # Can be set later
+ }
+
+ self.sessions[session_id] = session_data
+
+ # Create user-specific directory
+ session_dir = self.get_session_directory(session_id)
+ os.makedirs(session_dir, exist_ok=True)
+
+ self.save_sessions()
+ logger.info(f"Created new session: {session_id}")
+ return session_id
+
+ def get_session_directory(self, session_id: str) -> str:
+ """Get the directory path for a specific session"""
+ return os.path.join(self.base_output_dir, f"session_{session_id}")
+
+ def add_output_file(self, session_id: str, file_path: str) -> bool:
+ """Add an output file to a session"""
+ with self.lock:
+ if session_id not in self.sessions:
+ logger.error(f"Session not found: {session_id}")
+ return False
+
+ # Update last accessed time
+ self.sessions[session_id]["last_accessed"] = datetime.now().isoformat()
+
+ # Add file to session
+ if file_path not in self.sessions[session_id]["output_files"]:
+ self.sessions[session_id]["output_files"].append({
+ "file_path": file_path,
+ "created_at": datetime.now().isoformat(),
+ "file_size": os.path.getsize(file_path) if os.path.exists(file_path) else 0
+ })
+
+ self.save_sessions()
+ logger.info(f"Added output file to session {session_id}: {file_path}")
+ return True
+
+ return False
+
+ def get_session_files(self, session_id: str) -> List[dict]:
+ """Get all output files for a session"""
+ with self.lock:
+ if session_id not in self.sessions:
+ return []
+
+ # Update last accessed time
+ self.sessions[session_id]["last_accessed"] = datetime.now().isoformat()
+ self.save_sessions()
+
+ return self.sessions[session_id]["output_files"]
+
+ def get_latest_file(self, session_id: str) -> Optional[str]:
+ """Get the most recent output file for a session"""
+ files = self.get_session_files(session_id)
+ if not files:
+ return None
+
+ # Sort by creation time and return the latest
+ latest_file = max(files, key=lambda f: f["created_at"])
+ file_path = latest_file["file_path"]
+
+ # Verify file still exists
+ if os.path.exists(file_path):
+ return file_path
+ else:
+ logger.warning(f"File not found: {file_path}")
+ return None
+
+ def is_valid_session(self, session_id: str) -> bool:
+ """Check if a session ID is valid and active"""
+ with self.lock:
+ return session_id in self.sessions and self.sessions[session_id]["status"] == "active"
+
+ def update_session_metadata(self, session_id: str, user_ip: str = None, user_agent: str = None):
+ """Update session metadata with user information"""
+ with self.lock:
+ if session_id in self.sessions:
+ if user_ip:
+ self.sessions[session_id]["user_ip"] = user_ip
+ if user_agent:
+ self.sessions[session_id]["user_agent"] = user_agent
+
+ self.sessions[session_id]["last_accessed"] = datetime.now().isoformat()
+ self.save_sessions()
+
+ def cleanup_old_sessions(self, max_age_hours: int = 24):
+ """Clean up old sessions and their files"""
+ with self.lock:
+ current_time = datetime.now()
+ sessions_to_remove = []
+
+ for session_id, session_data in self.sessions.items():
+ last_accessed = datetime.fromisoformat(session_data["last_accessed"])
+ age_hours = (current_time - last_accessed).total_seconds() / 3600
+
+ if age_hours > max_age_hours:
+ sessions_to_remove.append(session_id)
+
+ for session_id in sessions_to_remove:
+ self.remove_session(session_id)
+
+ logger.info(f"Cleaned up {len(sessions_to_remove)} old sessions")
+ return len(sessions_to_remove)
+
+ def remove_session(self, session_id: str):
+ """Remove a session and clean up its files"""
+ with self.lock:
+ if session_id not in self.sessions:
+ return
+
+ session_data = self.sessions[session_id]
+
+ # Remove output files
+ for file_info in session_data["output_files"]:
+ file_path = file_info["file_path"]
+ try:
+ if os.path.exists(file_path):
+ os.remove(file_path)
+ logger.info(f"Removed file: {file_path}")
+ except Exception as e:
+ logger.error(f"Error removing file {file_path}: {e}")
+
+ # Remove session directory
+ session_dir = self.get_session_directory(session_id)
+ try:
+ if os.path.exists(session_dir):
+ import shutil
+ shutil.rmtree(session_dir)
+ logger.info(f"Removed session directory: {session_dir}")
+ except Exception as e:
+ logger.error(f"Error removing session directory {session_dir}: {e}")
+
+ # Remove from sessions
+ del self.sessions[session_id]
+ self.save_sessions()
+ logger.info(f"Removed session: {session_id}")
+
+ def get_session_stats(self) -> dict:
+ """Get statistics about all sessions"""
+ with self.lock:
+ total_sessions = len(self.sessions)
+ active_sessions = sum(1 for s in self.sessions.values() if s["status"] == "active")
+ total_files = sum(len(s["output_files"]) for s in self.sessions.values())
+
+ return {
+ "total_sessions": total_sessions,
+ "active_sessions": active_sessions,
+ "total_files": total_files,
+ "base_output_dir": self.base_output_dir
+ }
+
+ def save_sessions(self):
+ """Save sessions to disk"""
+ try:
+ with open(self.sessions_file, 'w') as f:
+ json.dump(self.sessions, f, indent=2)
+ except Exception as e:
+ logger.error(f"Error saving sessions: {e}")
+
+ def load_sessions(self):
+ """Load sessions from disk"""
+ try:
+ if os.path.exists(self.sessions_file):
+ with open(self.sessions_file, 'r') as f:
+ self.sessions = json.load(f)
+ logger.info(f"Loaded {len(self.sessions)} sessions from disk")
+ else:
+ self.sessions = {}
+ logger.info("No existing sessions file found, starting fresh")
+ except Exception as e:
+ logger.error(f"Error loading sessions: {e}")
+ self.sessions = {}
+
+# Global session manager instance
+_session_manager = None
+
+def get_session_manager() -> SessionManager:
+ """Get the global session manager instance"""
+ global _session_manager
+ if _session_manager is None:
+ # Use the outputs directory from the app structure
+ output_dir = os.path.join(os.path.dirname(__file__), "../outputs")
+ _session_manager = SessionManager(output_dir)
+ return _session_manager
+
+def create_user_session() -> str:
+ """Convenience function to create a new user session"""
+ return get_session_manager().create_session()
+
+def get_user_session_directory(session_id: str) -> str:
+ """Convenience function to get session directory"""
+ return get_session_manager().get_session_directory(session_id)
+
+def add_session_file(session_id: str, file_path: str) -> bool:
+ """Convenience function to add file to session"""
+ return get_session_manager().add_output_file(session_id, file_path)
+
+def get_session_latest_file(session_id: str) -> Optional[str]:
+ """Convenience function to get latest file for session"""
+ return get_session_manager().get_latest_file(session_id)
+
+def validate_session(session_id: str) -> bool:
+ """Convenience function to validate session"""
+ return get_session_manager().is_valid_session(session_id)
\ No newline at end of file
diff --git a/Evaluation/RAG_Evaluator/src/utils/unusedChunkAnalyzer.py b/Evaluation/RAG_Evaluator/src/utils/unusedChunkAnalyzer.py
new file mode 100644
index 00000000..7bb9248c
--- /dev/null
+++ b/Evaluation/RAG_Evaluator/src/utils/unusedChunkAnalyzer.py
@@ -0,0 +1,663 @@
+"""
+Unused Chunk Analysis Module
+============================
+Advanced analysis of questions where chunks were sent to LLM but not used in answers.
+
+This module provides comprehensive categorization and analysis of unused chunks to identify:
+1. Context relevance issues from qualified chunks
+2. Ground truth validity problems
+3. Context overload causing answer generation failures
+
+Author: RAG Evaluator Team
+Version: 1.0.0
+"""
+
+import json
+import logging
+from typing import Dict, List, Optional, Tuple, Any
+from dataclasses import dataclass, field
+from enum import Enum
+
+# Try to import pandas, but handle gracefully if not available
+try:
+ import pandas as pd
+ PANDAS_AVAILABLE = True
+except ImportError:
+ PANDAS_AVAILABLE = False
+ pd = None
+
+# Configure logging
+logger = logging.getLogger(__name__)
+
+class UnusedChunkCategory(Enum):
+ """Categories for questions with unused chunks sent to LLM."""
+ CONTEXT_IRRELEVANT = "context_irrelevant"
+ GROUND_TRUTH_INVALID = "ground_truth_invalid"
+ CONTEXT_OVERLOAD = "context_overload"
+ ANSWER_GENERATION_FAILURE = "answer_generation_failure"
+ MIXED_ISSUES = "mixed_issues"
+
+@dataclass
+class UnusedChunkAnalysis:
+ """Data structure for unused chunk analysis."""
+ query: str
+ answer: str
+ ground_truth: str
+ sent_to_llm_chunks: List[str]
+ used_chunks: List[str]
+ unused_chunks: List[str]
+ chunk_qualification_stats: Dict[str, int]
+ total_sent_to_llm: int
+ total_used: int
+ unused_count: int
+ category: UnusedChunkCategory
+ reasoning: str
+ context_relevance_score: Optional[float] = None
+ ground_truth_validity_score: Optional[float] = None
+ context_overload_score: Optional[float] = None
+
+@dataclass
+class UnusedChunkSummary:
+ """Summary statistics for unused chunk analysis."""
+ total_questions_analyzed: int
+ questions_with_unused_chunks: int
+ category_distribution: Dict[str, int]
+ avg_unused_chunks_per_question: float
+ avg_context_relevance_score: float
+ avg_ground_truth_validity_score: float
+ avg_context_overload_score: float
+ recommendations: List[str]
+
+class UnusedChunkAnalyzer:
+ """
+ Analyzes questions where chunks were sent to LLM but not used in answers.
+
+ This class provides comprehensive categorization and analysis to identify
+ the root causes of chunk underutilization in RAG systems.
+ """
+
+ # Thresholds for categorization
+ CONTEXT_OVERLOAD_THRESHOLD = 15 # More than 15 chunks sent to LLM
+ LOW_UTILIZATION_THRESHOLD = 0.3 # Less than 30% of sent chunks used
+ HIGH_UNUSED_THRESHOLD = 0.7 # More than 70% of sent chunks unused
+
+ @staticmethod
+ def analyze_unused_chunks(
+ queries: List[str],
+ answers: List[str],
+ ground_truths: List[str],
+ chunk_statistics_list: List[Dict],
+ llm_evaluation_results: Optional[List[Dict]] = None
+ ) -> Tuple[List[UnusedChunkAnalysis], UnusedChunkSummary]:
+ """
+ Analyze questions with unused chunks sent to LLM.
+
+ Args:
+ queries: List of user queries
+ answers: List of generated answers
+ ground_truths: List of ground truth answers
+ chunk_statistics_list: List of chunk statistics dictionaries
+ llm_evaluation_results: Optional LLM evaluation results for additional context
+
+ Returns:
+ Tuple of (analysis_list, summary_statistics)
+ """
+ try:
+ logger.info(f"๐ Starting unused chunk analysis for {len(queries)} queries")
+
+ analysis_list = []
+ questions_with_unused_chunks = 0
+
+ for i, (query, answer, ground_truth, chunk_stats) in enumerate(
+ zip(queries, answers, ground_truths, chunk_statistics_list)
+ ):
+ # Check if this question has unused chunks
+ sent_to_llm_count = chunk_stats.get('sent_to_llm_chunk_count', 0)
+ used_count = chunk_stats.get('used_in_answer_chunk_count', 0)
+
+ if sent_to_llm_count > 0 and sent_to_llm_count > used_count:
+ questions_with_unused_chunks += 1
+
+ # Analyze this question
+ analysis = UnusedChunkAnalyzer._analyze_single_question(
+ query, answer, ground_truth, chunk_stats,
+ llm_evaluation_results[i] if llm_evaluation_results else None
+ )
+ analysis_list.append(analysis)
+
+ # Generate summary statistics
+ summary = UnusedChunkAnalyzer._generate_summary(
+ analysis_list, len(queries), questions_with_unused_chunks
+ )
+
+ logger.info(f"โ
Unused chunk analysis completed: {questions_with_unused_chunks} questions analyzed")
+ return analysis_list, summary
+
+ except Exception as e:
+ logger.error(f"โ Error in unused chunk analysis: {e}")
+ return [], UnusedChunkAnalyzer._create_empty_summary()
+
+ @staticmethod
+ def _analyze_single_question(
+ query: str,
+ answer: str,
+ ground_truth: str,
+ chunk_stats: Dict,
+ llm_eval: Optional[Dict] = None
+ ) -> UnusedChunkAnalysis:
+ """
+ Analyze a single question for unused chunk issues.
+
+ Args:
+ query: User query
+ answer: Generated answer
+ ground_truth: Expected answer
+ chunk_stats: Chunk statistics for this question
+ llm_eval: Optional LLM evaluation results
+
+ Returns:
+ UnusedChunkAnalysis object
+ """
+ sent_to_llm_chunks = chunk_stats.get('sent_to_llm_chunk_ids', [])
+ used_chunks = chunk_stats.get('used_in_answer_chunk_ids', [])
+ unused_chunks = [chunk for chunk in sent_to_llm_chunks if chunk not in used_chunks]
+
+ sent_count = len(sent_to_llm_chunks)
+ used_count = len(used_chunks)
+ unused_count = len(unused_chunks)
+
+ # Determine category and reasoning
+ category, reasoning = UnusedChunkAnalyzer._categorize_question(
+ query, answer, ground_truth, chunk_stats, llm_eval
+ )
+
+ # Calculate scores
+ context_relevance_score = UnusedChunkAnalyzer._calculate_context_relevance_score(
+ chunk_stats, llm_eval
+ )
+ ground_truth_validity_score = UnusedChunkAnalyzer._calculate_ground_truth_validity_score(
+ query, ground_truth, llm_eval
+ )
+ context_overload_score = UnusedChunkAnalyzer._calculate_context_overload_score(
+ sent_count, used_count, unused_count
+ )
+
+ return UnusedChunkAnalysis(
+ query=query,
+ answer=answer,
+ ground_truth=ground_truth,
+ sent_to_llm_chunks=sent_to_llm_chunks,
+ used_chunks=used_chunks,
+ unused_chunks=unused_chunks,
+ chunk_qualification_stats=chunk_stats.get('chunk_qualification_stats', {}),
+ total_sent_to_llm=sent_count,
+ total_used=used_count,
+ unused_count=unused_count,
+ category=category,
+ reasoning=reasoning,
+ context_relevance_score=context_relevance_score,
+ ground_truth_validity_score=ground_truth_validity_score,
+ context_overload_score=context_overload_score
+ )
+
+ @staticmethod
+ def _categorize_question(
+ query: str,
+ answer: str,
+ ground_truth: str,
+ chunk_stats: Dict,
+ llm_eval: Optional[Dict] = None
+ ) -> Tuple[UnusedChunkCategory, str]:
+ """
+ Categorize the question based on unused chunk analysis.
+
+ Args:
+ query: User query
+ answer: Generated answer
+ ground_truth: Expected answer
+ chunk_stats: Chunk statistics
+ llm_eval: Optional LLM evaluation results
+
+ Returns:
+ Tuple of (category, reasoning)
+ """
+ sent_count = chunk_stats.get('sent_to_llm_chunk_count', 0)
+ used_count = chunk_stats.get('used_in_answer_chunk_count', 0)
+ unused_count = sent_count - used_count
+
+ # Check for context overload
+ if sent_count > UnusedChunkAnalyzer.CONTEXT_OVERLOAD_THRESHOLD:
+ return UnusedChunkCategory.CONTEXT_OVERLOAD, f"Too many chunks ({sent_count}) sent to LLM, causing information overload"
+
+ # Check utilization ratio
+ utilization_ratio = used_count / sent_count if sent_count > 0 else 0
+ if utilization_ratio < UnusedChunkAnalyzer.LOW_UTILIZATION_THRESHOLD:
+ return UnusedChunkCategory.CONTEXT_IRRELEVANT, f"Low chunk utilization ({utilization_ratio:.2%}), suggesting irrelevant context"
+
+ # Check for answer generation issues
+ if answer.strip().lower() in ['', 'no answer', 'i cannot answer', 'insufficient information']:
+ return UnusedChunkCategory.ANSWER_GENERATION_FAILURE, "LLM failed to generate meaningful answer despite having context"
+
+ # Check ground truth validity using LLM evaluation if available
+ if llm_eval and 'ground_truth_validity_score' in llm_eval:
+ validity_score = llm_eval['ground_truth_validity_score']
+ if validity_score < 0.6: # Low validity threshold
+ return UnusedChunkCategory.GROUND_TRUTH_INVALID, f"Ground truth validity score ({validity_score:.2f}) suggests invalid reference answer"
+
+ # Default to mixed issues if no clear category
+ return UnusedChunkCategory.MIXED_ISSUES, "Multiple factors contributing to chunk underutilization"
+
+ @staticmethod
+ def _calculate_context_relevance_score(chunk_stats: Dict, llm_eval: Optional[Dict] = None) -> Optional[float]:
+ """Calculate context relevance score."""
+ if llm_eval and 'context_relevancy_score' in llm_eval:
+ return llm_eval['context_relevancy_score']
+
+ # Fallback: use chunk qualification stats
+ qualification_stats = chunk_stats.get('chunk_qualification_stats', {})
+ qualified_count = qualification_stats.get('qualified', 0)
+ total_sent = chunk_stats.get('sent_to_llm_chunk_count', 0)
+
+ if total_sent > 0:
+ return qualified_count / total_sent
+ return None
+
+ @staticmethod
+ def _calculate_ground_truth_validity_score(
+ query: str,
+ ground_truth: str,
+ llm_eval: Optional[Dict] = None
+ ) -> Optional[float]:
+ """Calculate ground truth validity score."""
+ if llm_eval and 'ground_truth_validity_score' in llm_eval:
+ return llm_eval['ground_truth_validity_score']
+
+ # Fallback: basic heuristics
+ if not ground_truth or ground_truth.strip() == '':
+ return 0.0
+ if ground_truth.lower() in ['invalid question', 'cannot answer', 'no answer']:
+ return 0.3
+ return 0.7 # Default moderate validity
+
+ @staticmethod
+ def _calculate_context_overload_score(sent_count: int, used_count: int, unused_count: int) -> float:
+ """Calculate context overload score (0-1, higher = more overload)."""
+ if sent_count == 0:
+ return 0.0
+
+ # Factors: high chunk count, low utilization, high unused ratio
+ chunk_count_factor = min(sent_count / 20.0, 1.0) # Normalize to 0-1
+ utilization_factor = 1.0 - (used_count / sent_count)
+ unused_ratio_factor = unused_count / sent_count
+
+ # Weighted combination
+ overload_score = (chunk_count_factor * 0.4 +
+ utilization_factor * 0.3 +
+ unused_ratio_factor * 0.3)
+
+ return min(overload_score, 1.0)
+
+ @staticmethod
+ def _generate_summary(
+ analysis_list: List[UnusedChunkAnalysis],
+ total_questions: int,
+ questions_with_unused_chunks: int
+ ) -> UnusedChunkSummary:
+ """Generate summary statistics for unused chunk analysis."""
+ if not analysis_list:
+ return UnusedChunkAnalyzer._create_empty_summary()
+
+ # Category distribution
+ category_distribution = {}
+ for analysis in analysis_list:
+ category = analysis.category.value
+ category_distribution[category] = category_distribution.get(category, 0) + 1
+
+ # Calculate averages
+ total_unused = sum(analysis.unused_count for analysis in analysis_list)
+ avg_unused_chunks = total_unused / len(analysis_list) if analysis_list else 0
+
+ # Calculate average scores
+ context_scores = [a.context_relevance_score for a in analysis_list if a.context_relevance_score is not None]
+ validity_scores = [a.ground_truth_validity_score for a in analysis_list if a.ground_truth_validity_score is not None]
+ overload_scores = [a.context_overload_score for a in analysis_list if a.context_overload_score is not None]
+
+ avg_context_relevance = sum(context_scores) / len(context_scores) if context_scores else 0.0
+ avg_ground_truth_validity = sum(validity_scores) / len(validity_scores) if validity_scores else 0.0
+ avg_context_overload = sum(overload_scores) / len(overload_scores) if overload_scores else 0.0
+
+ # Generate recommendations
+ recommendations = UnusedChunkAnalyzer._generate_recommendations(
+ analysis_list, category_distribution
+ )
+
+ return UnusedChunkSummary(
+ total_questions_analyzed=total_questions,
+ questions_with_unused_chunks=questions_with_unused_chunks,
+ category_distribution=category_distribution,
+ avg_unused_chunks_per_question=round(avg_unused_chunks, 2),
+ avg_context_relevance_score=round(avg_context_relevance, 3),
+ avg_ground_truth_validity_score=round(avg_ground_truth_validity, 3),
+ avg_context_overload_score=round(avg_context_overload, 3),
+ recommendations=recommendations
+ )
+
+ @staticmethod
+ def _generate_recommendations(
+ analysis_list: List[UnusedChunkAnalysis],
+ category_distribution: Dict[str, int]
+ ) -> List[str]:
+ """Generate actionable recommendations based on analysis."""
+ recommendations = []
+
+ # Context overload recommendations
+ if category_distribution.get('context_overload', 0) > 0:
+ recommendations.append("๐ Reduce chunks sent to LLM to prevent information overload")
+ recommendations.append("โก Implement dynamic chunk selection based on query complexity")
+
+ # Context irrelevance recommendations
+ if category_distribution.get('context_irrelevant', 0) > 0:
+ recommendations.append("๐ฏ Improve retrieval relevance by fine-tuning embedding models")
+ recommendations.append("๐ Implement chunk pre-filtering based on semantic similarity")
+
+ # Ground truth validity recommendations
+ if category_distribution.get('ground_truth_invalid', 0) > 0:
+ recommendations.append("โ
Review and validate ground truth answers for accuracy")
+ recommendations.append("๐ Implement ground truth quality assessment pipeline")
+
+ # Answer generation failure recommendations
+ if category_distribution.get('answer_generation_failure', 0) > 0:
+ recommendations.append("๐ค Optimize LLM prompts for better context utilization")
+ recommendations.append("๐ Implement answer generation quality checks")
+
+ # General recommendations
+ if len(analysis_list) > 0:
+ avg_unused = sum(a.unused_count for a in analysis_list) / len(analysis_list)
+ if avg_unused > 5:
+ recommendations.append("โ๏ธ Balance chunk quantity vs. quality for optimal performance")
+
+ recommendations.append("๐ Monitor chunk utilization patterns for continuous improvement")
+
+ return recommendations
+
+ @staticmethod
+ def _create_empty_summary() -> UnusedChunkSummary:
+ """Create empty summary structure."""
+ return UnusedChunkSummary(
+ total_questions_analyzed=0,
+ questions_with_unused_chunks=0,
+ category_distribution={},
+ avg_unused_chunks_per_question=0.0,
+ avg_context_relevance_score=0.0,
+ avg_ground_truth_validity_score=0.0,
+ avg_context_overload_score=0.0,
+ recommendations=[]
+ )
+
+ @staticmethod
+ def format_analysis_for_excel(analysis_list: List[UnusedChunkAnalysis]) -> List[Dict]:
+ """
+ Format unused chunk analysis for Excel export.
+
+ Args:
+ analysis_list: List of UnusedChunkAnalysis objects
+
+ Returns:
+ List of dictionaries formatted for Excel
+ """
+ formatted_data = []
+
+ for analysis in analysis_list:
+ formatted_row = {
+ 'Query': analysis.query,
+ 'Generated Answer': analysis.answer,
+ 'Ground Truth': analysis.ground_truth,
+ 'Sent to LLM Chunk Count': analysis.total_sent_to_llm,
+ 'Used Chunk Count': analysis.total_used,
+ 'Unused Chunk Count': analysis.unused_count,
+ 'Unused Chunk IDs': json.dumps(analysis.unused_chunks, separators=(',', ':')),
+ 'Category': analysis.category.value,
+ 'Reasoning': analysis.reasoning,
+ 'Context Relevance Score': analysis.context_relevance_score or 'N/A',
+ 'Ground Truth Validity Score': analysis.ground_truth_validity_score or 'N/A',
+ 'Context Overload Score': analysis.context_overload_score or 'N/A',
+ 'Chunk Qualification Stats': json.dumps(analysis.chunk_qualification_stats, separators=(',', ':'))
+ }
+ formatted_data.append(formatted_row)
+
+ return formatted_data
+
+ @staticmethod
+ def format_summary_for_excel(summary: UnusedChunkSummary) -> Dict:
+ """
+ Format summary statistics for Excel export.
+
+ Args:
+ summary: UnusedChunkSummary object
+
+ Returns:
+ Dictionary formatted for Excel
+ """
+ return {
+ 'Total Questions Analyzed': summary.total_questions_analyzed,
+ 'Questions with Unused Chunks': summary.questions_with_unused_chunks,
+ 'Percentage with Unused Chunks': f"{(summary.questions_with_unused_chunks / summary.total_questions_analyzed * 100):.1f}%" if summary.total_questions_analyzed > 0 else "0%",
+ 'Average Unused Chunks per Question': summary.avg_unused_chunks_per_question,
+ 'Average Context Relevance Score': summary.avg_context_relevance_score,
+ 'Average Ground Truth Validity Score': summary.avg_ground_truth_validity_score,
+ 'Average Context Overload Score': summary.avg_context_overload_score,
+ 'Category Distribution': json.dumps(summary.category_distribution, separators=(',', ':')),
+ 'Recommendations': ' | '.join(summary.recommendations)
+ }
+
+ @staticmethod
+ def _create_quality_analysis_tab(summary_list: List[Dict]):
+ """
+ Create a comprehensive Quality Analysis tab for Excel export.
+
+ Args:
+ summary_list: List of unused chunk summary dictionaries
+
+ Returns:
+ DataFrame formatted for Quality Analysis tab
+ """
+ try:
+ if not PANDAS_AVAILABLE:
+ logger.error("Pandas is not available. Cannot create quality analysis tab.")
+ return None
+
+ if not summary_list:
+ return pd.DataFrame({
+ 'Metric': ['No Data Available'],
+ 'Value': ['No unused chunk analysis performed'],
+ 'Description': ['Please ensure chunk statistics are available']
+ })
+
+ # Aggregate data across all sheets
+ total_questions = sum(s.get('total_questions_analyzed', 0) for s in summary_list)
+ total_with_unused = sum(s.get('questions_with_unused_chunks', 0) for s in summary_list)
+
+ # Calculate weighted averages
+ total_weight = sum(s.get('total_questions_analyzed', 0) for s in summary_list)
+ weighted_avg_unused = 0
+ weighted_avg_context_relevance = 0
+ weighted_avg_ground_truth_validity = 0
+ weighted_avg_context_overload = 0
+
+ if total_weight > 0:
+ for summary in summary_list:
+ weight = summary.get('total_questions_analyzed', 0) / total_weight
+ weighted_avg_unused += summary.get('avg_unused_chunks_per_question', 0) * weight
+ weighted_avg_context_relevance += summary.get('avg_context_relevance_score', 0) * weight
+ weighted_avg_ground_truth_validity += summary.get('avg_ground_truth_validity_score', 0) * weight
+ weighted_avg_context_overload += summary.get('avg_context_overload_score', 0) * weight
+
+ # Aggregate category distribution
+ combined_category_dist = {}
+ for summary in summary_list:
+ category_dist = summary.get('category_distribution', {})
+ for category, count in category_dist.items():
+ combined_category_dist[category] = combined_category_dist.get(category, 0) + count
+
+ # Collect all recommendations
+ all_recommendations = []
+ for summary in summary_list:
+ recommendations = summary.get('recommendations', [])
+ all_recommendations.extend(recommendations)
+
+ # Remove duplicates while preserving order
+ unique_recommendations = []
+ for rec in all_recommendations:
+ if rec not in unique_recommendations:
+ unique_recommendations.append(rec)
+
+ # Create comprehensive quality analysis DataFrame
+ quality_data = [
+ # Overall Statistics
+ {
+ 'Metric': 'Total Questions Analyzed',
+ 'Value': str(total_questions),
+ 'Description': 'Total number of questions across all sheets'
+ },
+ {
+ 'Metric': 'Questions with Unused Chunks',
+ 'Value': str(total_with_unused),
+ 'Description': 'Questions where chunks were sent to LLM but not used'
+ },
+ {
+ 'Metric': 'Percentage with Unused Chunks',
+ 'Value': f"{(total_with_unused / total_questions * 100):.1f}%" if total_questions > 0 else "0%",
+ 'Description': 'Percentage of questions experiencing chunk underutilization'
+ },
+
+ # Average Metrics
+ {
+ 'Metric': 'Average Unused Chunks per Question',
+ 'Value': f"{weighted_avg_unused:.2f}",
+ 'Description': 'Weighted average of unused chunks across all sheets'
+ },
+ {
+ 'Metric': 'Average Context Relevance Score',
+ 'Value': f"{weighted_avg_context_relevance:.3f}",
+ 'Description': 'Weighted average context relevance (0-1, higher is better)'
+ },
+ {
+ 'Metric': 'Average Ground Truth Validity Score',
+ 'Value': f"{weighted_avg_ground_truth_validity:.3f}",
+ 'Description': 'Weighted average ground truth validity (0-1, higher is better)'
+ },
+ {
+ 'Metric': 'Average Context Overload Score',
+ 'Value': f"{weighted_avg_context_overload:.3f}",
+ 'Description': 'Weighted average context overload (0-1, lower is better)'
+ },
+
+ # Category Distribution
+ {
+ 'Metric': 'Context Irrelevant Issues',
+ 'Value': str(combined_category_dist.get('context_irrelevant', 0)),
+ 'Description': 'Questions with low chunk utilization due to irrelevant context'
+ },
+ {
+ 'Metric': 'Ground Truth Invalid Issues',
+ 'Value': str(combined_category_dist.get('ground_truth_invalid', 0)),
+ 'Description': 'Questions with invalid or incorrect ground truth answers'
+ },
+ {
+ 'Metric': 'Context Overload Issues',
+ 'Value': str(combined_category_dist.get('context_overload', 0)),
+ 'Description': 'Questions with too many chunks causing information overload'
+ },
+ {
+ 'Metric': 'Answer Generation Failures',
+ 'Value': str(combined_category_dist.get('answer_generation_failure', 0)),
+ 'Description': 'Questions where LLM failed to generate meaningful answers'
+ },
+ {
+ 'Metric': 'Mixed Issues',
+ 'Value': str(combined_category_dist.get('mixed_issues', 0)),
+ 'Description': 'Questions with multiple contributing factors'
+ },
+
+ # Recommendations
+ {
+ 'Metric': 'Total Recommendations',
+ 'Value': str(len(unique_recommendations)),
+ 'Description': 'Number of actionable recommendations generated'
+ }
+ ]
+
+ # Add individual recommendations
+ for i, recommendation in enumerate(unique_recommendations, 1):
+ quality_data.append({
+ 'Metric': f'Recommendation {i}',
+ 'Value': recommendation,
+ 'Description': 'Actionable improvement suggestion'
+ })
+
+ return pd.DataFrame(quality_data)
+
+ except Exception as e:
+ logger.error(f"Error creating quality analysis tab: {e}")
+ return pd.DataFrame({
+ 'Metric': ['Error'],
+ 'Value': [f'Failed to create quality analysis: {str(e)}'],
+ 'Description': ['Please check logs for details']
+ })
+
+ @staticmethod
+ def _create_detailed_analysis_tab(analysis_list: List[UnusedChunkAnalysis]):
+ """
+ Create a detailed analysis tab showing individual questions with unused chunks.
+
+ Args:
+ analysis_list: List of UnusedChunkAnalysis objects
+
+ Returns:
+ DataFrame formatted for detailed analysis tab
+ """
+ try:
+ if not PANDAS_AVAILABLE:
+ logger.error("Pandas is not available. Cannot create detailed analysis tab.")
+ return None
+
+ if not analysis_list:
+ return pd.DataFrame({
+ 'Query': ['No Data Available'],
+ 'Generated Answer': ['No unused chunk analysis performed'],
+ 'Ground Truth': ['Please ensure chunk statistics are available'],
+ 'Category': ['N/A'],
+ 'Reasoning': ['N/A']
+ })
+
+ # Convert analysis objects to DataFrame format
+ detailed_data = []
+ for analysis in analysis_list:
+ row_data = {
+ 'Query': analysis.query,
+ 'Generated Answer': analysis.answer,
+ 'Ground Truth': analysis.ground_truth,
+ 'Sent to LLM Chunk Count': analysis.total_sent_to_llm,
+ 'Used Chunk Count': analysis.total_used,
+ 'Unused Chunk Count': analysis.unused_count,
+ 'Unused Chunk IDs': json.dumps(analysis.unused_chunks, separators=(',', ':')),
+ 'Category': analysis.category.value,
+ 'Reasoning': analysis.reasoning,
+ 'Context Relevance Score': analysis.context_relevance_score or 'N/A',
+ 'Ground Truth Validity Score': analysis.ground_truth_validity_score or 'N/A',
+ 'Context Overload Score': analysis.context_overload_score or 'N/A',
+ 'Chunk Qualification Stats': json.dumps(analysis.chunk_qualification_stats, separators=(',', ':'))
+ }
+ detailed_data.append(row_data)
+
+ return pd.DataFrame(detailed_data)
+
+ except Exception as e:
+ logger.error(f"Error creating detailed analysis tab: {e}")
+ return pd.DataFrame({
+ 'Query': ['Error'],
+ 'Generated Answer': [f'Failed to create detailed analysis: {str(e)}'],
+ 'Ground Truth': ['Please check logs for details'],
+ 'Category': ['Error'],
+ 'Reasoning': ['Error']
+ })
diff --git a/Evaluation/RAG_Evaluator/start_ui.py b/Evaluation/RAG_Evaluator/start_ui.py
new file mode 100644
index 00000000..2fe39329
--- /dev/null
+++ b/Evaluation/RAG_Evaluator/start_ui.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+# Prevent Python from creating .pyc files
+import sys
+sys.dont_write_bytecode = True
+
+"""
+RAG Evaluator UI Startup Script
+===============================
+
+This script starts the RAG Evaluator web interface with the enhanced UI.
+
+Usage:
+ python start_ui.py
+
+The UI will be available at:
+ - Main Interface: http://localhost:8001
+ - API Documentation: http://localhost:8001/api/docs
+ - Interactive API: http://localhost:8001/api/redoc
+
+Features:
+ - Modern drag & drop file upload
+ - Real-time progress tracking
+ - Performance optimization settings
+ - Beautiful result visualization
+ - Downloadable reports
+"""
+
+import os
+import sys
+import webbrowser
+import time
+from pathlib import Path
+
+def check_dependencies():
+ """Check if required dependencies are installed"""
+ required_packages = [
+ 'fastapi',
+ 'uvicorn',
+ 'pandas',
+ 'openpyxl',
+ 'aiohttp',
+ 'ragas',
+ 'openai',
+ 'langchain_openai',
+ 'datasets',
+ 'transformers'
+ ]
+
+ missing_packages = []
+
+ for package in required_packages:
+ try:
+ __import__(package)
+ except ImportError:
+ missing_packages.append(package)
+
+ if missing_packages:
+ print("โ Missing required packages:")
+ for package in missing_packages:
+ print(f" โข {package}")
+
+ print("\n๐ก Installation options:")
+ print(" 1. Install all requirements:")
+ print(" pip install -r src/requirements.txt")
+ print("\n 2. Install missing packages individually:")
+ print(f" pip install {' '.join(missing_packages)}")
+
+ # Provide specific guidance for problematic packages
+ if 'ragas' in missing_packages:
+ print("\n ๐ก For RAGAS: pip install ragas")
+ if 'transformers' in missing_packages:
+ print(" ๐ก For Transformers: pip install transformers torch")
+ if any(pkg in missing_packages for pkg in ['openai', 'langchain_openai']):
+ print(" ๐ก For OpenAI: pip install openai langchain-openai")
+
+ print("\nโ ๏ธ Note: Some packages require additional system dependencies")
+ print(" See README.md for detailed installation instructions")
+ return False
+
+ return True
+
+def start_server():
+ """Start the FastAPI server"""
+ try:
+ # Add src directory to Python path
+ src_path = Path(__file__).parent / "src"
+ if str(src_path) not in sys.path:
+ sys.path.insert(0, str(src_path))
+
+ # Change to src directory
+ os.chdir(src_path)
+
+ # Import and start the app
+ from routes.app import app
+ import uvicorn
+
+ print("๐ Starting RAG Evaluator UI Server...")
+ print("=" * 60)
+ print("๐ Main Interface: http://localhost:8001")
+ print("๐ API Documentation: http://localhost:8001/api/docs")
+ print("๐ Interactive API: http://localhost:8001/api/redoc")
+ print("๐ก Health Check: http://localhost:8001/api/health")
+ print("=" * 60)
+ print("๐ฏ Features:")
+ print(" โข Drag & drop file upload")
+ print(" โข Real-time progress tracking")
+ print(" โข Performance optimization presets")
+ print(" โข Beautiful result visualization")
+ print(" โข Async batch processing (3-5x faster)")
+ print("=" * 60)
+ print("โน๏ธ Press Ctrl+C to stop the server")
+ print("")
+
+ # Open browser after a short delay
+ def open_browser():
+ time.sleep(2)
+ try:
+ webbrowser.open("http://localhost:8001")
+ print("๐ Opened browser automatically")
+ except Exception as e:
+ print(f"๐ป Could not open browser automatically: {e}")
+ print("๐ป Please open http://localhost:8001 in your browser manually")
+
+ import threading
+ browser_thread = threading.Thread(target=open_browser)
+ browser_thread.daemon = True
+ browser_thread.start()
+
+ # Start the server
+ uvicorn.run(
+ app,
+ host="0.0.0.0",
+ port=8001,
+ reload=False, # Disable reload in production
+ access_log=True
+ )
+
+ except KeyboardInterrupt:
+ print("\n")
+ print("๐ Server stopped by user")
+ print("๐ Thank you for using RAG Evaluator!")
+ except ImportError as e:
+ print(f"โ Import error: {e}")
+ print("๐ก Make sure you're in the RAG_Evaluator directory")
+ print("๐ก Try: cd RAG_Evaluator && python start_ui.py")
+ print("๐ก Install missing dependencies: pip install -r src/requirements.txt")
+ except PermissionError as e:
+ print(f"โ Permission error: {e}")
+ print("๐ก Try running with appropriate permissions")
+ print("๐ก Check if port 8001 is available or in use by another application")
+ except OSError as e:
+ if "Address already in use" in str(e):
+ print("โ Port 8001 is already in use")
+ print("๐ก Stop any other instances of the application")
+ print("๐ก Or wait a few moments and try again")
+ else:
+ print(f"โ Network error: {e}")
+ print("๐ก Check your network configuration")
+ except Exception as e:
+ print(f"โ Unexpected error starting server: {e}")
+ print("๐ก Check the error message above and try again")
+ print("๐ก For support, see README.md or create an issue on GitHub")
+
+def show_help():
+ """Show help information"""
+ print(__doc__)
+ print("\n๐ง Troubleshooting:")
+ print(" โข Make sure you're in the RAG_Evaluator directory")
+ print(" โข Install dependencies: pip install -r src/requirements.txt")
+ print(" โข Check Python version: Python 3.8+ required")
+ print(" โข Ensure you have sufficient disk space (>1GB recommended)")
+ print(" โข Check internet connectivity for API calls")
+ print(" โข For issues, check the console output for error details")
+ print("\n๐ Command Line Options:")
+ print(" โข python start_ui.py - Start the server")
+ print(" โข python start_ui.py --help - Show this help")
+ print(" โข python start_ui.py --version - Show version information")
+ print("\n๐ Accessing the Application:")
+ print(" โข Web Interface: http://localhost:8001")
+ print(" โข API Docs: http://localhost:8001/api/docs")
+ print(" โข Health Check: http://localhost:8001/api/health")
+ print("\n๐ For detailed documentation, see README.md and UI_README.md")
+
+def check_python_version():
+ """Check if Python version is compatible"""
+ if sys.version_info < (3, 8):
+ print("โ Python 3.8 or higher is required")
+ print(f"๐ก Current Python version: {sys.version}")
+ print("๐ก Please upgrade Python and try again")
+ return False
+ return True
+
+def main():
+ """Main entry point"""
+
+ # Check command line arguments
+ if len(sys.argv) > 1:
+ if sys.argv[1] in ['-h', '--help', 'help']:
+ show_help()
+ return
+ elif sys.argv[1] in ['-v', '--version', 'version']:
+ print("๐ง RAG Evaluator UI v2.0.0 - Async Batch Processing Edition")
+ print("๐
Built: 2024")
+ print(f"๐ Python: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} (3.8+ required)")
+ print("๐ FastAPI + Modern Web UI")
+ print("๐ RAGAS + CRAG + LLM Evaluation")
+ print("โก 3-5x faster async processing")
+ return
+
+ print("๐ง RAG Evaluator - Advanced Performance Testing Suite")
+ print("=" * 60)
+
+ # Check Python version
+ if not check_python_version():
+ return
+
+ # Check if we're in the right directory
+ if not Path("src/routes/app.py").exists():
+ print("โ Error: Not in RAG_Evaluator directory")
+ print("๐ก Please run this script from the RAG_Evaluator root directory")
+ print("๐ก Example: cd RAG_Evaluator && python start_ui.py")
+ return
+
+ # Check dependencies
+ print("๐ Checking dependencies...")
+ if not check_dependencies():
+ return
+
+ print("โ
All dependencies found")
+
+ # Check for optional dependencies
+ optional_packages = ['pymongo']
+ missing_optional = []
+
+ for package in optional_packages:
+ try:
+ __import__(package)
+ except ImportError:
+ missing_optional.append(package)
+
+ if missing_optional:
+ print("โน๏ธ Optional features available with additional packages:")
+ if 'pymongo' in missing_optional:
+ print(" โข MongoDB result storage: pip install pymongo")
+
+ # System info
+ print(f"๐ Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")
+ print(f"๐ Working directory: {os.getcwd()}")
+ print(f"๐ง Platform: {sys.platform}")
+
+ print("")
+
+ # Start the server
+ start_server()
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/Evaluation/RAG_Evaluator/test_ui_integration.html b/Evaluation/RAG_Evaluator/test_ui_integration.html
new file mode 100644
index 00000000..e24ca03b
--- /dev/null
+++ b/Evaluation/RAG_Evaluator/test_ui_integration.html
@@ -0,0 +1,379 @@
+
+
+
+
+
+ Test Quality Analysis UI Integration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
100
+
Total Processed
+
+
+
+
+
+
+
+
+
+ Download Results
+
+
+
+ Show Quality Analysis
+
+
+
+
+
+
Quality Analysis - Unused Chunk Analysis
+
+
+
+
+
+
+
100
+
Total Questions
+
+
+
+
25
+
With Unused Chunks
+
+
+
+
25%
+
Unused Chunks %
+
+
+
+
3.2
+
Avg Unused/Question
+
+
+
+
+
+
+
Issue Categories
+
+
+
+
+
+
+
+
Recommendations
+
+
+
+
+
+
+
+
Detailed Analysis
+
+
+
+
+ Query
+ Category
+ Unused Chunks
+ Context Relevance
+ Ground Truth Validity
+ Context Overload
+ Reasoning
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Evaluation/RAG_Evaluator/test_unused_chunk_analysis.py b/Evaluation/RAG_Evaluator/test_unused_chunk_analysis.py
new file mode 100644
index 00000000..c8038fba
--- /dev/null
+++ b/Evaluation/RAG_Evaluator/test_unused_chunk_analysis.py
@@ -0,0 +1,203 @@
+#!/usr/bin/env python3
+"""
+Test script for the new Unused Chunk Analysis functionality.
+This script tests the core functionality without requiring the full RAG evaluation system.
+"""
+
+import sys
+import os
+sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
+
+from utils.unusedChunkAnalyzer import UnusedChunkAnalyzer, UnusedChunkAnalysis, UnusedChunkSummary
+
+def test_unused_chunk_analyzer():
+ """Test the UnusedChunkAnalyzer functionality."""
+ print("๐งช Testing Unused Chunk Analyzer...")
+
+ # Sample data for testing
+ queries = [
+ "What is the capital of France?",
+ "How does photosynthesis work?",
+ "What is the meaning of life?",
+ "Explain quantum computing",
+ "What is machine learning?"
+ ]
+
+ answers = [
+ "The capital of France is Paris.",
+ "Photosynthesis is the process by which plants convert sunlight into energy.",
+ "I cannot answer that question.",
+ "Quantum computing uses quantum mechanical phenomena to process information.",
+ "Machine learning is a subset of artificial intelligence."
+ ]
+
+ ground_truths = [
+ "Paris",
+ "Photosynthesis is the process by which plants convert sunlight into energy.",
+ "The meaning of life is a philosophical question with no single answer.",
+ "Quantum computing uses quantum mechanical phenomena to process information.",
+ "Machine learning is a subset of artificial intelligence."
+ ]
+
+ # Sample chunk statistics
+ chunk_statistics_list = [
+ {
+ 'sent_to_llm_chunk_count': 8,
+ 'used_in_answer_chunk_count': 3,
+ 'sent_to_llm_chunk_ids': ['chunk1', 'chunk2', 'chunk3', 'chunk4', 'chunk5', 'chunk6', 'chunk7', 'chunk8'],
+ 'used_in_answer_chunk_ids': ['chunk1', 'chunk2', 'chunk3'],
+ 'chunk_qualification_stats': {'qualified': 6, 'unqualified': 2}
+ },
+ {
+ 'sent_to_llm_chunk_count': 5,
+ 'used_in_answer_chunk_count': 4,
+ 'sent_to_llm_chunk_ids': ['chunk1', 'chunk2', 'chunk3', 'chunk4', 'chunk5'],
+ 'used_in_answer_chunk_ids': ['chunk1', 'chunk2', 'chunk3', 'chunk4'],
+ 'chunk_qualification_stats': {'qualified': 5, 'unqualified': 0}
+ },
+ {
+ 'sent_to_llm_chunk_count': 20,
+ 'used_in_answer_chunk_count': 2,
+ 'sent_to_llm_chunk_ids': [f'chunk{i}' for i in range(1, 21)],
+ 'used_in_answer_chunk_ids': ['chunk1', 'chunk2'],
+ 'chunk_qualification_stats': {'qualified': 15, 'unqualified': 5}
+ },
+ {
+ 'sent_to_llm_chunk_count': 6,
+ 'used_in_answer_chunk_count': 5,
+ 'sent_to_llm_chunk_ids': ['chunk1', 'chunk2', 'chunk3', 'chunk4', 'chunk5', 'chunk6'],
+ 'used_in_answer_chunk_ids': ['chunk1', 'chunk2', 'chunk3', 'chunk4', 'chunk5'],
+ 'chunk_qualification_stats': {'qualified': 6, 'unqualified': 0}
+ },
+ {
+ 'sent_to_llm_chunk_count': 4,
+ 'used_in_answer_chunk_count': 3,
+ 'sent_to_llm_chunk_ids': ['chunk1', 'chunk2', 'chunk3', 'chunk4'],
+ 'used_in_answer_chunk_ids': ['chunk1', 'chunk2', 'chunk3'],
+ 'chunk_qualification_stats': {'qualified': 4, 'unqualified': 0}
+ }
+ ]
+
+ # Sample LLM evaluation results
+ llm_evaluation_results = [
+ {'context_relevancy_score': 0.8, 'ground_truth_validity_score': 0.9},
+ {'context_relevancy_score': 0.9, 'ground_truth_validity_score': 0.9},
+ {'context_relevancy_score': 0.6, 'ground_truth_validity_score': 0.7},
+ {'context_relevancy_score': 0.8, 'ground_truth_validity_score': 0.9},
+ {'context_relevancy_score': 0.9, 'ground_truth_validity_score': 0.9}
+ ]
+
+ try:
+ # Test the main analysis function
+ print("๐ Running unused chunk analysis...")
+ analysis_list, summary = UnusedChunkAnalyzer.analyze_unused_chunks(
+ queries, answers, ground_truths, chunk_statistics_list, llm_evaluation_results
+ )
+
+ print(f"โ
Analysis completed successfully!")
+ print(f"๐ Questions analyzed: {summary.total_questions_analyzed}")
+ print(f"๐ Questions with unused chunks: {summary.questions_with_unused_chunks}")
+ print(f"๐ Category distribution: {summary.category_distribution}")
+ print(f"๐ก Recommendations: {len(summary.recommendations)}")
+
+ # Test Excel formatting
+ print("\n๐ Testing Excel formatting...")
+ excel_data = UnusedChunkAnalyzer.format_analysis_for_excel(analysis_list)
+ print(f"โ
Excel formatting successful: {len(excel_data)} rows")
+
+ summary_excel = UnusedChunkAnalyzer.format_summary_for_excel(summary)
+ print(f"โ
Summary Excel formatting successful: {len(summary_excel)} fields")
+
+ # Test quality analysis tab creation
+ print("\n๐ Testing Quality Analysis tab creation...")
+ summary_list = [summary.__dict__]
+ quality_tab = UnusedChunkAnalyzer._create_quality_analysis_tab(summary_list)
+ if quality_tab is not None:
+ print(f"โ
Quality Analysis tab created: {len(quality_tab)} rows")
+ else:
+ print("โ ๏ธ Quality Analysis tab creation failed (pandas not available)")
+
+ # Test detailed analysis tab creation
+ print("\n๐ Testing Detailed Analysis tab creation...")
+ detailed_tab = UnusedChunkAnalyzer._create_detailed_analysis_tab(analysis_list)
+ if detailed_tab is not None:
+ print(f"โ
Detailed Analysis tab created: {len(detailed_tab)} rows")
+ else:
+ print("โ ๏ธ Detailed Analysis tab creation failed (pandas not available)")
+
+ print("\n๐ All tests passed successfully!")
+ return True
+
+ except Exception as e:
+ print(f"โ Test failed with error: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+def test_categorization_logic():
+ """Test the categorization logic specifically."""
+ print("\n๐งช Testing categorization logic...")
+
+ # Test different scenarios
+ test_cases = [
+ {
+ 'name': 'Context Overload',
+ 'sent_count': 18,
+ 'used_count': 3,
+ 'answer': 'Good answer',
+ 'expected_category': 'context_overload'
+ },
+ {
+ 'name': 'Context Irrelevant',
+ 'sent_count': 8,
+ 'used_count': 2,
+ 'answer': 'Good answer',
+ 'expected_category': 'context_irrelevant'
+ },
+ {
+ 'name': 'Answer Generation Failure',
+ 'sent_count': 5,
+ 'used_count': 2,
+ 'answer': 'I cannot answer',
+ 'expected_category': 'answer_generation_failure'
+ }
+ ]
+
+ for test_case in test_cases:
+ print(f" Testing: {test_case['name']}")
+
+ # Create mock chunk stats
+ chunk_stats = {
+ 'sent_to_llm_chunk_count': test_case['sent_count'],
+ 'used_in_answer_chunk_count': test_case['used_count']
+ }
+
+ # Test categorization
+ category, reasoning = UnusedChunkAnalyzer._categorize_question(
+ "test query", test_case['answer'], "test ground truth", chunk_stats
+ )
+
+ print(f" Expected: {test_case['expected_category']}")
+ print(f" Got: {category.value}")
+ print(f" Reasoning: {reasoning}")
+
+ if category.value == test_case['expected_category']:
+ print(" โ
PASS")
+ else:
+ print(" โ FAIL")
+
+ print("โ
Categorization logic tests completed!")
+
+if __name__ == "__main__":
+ print("๐ Starting Unused Chunk Analysis Tests...")
+
+ # Run main functionality test
+ success = test_unused_chunk_analyzer()
+
+ if success:
+ # Run categorization logic test
+ test_categorization_logic()
+ print("\n๐ All tests completed successfully!")
+ else:
+ print("\nโ Tests failed. Please check the implementation.")
+ sys.exit(1)