Predict_Rating / app /templates /dashboard.html
vtdung23's picture
Enhanced app with Dark Mode, Toast Notifications, SHAP Explanation, N-gram Analysis, Keyword Highlighting
92db116
Raw
History Blame Contribute Delete
44.6 kB
{% extends "base.html" %}
{% block title %}Dashboard - Rating Predictor{% endblock %}
{% block nav_items %}
<div class="flex items-center space-x-4">
<span class="text-gray-700 dark:text-gray-300" id="username-display">
<i class="fas fa-user mr-2"></i><span id="current-username"></span>
</span>
<button
onclick="logout()"
class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 transition"
>
<i class="fas fa-sign-out-alt mr-2"></i>Logout
</button>
</div>
{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto">
<!-- Welcome Section -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-lg p-6 mb-8 fade-in">
<h2 class="text-3xl font-bold text-gray-800 dark:text-gray-100 mb-2">
<i class="fas fa-chart-line text-indigo-600 mr-3"></i>
Prediction Dashboard
</h2>
<p class="text-gray-600 dark:text-gray-400">Dự đoán đánh giá sản phẩm từ bình luận tiếng Việt với AI giải thích</p>
<div class="flex flex-wrap gap-2 mt-4">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
<i class="fas fa-brain mr-1"></i>AI Explainability
</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
<i class="fas fa-highlighter mr-1"></i>Keyword Highlighting
</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
<i class="fas fa-chart-bar mr-1"></i>N-gram Analysis
</span>
</div>
</div>
<!-- Input Mode Tabs -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-lg p-6 mb-8">
<div class="flex space-x-4 mb-6 border-b dark:border-slate-700">
<button
onclick="switchTab('single')"
id="tab-single"
class="tab-button px-6 py-3 font-medium border-b-2 border-indigo-600 text-indigo-600"
>
<i class="fas fa-comment mr-2"></i>Single Comment
</button>
<button
onclick="switchTab('batch')"
id="tab-batch"
class="tab-button px-6 py-3 font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
>
<i class="fas fa-file-csv mr-2"></i>Upload CSV
</button>
</div>
<!-- Single Prediction Form -->
<div id="single-form" class="tab-content">
<form id="singlePredictionForm">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Enter your comment (Vietnamese):
</label>
<textarea
id="single-comment"
rows="4"
class="w-full px-4 py-3 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100"
placeholder="Sản phẩm rất tốt, chất lượng cao..."
></textarea>
<!-- Explanation Toggle -->
<div class="flex items-center mt-4 mb-4">
<input
type="checkbox"
id="include-explanation"
class="w-4 h-4 text-indigo-600 bg-gray-100 border-gray-300 rounded focus:ring-indigo-500"
checked
>
<label for="include-explanation" class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">
<i class="fas fa-brain text-indigo-600 mr-1"></i>
Bao gồm giải thích AI (Word Importance)
</label>
</div>
<button
type="submit"
id="single-submit-btn"
class="bg-indigo-600 text-white px-6 py-3 rounded-lg hover:bg-indigo-700 transition font-medium shadow-lg"
>
<i class="fas fa-magic mr-2"></i>Predict Rating
</button>
</form>
<!-- Loading Skeleton for Single Result -->
<div id="single-loading" class="hidden mt-6">
<div class="p-6 bg-gradient-to-r from-gray-100 to-gray-50 dark:from-slate-700 dark:to-slate-600 rounded-xl animate-pulse">
<div class="flex items-center space-x-6">
<div class="w-20 h-20 bg-gray-300 dark:bg-slate-500 rounded-full"></div>
<div class="flex-1">
<div class="h-6 bg-gray-300 dark:bg-slate-500 rounded w-1/3 mb-3"></div>
<div class="h-4 bg-gray-300 dark:bg-slate-500 rounded w-1/4"></div>
</div>
</div>
</div>
</div>
<!-- Single Result -->
<div id="single-result" class="hidden mt-6 p-6 bg-gradient-to-r from-green-50 to-blue-50 dark:from-slate-700 dark:to-slate-600 rounded-xl border-2 border-green-200 dark:border-slate-500">
<h3 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4">
<i class="fas fa-star text-yellow-500 mr-2"></i>Prediction Result
</h3>
<div class="flex items-center space-x-6 mb-4">
<div class="text-center">
<div class="text-5xl font-bold text-indigo-600" id="predicted-rating"></div>
<div class="text-sm text-gray-600 dark:text-gray-400 mt-2">Rating</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-green-600" id="confidence-score"></div>
<div class="text-sm text-gray-600 dark:text-gray-400 mt-2">Confidence</div>
</div>
<div class="flex-1">
<div id="rating-stars" class="text-4xl"></div>
</div>
</div>
<!-- Highlighted Comment -->
<div class="mt-4 p-4 bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-600">
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">
<i class="fas fa-highlighter mr-1"></i>Comment với Keyword Highlighting:
</h4>
<p id="highlighted-comment" class="text-gray-800 dark:text-gray-200 leading-relaxed"></p>
</div>
<!-- Explanation Section -->
<div id="explanation-section" class="mt-4"></div>
<!-- Keywords Found -->
<div id="keywords-section" class="hidden mt-4 p-4 bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-600">
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">
<i class="fas fa-tags mr-1"></i>Keywords Found:
</h4>
<div class="flex flex-wrap gap-2">
<div id="positive-keywords"></div>
<div id="negative-keywords"></div>
</div>
</div>
</div>
</div>
<!-- Batch Prediction Form -->
<div id="batch-form" class="tab-content hidden">
<form id="batchPredictionForm">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<i class="fas fa-tag mr-2"></i>Product/Item Name (optional):
</label>
<input
type="text"
id="batch-product-name"
class="w-full px-4 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition mb-4 bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100"
placeholder="e.g., iPhone 15, Laptop, Shoes..."
>
<div class="border-2 border-dashed border-gray-300 dark:border-slate-600 rounded-lg p-8 text-center hover:border-indigo-500 transition">
<i class="fas fa-cloud-upload-alt text-5xl text-gray-400 mb-4"></i>
<label for="csv-file" class="block text-lg font-medium text-gray-700 dark:text-gray-300 mb-2 cursor-pointer">
Upload CSV File
</label>
<input
type="file"
id="csv-file"
accept=".csv"
class="hidden"
onchange="displayFileName(this)"
>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2">CSV must contain a "Comment" column</p>
<p id="file-name" class="text-sm font-medium text-indigo-600"></p>
<label for="csv-file" class="inline-block mt-4 bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 cursor-pointer transition">
Choose File
</label>
</div>
<button
type="submit"
id="batch-submit-btn"
class="mt-6 bg-indigo-600 text-white px-6 py-3 rounded-lg hover:bg-indigo-700 transition font-medium shadow-lg"
>
<i class="fas fa-magic mr-2"></i>Predict Batch
</button>
</form>
<!-- Loading Skeleton for Batch -->
<div id="batch-loading" class="hidden mt-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-white dark:bg-slate-700 p-6 rounded-xl shadow animate-pulse">
<div class="h-6 bg-gray-300 dark:bg-slate-500 rounded w-1/3 mb-4"></div>
<div class="h-48 bg-gray-300 dark:bg-slate-500 rounded"></div>
</div>
<div class="bg-white dark:bg-slate-700 p-6 rounded-xl shadow animate-pulse">
<div class="h-6 bg-gray-300 dark:bg-slate-500 rounded w-1/3 mb-4"></div>
<div class="h-48 bg-gray-300 dark:bg-slate-500 rounded"></div>
</div>
</div>
</div>
<!-- Batch Results -->
<div id="batch-results" class="hidden mt-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Rating Distribution Chart -->
<div class="bg-white dark:bg-slate-700 p-6 rounded-xl shadow">
<h3 class="text-lg font-bold text-gray-800 dark:text-gray-100 mb-4">
<i class="fas fa-chart-pie text-indigo-600 mr-2"></i>Rating Distribution
</h3>
<canvas id="ratingChart"></canvas>
</div>
<!-- Word Cloud -->
<div class="bg-white dark:bg-slate-700 p-6 rounded-xl shadow">
<h3 class="text-lg font-bold text-gray-800 dark:text-gray-100 mb-4">
<i class="fas fa-cloud text-indigo-600 mr-2"></i>Word Cloud
</h3>
<img id="wordcloud-image" src="" alt="Word Cloud" class="w-full rounded-lg">
</div>
</div>
<!-- N-gram Analysis Section -->
<div class="bg-white dark:bg-slate-700 p-6 rounded-xl shadow mb-6">
<h3 class="text-lg font-bold text-gray-800 dark:text-gray-100 mb-4">
<i class="fas fa-chart-bar text-indigo-600 mr-2"></i>N-gram Analysis
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Unigrams -->
<div>
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">Top Unigrams (1-word)</h4>
<div id="unigrams-container" class="ngram-container"></div>
</div>
<!-- Bigrams -->
<div>
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">Top Bigrams (2-word)</h4>
<div id="bigrams-container" class="ngram-container"></div>
</div>
<!-- Trigrams -->
<div>
<h4 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">Top Trigrams (3-word)</h4>
<div id="trigrams-container" class="ngram-container"></div>
</div>
</div>
<!-- N-gram Chart -->
<div class="ngram-chart-container mt-6">
<canvas id="ngramChart"></canvas>
</div>
</div>
<!-- Keyword Frequency Section -->
<div class="bg-white dark:bg-slate-700 p-6 rounded-xl shadow mb-6">
<h3 class="text-lg font-bold text-gray-800 dark:text-gray-100 mb-4">
<i class="fas fa-tags text-indigo-600 mr-2"></i>Keyword Frequency
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 class="text-sm font-semibold text-green-600 dark:text-green-400 mb-2">
<i class="fas fa-smile mr-1"></i>Positive Keywords
</h4>
<div id="positive-freq-container" class="flex flex-wrap gap-2"></div>
</div>
<div>
<h4 class="text-sm font-semibold text-red-600 dark:text-red-400 mb-2">
<i class="fas fa-frown mr-1"></i>Negative Keywords
</h4>
<div id="negative-freq-container" class="flex flex-wrap gap-2"></div>
</div>
</div>
</div>
</div>
<!-- Results Table -->
<div class="bg-white dark:bg-slate-700 p-6 rounded-xl shadow">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-gray-800 dark:text-gray-100">
<i class="fas fa-table text-indigo-600 mr-2"></i>Prediction Results
</h3>
<div class="space-x-3">
<button
onclick="downloadPDF()"
class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition"
>
<i class="fas fa-file-pdf mr-2"></i>Download PDF
</button>
<button
onclick="downloadCSV()"
class="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition"
>
<i class="fas fa-download mr-2"></i>Download CSV
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full" id="results-table">
<thead class="bg-gray-100 dark:bg-slate-600">
<tr>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">Comment</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-700 dark:text-gray-200">Rating</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-700 dark:text-gray-200">Confidence</th>
</tr>
</thead>
<tbody id="results-tbody" class="divide-y divide-gray-200 dark:divide-slate-600">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- History Section -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-lg p-6 mb-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-100">
<i class="fas fa-history text-indigo-600 mr-2"></i>Prediction History
</h2>
<button
onclick="refreshHistory()"
class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition font-medium"
>
<i class="fas fa-sync-alt mr-2"></i>Refresh
</button>
</div>
<!-- History Loading Skeleton -->
<div id="history-loading" class="hidden">
<div class="space-y-3">
<div class="h-12 bg-gray-200 dark:bg-slate-600 rounded animate-pulse"></div>
<div class="h-12 bg-gray-200 dark:bg-slate-600 rounded animate-pulse"></div>
<div class="h-12 bg-gray-200 dark:bg-slate-600 rounded animate-pulse"></div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full" id="history-table">
<thead class="bg-gray-100 dark:bg-slate-700">
<tr>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">Date/Time</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">Comment</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-700 dark:text-gray-200">Rating</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-700 dark:text-gray-200">Confidence</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-700 dark:text-gray-200">Type</th>
</tr>
</thead>
<tbody id="history-tbody" class="divide-y divide-gray-200 dark:divide-slate-600">
<tr class="text-center text-gray-500 dark:text-gray-400 py-8">
<td colspan="6" class="px-4 py-8">Loading history...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Check authentication
const token = localStorage.getItem('access_token');
const username = localStorage.getItem('username');
if (!token) {
window.location.href = '/login';
}
document.getElementById('current-username').textContent = username || 'User';
// Global variables
let currentResults = [];
let currentDistribution = {};
let currentWordcloudUrl = '';
let currentNgrams = null;
let chartInstance = null;
let ngramChartInstance = null;
// Load history on page load
document.addEventListener('DOMContentLoaded', () => {
loadHistory();
});
// Logout function
function logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('username');
toast.info('Logged Out', 'You have been logged out successfully');
setTimeout(() => {
window.location.href = '/login';
}, 1000);
}
// Tab switching
function switchTab(tab) {
const tabs = ['single', 'batch'];
tabs.forEach(t => {
const button = document.getElementById(`tab-${t}`);
const content = document.getElementById(`${t}-form`);
if (t === tab) {
button.classList.add('border-indigo-600', 'text-indigo-600');
button.classList.remove('text-gray-500', 'dark:text-gray-400');
content.classList.remove('hidden');
} else {
button.classList.remove('border-indigo-600', 'text-indigo-600');
button.classList.add('text-gray-500', 'dark:text-gray-400');
content.classList.add('hidden');
}
});
// Hide results when switching
document.getElementById('single-result').classList.add('hidden');
document.getElementById('batch-results').classList.add('hidden');
}
// Display selected file name
function displayFileName(input) {
const fileName = input.files[0]?.name || '';
document.getElementById('file-name').textContent = fileName ? `Selected: ${fileName}` : '';
}
// Single Prediction
document.getElementById('singlePredictionForm').addEventListener('submit', async (e) => {
e.preventDefault();
const comment = document.getElementById('single-comment').value;
const includeExplanation = document.getElementById('include-explanation').checked;
if (!comment.trim()) {
toast.warning('Missing Input', 'Please enter a comment!');
return;
}
// Show loading
document.getElementById('single-loading').classList.remove('hidden');
document.getElementById('single-result').classList.add('hidden');
document.getElementById('single-submit-btn').disabled = true;
document.getElementById('single-submit-btn').innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Predicting...';
try {
const response = await fetch('/api/predict/single', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
product_name: '',
comment: comment,
include_explanation: includeExplanation
})
});
if (response.ok) {
const data = await response.json();
displaySingleResult(data);
toast.success('Prediction Complete', `Rating: ${data.predicted_rating}⭐ (${(data.confidence_score * 100).toFixed(1)}% confidence)`);
// Reload history
setTimeout(() => loadHistory(), 500);
} else {
const error = await response.json();
toast.error('Prediction Failed', error.detail || 'Unknown error');
}
} catch (error) {
toast.error('Error', 'An error occurred: ' + error.message);
} finally {
// Hide loading
document.getElementById('single-loading').classList.add('hidden');
document.getElementById('single-submit-btn').disabled = false;
document.getElementById('single-submit-btn').innerHTML = '<i class="fas fa-magic mr-2"></i>Predict Rating';
}
});
function displaySingleResult(data) {
document.getElementById('predicted-rating').textContent = data.predicted_rating;
document.getElementById('confidence-score').textContent = (data.confidence_score * 100).toFixed(1) + '%';
// Display stars
const stars = '⭐'.repeat(data.predicted_rating);
document.getElementById('rating-stars').textContent = stars;
// Display highlighted comment
if (data.highlighted_comment) {
document.getElementById('highlighted-comment').innerHTML = data.highlighted_comment;
} else {
document.getElementById('highlighted-comment').textContent = data.comment;
}
// Display explanation if available
const explanationSection = document.getElementById('explanation-section');
if (data.explanation) {
ExplanationVisualizer.render('explanation-section', data.explanation);
} else {
explanationSection.innerHTML = '';
}
// Display keywords if available
const keywordsSection = document.getElementById('keywords-section');
if (data.keywords) {
const positiveContainer = document.getElementById('positive-keywords');
const negativeContainer = document.getElementById('negative-keywords');
if (data.keywords.positive_keywords && data.keywords.positive_keywords.length > 0) {
positiveContainer.innerHTML = data.keywords.positive_keywords.map(kw =>
`<span class="inline-block px-2 py-1 m-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">${kw}</span>`
).join('');
} else {
positiveContainer.innerHTML = '<span class="text-gray-400 text-sm">None found</span>';
}
if (data.keywords.negative_keywords && data.keywords.negative_keywords.length > 0) {
negativeContainer.innerHTML = data.keywords.negative_keywords.map(kw =>
`<span class="inline-block px-2 py-1 m-1 text-xs rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">${kw}</span>`
).join('');
} else {
negativeContainer.innerHTML = '<span class="text-gray-400 text-sm">None found</span>';
}
keywordsSection.classList.remove('hidden');
} else {
keywordsSection.classList.add('hidden');
}
document.getElementById('single-result').classList.remove('hidden');
}
// Batch Prediction
document.getElementById('batchPredictionForm').addEventListener('submit', async (e) => {
e.preventDefault();
const productName = document.getElementById('batch-product-name').value || '';
const fileInput = document.getElementById('csv-file');
const file = fileInput.files[0];
if (!file) {
toast.warning('Missing File', 'Please select a CSV file!');
return;
}
// Show loading
document.getElementById('batch-loading').classList.remove('hidden');
document.getElementById('batch-results').classList.add('hidden');
document.getElementById('batch-submit-btn').disabled = true;
document.getElementById('batch-submit-btn').innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Processing...';
const formData = new FormData();
formData.append('product_name', productName);
formData.append('file', file);
try {
const response = await fetch('/api/predict/batch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
if (response.ok) {
const data = await response.json();
displayBatchResults(data);
toast.success('Batch Complete', `Processed ${data.total_predictions} predictions`);
// Reload history
setTimeout(() => loadHistory(), 500);
} else {
const error = await response.json();
toast.error('Batch Failed', error.detail || 'Prediction failed');
}
} catch (error) {
toast.error('Error', 'An error occurred: ' + error.message);
} finally {
// Hide loading
document.getElementById('batch-loading').classList.add('hidden');
document.getElementById('batch-submit-btn').disabled = false;
document.getElementById('batch-submit-btn').innerHTML = '<i class="fas fa-magic mr-2"></i>Predict Batch';
}
});
function displayBatchResults(data) {
currentResults = data.results;
currentDistribution = data.rating_distribution;
currentWordcloudUrl = data.wordcloud_url;
currentNgrams = data.ngrams;
// Display word cloud
document.getElementById('wordcloud-image').src = data.wordcloud_url;
// Create chart
createRatingChart(data.rating_distribution);
// Display N-grams
if (data.ngrams) {
displayNgrams(data.ngrams);
}
// Display keyword frequency
if (data.keyword_frequency) {
displayKeywordFrequency(data.keyword_frequency);
}
// Populate table
const tbody = document.getElementById('results-tbody');
tbody.innerHTML = '';
data.results.forEach(result => {
// Apply highlighting to comment
const highlightedComment = highlighter.highlight(result.Comment);
const row = `
<tr class="hover:bg-gray-50 dark:hover:bg-slate-600">
<td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">${highlightedComment}</td>
<td class="px-4 py-3 text-center">
<span class="inline-block bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 px-3 py-1 rounded-full font-semibold">
${result.Predicted_Rating}
</span>
</td>
<td class="px-4 py-3 text-center text-sm text-gray-600 dark:text-gray-400">
${(result.Confidence * 100).toFixed(1)}%
</td>
</tr>
`;
tbody.innerHTML += row;
});
document.getElementById('batch-results').classList.remove('hidden');
}
function displayNgrams(ngrams) {
// Display unigrams
const unigramsHtml = ngrams.unigrams.slice(0, 8).map(item =>
`<div class="ngram-badge"><span>${item.ngram}</span><span class="ngram-count">${item.count}</span></div>`
).join('');
document.getElementById('unigrams-container').innerHTML = unigramsHtml;
// Display bigrams
const bigramsHtml = ngrams.bigrams.slice(0, 8).map(item =>
`<div class="ngram-badge"><span>${item.ngram}</span><span class="ngram-count">${item.count}</span></div>`
).join('');
document.getElementById('bigrams-container').innerHTML = bigramsHtml;
// Display trigrams
const trigramsHtml = ngrams.trigrams.slice(0, 6).map(item =>
`<div class="ngram-badge"><span>${item.ngram}</span><span class="ngram-count">${item.count}</span></div>`
).join('');
document.getElementById('trigrams-container').innerHTML = trigramsHtml;
// Create N-gram chart (bigrams)
createNgramChart(ngrams.bigrams);
}
function displayKeywordFrequency(keywordFreq) {
// Positive keywords
const positiveHtml = keywordFreq.positive.map(item =>
`<span class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
${item.word} <span class="ml-1 bg-green-200 dark:bg-green-800 px-2 py-0.5 rounded-full text-xs">${item.count}</span>
</span>`
).join('');
document.getElementById('positive-freq-container').innerHTML = positiveHtml || '<span class="text-gray-400">No positive keywords found</span>';
// Negative keywords
const negativeHtml = keywordFreq.negative.map(item =>
`<span class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200">
${item.word} <span class="ml-1 bg-red-200 dark:bg-red-800 px-2 py-0.5 rounded-full text-xs">${item.count}</span>
</span>`
).join('');
document.getElementById('negative-freq-container').innerHTML = negativeHtml || '<span class="text-gray-400">No negative keywords found</span>';
}
function createNgramChart(bigrams) {
const ctx = document.getElementById('ngramChart').getContext('2d');
const isDark = document.documentElement.classList.contains('dark');
// Destroy existing chart
if (ngramChartInstance) {
ngramChartInstance.destroy();
}
const topBigrams = bigrams.slice(0, 10);
ngramChartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: topBigrams.map(item => item.ngram),
datasets: [{
label: 'Frequency',
data: topBigrams.map(item => item.count),
backgroundColor: topBigrams.map((_, i) => {
const hue = 250 + (i * 15);
return `hsla(${hue}, 70%, 60%, 0.8)`;
}),
borderColor: topBigrams.map((_, i) => {
const hue = 250 + (i * 15);
return `hsla(${hue}, 70%, 50%, 1)`;
}),
borderWidth: 2
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
title: {
display: true,
text: 'Top Bigrams Frequency',
color: isDark ? '#e2e8f0' : '#1f2937'
}
},
scales: {
x: {
beginAtZero: true,
grid: {
color: isDark ? '#334155' : '#e5e7eb'
},
ticks: {
color: isDark ? '#94a3b8' : '#4b5563'
}
},
y: {
grid: {
display: false
},
ticks: {
color: isDark ? '#94a3b8' : '#4b5563'
}
}
}
}
});
}
function createRatingChart(distribution) {
const ctx = document.getElementById('ratingChart').getContext('2d');
const isDark = document.documentElement.classList.contains('dark');
// Destroy existing chart
if (chartInstance) {
chartInstance.destroy();
}
chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: ['1⭐', '2⭐', '3⭐', '4⭐', '5⭐'],
datasets: [{
label: 'Number of Reviews',
data: [
distribution[1] || 0,
distribution[2] || 0,
distribution[3] || 0,
distribution[4] || 0,
distribution[5] || 0
],
backgroundColor: [
'rgba(239, 68, 68, 0.8)',
'rgba(251, 146, 60, 0.8)',
'rgba(250, 204, 21, 0.8)',
'rgba(132, 204, 22, 0.8)',
'rgba(34, 197, 94, 0.8)'
],
borderColor: [
'rgba(239, 68, 68, 1)',
'rgba(251, 146, 60, 1)',
'rgba(250, 204, 21, 1)',
'rgba(132, 204, 22, 1)',
'rgba(34, 197, 94, 1)'
],
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1,
color: isDark ? '#94a3b8' : '#4b5563'
},
grid: {
color: isDark ? '#334155' : '#e5e7eb'
}
},
x: {
ticks: {
color: isDark ? '#94a3b8' : '#4b5563'
},
grid: {
color: isDark ? '#334155' : '#e5e7eb'
}
}
}
}
});
}
function downloadCSV() {
if (currentResults.length === 0) {
toast.warning('No Data', 'No results to download');
return;
}
// Create CSV content
const headers = ['Comment', 'Predicted_Rating', 'Confidence'];
const csvContent = [
headers.join(','),
...currentResults.map(r =>
`"${r.Comment.replace(/"/g, '""')}",${r.Predicted_Rating},${r.Confidence}`
)
].join('\n');
// Create download link
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `predictions_${new Date().getTime()}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success('Downloaded', 'CSV file downloaded successfully');
}
function downloadPDF() {
if (currentResults.length === 0) {
toast.warning('No Data', 'No results to download');
return;
}
toast.info('Generating PDF', 'Please wait...');
try {
// Prepare data
const predictions = currentResults.map(r => ({
text: r.Comment,
rating: r.Predicted_Rating,
confidence: r.Confidence
}));
// Send request to generate PDF
fetch('/api/predict/download-pdf', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
predictions: predictions,
distribution: currentDistribution,
wordcloud_path: currentWordcloudUrl
})
})
.then(response => {
if (response.ok) {
return response.blob();
}
throw new Error('Failed to generate PDF');
})
.then(blob => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `predictions_report_${new Date().getTime()}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
toast.success('Downloaded', 'PDF report downloaded successfully');
})
.catch(error => {
console.error('Error downloading PDF:', error);
toast.error('Error', 'Error generating PDF report. Please try again.');
});
} catch (error) {
console.error('Error preparing PDF download:', error);
toast.error('Error', 'Error preparing PDF report');
}
}
// Load and display prediction history
async function loadHistory() {
document.getElementById('history-loading').classList.remove('hidden');
try {
const response = await fetch('/api/predict/history', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const history = await response.json();
displayHistory(history);
} else {
console.error('Failed to load history');
}
} catch (error) {
console.error('Error loading history:', error);
} finally {
document.getElementById('history-loading').classList.add('hidden');
}
}
function displayHistory(history) {
const tbody = document.getElementById('history-tbody');
if (history.length === 0) {
tbody.innerHTML = `
<tr class="text-center text-gray-500 dark:text-gray-400">
<td colspan="5" class="px-4 py-8">
<i class="fas fa-inbox text-3xl text-gray-300 dark:text-gray-600 mb-2"></i>
<p>No prediction history yet</p>
</td>
</tr>
`;
return;
}
tbody.innerHTML = '';
history.forEach(item => {
const date = new Date(item.created_at).toLocaleString('vi-VN');
const shortComment = item.comment.length > 50
? item.comment.substring(0, 50) + '...'
: item.comment;
// Apply highlighting
const highlightedComment = highlighter.highlight(shortComment);
const row = `
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700">
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">${date}</td>
<td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300" title="${item.comment}">${highlightedComment}</td>
<td class="px-4 py-3 text-center">
<span class="inline-block bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 px-3 py-1 rounded-full font-semibold text-sm">
${item.predicted_rating}⭐
</span>
</td>
<td class="px-4 py-3 text-center text-sm text-gray-600 dark:text-gray-400">
${(item.confidence_score * 100).toFixed(1)}%
</td>
<td class="px-4 py-3 text-center text-sm">
<span class="inline-block ${item.prediction_type === 'single' ? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200' : 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'} px-2 py-1 rounded text-xs font-semibold">
${item.prediction_type}
</span>
</td>
</tr>
`;
tbody.innerHTML += row;
});
}
function refreshHistory() {
toast.info('Refreshing', 'Loading history...');
loadHistory();
}
</script>
{% endblock %}