// Student Feedback Analysis - Optimized JavaScript const Utils = { // Smooth scrolling with automatic offset calculation scrollToSection(el, extraOffset = 0) { if (!el) return; const fixed = document.querySelector('.navbar.fixed-top, header.sticky-top, .sticky-top, .fixed-top'); const base = fixed ? Math.max(0, fixed.getBoundingClientRect().height + 8) : 56; const offset = Math.max(0, base + extraOffset); const rect = el.getBoundingClientRect(); const top = window.pageYOffset + rect.top - offset; window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' }); }, // Auto-hide alerts functionality initAutoHideAlerts() { const alerts = document.querySelectorAll('.alert'); alerts.forEach(alert => { setTimeout(() => { alert.classList.add('fade-out'); setTimeout(() => alert.remove(), 500); }, 2000); }); } }; document.addEventListener('DOMContentLoaded', function() { // DOM Elements const elements = { form: document.getElementById('feedbackForm'), textarea: document.getElementById('feedbackText'), loadingSpinner: document.getElementById('loadingSpinner'), results: document.getElementById('results'), errorMessage: document.getElementById('errorMessage'), analyzeBtn: document.getElementById('analyzeBtn'), originalText: document.getElementById('originalText'), errorText: document.getElementById('errorText'), sentimentResult: document.getElementById('sentimentResult'), sentimentIcon: document.getElementById('sentimentIcon'), sentimentDescription: document.getElementById('sentimentDescription'), topicResult: document.getElementById('topicResult'), topicIcon: document.getElementById('topicIcon'), topicDescription: document.getElementById('topicDescription') }; // Configuration Objects const sentimentConfigs = { positive: { icon: 'fa-smile', label: 'Tích Cực', colorClass: 'sentiment-positive', description: 'Feedback thể hiện thái độ tích cực và hài lòng' }, neutral: { icon: 'fa-meh', label: 'Trung Tính', colorClass: 'sentiment-neutral', description: 'Feedback thể hiện thái độ trung lập, không rõ ràng' }, negative: { icon: 'fa-frown', label: 'Tiêu Cực', colorClass: 'sentiment-negative', description: 'Feedback thể hiện thái độ tiêu cực và không hài lòng' } }; const topicConfigs = { lecturer: { icon: 'fa-user-tie', label: 'Giảng Viên', colorClass: 'topic-lecturer', description: 'Feedback liên quan đến chất lượng giảng dạy của giảng viên' }, training_program: { icon: 'fa-graduation-cap', label: 'Chương Trình Đào Tạo', colorClass: 'topic-training_program', description: 'Feedback về nội dung và cấu trúc chương trình học' }, facility: { icon: 'fa-building', label: 'Cơ Sở Vật Chất', colorClass: 'topic-facility', description: 'Feedback về phòng học, thiết bị và cơ sở hạ tầng' }, others: { icon: 'fa-ellipsis-h', label: 'Khác', colorClass: 'topic-others', description: 'Feedback về các chủ đề khác không thuộc các danh mục trên' } }; const examples = [ "Giảng viên giảng bài rất hay và dễ hiểu, sinh viên rất thích", "Thầy cô rất nhiệt tình và hỗ trợ sinh viên học tập tốt", "Chương trình học rất phù hợp và bổ ích cho sinh viên", "Cơ sở vật chất rất hiện đại và đầy đủ tiện nghi", "Giảng viên giảng bài quá nhanh và khó hiểu", "Chương trình học quá khó và không phù hợp với sinh viên", "Phòng học quá nóng và không có điều hòa, rất khó chịu", "Môn học này có nội dung bình thường, không có gì đặc biệt" ]; // Utility Functions const utils = { showLoading() { elements.loadingSpinner.style.display = 'block'; elements.results.style.display = 'none'; elements.errorMessage.style.display = 'none'; elements.analyzeBtn.disabled = true; elements.analyzeBtn.innerHTML = 'Đang phân tích...'; }, hideLoading() { elements.loadingSpinner.style.display = 'none'; elements.analyzeBtn.disabled = false; elements.analyzeBtn.innerHTML = 'Phân Tích Feedback'; }, showError(message) { elements.errorText.textContent = message; elements.errorMessage.style.display = 'block'; elements.errorMessage.classList.add('fade-in'); elements.results.style.display = 'none'; elements.errorMessage.scrollIntoView({ behavior: 'smooth' }); }, clearForm() { elements.textarea.value = ''; elements.results.style.display = 'none'; elements.errorMessage.style.display = 'none'; this.updateCharCounter(); // Cuộn lên form giống như khi nhấn "Ví Dụ" const formElement = elements.form; const formRect = formElement.getBoundingClientRect(); const scrollTop = window.pageYOffset + formRect.top - 120; // Cùng vị trí như "Ví Dụ" window.scrollTo({ top: scrollTop, behavior: 'smooth' }); }, updateResult(type, value) { const configs = type === 'sentiment' ? sentimentConfigs : topicConfigs; const config = configs[value] || configs[type === 'sentiment' ? 'neutral' : 'others']; const resultEl = elements[`${type}Result`]; const iconEl = elements[`${type}Icon`]; const descEl = elements[`${type}Description`]; resultEl.className = `mb-2 ${config.colorClass}`; iconEl.innerHTML = ``; resultEl.textContent = config.label; descEl.textContent = config.description; }, scrollToResults() { Utils.scrollToSection(elements.results, 105); }, updateCharCounter() { const count = elements.textarea.value.length; const charCount = document.getElementById('charCount'); const charCounter = document.querySelector('.form-text'); if (charCount) charCount.textContent = count; if (charCounter) { charCounter.style.color = count > 500 ? '#dc3545' : count > 300 ? '#ffc107' : '#6c757d'; } } }; // Main Functions async function handleFormSubmit(e) { e.preventDefault(); const feedbackText = elements.textarea.value.trim(); if (!feedbackText) { utils.showError('Vui lòng nhập feedback trước khi phân tích!'); return; } utils.showLoading(); try { const response = await fetch('/predict', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: feedbackText }) }); const data = await response.json(); console.log('Predict response:', data); if (response.ok) { // Display multiple topics with sentiments displayMultipleResults(data.results, feedbackText); // Clear the textarea after successful analysis elements.textarea.value = ''; utils.updateCharCounter(); // Reload feedback history after successful analysis with delay // to ensure database has committed the new data console.log('Reloading feedback history...'); setTimeout(() => { loadFeedbackHistory(1, false); }, 500); } else { utils.showError(data.error || 'Có lỗi xảy ra khi phân tích feedback!'); } } catch (error) { utils.showError('Không thể kết nối đến server. Vui lòng thử lại!'); console.error('Error:', error); } finally { utils.hideLoading(); } } function showExamples() { const randomExample = examples[Math.floor(Math.random() * examples.length)]; elements.textarea.value = randomExample; elements.textarea.dispatchEvent(new Event('input')); // Cuộn trực tiếp với vị trí cao hơn const formElement = elements.form; const formRect = formElement.getBoundingClientRect(); const scrollTop = window.pageYOffset + formRect.top - 120; // Tăng lên 120px để cuộn cao hơn window.scrollTo({ top: scrollTop, behavior: 'smooth' }); } // Event Listeners elements.form.addEventListener('submit', handleFormSubmit); elements.textarea.addEventListener('input', utils.updateCharCounter); document.addEventListener('keydown', function(e) { if (e.ctrlKey && e.key === 'Enter') { elements.form.dispatchEvent(new Event('submit')); } else if (e.key === 'Escape') { utils.clearForm(); } }); // Add utility buttons const buttonContainer = document.createElement('div'); buttonContainer.className = 'd-flex justify-content-center gap-2 mt-3'; const clearBtn = document.createElement('button'); clearBtn.type = 'button'; clearBtn.className = 'btn btn-outline-secondary'; clearBtn.innerHTML = 'Xóa Form'; clearBtn.onclick = () => utils.clearForm(); const exampleBtn = document.createElement('button'); exampleBtn.type = 'button'; exampleBtn.className = 'btn btn-outline-info'; exampleBtn.innerHTML = 'Ví Dụ'; exampleBtn.onclick = showExamples; buttonContainer.appendChild(clearBtn); buttonContainer.appendChild(exampleBtn); elements.form.appendChild(buttonContainer); // Add character counter const charCounter = document.createElement('small'); charCounter.className = 'form-text text-muted'; charCounter.innerHTML = 'Độ dài: 0 ký tự'; elements.textarea.parentNode.appendChild(charCounter); // Load feedback history loadFeedbackHistory(); // Initialize auto-hide alerts Utils.initAutoHideAlerts(); // Initialize time filter initTimeFilter(); // Initialize analysis mode toggle initAnalysisModeToggle(); }); // ===== Feedback History Functions ===== let currentPage = 1; const itemsPerPage = 5; async function loadFeedbackHistory(page = 1, shouldScroll = false) { const historyLoading = document.getElementById('historyLoading'); const historyContent = document.getElementById('historyContent'); const historyPagination = document.getElementById('historyPagination'); const feedbackHistorySection = document.getElementById('feedbackHistory'); historyLoading.style.display = 'block'; historyContent.style.opacity = '0.5'; historyPagination.style.opacity = '0.5'; try { // Get filter parameters const filterParams = getTimeFilterParams(); const queryParams = new URLSearchParams({ page: page, per_page: itemsPerPage, ...filterParams }); console.log('Loading feedback history, page:', page); const response = await fetch(`/api/feedback-history?${queryParams.toString()}`); const data = await response.json(); console.log('Feedback history response:', data); if (response.ok) { displayFeedbackHistory(data.feedbacks); displayPagination(data, page); updateFeedbackCount(data.total); if (shouldScroll && feedbackHistorySection) { // Pagination cuộn thấp xuống một chút Utils.scrollToSection(feedbackHistorySection, -40); } } else { historyContent.innerHTML = '

