| | |
| | const Utils = { |
| | |
| | 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' }); |
| | }, |
| |
|
| | |
| | initAutoHideAlerts() { |
| | const alerts = document.querySelectorAll('.alert'); |
| | alerts.forEach(alert => { |
| | setTimeout(() => { |
| | alert.classList.add('fade-out'); |
| | setTimeout(() => alert.remove(), 500); |
| | }, 2000); |
| | }); |
| | } |
| | }; |
| |
|
| | document.addEventListener('DOMContentLoaded', function() { |
| | |
| | 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') |
| | }; |
| |
|
| | |
| | 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" |
| | ]; |
| |
|
| | |
| | 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 = '<i class="fas fa-spinner fa-spin me-2"></i>Đang phân tích...'; |
| | }, |
| | hideLoading() { |
| | elements.loadingSpinner.style.display = 'none'; |
| | elements.analyzeBtn.disabled = false; |
| | elements.analyzeBtn.innerHTML = '<i class="fas fa-search me-2"></i>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(); |
| | |
| | |
| | const formElement = elements.form; |
| | const formRect = formElement.getBoundingClientRect(); |
| | const scrollTop = window.pageYOffset + formRect.top - 120; |
| | |
| | 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 = `<i class="fas ${config.icon} fa-3x ${config.colorClass}"></i>`; |
| | 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'; |
| | } |
| | } |
| | }; |
| |
|
| | |
| | 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) { |
| | |
| | displayMultipleResults(data.results, feedbackText); |
| | |
| | |
| | elements.textarea.value = ''; |
| | utils.updateCharCounter(); |
| | |
| | |
| | |
| | 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')); |
| | |
| | |
| | const formElement = elements.form; |
| | const formRect = formElement.getBoundingClientRect(); |
| | const scrollTop = window.pageYOffset + formRect.top - 120; |
| | |
| | window.scrollTo({ |
| | top: scrollTop, |
| | behavior: 'smooth' |
| | }); |
| | } |
| |
|
| | |
| | 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(); |
| | } |
| | }); |
| |
|
| | |
| | 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 = '<i class="fas fa-trash me-2"></i>Xóa Form'; |
| | clearBtn.onclick = () => utils.clearForm(); |
| | const exampleBtn = document.createElement('button'); |
| | exampleBtn.type = 'button'; |
| | exampleBtn.className = 'btn btn-outline-info'; |
| | exampleBtn.innerHTML = '<i class="fas fa-lightbulb me-2"></i>Ví Dụ'; |
| | exampleBtn.onclick = showExamples; |
| | buttonContainer.appendChild(clearBtn); |
| | buttonContainer.appendChild(exampleBtn); |
| | elements.form.appendChild(buttonContainer); |
| |
|
| | |
| | const charCounter = document.createElement('small'); |
| | charCounter.className = 'form-text text-muted'; |
| | charCounter.innerHTML = '<i class="fas fa-info-circle me-1"></i>Độ dài: <span id="charCount">0</span> ký tự'; |
| | elements.textarea.parentNode.appendChild(charCounter); |
| |
|
| | |
| | loadFeedbackHistory(); |
| | |
| | |
| | Utils.initAutoHideAlerts(); |
| | |
| | |
| | initTimeFilter(); |
| | |
| | |
| | initAnalysisModeToggle(); |
| | }); |
| |
|
| | |
| | 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 { |
| | |
| | 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) { |
| | |
| | Utils.scrollToSection(feedbackHistorySection, -40); |
| | } |
| | } else { |
| | historyContent.innerHTML = '<p class="text-muted text-center">Không thể tải lịch sử feedback.</p>'; |
| | } |
| | } catch (error) { |
| | console.error('Error loading feedback history:', error); |
| | historyContent.innerHTML = '<p class="text-muted text-center">Có lỗi xảy ra khi tải lịch sử.</p>'; |
| | } 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 = '<p class="text-muted text-center">Chưa có feedback nào. Hãy phân tích feedback đầu tiên!</p>'; |
| | return; |
| | } |
| | let html = ''; |
| | feedbacks.forEach(feedback => { |
| | const sentimentConfig = getSentimentConfig(feedback.sentiment); |
| | const topicConfig = getTopicConfig(feedback.topic); |
| | const date = feedback.created_at; |
| | html += ` |
| | <div class="card mb-3 border-start border-4 border-${getSentimentColor(feedback.sentiment)}"> |
| | <div class="card-body"> |
| | <div class="row mb-3"> |
| | <div class="col-12"> |
| | <p class="card-text">${feedback.text}</p> |
| | <small class="text-muted"> |
| | <i class="fas fa-clock me-1"></i>${date} |
| | </small> |
| | </div> |
| | </div> |
| | <div class="row"> |
| | <div class="col-6"> |
| | <div class="d-flex align-items-center"> |
| | <span class="badge bg-${getSentimentColor(feedback.sentiment)} me-2"> |
| | <i class="fas ${sentimentConfig.icon} me-1"></i> |
| | ${sentimentConfig.label} |
| | </span> |
| | <small class="text-muted">Tin cậy: ${(feedback.sentiment_confidence * 100).toFixed(1)}%</small> |
| | </div> |
| | </div> |
| | <div class="col-6"> |
| | <div class="d-flex align-items-center"> |
| | <span class="badge bg-secondary me-2"> |
| | <i class="fas ${topicConfig.icon} me-1"></i> |
| | ${topicConfig.label} |
| | </span> |
| | <small class="text-muted">Tin cậy: ${(feedback.topic_confidence * 100).toFixed(1)}%</small> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | `; |
| | }); |
| | historyContent.innerHTML = html; |
| | } |
| |
|
| | function displayPagination(data, currentPage) { |
| | const historyPagination = document.getElementById('historyPagination'); |
| | if (!data || data.pages <= 1) { |
| | historyPagination.innerHTML = ''; |
| | return; |
| | } |
| | |
| | |
| | const maxPages = 5; |
| | let startPage = Math.max(1, currentPage - Math.floor(maxPages / 2)); |
| | let endPage = Math.min(data.pages, startPage + maxPages - 1); |
| | |
| | |
| | if (endPage - startPage + 1 < maxPages) { |
| | startPage = Math.max(1, endPage - maxPages + 1); |
| | } |
| | |
| | let html = '<nav><ul class="pagination pagination-sm">'; |
| | if (data.has_prev) { |
| | html += `<li class="page-item"> |
| | <a class="page-link" href="javascript:void(0)" onclick="loadFeedbackHistory(${currentPage - 1}, true); return false;">Trước</a> |
| | </li>`; |
| | } |
| | |
| | |
| | if (startPage > 1) { |
| | html += `<li class="page-item"> |
| | <a class="page-link" href="javascript:void(0)" onclick="loadFeedbackHistory(1, true); return false;">1</a> |
| | </li>`; |
| | if (startPage > 2) { |
| | html += '<li class="page-item disabled"><span class="page-link">...</span></li>'; |
| | } |
| | } |
| | |
| | for (let i = startPage; i <= endPage; i++) { |
| | const active = i === currentPage ? 'active' : ''; |
| | html += `<li class="page-item ${active}"> |
| | <a class="page-link" href="javascript:void(0)" onclick="loadFeedbackHistory(${i}, true); return false;">${i}</a> |
| | </li>`; |
| | } |
| | |
| | |
| | if (endPage < data.pages) { |
| | if (endPage < data.pages - 1) { |
| | html += '<li class="page-item disabled"><span class="page-link">...</span></li>'; |
| | } |
| | html += `<li class="page-item"> |
| | <a class="page-link" href="javascript:void(0)" onclick="loadFeedbackHistory(${data.pages}, true); return false;">${data.pages}</a> |
| | </li>`; |
| | } |
| | |
| | if (data.has_next) { |
| | html += `<li class="page-item"> |
| | <a class="page-link" href="javascript:void(0)" onclick="loadFeedbackHistory(${currentPage + 1}, true); return false;">Sau</a> |
| | </li>`; |
| | } |
| | html += '</ul></nav>'; |
| | |
| | |
| | html += ` |
| | <div class="d-flex align-items-center justify-content-center" style="margin-left: 20px; margin-top: -3px;"> |
| | <span class="me-2 text-muted" style="font-size: 0.875rem; line-height: 2.2;">Đến:</span> |
| | <input type="number" id="pageInput" class="form-control form-control-sm me-2" |
| | style="width: 80px; height: 38px; font-size: 0.875rem; text-align: center; line-height: 1;" min="1" max="${data.pages}" value="${currentPage}"> |
| | <button class="btn btn-outline-secondary btn-sm" onclick="goToPage()" style="height: 38px; width: 38px; padding: 0; display: flex; align-items: center; justify-content: center;"> |
| | <i class="fas fa-arrow-right" style="font-size: 0.875rem;"></i> |
| | </button> |
| | </div> |
| | `; |
| | |
| | historyPagination.innerHTML = html; |
| | } |
| |
|
| | function goToPage() { |
| | const pageInput = document.getElementById('pageInput'); |
| | const page = parseInt(pageInput.value); |
| | |
| | |
| | const paginationNav = document.querySelector('.pagination'); |
| | if (!paginationNav) return; |
| | |
| | |
| | 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; |
| | } else { |
| | showAlert('Vui lòng nhập số trang hợp lệ', 'warning'); |
| | } |
| | } |
| |
|
| | |
| | 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'; |
| | } |
| |
|
| | |
| | function displayMultipleResults(results, text) { |
| | const elements = { |
| | results: document.getElementById('results'), |
| | originalText: document.getElementById('originalText') |
| | }; |
| | |
| | |
| | if (!elements.results) { |
| | console.error('❌ Results element not found'); |
| | return; |
| | } |
| | |
| | if (!results || results.length === 0) { |
| | |
| | if (elements.originalText) elements.originalText.textContent = text; |
| | elements.results.innerHTML = ` |
| | <div class="alert alert-warning"> |
| | <i class="fas fa-info-circle me-2"></i> |
| | Không phát hiện topic rõ ràng trong feedback này. |
| | </div> |
| | `; |
| | elements.results.style.display = 'block'; |
| | elements.results.classList.add('fade-in'); |
| | Utils.scrollToSection(elements.results, 105); |
| | return; |
| | } |
| | |
| | |
| | if (elements.originalText) { |
| | elements.originalText.textContent = text; |
| | } |
| | |
| | |
| | let html = '<div class="mb-4"><h5 class="mb-3"><i class="fas fa-check-circle me-2"></i>Kết quả phân tích:</h5></div>'; |
| | html += '<div class="multiple-topics-container">'; |
| | |
| | results.forEach(result => { |
| | const sentimentConfig = getSentimentConfig(result.sentiment); |
| | const topicConfig = getTopicConfig(result.topic); |
| | const sentimentColor = getSentimentColor(result.sentiment); |
| | |
| | html += ` |
| | <div class="topic-sentiment-item mb-3"> |
| | <div class="d-flex align-items-center"> |
| | <div class="flex-grow-1"> |
| | <div class="d-flex align-items-center mb-2"> |
| | <i class="fas ${topicConfig.icon} me-2" style="color: #6B7280;"></i> |
| | <span class="fw-semibold">${topicConfig.label}</span> |
| | </div> |
| | <div> |
| | <span class="badge bg-${sentimentColor} me-2"> |
| | <i class="fas ${sentimentConfig.icon} me-1"></i> |
| | ${sentimentConfig.label} |
| | </span> |
| | <small class="text-muted"> |
| | <i class="fas fa-percentage me-1"></i> |
| | Độ tin cậy: ${(result.confidence * 100).toFixed(1)}% |
| | </small> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | `; |
| | }); |
| | |
| | html += '</div>'; |
| | |
| | |
| | html += ` |
| | <div class="card shadow-sm"> |
| | <div class="card-header bg-primary text-white"> |
| | <h5 class="card-title mb-0"> |
| | <i class="fas fa-quote-left me-2"></i> |
| | Feedback Gốc |
| | </h5> |
| | </div> |
| | <div class="card-body"> |
| | <blockquote class="blockquote mb-0"> |
| | <p class="mb-0">${text}</p> |
| | </blockquote> |
| | </div> |
| | </div> |
| | `; |
| | |
| | elements.results.innerHTML = html; |
| | elements.results.style.display = 'block'; |
| | elements.results.classList.add('fade-in'); |
| | Utils.scrollToSection(elements.results, 105); |
| | } |
| |
|
| | |
| | function initTimeFilter() { |
| | const timeFilterInputs = document.querySelectorAll('input[name="timeFilter"]'); |
| | const customDateRange = document.getElementById('customDateRange'); |
| | const filterInfo = document.getElementById('filterInfo'); |
| | |
| | |
| | const today = new Date().toISOString().split('T')[0]; |
| | document.getElementById('endDate').value = today; |
| | |
| | |
| | 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); |
| | } |
| | }); |
| | }); |
| | |
| | |
| | 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; |
| | |
| | |
| | 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'; |
| | } |
| | } |
| | } |
| |
|
| | |
| | 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'); |
| | |
| | |
| | singleForm.style.display = 'block'; |
| | csvForm.style.display = 'none'; |
| | if (results) results.style.display = 'none'; |
| | |
| | singleModeInput.addEventListener('change', function() { |
| | if (this.checked) { |
| | singleForm.style.display = 'block'; |
| | csvForm.style.display = 'none'; |
| | if (results) results.style.display = 'none'; |
| | } |
| | }); |
| | |
| | csvModeInput.addEventListener('change', function() { |
| | if (this.checked) { |
| | singleForm.style.display = 'none'; |
| | csvForm.style.display = 'block'; |
| | } |
| | }); |
| | |
| | |
| | document.getElementById('csvForm').addEventListener('submit', handleCsvUpload); |
| | |
| | |
| | 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; |
| | } |
| | |
| | |
| | const maxSize = 10 * 1024 * 1024; |
| | if (csvFile.size > maxSize) { |
| | showAlert('File quá lớn. Kích thước tối đa là 10MB.', 'danger'); |
| | return; |
| | } |
| | |
| | |
| | if (!csvFile.name.toLowerCase().endsWith('.csv')) { |
| | showAlert('Vui lòng chọn file có định dạng .csv', 'danger'); |
| | return; |
| | } |
| | |
| | |
| | analyzeBtn.disabled = true; |
| | analyzeBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Đ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'); |
| | |
| | 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 { |
| | |
| | analyzeBtn.disabled = false; |
| | analyzeBtn.innerHTML = '<i class="fas fa-chart-bar me-2"></i>Phân Tích File CSV'; |
| | loadingSpinner.style.display = 'none'; |
| | |
| | |
| | document.getElementById('csvFile').value = ''; |
| | } |
| | } |
| |
|
| | function showCsvResults(data) { |
| | const results = document.getElementById('results'); |
| | |
| | let html = ` |
| | <div class="card shadow-lg mb-3"> |
| | <div class="card-header bg-secondary text-white"> |
| | <h4 class="card-title mb-0"> |
| | <i class="fas fa-check-circle me-2"></i> |
| | Kết quả phân tích CSV |
| | </h4> |
| | </div> |
| | <div class="card-body"> |
| | <div class="row mb-3"> |
| | <div class="col-md-4"> |
| | <div class="text-center"> |
| | <h5 class="text-success">${data.processed_count}</h5> |
| | <small class="text-muted">Thành công</small> |
| | </div> |
| | </div> |
| | <div class="col-md-4"> |
| | <div class="text-center"> |
| | <h5 class="text-danger">${data.error_count}</h5> |
| | <small class="text-muted">Lỗi</small> |
| | </div> |
| | </div> |
| | <div class="col-md-4"> |
| | <div class="text-center"> |
| | <h5 class="text-secondary">${data.total_rows}</h5> |
| | <small class="text-muted">Tổng cộng</small> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div class="table-responsive"> |
| | <table class="table table-sm" id="csvResultsTable"> |
| | <thead> |
| | <tr> |
| | <th>Dòng</th> |
| | <th>Feedback</th> |
| | <th>Sentiment</th> |
| | <th>Topic</th> |
| | <th>Tin cậy</th> |
| | </tr> |
| | </thead> |
| | <tbody> |
| | `; |
| | |
| | |
| | const displayResults = data.results.slice(0, 10); |
| | |
| | displayResults.forEach(result => { |
| | if (result.success) { |
| | const sentimentConfig = getSentimentConfig(result.sentiment); |
| | const topicConfig = getTopicConfig(result.topic); |
| | |
| | html += ` |
| | <tr> |
| | <td>${result.row}</td> |
| | <td>${result.text}</td> |
| | <td> |
| | <span class="badge bg-${getSentimentColor(result.sentiment)}"> |
| | <i class="fas fa-${sentimentConfig.icon} me-1"></i> |
| | ${sentimentConfig.label} |
| | </span> |
| | </td> |
| | <td> |
| | <span class="badge bg-secondary"> |
| | <i class="fas fa-${topicConfig.icon} me-1"></i> |
| | ${topicConfig.label} |
| | </span> |
| | </td> |
| | <td> |
| | <small class="text-muted"> |
| | S: ${result.sentiment_confidence}%<br> |
| | T: ${result.topic_confidence}% |
| | </small> |
| | </td> |
| | </tr> |
| | `; |
| | } else { |
| | html += ` |
| | <tr class="table-danger"> |
| | <td>${result.row}</td> |
| | <td>${result.text}</td> |
| | <td colspan="3"> |
| | <small class="text-danger"> |
| | <i class="fas fa-exclamation-triangle me-1"></i> |
| | ${result.error} |
| | </small> |
| | </td> |
| | </tr> |
| | `; |
| | } |
| | }); |
| | |
| | html += ` |
| | </tbody> |
| | </table> |
| | </div> |
| | </div> |
| | |
| | ${data.total_rows > 10 ? ` |
| | <div class="alert alert-info mb-0 d-flex align-items-center custom-alert-left"> |
| | <i class="fas fa-info-circle me-2"></i> |
| | Chỉ hiển thị 10 dòng đầu tiên trong tổng số ${data.total_rows} dòng |
| | </div> |
| | ` : ''} |
| | `; |
| |
|
| |
|
| |
|
| |
|
| |
|
| | |
| | results.innerHTML = html; |
| | results.style.display = 'block'; |
| | |
| | |
| | setTimeout(() => { |
| | |
| | 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); |
| | } |
| | } |
| |
|