handwritten / static /script.js
IZERE HIRWA Roger
olm
b7d66ac
// Global variables
let categories = [];
let documents = [];
let authToken = null;
let currentUser = null;
let currentView = 'list';
// Initialize app
document.addEventListener('DOMContentLoaded', function() {
checkAuth();
setupSidebar();
});
// Sidebar functionality
function setupSidebar() {
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const mainContent = document.querySelector('.main-content');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', function() {
sidebar.classList.toggle('collapsed');
mainContent.classList.toggle('sidebar-collapsed');
});
}
// Auto-collapse on mobile
if (window.innerWidth <= 768) {
sidebar.classList.add('collapsed');
mainContent.classList.add('sidebar-collapsed');
}
}
// Authentication functions
function checkAuth() {
authToken = localStorage.getItem('authToken');
currentUser = localStorage.getItem('currentUser');
if (authToken && currentUser) {
showMainApp();
document.getElementById('welcomeUser').textContent = `Welcome, ${currentUser}`;
loadStats();
loadCategories();
setupFileUploads();
} else {
showLoginModal();
}
}
function showLoginModal() {
document.getElementById('loginModal').style.display = 'flex';
document.getElementById('mainApp').style.display = 'none';
}
function showMainApp() {
document.getElementById('loginModal').style.display = 'none';
document.getElementById('mainApp').style.display = 'block';
}
function logout() {
localStorage.removeItem('authToken');
localStorage.removeItem('currentUser');
authToken = null;
currentUser = null;
showLoginModal();
}
// Login form handler
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const resultDiv = document.getElementById('loginResult');
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
try {
const response = await fetch('/api/login', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
authToken = result.access_token;
currentUser = result.username;
localStorage.setItem('authToken', authToken);
localStorage.setItem('currentUser', currentUser);
showMainApp();
document.getElementById('welcomeUser').textContent = `Welcome, ${currentUser}`;
loadStats();
loadCategories();
setupFileUploads();
} else {
showResult(resultDiv, result.detail, 'error');
}
} catch (error) {
showResult(resultDiv, 'Login failed: ' + error.message, 'error');
}
});
// API request with authentication
async function authenticatedFetch(url, options = {}) {
if (!authToken) {
throw new Error('No authentication token');
}
const defaultOptions = {
headers: {
'Authorization': `Bearer ${authToken}`,
...options.headers
}
};
const response = await fetch(url, { ...options, ...defaultOptions });
if (response.status === 401) {
logout();
throw new Error('Authentication failed');
}
return response;
}
// Enhanced tab management with sidebar highlighting
function showTab(tabName) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// Remove active class from all sidebar links
document.querySelectorAll('.sidebar-nav .nav-link').forEach(link => {
link.classList.remove('active');
});
// Show selected tab
document.getElementById(tabName).classList.add('active');
// Highlight active sidebar link
const activeLink = document.querySelector(`[onclick="showTab('${tabName}')"]`);
if (activeLink) {
activeLink.classList.add('active');
}
// Load data for specific tabs
if (tabName === 'browse') {
loadCategories();
loadAllDocuments();
} else if (tabName === 'dashboard') {
loadStats();
loadRecentActivity();
createCategoryChart();
}
}
// Setup file upload drag & drop
function setupFileUploads() {
const uploads = [
{ div: 'categoryUpload', input: 'categoryFile' },
{ div: 'classifyUpload', input: 'classifyFile' },
{ div: 'ocrUpload', input: 'ocrFile' }
];
uploads.forEach(upload => {
const uploadDiv = document.getElementById(upload.div);
const fileInput = document.getElementById(upload.input);
uploadDiv.addEventListener('click', () => fileInput.click());
uploadDiv.addEventListener('dragover', (e) => {
e.preventDefault();
uploadDiv.classList.add('dragover');
});
uploadDiv.addEventListener('dragleave', () => {
uploadDiv.classList.remove('dragover');
});
uploadDiv.addEventListener('drop', (e) => {
e.preventDefault();
uploadDiv.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
uploadDiv.querySelector('p').textContent = files[0].name;
}
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
uploadDiv.querySelector('p').textContent = fileInput.files[0].name;
}
});
});
}
// Enhanced stats loading with sidebar display
async function loadStats() {
try {
const response = await authenticatedFetch('/api/stats');
const stats = await response.json();
// Dashboard stats
const dashboardStatsHtml = `
<div class="col-md-4">
<div class="card stat-card">
<div class="card-body">
<h3>${stats.total_categories}</h3>
<p><i class="fas fa-tags me-2"></i>Total Categories</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card">
<div class="card-body">
<h3>${stats.total_documents}</h3>
<p><i class="fas fa-file me-2"></i>Documents Archived</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card">
<div class="card-body">
<h3>35%</h3>
<p><i class="fas fa-percentage me-2"></i>Min Confidence</p>
</div>
</div>
</div>
`;
// Sidebar stats
const sidebarStatsHtml = `
<div class="sidebar-stat">
<h6>${stats.total_categories}</h6>
<small>Categories</small>
</div>
<div class="sidebar-stat">
<h6>${stats.total_documents}</h6>
<small>Documents</small>
</div>
`;
document.getElementById('dashboardStats').innerHTML = dashboardStatsHtml;
document.getElementById('sidebarStats').innerHTML = sidebarStatsHtml;
} catch (error) {
console.error('Error loading stats:', error);
}
}
// Enhanced categories loading with sidebar display
async function loadCategories() {
try {
const response = await authenticatedFetch('/api/categories');
const data = await response.json();
categories = data.categories;
// Main category buttons
const buttonsHtml = `
<button class="category-btn active" onclick="filterDocuments('all')">
<i class="fas fa-th-large me-2"></i>All Documents
</button>
${categories.map(cat => `
<button class="category-btn" onclick="filterDocuments('${cat}')">
<i class="fas fa-folder me-2"></i>${cat} (${data.counts[cat] || 0})
</button>
`).join('')}
`;
// Sidebar categories
const sidebarCategoriesHtml = categories.map(cat => `
<div class="sidebar-category" onclick="filterDocuments('${cat}')">
<div class="sidebar-category-icon"></div>
<span>${cat} (${data.counts[cat] || 0})</span>
</div>
`).join('');
document.getElementById('categoryButtons').innerHTML = buttonsHtml;
document.getElementById('sidebarCategories').innerHTML = sidebarCategoriesHtml;
} catch (error) {
console.error('Error loading categories:', error);
}
}
// Load all documents
async function loadAllDocuments() {
try {
const response = await authenticatedFetch('/api/documents');
const data = await response.json();
documents = data.documents;
displayDocuments(documents);
} catch (error) {
console.error('Error loading documents:', error);
}
}
// Filter documents by category
async function filterDocuments(category) {
// Update active button
document.querySelectorAll('.category-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
try {
let filteredDocs;
if (category === 'all') {
const response = await authenticatedFetch('/api/documents');
const data = await response.json();
filteredDocs = data.documents;
} else {
const response = await authenticatedFetch(`/api/documents/${category}`);
const data = await response.json();
filteredDocs = data.documents;
}
displayDocuments(filteredDocs);
} catch (error) {
console.error('Error filtering documents:', error);
}
}
// Delete document
async function deleteDocument(documentId, filename) {
if (!confirm(`Are you sure you want to delete "${filename}"? This action cannot be undone.`)) {
return;
}
try {
const response = await authenticatedFetch(`/api/documents/${documentId}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok) {
// Refresh the current view
loadAllDocuments();
loadStats();
loadCategories();
alert('Document deleted successfully');
} else {
alert('Failed to delete document: ' + result.detail);
}
} catch (error) {
alert('Error deleting document: ' + error.message);
}
}
// Enhanced document display with image preview
function displayDocuments(docs) {
const container = document.getElementById('documentsContainer');
if (docs.length === 0) {
container.innerHTML = `
<div class="text-center py-5">
<i class="fas fa-folder-open fa-4x text-muted mb-3"></i>
<h4 class="text-muted">No documents found</h4>
<p class="text-muted">Upload some documents to get started</p>
</div>
`;
return;
}
const docsHtml = docs.map(doc => {
const similarityClass = doc.similarity >= 0.7 ? 'similarity-high' :
doc.similarity >= 0.5 ? 'similarity-medium' : 'similarity-low';
const confidenceText = doc.similarity >= 0.7 ? 'High' :
doc.similarity >= 0.5 ? 'Medium' : 'Low';
return `
<div class="document-card">
<img src="/api/document-preview/${doc.id}"
class="document-preview"
alt="${doc.original_filename}"
onclick="showDocumentPreview('${doc.id}', '${doc.original_filename}')"
onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iI2Y4ZjlmYSIvPgo8dGV4dCB4PSI1MCUiIHk9IjUwJSIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBmaWxsPSIjNmM3NTdkIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSI+RG9jdW1lbnQ8L3RleHQ+Cjwvc3ZnPg=='">
<div class="document-title">${doc.original_filename}</div>
<div class="document-meta">
<i class="fas fa-tag me-1"></i>
<strong>${doc.category}</strong>
</div>
<div class="document-meta">
<i class="fas fa-chart-line me-1"></i>
Confidence: <span class="similarity-badge ${similarityClass}">
${(doc.similarity * 100).toFixed(1)}% ${confidenceText}
</span>
</div>
<div class="document-meta">
<i class="fas fa-calendar me-1"></i>
${new Date(doc.upload_date).toLocaleDateString()}
</div>
<div class="mt-3">
<button class="btn btn-primary btn-sm me-2" onclick="showDocumentPreview('${doc.id}', '${doc.original_filename}')">
<i class="fas fa-eye me-1"></i>Preview
</button>
<button class="btn btn-danger btn-sm" onclick="deleteDocument('${doc.id}', '${doc.original_filename}')">
<i class="fas fa-trash me-1"></i>Delete
</button>
</div>
</div>
`;
}).join('');
container.innerHTML = `<div class="document-grid">${docsHtml}</div>`;
}
// Document preview functionality
function showDocumentPreview(documentId, filename) {
const modal = new bootstrap.Modal(document.getElementById('documentModal'));
const preview = document.getElementById('documentPreview');
const details = document.getElementById('documentDetails');
preview.src = `/api/document-preview/${documentId}`;
details.innerHTML = `<h6>${filename}</h6>`;
modal.show();
}
// View toggle functionality
function toggleView(viewType) {
currentView = viewType;
// Update button states
document.querySelectorAll('.btn-group .btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Reload documents with new view
loadAllDocuments();
}
// Create category distribution chart
function createCategoryChart() {
const ctx = document.getElementById('categoryChart');
if (!ctx) return;
// Sample data - you can modify this to use real data
const data = {
labels: categories.slice(0, 5), // Show top 5 categories
datasets: [{
label: 'Documents',
data: categories.slice(0, 5).map(() => Math.floor(Math.random() * 20) + 1),
backgroundColor: [
'rgba(13, 110, 253, 0.8)',
'rgba(25, 135, 84, 0.8)',
'rgba(255, 193, 7, 0.8)',
'rgba(220, 53, 69, 0.8)',
'rgba(13, 202, 240, 0.8)'
],
borderColor: [
'rgb(13, 110, 253)',
'rgb(25, 135, 84)',
'rgb(255, 193, 7)',
'rgb(220, 53, 69)',
'rgb(13, 202, 240)'
],
borderWidth: 2
}]
};
new Chart(ctx, {
type: 'doughnut',
data: data,
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
}
}
}
});
}
// Load recent activity
function loadRecentActivity() {
const recentActivity = document.getElementById('recentActivity');
// Sample recent activity - you can modify this to use real data
const activities = [
{ action: 'Document classified', item: 'passport.jpg', time: '2 min ago' },
{ action: 'Category added', item: 'driver_license', time: '1 hour ago' },
{ action: 'Document uploaded', item: 'certificate.pdf', time: '3 hours ago' }
];
const activityHtml = activities.map(activity => `
<div class="d-flex justify-content-between align-items-center mb-2 p-2 bg-light rounded">
<div>
<small class="fw-bold">${activity.action}</small><br>
<small class="text-muted">${activity.item}</small>
</div>
<small class="text-muted">${activity.time}</small>
</div>
`).join('');
recentActivity.innerHTML = activityHtml;
}
// Form submissions
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('categoryFile');
const labelInput = document.getElementById('categoryLabel');
const resultDiv = document.getElementById('uploadResult');
if (!fileInput.files[0] || !labelInput.value.trim()) {
showResult(resultDiv, 'Please select a file and enter a label.', 'error');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('label', labelInput.value.trim());
showResult(resultDiv, '<div class="loading"></div> Uploading...', 'info');
try {
const response = await authenticatedFetch('/api/upload-category', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
showResult(resultDiv, result.message, 'success');
labelInput.value = '';
fileInput.value = '';
document.querySelector('#categoryUpload p').textContent = 'Click to select or drag & drop files here';
loadStats();
loadCategories();
} else {
showResult(resultDiv, result.detail, 'error');
}
} catch (error) {
showResult(resultDiv, 'Upload failed: ' + error.message, 'error');
}
});
document.getElementById('classifyForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('classifyFile');
const resultDiv = document.getElementById('classifyResult');
if (!fileInput.files[0]) {
showResult(resultDiv, 'Please select a file to classify.', 'error');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
showResult(resultDiv, '<div class="loading"></div> Classifying...', 'info');
try {
const response = await authenticatedFetch('/api/classify-document', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
const confidenceText = result.confidence === 'high' ? 'βœ… High Confidence' : '⚠️ Low Confidence';
const savedText = result.document_saved ? '\nπŸ“ Document saved to archive' : '';
let matchesText = '\n\nTop matches:\n';
result.matches.forEach(match => {
matchesText += `β€’ ${match.category}: ${(match.similarity * 100).toFixed(1)}%\n`;
});
showResult(resultDiv,
`🎯 Classification: ${result.category}\n` +
`${confidenceText} (${(result.similarity * 100).toFixed(1)}%)${savedText}${matchesText}`,
result.confidence === 'high' ? 'success' : 'warning'
);
fileInput.value = '';
document.querySelector('#classifyUpload p').textContent = 'Click to select or drag & drop files here';
loadStats();
loadCategories();
} else {
// Handle different error types
let errorMessage = result.error || result.detail || 'Classification failed';
if (errorMessage.includes('No categories')) {
errorMessage = '⚠️ Please add some document categories first before classifying documents.';
} else if (errorMessage.includes('Failed to process')) {
errorMessage = '❌ Could not process the uploaded file. Please ensure it\'s a valid image or PDF.';
} else if (errorMessage.includes('JSON serializable')) {
errorMessage = 'πŸ”§ Processing error occurred. Please try again.';
}
showResult(resultDiv, errorMessage, 'error');
}
} catch (error) {
console.error('Classification error:', error);
showResult(resultDiv, '❌ Network error: Please check your connection and try again.', 'error');
}
});
document.getElementById('ocrForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('ocrFile');
const resultDiv = document.getElementById('ocrResult');
if (!fileInput.files[0]) {
showResult(resultDiv, 'Please select a file for OCR.', 'error');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
showResult(resultDiv, '<div class="loading"></div> Extracting text with advanced OCR...', 'info');
try {
const response = await authenticatedFetch('/api/ocr', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
const ocrInfo = result.enhanced_features ?
`πŸ€– Processed with ${result.ocr_method} (Enhanced Features: Tables, LaTeX, Watermarks)\n\n` :
`πŸ“ Processed with ${result.ocr_method}\n\n`;
showResult(resultDiv, ocrInfo + result.text, 'success');
} else {
showResult(resultDiv, result.error || result.detail, 'error');
}
} catch (error) {
showResult(resultDiv, 'OCR failed: ' + error.message, 'error');
}
});
// Utility function to show results
function showResult(element, message, type) {
const className = type === 'success' ? 'result-success' :
type === 'error' ? 'result-error' :
type === 'warning' ? 'result-warning' : '';
element.innerHTML = `<div class="result-box ${className}">${message}</div>`;
}