Không thể tải lịch sử feedback.

'; } } catch (error) { console.error('Error loading feedback history:', error); historyContent.innerHTML = '

Có lỗi xảy ra khi tải lịch sử.

'; } finally { historyLoading.style.display = 'none'; historyContent.style.opacity = '1'; historyPagination.style.opacity = '1'; currentPage = page; } } function displayFeedbackHistory(feedbacks) { const historyContent = document.getElementById('historyContent'); if (!feedbacks || feedbacks.length === 0) { historyContent.innerHTML = '

Chưa có feedback nào. Hãy phân tích feedback đầu tiên!

'; return; } let html = ''; feedbacks.forEach(feedback => { const sentimentConfig = getSentimentConfig(feedback.sentiment); const topicConfig = getTopicConfig(feedback.topic); const date = feedback.created_at; // Thời gian đã được format từ server html += `

${feedback.text}

${date}
${sentimentConfig.label} Tin cậy: ${(feedback.sentiment_confidence * 100).toFixed(1)}%
${topicConfig.label} Tin cậy: ${(feedback.topic_confidence * 100).toFixed(1)}%
`; }); historyContent.innerHTML = html; } function displayPagination(data, currentPage) { const historyPagination = document.getElementById('historyPagination'); if (!data || data.pages <= 1) { historyPagination.innerHTML = ''; return; } // Giới hạn hiển thị tối đa 5 trang const maxPages = 5; let startPage = Math.max(1, currentPage - Math.floor(maxPages / 2)); let endPage = Math.min(data.pages, startPage + maxPages - 1); // Điều chỉnh nếu gần cuối danh sách if (endPage - startPage + 1 < maxPages) { startPage = Math.max(1, endPage - maxPages + 1); } let html = ''; // Thêm phần nhập số trang cụ thể html += `
Đến:
`; historyPagination.innerHTML = html; } function goToPage() { const pageInput = document.getElementById('pageInput'); const page = parseInt(pageInput.value); // Lấy tổng số trang từ pagination data const paginationNav = document.querySelector('.pagination'); if (!paginationNav) return; // Tìm số trang lớn nhất từ pagination const pageLinks = paginationNav.querySelectorAll('.page-link'); let maxPage = 1; pageLinks.forEach(link => { const pageNum = parseInt(link.textContent); if (!isNaN(pageNum) && pageNum > maxPage) { maxPage = pageNum; } }); if (page && page > 0 && page <= maxPage) { loadFeedbackHistory(page, true); } else if (page > maxPage) { showAlert(`Trang tối đa là ${maxPage}`, 'warning'); pageInput.value = maxPage; // Reset về trang tối đa } else { showAlert('Vui lòng nhập số trang hợp lệ', 'warning'); } } // Thêm event listener cho Enter key document.addEventListener('keypress', function(e) { if (e.target.id === 'pageInput' && e.key === 'Enter') { goToPage(); } }); function getSentimentConfig(sentiment) { const configs = { positive: { icon: 'fa-smile', label: 'Tích Cực' }, neutral: { icon: 'fa-meh', label: 'Trung Tính' }, negative: { icon: 'fa-frown', label: 'Tiêu Cực' } }; return configs[sentiment] || configs.neutral; } function getTopicConfig(topic) { const configs = { lecturer: { icon: 'fa-user-tie', label: 'Giảng Viên' }, training_program: { icon: 'fa-graduation-cap', label: 'Chương Trình' }, facility: { icon: 'fa-building', label: 'Cơ Sở Vật Chất' }, others: { icon: 'fa-ellipsis-h', label: 'Khác' } }; return configs[topic] || configs.others; } function getSentimentColor(sentiment) { const colors = { positive: 'success', neutral: 'warning', negative: 'danger' }; return colors[sentiment] || 'secondary'; } // Display multiple topics with sentiments function displayMultipleResults(results, text) { const elements = { results: document.getElementById('results'), originalText: document.getElementById('originalText') }; // Validate elements exist if (!elements.results) { console.error('❌ Results element not found'); return; } if (!results || results.length === 0) { // No topics detected or all below threshold if (elements.originalText) elements.originalText.textContent = text; elements.results.innerHTML = `
Không phát hiện topic rõ ràng trong feedback này.
`; elements.results.style.display = 'block'; elements.results.classList.add('fade-in'); Utils.scrollToSection(elements.results, 105); return; } // Display original text first (before innerHTML clears it) if (elements.originalText) { elements.originalText.textContent = text; } // Build HTML for multiple topics let html = '
Kết quả phân tích:
'; html += '
'; results.forEach(result => { const sentimentConfig = getSentimentConfig(result.sentiment); const topicConfig = getTopicConfig(result.topic); const sentimentColor = getSentimentColor(result.sentiment); html += `
${topicConfig.label}
${sentimentConfig.label} Độ tin cậy: ${(result.confidence * 100).toFixed(1)}%
`; }); html += '
'; // Add original text to the end html += `
Feedback Gốc

