| | |
| | const API_BASE_URL = 'http://127.0.0.1:8000'; |
| | const POLLING_INTERVAL = 5000; |
| |
|
| | |
| | let currentModel = 'pretrained'; |
| | let showProbabilities = true; |
| | let apiStatus = 'connecting'; |
| |
|
| | |
| | document.addEventListener('DOMContentLoaded', function() { |
| | initializeApp(); |
| | setupEventListeners(); |
| | checkApiStatus(); |
| | createInitialCharts(); |
| | }); |
| |
|
| | |
| | function initializeApp() { |
| | console.log('Initializing Transformer Sentiment Analysis Demo'); |
| | updateApiStatus('connecting'); |
| | } |
| |
|
| | |
| | function setupEventListeners() { |
| | |
| | document.getElementById('analyze-btn').addEventListener('click', analyzeSingleText); |
| | document.getElementById('text-input').addEventListener('keypress', function(e) { |
| | if (e.key === 'Enter' && e.ctrlKey) { |
| | analyzeSingleText(); |
| | } |
| | }); |
| |
|
| | |
| | document.getElementById('batch-analyze-btn').addEventListener('click', analyzeBatchText); |
| |
|
| | |
| | document.getElementById('interpret-btn').addEventListener('click', analyzeInterpretability); |
| | document.getElementById('interpret-input').addEventListener('keypress', function(e) { |
| | if (e.key === 'Enter' && e.ctrlKey) { |
| | analyzeInterpretability(); |
| | } |
| | }); |
| |
|
| | |
| | document.querySelectorAll('.tab-btn').forEach(btn => { |
| | btn.addEventListener('click', function() { |
| | switchTab(this.dataset.tab); |
| | }); |
| | }); |
| |
|
| | |
| | document.getElementById('model-select').addEventListener('change', function(e) { |
| | currentModel = e.target.value; |
| | }); |
| | |
| | document.getElementById('show-probabilities').addEventListener('change', function(e) { |
| | showProbabilities = e.target.checked; |
| | }); |
| |
|
| | |
| | document.querySelectorAll('.nav-link').forEach(link => { |
| | link.addEventListener('click', function(e) { |
| | e.preventDefault(); |
| | const targetId = this.getAttribute('href'); |
| | document.querySelector(targetId).scrollIntoView({ |
| | behavior: 'smooth' |
| | }); |
| | }); |
| | }); |
| |
|
| | |
| | document.querySelectorAll('.arch-component').forEach(component => { |
| | component.addEventListener('click', function() { |
| | const componentType = this.getAttribute('data-component'); |
| | showComponentInfo(componentType); |
| | }); |
| | }); |
| | } |
| |
|
| | |
| | async function checkApiStatus() { |
| | try { |
| | const response = await fetch(`${API_BASE_URL}/health`); |
| | const data = await response.json(); |
| | |
| | if (response.ok && data.status === 'healthy') { |
| | updateApiStatus('online'); |
| | |
| | await getModelInfo(); |
| | } else { |
| | updateApiStatus('offline'); |
| | } |
| | } catch (error) { |
| | console.error('API Health check failed:', error); |
| | updateApiStatus('offline'); |
| | } |
| | |
| | |
| | setTimeout(checkApiStatus, POLLING_INTERVAL); |
| | } |
| |
|
| | function updateApiStatus(status) { |
| | apiStatus = status; |
| | const statusElement = document.getElementById('api-status'); |
| | statusElement.className = `api-status ${status}`; |
| | |
| | const messages = { |
| | 'connecting': 'Conectando a la API...', |
| | 'online': 'API conectada y funcionando', |
| | 'offline': 'API desconectada - usando modo demo' |
| | }; |
| | |
| | statusElement.querySelector('span').textContent = messages[status]; |
| | } |
| |
|
| | |
| | async function getModelInfo() { |
| | try { |
| | const response = await fetch(`${API_BASE_URL}/model/info`); |
| | const data = await response.json(); |
| | |
| | if (response.ok) { |
| | updateModelInfo(data); |
| | } |
| | } catch (error) { |
| | console.error('Failed to get model info:', error); |
| | } |
| | } |
| |
|
| | function updateModelInfo(info) { |
| | |
| | const accuracyElement = document.getElementById('model-accuracy'); |
| | if (accuracyElement) { |
| | |
| | accuracyElement.textContent = '74%'; |
| | } |
| | } |
| |
|
| | |
| | async function analyzeSingleText() { |
| | const textInput = document.getElementById('text-input'); |
| | const text = textInput.value.trim(); |
| | |
| | if (!text) { |
| | alert('Por favor ingresa un texto para analizar'); |
| | return; |
| | } |
| |
|
| | showLoading(true); |
| | |
| | try { |
| | let result; |
| | |
| | if (apiStatus === 'online') { |
| | |
| | const endpoint = showProbabilities ? '/predict/probabilities' : '/predict'; |
| | const response = await fetch(`${API_BASE_URL}${endpoint}`, { |
| | method: 'POST', |
| | headers: { |
| | 'Content-Type': 'application/json', |
| | }, |
| | body: JSON.stringify({ text: text }) |
| | }); |
| | |
| | if (!response.ok) { |
| | throw new Error(`API error: ${response.status}`); |
| | } |
| | |
| | result = await response.json(); |
| | } else { |
| | |
| | result = generateMockSentimentResult(text); |
| | await new Promise(resolve => setTimeout(resolve, 1000)); |
| | } |
| | |
| | displaySingleResult(result); |
| | |
| | } catch (error) { |
| | console.error('Analysis failed:', error); |
| | alert('Error al analizar el texto. Intรฉntalo de nuevo.'); |
| | } finally { |
| | showLoading(false); |
| | } |
| | } |
| |
|
| | function generateMockSentimentResult(text) { |
| | |
| | const positiveWords = ['good', 'great', 'excellent', 'amazing', 'love', 'fantastic', 'bueno', 'excelente', 'genial', 'increรญble']; |
| | const negativeWords = ['bad', 'terrible', 'awful', 'hate', 'horrible', 'worst', 'malo', 'terrible', 'horrible', 'odio']; |
| | |
| | const textLower = text.toLowerCase(); |
| | let positiveScore = 0; |
| | let negativeScore = 0; |
| | |
| | positiveWords.forEach(word => { |
| | if (textLower.includes(word)) positiveScore++; |
| | }); |
| | |
| | negativeWords.forEach(word => { |
| | if (textLower.includes(word)) negativeScore++; |
| | }); |
| | |
| | let predicted_label, confidence; |
| | |
| | if (positiveScore > negativeScore) { |
| | predicted_label = 'POSITIVE'; |
| | confidence = 0.7 + (positiveScore * 0.1); |
| | } else if (negativeScore > positiveScore) { |
| | predicted_label = 'NEGATIVE'; |
| | confidence = 0.7 + (negativeScore * 0.1); |
| | } else { |
| | predicted_label = Math.random() > 0.5 ? 'POSITIVE' : 'NEGATIVE'; |
| | confidence = 0.5 + Math.random() * 0.3; |
| | } |
| | |
| | confidence = Math.min(confidence, 0.99); |
| | |
| | const result = { |
| | text: text, |
| | predicted_label: predicted_label, |
| | confidence: confidence, |
| | model_path: currentModel === 'custom' ? './modelo_rapido' : 'distilbert-base-uncased-finetuned-sst-2-english' |
| | }; |
| | |
| | |
| | if (showProbabilities) { |
| | result.probability_distribution = { |
| | 'POSITIVE': predicted_label === 'POSITIVE' ? confidence : 1 - confidence, |
| | 'NEGATIVE': predicted_label === 'NEGATIVE' ? confidence : 1 - confidence |
| | }; |
| | } |
| | |
| | return result; |
| | } |
| |
|
| | function displaySingleResult(result) { |
| | const resultCard = document.getElementById('single-result'); |
| | const sentimentIcon = document.getElementById('sentiment-icon'); |
| | const sentimentLabel = document.getElementById('sentiment-label'); |
| | const confidenceText = document.getElementById('confidence-text'); |
| | const confidenceBadge = document.getElementById('confidence-badge'); |
| | |
| | |
| | const isPositive = result.predicted_label === 'POSITIVE' || result.predicted_label === 'LABEL_1'; |
| | const sentimentType = isPositive ? 'positive' : 'negative'; |
| | const sentimentName = isPositive ? 'Positivo' : 'Negativo'; |
| | |
| | |
| | sentimentIcon.className = `sentiment-icon ${sentimentType}`; |
| | sentimentLabel.textContent = sentimentName; |
| | confidenceText.textContent = `Confianza: ${(result.confidence * 100).toFixed(1)}%`; |
| | confidenceBadge.textContent = `${(result.confidence * 100).toFixed(1)}%`; |
| | confidenceBadge.style.background = isPositive ? '#28a745' : '#dc3545'; |
| | |
| | |
| | if (result.probability_distribution && showProbabilities) { |
| | createProbabilityChart(result.probability_distribution); |
| | } |
| | |
| | |
| | resultCard.style.display = 'block'; |
| | resultCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); |
| | } |
| |
|
| | function createProbabilityChart(probabilities) { |
| | const ctx = document.getElementById('probability-chart').getContext('2d'); |
| | |
| | |
| | if (window.probabilityChart instanceof Chart) { |
| | window.probabilityChart.destroy(); |
| | } |
| | |
| | const labels = Object.keys(probabilities).map(label => { |
| | return label === 'POSITIVE' || label === 'LABEL_1' ? 'Positivo' : 'Negativo'; |
| | }); |
| | |
| | const data = Object.values(probabilities); |
| | |
| | window.probabilityChart = new Chart(ctx, { |
| | type: 'doughnut', |
| | data: { |
| | labels: labels, |
| | datasets: [{ |
| | data: data, |
| | backgroundColor: ['#28a745', '#dc3545'], |
| | borderWidth: 2, |
| | borderColor: '#fff' |
| | }] |
| | }, |
| | options: { |
| | responsive: true, |
| | maintainAspectRatio: false, |
| | plugins: { |
| | legend: { |
| | position: 'bottom' |
| | }, |
| | tooltip: { |
| | callbacks: { |
| | label: function(context) { |
| | return context.label + ': ' + (context.parsed * 100).toFixed(1) + '%'; |
| | } |
| | } |
| | } |
| | } |
| | } |
| | }); |
| | } |
| |
|
| | |
| | async function analyzeBatchText() { |
| | const batchInput = document.getElementById('batch-input'); |
| | const texts = batchInput.value.trim().split('\n').filter(text => text.trim()); |
| | |
| | if (texts.length === 0) { |
| | alert('Por favor ingresa al menos un texto para analizar'); |
| | return; |
| | } |
| | |
| | if (texts.length > 10) { |
| | alert('Mรกximo 10 textos por lote para esta demo'); |
| | return; |
| | } |
| |
|
| | showLoading(true); |
| | |
| | try { |
| | let results; |
| | |
| | if (apiStatus === 'online') { |
| | |
| | const response = await fetch(`${API_BASE_URL}/predict/batch`, { |
| | method: 'POST', |
| | headers: { |
| | 'Content-Type': 'application/json', |
| | }, |
| | body: JSON.stringify({ texts: texts }) |
| | }); |
| | |
| | if (!response.ok) { |
| | throw new Error(`API error: ${response.status}`); |
| | } |
| | |
| | const data = await response.json(); |
| | results = data.predictions; |
| | } else { |
| | |
| | results = texts.map(text => generateMockSentimentResult(text)); |
| | await new Promise(resolve => setTimeout(resolve, 1500)); |
| | } |
| | |
| | displayBatchResults(results); |
| | |
| | } catch (error) { |
| | console.error('Batch analysis failed:', error); |
| | alert('Error al analizar los textos. Intรฉntalo de nuevo.'); |
| | } finally { |
| | showLoading(false); |
| | } |
| | } |
| |
|
| | function displayBatchResults(results) { |
| | const batchResults = document.getElementById('batch-results'); |
| | const batchResultsList = document.getElementById('batch-results-list'); |
| | |
| | |
| | batchResultsList.innerHTML = ''; |
| | |
| | |
| | results.forEach((result, index) => { |
| | const isPositive = result.predicted_label === 'POSITIVE' || result.predicted_label === 'LABEL_1'; |
| | const sentimentType = isPositive ? 'positive' : 'negative'; |
| | const sentimentName = isPositive ? 'Positivo' : 'Negativo'; |
| | |
| | const resultItem = document.createElement('div'); |
| | resultItem.className = `batch-result-item ${sentimentType}`; |
| | resultItem.innerHTML = ` |
| | <div class="batch-text">${result.text}</div> |
| | <div class="batch-sentiment">${sentimentName}</div> |
| | <div class="batch-confidence">${(result.confidence * 100).toFixed(1)}%</div> |
| | `; |
| | |
| | batchResultsList.appendChild(resultItem); |
| | }); |
| | |
| | |
| | createBatchChart(results); |
| | |
| | |
| | batchResults.style.display = 'block'; |
| | batchResults.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); |
| | } |
| |
|
| | function createBatchChart(results) { |
| | const ctx = document.getElementById('batch-chart').getContext('2d'); |
| | |
| | |
| | if (window.batchChart instanceof Chart) { |
| | window.batchChart.destroy(); |
| | } |
| | |
| | const positiveCount = results.filter(r => |
| | r.predicted_label === 'POSITIVE' || r.predicted_label === 'LABEL_1' |
| | ).length; |
| | const negativeCount = results.length - positiveCount; |
| | |
| | window.batchChart = new Chart(ctx, { |
| | type: 'bar', |
| | data: { |
| | labels: ['Positivo', 'Negativo'], |
| | datasets: [{ |
| | label: 'Cantidad de textos', |
| | data: [positiveCount, negativeCount], |
| | backgroundColor: ['#28a745', '#dc3545'], |
| | borderWidth: 1 |
| | }] |
| | }, |
| | options: { |
| | responsive: true, |
| | maintainAspectRatio: false, |
| | scales: { |
| | y: { |
| | beginAtZero: true, |
| | ticks: { |
| | stepSize: 1 |
| | } |
| | } |
| | }, |
| | plugins: { |
| | legend: { |
| | display: false |
| | }, |
| | title: { |
| | display: true, |
| | text: 'Distribuciรณn de Sentimientos' |
| | } |
| | } |
| | } |
| | }); |
| | } |
| |
|
| | |
| | function createInitialCharts() { |
| | createTrainingChart(); |
| | updatePerformanceCircles(); |
| | } |
| |
|
| | function createTrainingChart() { |
| | const ctx = document.getElementById('training-chart'); |
| | if (!ctx) return; |
| | |
| | |
| | if (window.trainingChart instanceof Chart) { |
| | window.trainingChart.destroy(); |
| | } |
| | |
| | |
| | const epochs = [1, 2, 3]; |
| | const trainLoss = [0.693, 0.350, 0.233]; |
| | const evalLoss = [0.589, 0.524, 0.471]; |
| | const accuracy = [0.65, 0.71, 0.74]; |
| | |
| | window.trainingChart = new Chart(ctx, { |
| | type: 'line', |
| | data: { |
| | labels: epochs.map(e => `Epoch ${e}`), |
| | datasets: [ |
| | { |
| | label: 'Training Loss', |
| | data: trainLoss, |
| | borderColor: '#dc3545', |
| | backgroundColor: 'rgba(220, 53, 69, 0.1)', |
| | tension: 0.1, |
| | yAxisID: 'y' |
| | }, |
| | { |
| | label: 'Validation Loss', |
| | data: evalLoss, |
| | borderColor: '#fd7e14', |
| | backgroundColor: 'rgba(253, 126, 20, 0.1)', |
| | tension: 0.1, |
| | yAxisID: 'y' |
| | }, |
| | { |
| | label: 'Accuracy', |
| | data: accuracy, |
| | borderColor: '#28a745', |
| | backgroundColor: 'rgba(40, 167, 69, 0.1)', |
| | tension: 0.1, |
| | yAxisID: 'y1' |
| | } |
| | ] |
| | }, |
| | options: { |
| | responsive: true, |
| | maintainAspectRatio: false, |
| | interaction: { |
| | mode: 'index', |
| | intersect: false, |
| | }, |
| | plugins: { |
| | title: { |
| | display: true, |
| | text: 'Progreso del Entrenamiento' |
| | }, |
| | legend: { |
| | display: true, |
| | position: 'bottom' |
| | } |
| | }, |
| | scales: { |
| | x: { |
| | display: true, |
| | title: { |
| | display: true, |
| | text: 'รpocas' |
| | } |
| | }, |
| | y: { |
| | type: 'linear', |
| | display: true, |
| | position: 'left', |
| | title: { |
| | display: true, |
| | text: 'Loss' |
| | }, |
| | grid: { |
| | drawOnChartArea: false, |
| | }, |
| | }, |
| | y1: { |
| | type: 'linear', |
| | display: true, |
| | position: 'right', |
| | title: { |
| | display: true, |
| | text: 'Accuracy' |
| | }, |
| | grid: { |
| | drawOnChartArea: false, |
| | }, |
| | min: 0, |
| | max: 1 |
| | }, |
| | } |
| | } |
| | }); |
| | } |
| |
|
| | function updatePerformanceCircles() { |
| | const circles = document.querySelectorAll('.performance-circle'); |
| | circles.forEach(circle => { |
| | const percentage = circle.getAttribute('data-percentage'); |
| | const degrees = (percentage / 100) * 360; |
| | circle.style.background = `conic-gradient(#667eea 0deg ${degrees}deg, #e9ecef ${degrees}deg 360deg)`; |
| | }); |
| | } |
| |
|
| | |
| | function showLoading(show) { |
| | const overlay = document.getElementById('loading-overlay'); |
| | overlay.style.display = show ? 'flex' : 'none'; |
| | } |
| |
|
| | function showComponentInfo(componentType) { |
| | const info = { |
| | 'data': 'Dataset IMDB con 50,000 reseรฑas de pelรญculas para anรกlisis de sentimientos', |
| | 'preprocessing': 'Tokenizaciรณn con DistilBERT, padding y truncation a 512 tokens', |
| | 'model': 'DistilBERT fine-tuneado con 66.9M parรกmetros y 6 capas transformer', |
| | 'api': 'FastAPI con endpoints REST para inferencia individual y por lotes', |
| | 'frontend': 'Interfaz web interactiva con visualizaciones en tiempo real' |
| | }; |
| | |
| | alert(info[componentType] || 'Informaciรณn no disponible'); |
| | } |
| |
|
| | |
| | const exampleTexts = [ |
| | "Esta pelรญcula es absolutamente increรญble!", |
| | "No me gustรณ para nada, muy aburrida", |
| | "El producto llegรณ en perfectas condiciones", |
| | "Terrible experiencia, no lo recomiendo", |
| | "Excelente servicio al cliente", |
| | "La comida estaba deliciosa", |
| | "Pรฉrdida total de tiempo y dinero" |
| | ]; |
| |
|
| | |
| | function addExampleText() { |
| | const textInput = document.getElementById('text-input'); |
| | const randomText = exampleTexts[Math.floor(Math.random() * exampleTexts.length)]; |
| | textInput.value = randomText; |
| | } |
| |
|
| | |
| | function addExampleButtons() { |
| | const inputGroup = document.querySelector('.input-group'); |
| | const exampleBtn = document.createElement('button'); |
| | exampleBtn.className = 'btn-secondary'; |
| | exampleBtn.innerHTML = '<i class="fas fa-lightbulb"></i> Ejemplo'; |
| | exampleBtn.onclick = addExampleText; |
| | inputGroup.appendChild(exampleBtn); |
| | } |
| |
|
| | |
| | document.addEventListener('DOMContentLoaded', function() { |
| | setTimeout(addExampleButtons, 100); |
| | }); |
| |
|
| | |
| | window.addEventListener('unhandledrejection', function(event) { |
| | console.error('Unhandled promise rejection:', event.reason); |
| | if (event.reason.message && event.reason.message.includes('fetch')) { |
| | updateApiStatus('offline'); |
| | } |
| | }); |
| |
|
| | |
| | if ('serviceWorker' in navigator) { |
| | window.addEventListener('load', function() { |
| | navigator.serviceWorker.register('/sw.js').then(function(registration) { |
| | console.log('ServiceWorker registration successful'); |
| | }, function(err) { |
| | console.log('ServiceWorker registration failed: ', err); |
| | }); |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | let currentAttentionData = null; |
| | let currentLayer = 0; |
| | let currentHead = 0; |
| |
|
| | |
| | async function analyzeInterpretability() { |
| | const text = document.getElementById('interpret-input').value.trim(); |
| | |
| | if (!text) { |
| | alert('Please enter a text to analyze.'); |
| | return; |
| | } |
| |
|
| | |
| | document.getElementById('interpret-btn').disabled = true; |
| | document.getElementById('interpret-btn').innerHTML = '<i class="fas fa-spinner fa-spin"></i> Analyzing...'; |
| | document.getElementById('attention-loading').style.display = 'block'; |
| | |
| | |
| | document.getElementById('interpret-prediction').style.display = 'none'; |
| | document.getElementById('attention-results').style.display = 'none'; |
| | document.getElementById('shap-results').style.display = 'none'; |
| | document.getElementById('token-importance').style.display = 'none'; |
| | |
| | |
| | const attentionPlaceholder = document.getElementById('attention-placeholder'); |
| | const shapPlaceholder = document.getElementById('shap-placeholder'); |
| | const tokenPlaceholder = document.getElementById('token-placeholder'); |
| | if (attentionPlaceholder) attentionPlaceholder.style.display = 'none'; |
| | if (shapPlaceholder) shapPlaceholder.style.display = 'none'; |
| | if (tokenPlaceholder) tokenPlaceholder.style.display = 'none'; |
| |
|
| | try { |
| | |
| | const response = await fetch(`${API_BASE_URL}/interpret`, { |
| | method: 'POST', |
| | headers: { |
| | 'Content-Type': 'application/json', |
| | }, |
| | body: JSON.stringify({ text: text }) |
| | }); |
| |
|
| | if (!response.ok) { |
| | throw new Error(`HTTP error! status: ${response.status}`); |
| | } |
| |
|
| | const data = await response.json(); |
| | |
| | |
| | displayInterpretationPrediction(data); |
| | |
| | |
| | displayAttentionVisualization(data); |
| | |
| | |
| | displayShapExplanation(data); |
| | |
| | |
| | await getDetailedAttentionData(text); |
| |
|
| | } catch (error) { |
| | console.error('Error in interpretability analysis:', error); |
| | alert('Error analyzing interpretability. Please check that the server is running.'); |
| | } finally { |
| | |
| | document.getElementById('interpret-btn').disabled = false; |
| | document.getElementById('interpret-btn').innerHTML = '<i class="fas fa-search"></i> Analyze Interpretability'; |
| | document.getElementById('attention-loading').style.display = 'none'; |
| | } |
| | } |
| |
|
| | |
| | function displayInterpretationPrediction(data) { |
| | const predictionDiv = document.getElementById('interpret-prediction'); |
| | const labelSpan = document.getElementById('interpret-pred-label'); |
| | const confidenceSpan = document.getElementById('interpret-pred-confidence'); |
| | |
| | const sentiment = data.predicted_class === 1 ? 'Positive' : 'Negative'; |
| | const confidence = (data.confidence * 100).toFixed(1); |
| | |
| | labelSpan.textContent = sentiment; |
| | labelSpan.className = `prediction-label ${sentiment.toLowerCase()}`; |
| | confidenceSpan.textContent = `${confidence}%`; |
| | |
| | predictionDiv.style.display = 'block'; |
| | } |
| |
|
| | |
| | function displayAttentionVisualization(data) { |
| | const resultsDiv = document.getElementById('attention-results'); |
| | |
| | |
| | if (data.attention_summary_plot) { |
| | const summaryImg = document.getElementById('attention-summary-img'); |
| | summaryImg.src = 'data:image/png;base64,' + data.attention_summary_plot; |
| | summaryImg.style.display = 'block'; |
| | } |
| | |
| | |
| | if (data.attention_heatmap_plot) { |
| | const heatmapImg = document.getElementById('attention-heatmap-img'); |
| | heatmapImg.src = 'data:image/png;base64,' + data.attention_heatmap_plot; |
| | heatmapImg.style.display = 'block'; |
| | } |
| | |
| | resultsDiv.style.display = 'block'; |
| | } |
| |
|
| | |
| | function displayShapExplanation(data) { |
| | const shapDiv = document.getElementById('shap-results'); |
| | const shapImg = document.getElementById('shap-explanation-img'); |
| | const shapNotAvailable = document.getElementById('shap-not-available'); |
| | |
| | if (data.shap_explanation) { |
| | shapImg.src = 'data:image/png;base64,' + data.shap_explanation; |
| | shapImg.style.display = 'block'; |
| | shapNotAvailable.style.display = 'none'; |
| | } else { |
| | shapImg.style.display = 'none'; |
| | shapNotAvailable.style.display = 'block'; |
| | } |
| | |
| | shapDiv.style.display = 'block'; |
| | } |
| |
|
| | |
| | async function getDetailedAttentionData(text) { |
| | try { |
| | const response = await fetch(`${API_BASE_URL}/interpret/attention`, { |
| | method: 'POST', |
| | headers: { |
| | 'Content-Type': 'application/json', |
| | }, |
| | body: JSON.stringify({ text: text }) |
| | }); |
| |
|
| | if (!response.ok) { |
| | throw new Error(`HTTP error! status: ${response.status}`); |
| | } |
| |
|
| | currentAttentionData = await response.json(); |
| | setupInteractiveAttention(); |
| | displayTokenImportance(); |
| |
|
| | } catch (error) { |
| | console.error('Error getting detailed attention data:', error); |
| | } |
| | } |
| |
|
| | |
| | function setupInteractiveAttention() { |
| | if (!currentAttentionData) return; |
| | |
| | const layerSelect = document.getElementById('layer-select'); |
| | const headSelect = document.getElementById('head-select'); |
| | |
| | |
| | layerSelect.innerHTML = ''; |
| | headSelect.innerHTML = ''; |
| | |
| | |
| | const numLayers = currentAttentionData.attention_weights.length; |
| | for (let i = 0; i < numLayers; i++) { |
| | const option = document.createElement('option'); |
| | option.value = i; |
| | option.textContent = `Layer ${i + 1}`; |
| | layerSelect.appendChild(option); |
| | } |
| | |
| | |
| | const numHeads = currentAttentionData.attention_weights[0].length; |
| | for (let i = 0; i < numHeads; i++) { |
| | const option = document.createElement('option'); |
| | option.value = i; |
| | option.textContent = `Head ${i + 1}`; |
| | headSelect.appendChild(option); |
| | } |
| | |
| | |
| | layerSelect.value = numLayers - 1; |
| | headSelect.value = 0; |
| | currentLayer = numLayers - 1; |
| | currentHead = 0; |
| | |
| | |
| | layerSelect.addEventListener('change', function() { |
| | currentLayer = parseInt(this.value); |
| | updateAttentionMatrix(); |
| | }); |
| | |
| | headSelect.addEventListener('change', function() { |
| | currentHead = parseInt(this.value); |
| | updateAttentionMatrix(); |
| | }); |
| | |
| | |
| | updateAttentionMatrix(); |
| | } |
| |
|
| | |
| | function updateAttentionMatrix() { |
| | if (!currentAttentionData) return; |
| | |
| | const matrixDiv = document.getElementById('attention-matrix'); |
| | const attentionWeights = currentAttentionData.attention_weights[currentLayer][currentHead]; |
| | const tokens = currentAttentionData.tokens; |
| | |
| | |
| | const maxTokens = 20; |
| | const displayTokens = tokens.slice(0, maxTokens); |
| | const displayWeights = attentionWeights.slice(0, maxTokens).map(row => row.slice(0, maxTokens)); |
| | |
| | |
| | let html = '<div class="attention-heatmap-table">'; |
| | html += '<table>'; |
| | |
| | |
| | html += '<tr><td></td>'; |
| | displayTokens.forEach(token => { |
| | html += `<td class="token-header">${token}</td>`; |
| | }); |
| | html += '</tr>'; |
| | |
| | |
| | displayTokens.forEach((token, i) => { |
| | html += `<tr><td class="token-header">${token}</td>`; |
| | displayWeights[i].forEach(weight => { |
| | const intensity = weight * 255; |
| | const color = `rgba(102, 126, 234, ${weight})`; |
| | html += `<td style="background-color: ${color}; color: ${weight > 0.5 ? 'white' : 'black'};" title="${weight.toFixed(3)}">${weight.toFixed(2)}</td>`; |
| | }); |
| | html += '</tr>'; |
| | }); |
| | |
| | html += '</table></div>'; |
| | matrixDiv.innerHTML = html; |
| | } |
| |
|
| | |
| | function displayTokenImportance() { |
| | if (!currentAttentionData) return; |
| | |
| | const tokenDiv = document.getElementById('token-importance'); |
| | const barsDiv = document.getElementById('token-bars'); |
| | |
| | |
| | const lastLayerAttention = currentAttentionData.attention_weights[currentAttentionData.attention_weights.length - 1][0]; |
| | const tokenImportance = lastLayerAttention[0].map((_, i) => { |
| | return lastLayerAttention.reduce((sum, row) => sum + row[i], 0) / lastLayerAttention.length; |
| | }); |
| | |
| | |
| | let html = ''; |
| | const maxTokens = 15; |
| | const displayTokens = currentAttentionData.tokens.slice(0, maxTokens); |
| | const displayImportance = tokenImportance.slice(0, maxTokens); |
| | const maxImportance = Math.max(...displayImportance); |
| | |
| | displayTokens.forEach((token, i) => { |
| | const importance = displayImportance[i]; |
| | const percentage = (importance / maxImportance) * 100; |
| | |
| | html += ` |
| | <div class="token-bar"> |
| | <div class="token-bar-label">${token}</div> |
| | <div class="token-bar-fill" style="width: ${percentage}%"></div> |
| | <div class="token-bar-value">${importance.toFixed(3)}</div> |
| | </div> |
| | `; |
| | }); |
| | |
| | barsDiv.innerHTML = html; |
| | tokenDiv.style.display = 'block'; |
| | } |
| |
|
| | |
| | function switchTab(tabName) { |
| | |
| | document.querySelectorAll('.tab-btn').forEach(btn => { |
| | btn.classList.remove('active'); |
| | }); |
| | document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); |
| | |
| | |
| | document.querySelectorAll('.tab-panel').forEach(panel => { |
| | panel.classList.remove('active'); |
| | }); |
| | document.getElementById(`tab-${tabName}`).classList.add('active'); |
| | } |