Spaces:
Sleeping
Sleeping
| // CEFR Assessment Web App JavaScript | |
| class CEFRApp { | |
| constructor() { | |
| this.elements = { | |
| form: document.getElementById('assessment-form'), | |
| textInput: document.getElementById('text-input'), | |
| charCount: document.getElementById('char-count'), | |
| assessBtn: document.getElementById('assess-btn'), | |
| btnLoader: document.getElementById('btn-loader'), | |
| btnText: document.querySelector('#assess-btn .btn-text'), | |
| clearBtn: document.getElementById('clear-btn'), | |
| resultsSection: document.getElementById('results-section'), | |
| totalSentences: document.getElementById('total-sentences'), | |
| avgConfidence: document.getElementById('avg-confidence'), | |
| dominantLevel: document.getElementById('dominant-level'), | |
| distributionBars: document.getElementById('distribution-bars'), | |
| annotatedText: document.getElementById('annotated-text'), | |
| sentenceTbody: document.getElementById('sentence-tbody'), | |
| toggleHighlight: document.getElementById('toggle-highlight'), | |
| errorModal: document.getElementById('error-modal'), | |
| errorMessage: document.getElementById('error-message'), | |
| }; | |
| this.cefrStyles = { | |
| 'A1': { color: '#E74C3C', name: 'A1 - Beginner' }, | |
| 'A2': { color: '#E67E22', name: 'A2 - Elementary' }, | |
| 'B1': { color: '#F39C12', name: 'B1 - Intermediate' }, | |
| 'B2': { color: '#27AE60', name: 'B2 - Upper Intermediate' }, | |
| 'C1': { color: '#3498DB', name: 'C1 - Advanced' }, | |
| 'C2': { color: '#9B59B6', name: 'C2 - Proficient' }, | |
| }; | |
| this.showHighlights = true; | |
| this.init(); | |
| } | |
| init() { | |
| // Event Listeners | |
| this.elements.form.addEventListener('submit', (e) => this.handleSubmit(e)); | |
| this.elements.clearBtn.addEventListener('click', () => this.clearText()); | |
| this.elements.textInput.addEventListener('input', () => this.updateCharCount()); | |
| this.elements.toggleHighlight.addEventListener('click', () => this.toggleHighlighting()); | |
| // Modal close events | |
| document.querySelectorAll('.modal-close').forEach(btn => { | |
| btn.addEventListener('click', () => this.hideError()); | |
| }); | |
| this.elements.errorModal.addEventListener('click', (e) => { | |
| if (e.target === this.elements.errorModal) { | |
| this.hideError(); | |
| } | |
| }); | |
| // Initial char count | |
| this.updateCharCount(); | |
| } | |
| updateCharCount() { | |
| const count = this.elements.textInput.value.length; | |
| const maxLength = 50000; | |
| this.elements.charCount.textContent = `${count.toLocaleString()} / ${maxLength.toLocaleString()} characters`; | |
| if (count > maxLength * 0.9) { | |
| this.elements.charCount.style.color = '#E74C3C'; | |
| } else if (count > maxLength * 0.8) { | |
| this.elements.charCount.style.color = '#F39C12'; | |
| } else { | |
| this.elements.charCount.style.color = '#64748B'; | |
| } | |
| } | |
| async handleSubmit(e) { | |
| e.preventDefault(); | |
| const text = this.elements.textInput.value.trim(); | |
| if (!text) { | |
| this.showError('Please enter some text to analyze.'); | |
| return; | |
| } | |
| this.setLoading(true); | |
| this.hideResults(); | |
| try { | |
| const response = await fetch('/assess', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ text }), | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(data.error || 'An error occurred'); | |
| } | |
| this.displayResults(data); | |
| this.showResults(); | |
| // Scroll to results | |
| setTimeout(() => { | |
| this.elements.resultsSection.scrollIntoView({ behavior: 'smooth' }); | |
| }, 100); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| this.showError(error.message); | |
| } finally { | |
| this.setLoading(false); | |
| } | |
| } | |
| setLoading(loading) { | |
| if (loading) { | |
| this.elements.assessBtn.disabled = true; | |
| this.elements.btnLoader.classList.add('active'); | |
| this.elements.btnText.textContent = 'Analyzing...'; | |
| } else { | |
| this.elements.assessBtn.disabled = false; | |
| this.elements.btnLoader.classList.remove('active'); | |
| this.elements.btnText.textContent = 'Analyze Text'; | |
| } | |
| } | |
| displayResults(data) { | |
| // Update stats | |
| this.elements.totalSentences.textContent = data.stats.total_sentences; | |
| this.elements.avgConfidence.textContent = | |
| Math.round(data.stats.avg_confidence * 100) + '%'; | |
| this.elements.dominantLevel.textContent = data.stats.most_common_level.level; | |
| this.elements.dominantLevel.style.color = | |
| this.cefrStyles[data.stats.most_common_level.level]?.color || '#000'; | |
| // Update distribution | |
| this.displayDistribution(data.stats.level_distribution, data.stats.total_sentences); | |
| // Update annotated text | |
| this.displayAnnotatedText(data.results); | |
| // Update table | |
| this.displayTable(data.results); | |
| } | |
| displayDistribution(distribution, total) { | |
| const levels = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']; | |
| this.elements.distributionBars.innerHTML = ''; | |
| levels.forEach(level => { | |
| const count = distribution[level] || 0; | |
| const percentage = total > 0 ? (count / total) * 100 : 0; | |
| const style = this.cefrStyles[level] || { color: '#000' }; | |
| const bar = document.createElement('div'); | |
| bar.className = 'distribution-bar'; | |
| bar.innerHTML = ` | |
| <div class="distribution-label" style="color: ${style.color}"> | |
| ${level} | |
| </div> | |
| <div class="distribution-track"> | |
| <div class="distribution-fill level-${level.toLowerCase()}" | |
| style="width: ${percentage}%;"> | |
| ${percentage > 10 ? Math.round(percentage) + '%' : ''} | |
| </div> | |
| </div> | |
| <div class="distribution-count">${count}</div> | |
| `; | |
| this.elements.distributionBars.appendChild(bar); | |
| }); | |
| } | |
| displayAnnotatedText(results) { | |
| this.elements.annotatedText.innerHTML = ''; | |
| results.forEach((item, index) => { | |
| const style = this.cefrStyles[item.level] || { color: '#000' }; | |
| const annotation = document.createElement('span'); | |
| annotation.className = `annotation level-${item.level.toLowerCase()}`; | |
| annotation.title = `${item.level} - ${this.cefrStyles[item.level].name}`; | |
| annotation.textContent = item.sentence; | |
| this.elements.annotatedText.appendChild(annotation); | |
| // Add single space between sentences instead of newline | |
| if (index < results.length - 1) { | |
| this.elements.annotatedText.appendChild(document.createTextNode(' ')); | |
| } | |
| }); | |
| } | |
| displayTable(results) { | |
| this.elements.sentenceTbody.innerHTML = ''; | |
| results.forEach((item, index) => { | |
| const style = this.cefrStyles[item.level] || { color: '#000' }; | |
| const confidence = Math.round(item.confidence * 100); | |
| const confidenceWidth = confidence; | |
| const row = document.createElement('tr'); | |
| row.innerHTML = ` | |
| <td class="sentence-text">${item.sentence}</td> | |
| <td> | |
| <div class="level-cell"> | |
| <div class="level-indicator level-${item.level.toLowerCase()}" | |
| style="background-color: ${style.color}"> | |
| </div> | |
| <span>${item.level}</span> | |
| </div> | |
| </td> | |
| <td> | |
| ${confidence}% | |
| <div class="confidence-bar"> | |
| <div class="confidence-fill" style="width: ${confidenceWidth}%"></div> | |
| </div> | |
| </td> | |
| `; | |
| this.elements.sentenceTbody.appendChild(row); | |
| }); | |
| } | |
| toggleHighlighting() { | |
| this.showHighlights = !this.showHighlights; | |
| if (this.showHighlights) { | |
| this.elements.toggleHighlight.textContent = 'Hide Markers'; | |
| document.querySelectorAll('.annotation').forEach(annotation => { | |
| annotation.classList.remove('annotation-hidden'); | |
| }); | |
| } else { | |
| this.elements.toggleHighlight.textContent = 'Show Markers'; | |
| document.querySelectorAll('.annotation').forEach(annotation => { | |
| annotation.classList.add('annotation-hidden'); | |
| }); | |
| } | |
| } | |
| clearText() { | |
| this.elements.textInput.value = ''; | |
| this.updateCharCount(); | |
| this.hideResults(); | |
| } | |
| showResults() { | |
| this.elements.resultsSection.style.display = 'block'; | |
| } | |
| hideResults() { | |
| this.elements.resultsSection.style.display = 'none'; | |
| } | |
| showError(message) { | |
| this.elements.errorMessage.textContent = message; | |
| this.elements.errorModal.style.display = 'flex'; | |
| } | |
| hideError() { | |
| this.elements.errorModal.style.display = 'none'; | |
| this.elements.errorMessage.textContent = ''; | |
| } | |
| } | |
| // Initialize app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new CEFRApp(); | |
| }); | |