${text}

`; elements.results.innerHTML = html; elements.results.style.display = 'block'; elements.results.classList.add('fade-in'); Utils.scrollToSection(elements.results, 105); } // Time Filter Functions function initTimeFilter() { const timeFilterInputs = document.querySelectorAll('input[name="timeFilter"]'); const customDateRange = document.getElementById('customDateRange'); const filterInfo = document.getElementById('filterInfo'); // Set default end date to today const today = new Date().toISOString().split('T')[0]; document.getElementById('endDate').value = today; // Ensure date picker is hidden by default customDateRange.style.setProperty('display', 'none', 'important'); timeFilterInputs.forEach(input => { input.addEventListener('change', function() { if (this.value === 'custom') { customDateRange.style.setProperty('display', 'flex', 'important'); updateFilterInfo(); } else { customDateRange.style.setProperty('display', 'none', 'important'); updateFilterInfo(); loadFeedbackHistory(1, true); // Reload with new filter } }); }); // Date change handlers document.getElementById('startDate').addEventListener('change', function() { if (document.querySelector('input[name="timeFilter"]:checked').value === 'custom') { updateFilterInfo(); loadFeedbackHistory(1, true); } }); document.getElementById('endDate').addEventListener('change', function() { if (document.querySelector('input[name="timeFilter"]:checked').value === 'custom') { updateFilterInfo(); loadFeedbackHistory(1, true); } }); } function updateFilterInfo() { const selectedFilter = document.querySelector('input[name="timeFilter"]:checked'); const filterInfo = document.getElementById('filterInfo'); const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; let infoText = ''; switch(selectedFilter.value) { case 'all': infoText = 'Hiển thị tất cả feedback'; break; case 'today': infoText = 'Hiển thị feedback trong ngày hôm nay'; break; case 'week': infoText = 'Hiển thị feedback trong 7 ngày qua'; break; case 'month': infoText = 'Hiển thị feedback trong 30 ngày qua'; break; case 'custom': if (startDate && endDate) { const start = new Date(startDate).toLocaleDateString('vi-VN'); const end = new Date(endDate).toLocaleDateString('vi-VN'); infoText = `Hiển thị feedback từ ${start} đến ${end}`; } else { infoText = 'Vui lòng chọn khoảng thời gian'; } break; } filterInfo.textContent = infoText; } function getTimeFilterParams() { const selectedFilter = document.querySelector('input[name="timeFilter"]:checked'); const params = { time_filter: selectedFilter.value }; if (selectedFilter.value === 'custom') { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; if (startDate) params.start_date = startDate; if (endDate) params.end_date = endDate; } return params; } function updateFeedbackCount(total) { const feedbackCount = document.getElementById('feedbackCount'); if (feedbackCount) { feedbackCount.textContent = total; // Cập nhật màu sắc badge dựa trên số lượng - sử dụng màu tối để dễ nhìn feedbackCount.className = 'badge bg-dark text-white ms-2 rounded-pill px-3 py-1'; if (total === 0) { feedbackCount.className = 'badge bg-secondary text-white ms-2 rounded-pill px-3 py-1'; } else if (total < 5) { feedbackCount.className = 'badge bg-dark text-white ms-2 rounded-pill px-3 py-1'; } else if (total < 20) { feedbackCount.className = 'badge bg-dark text-white ms-2 rounded-pill px-3 py-1'; } else { feedbackCount.className = 'badge bg-dark text-white ms-2 rounded-pill px-3 py-1'; } } } // ===== CSV Analysis Functions ===== function initAnalysisModeToggle() { const singleModeInput = document.getElementById('singleMode'); const csvModeInput = document.getElementById('csvMode'); const singleForm = document.getElementById('singleFeedbackForm'); const csvForm = document.getElementById('csvUploadForm'); const results = document.getElementById('results'); // Show single form by default singleForm.style.display = 'block'; csvForm.style.display = 'none'; if (results) results.style.display = 'none'; // Hide results when switching to single mode singleModeInput.addEventListener('change', function() { if (this.checked) { singleForm.style.display = 'block'; csvForm.style.display = 'none'; if (results) results.style.display = 'none'; // Hide CSV results } }); csvModeInput.addEventListener('change', function() { if (this.checked) { singleForm.style.display = 'none'; csvForm.style.display = 'block'; } }); // Handle CSV form submission document.getElementById('csvForm').addEventListener('submit', handleCsvUpload); // Handle CSV template download document.getElementById('downloadTemplate').addEventListener('click', downloadCsvTemplate); } async function handleCsvUpload(event) { event.preventDefault(); const csvFile = document.getElementById('csvFile').files[0]; const analyzeBtn = document.getElementById('analyzeCsvBtn'); const loadingSpinner = document.getElementById('loadingSpinner'); const results = document.getElementById('results'); if (!csvFile) { showAlert('Vui lòng chọn file CSV', 'warning'); return; } // Validate file size (max 10MB) const maxSize = 10 * 1024 * 1024; // 10MB if (csvFile.size > maxSize) { showAlert('File quá lớn. Kích thước tối đa là 10MB.', 'danger'); return; } // Validate file type if (!csvFile.name.toLowerCase().endsWith('.csv')) { showAlert('Vui lòng chọn file có định dạng .csv', 'danger'); return; } // Show loading analyzeBtn.disabled = true; analyzeBtn.innerHTML = 'Đang phân tích...'; loadingSpinner.style.display = 'block'; results.style.display = 'none'; const formData = new FormData(); formData.append('csvFile', csvFile); try { const response = await fetch('/analyze-csv', { method: 'POST', body: formData }); const data = await response.json(); if (response.ok && data.success) { showCsvResults(data); showAlert(data.message, 'success'); // Reload feedback history with small delay to ensure database is updated setTimeout(() => { loadFeedbackHistory(1, true); }, 500); } else { showAlert(data.error || 'Có lỗi xảy ra khi xử lý file CSV', 'danger'); } } catch (error) { console.error('CSV upload error:', error); showAlert('Có lỗi xảy ra khi upload file CSV', 'danger'); } finally { // Hide loading and clear CSV file analyzeBtn.disabled = false; analyzeBtn.innerHTML = 'Phân Tích File CSV'; loadingSpinner.style.display = 'none'; // Clear CSV file in all cases (success or error) document.getElementById('csvFile').value = ''; } } function showCsvResults(data) { const results = document.getElementById('results'); let html = `

