fffffwl's picture
Initial HF Space for Swedish CEFR web app
0b8530c
// 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();
});