Kết quả phân tích CSV

${data.processed_count}
Thành công
${data.error_count}
Lỗi
${data.total_rows}
Tổng cộng
`; // Chỉ hiển thị 10 dòng đầu tiên const displayResults = data.results.slice(0, 10); displayResults.forEach(result => { if (result.success) { const sentimentConfig = getSentimentConfig(result.sentiment); const topicConfig = getTopicConfig(result.topic); html += ` `; } else { html += ` `; } }); html += `
Dòng Feedback Sentiment Topic Tin cậy
${result.row} ${result.text} ${sentimentConfig.label} ${topicConfig.label} S: ${result.sentiment_confidence}%
T: ${result.topic_confidence}%
${result.row} ${result.text} ${result.error}
${data.total_rows > 10 ? `
Chỉ hiển thị 10 dòng đầu tiên trong tổng số ${data.total_rows} dòng
` : ''} `; results.innerHTML = html; results.style.display = 'block'; // Scroll to results with better positioning setTimeout(() => { // Điều chỉnh vị trí cuộn dựa trên số dòng const blockPosition = data.total_rows > 10 ? 'start' : 'center'; results.scrollIntoView({ behavior: 'smooth', block: blockPosition }); }, 100); } function downloadCsvTemplate(event) { event.preventDefault(); const csvContent = `feedback "Giảng viên rất nhiệt tình và giảng bài dễ hiểu" "Chương trình học có nhiều kiến thức bổ ích" "Cơ sở vật chất hiện đại và tiện nghi" "Thời gian học hợp lý và không quá căng thẳng" "Tài liệu học tập đầy đủ và chất lượng"`; const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', 'feedback_template.csv'); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } }