msse-ai-engineering / templates /management.html
Tobias Pasquale
Complete document management system implementation
3d8e949
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PolicyWise - Document Management</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* Management Dashboard Styles */
.management-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.dashboard-header {
text-align: center;
margin-bottom: 3rem;
}
.dashboard-header h1 {
color: var(--primary-color, #2563eb);
margin-bottom: 0.5rem;
}
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 3rem;
}
.card {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
}
.card h2 {
margin-top: 0;
color: #374151;
font-size: 1.5rem;
margin-bottom: 1.5rem;
}
/* Upload Section */
.upload-area {
border: 2px dashed #d1d5db;
border-radius: 8px;
padding: 3rem;
text-align: center;
background: #f9fafb;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #6366f1;
background: #f0f9ff;
}
.upload-area.dragover {
border-color: #6366f1;
background: #eff6ff;
}
.upload-icon {
font-size: 3rem;
margin-bottom: 1rem;
color: #6b7280;
}
.upload-area h3 {
margin: 0 0 0.5rem 0;
color: #374151;
}
.upload-area p {
margin: 0;
color: #6b7280;
}
.file-input {
display: none;
}
.upload-btn {
background: #6366f1;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
margin-top: 1rem;
transition: background 0.2s;
}
.upload-btn:hover {
background: #5856eb;
}
.upload-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
/* Progress Section */
.progress-section {
margin-top: 2rem;
display: none;
}
.progress-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: #f3f4f6;
border-radius: 6px;
margin-bottom: 0.5rem;
}
.progress-bar {
width: 100px;
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #10b981;
border-radius: 4px;
transition: width 0.3s ease;
}
/* Status Section */
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.status-card {
background: #f8fafc;
border-radius: 8px;
padding: 1.5rem;
text-align: center;
}
.status-value {
display: block;
font-size: 2rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 0.5rem;
}
.status-label {
color: #6b7280;
font-size: 0.875rem;
}
/* Jobs List */
.jobs-list {
max-height: 400px;
overflow-y: auto;
}
.job-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.job-item:last-child {
border-bottom: none;
}
.job-info {
flex: 1;
}
.job-name {
font-weight: 500;
color: #374151;
}
.job-status {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.status-completed {
background: #d1fae5;
color: #065f46;
}
.status-processing {
background: #dbeafe;
color: #1e40af;
}
.status-failed {
background: #fee2e2;
color: #991b1b;
}
.status-pending {
background: #fef3c7;
color: #92400e;
}
/* Navigation */
.nav-link {
display: inline-block;
margin-bottom: 2rem;
color: #6366f1;
text-decoration: none;
font-weight: 500;
}
.nav-link:hover {
text-decoration: underline;
}
/* Responsive */
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
.management-container {
padding: 1rem;
}
.upload-area {
padding: 2rem;
}
}
/* Notification */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 1000;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.notification.show {
transform: translateX(0);
}
.notification.success {
background: #10b981;
}
.notification.error {
background: #ef4444;
}
.notification.info {
background: #6366f1;
}
</style>
</head>
<body>
<div class="management-container">
<a href="/" class="nav-link">← Back to Chat</a>
<header class="dashboard-header">
<h1>Document Management</h1>
<p>Upload and manage documents for the PolicyWise knowledge base</p>
</header>
<div class="dashboard-grid">
<!-- Upload Section -->
<div class="card">
<h2>Upload Documents</h2>
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📄</div>
<h3>Drag and drop files here</h3>
<p>or click to select files</p>
<p style="font-size: 0.75rem; margin-top: 1rem; color: #9ca3af;">
Supported: PDF, Word, Markdown, Text files (max 50MB each)
</p>
</div>
<input type="file" id="fileInput" class="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md">
<button id="uploadBtn" class="upload-btn" disabled>Select Files to Upload</button>
<div class="progress-section" id="progressSection">
<h3>Upload Progress</h3>
<div id="progressList"></div>
</div>
</div>
<!-- System Status -->
<div class="card">
<h2>System Status</h2>
<div class="status-grid" id="statusGrid">
<div class="status-card">
<span class="status-value" id="totalFiles">-</span>
<span class="status-label">Total Files</span>
</div>
<div class="status-card">
<span class="status-value" id="queueSize">-</span>
<span class="status-label">Queue Size</span>
</div>
<div class="status-card">
<span class="status-value" id="activeJobs">-</span>
<span class="status-label">Processing</span>
</div>
<div class="status-card">
<span class="status-value" id="completedJobs">-</span>
<span class="status-label">Completed</span>
</div>
</div>
</div>
</div>
<!-- Processing Jobs -->
<div class="card">
<h2>Recent Processing Jobs</h2>
<div class="jobs-list" id="jobsList">
<div style="text-align: center; color: #6b7280; padding: 2rem;">
Loading jobs...
</div>
</div>
</div>
</div>
<script>
class DocumentManager {
constructor() {
this.apiBase = '/api/documents';
this.uploadQueue = [];
this.init();
}
init() {
this.setupUploadHandlers();
this.loadStatus();
this.loadJobs();
// Refresh data every 5 seconds
setInterval(() => {
this.loadStatus();
this.loadJobs();
}, 5000);
}
setupUploadHandlers() {
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
// Drag and drop
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
this.handleFiles(e.dataTransfer.files);
});
uploadArea.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
this.handleFiles(e.target.files);
});
uploadBtn.addEventListener('click', () => {
this.uploadFiles();
});
}
handleFiles(files) {
this.uploadQueue = Array.from(files);
const uploadBtn = document.getElementById('uploadBtn');
if (this.uploadQueue.length > 0) {
uploadBtn.disabled = false;
uploadBtn.textContent = `Upload ${this.uploadQueue.length} files`;
} else {
uploadBtn.disabled = true;
uploadBtn.textContent = 'Select Files to Upload';
}
}
async uploadFiles() {
if (this.uploadQueue.length === 0) return;
const progressSection = document.getElementById('progressSection');
const progressList = document.getElementById('progressList');
const uploadBtn = document.getElementById('uploadBtn');
progressSection.style.display = 'block';
progressList.innerHTML = '';
uploadBtn.disabled = true;
uploadBtn.textContent = 'Uploading...';
for (let i = 0; i < this.uploadQueue.length; i++) {
const file = this.uploadQueue[i];
const progressItem = this.createProgressItem(file, i);
progressList.appendChild(progressItem);
try {
await this.uploadSingleFile(file, i);
} catch (error) {
console.error('Upload failed:', error);
this.updateProgress(i, 'failed', error.message);
}
}
this.showNotification('Upload completed', 'success');
uploadBtn.disabled = false;
uploadBtn.textContent = 'Select Files to Upload';
this.uploadQueue = [];
// Refresh status after upload
setTimeout(() => {
this.loadStatus();
this.loadJobs();
}, 1000);
}
createProgressItem(file, index) {
const item = document.createElement('div');
item.className = 'progress-item';
item.innerHTML = `
<div class="job-info">
<div class="job-name">${file.name}</div>
<div class="job-status" id="status-${index}">Preparing...</div>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progress-${index}" style="width: 0%"></div>
</div>
`;
return item;
}
async uploadSingleFile(file, index) {
const formData = new FormData();
formData.append('files', file);
formData.append('auto_process', 'true');
this.updateProgress(index, 'uploading', 'Uploading...');
const response = await fetch(`${this.apiBase}/upload`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const result = await response.json();
if (result.status === 'success') {
this.updateProgress(index, 'completed', 'Upload completed');
} else {
throw new Error(result.message || 'Upload failed');
}
}
updateProgress(index, status, message) {
const statusEl = document.getElementById(`status-${index}`);
const progressEl = document.getElementById(`progress-${index}`);
if (statusEl) statusEl.textContent = message;
if (progressEl) {
switch (status) {
case 'uploading':
progressEl.style.width = '50%';
break;
case 'completed':
progressEl.style.width = '100%';
progressEl.style.background = '#10b981';
break;
case 'failed':
progressEl.style.width = '100%';
progressEl.style.background = '#ef4444';
break;
}
}
}
async loadStatus() {
try {
const response = await fetch(`${this.apiBase}/stats`);
const data = await response.json();
if (data.status === 'success') {
this.updateStatusDisplay(data.stats);
}
} catch (error) {
console.error('Failed to load status:', error);
}
}
updateStatusDisplay(stats) {
const elements = {
totalFiles: document.getElementById('totalFiles'),
queueSize: document.getElementById('queueSize'),
activeJobs: document.getElementById('activeJobs'),
completedJobs: document.getElementById('completedJobs')
};
if (stats.upload_stats) {
elements.totalFiles.textContent = stats.upload_stats.total_files || 0;
}
if (stats.processing_queue) {
elements.queueSize.textContent = stats.processing_queue.queue_size || 0;
elements.activeJobs.textContent = stats.processing_queue.active_jobs || 0;
elements.completedJobs.textContent = stats.processing_queue.completed_jobs || 0;
}
}
async loadJobs() {
try {
const response = await fetch(`${this.apiBase}/jobs`);
const data = await response.json();
if (data.status === 'success') {
this.updateJobsDisplay(data.jobs);
}
} catch (error) {
console.error('Failed to load jobs:', error);
}
}
updateJobsDisplay(jobs) {
const jobsList = document.getElementById('jobsList');
if (jobs.length === 0) {
jobsList.innerHTML = '<div style="text-align: center; color: #6b7280; padding: 2rem;">No processing jobs found</div>';
return;
}
jobsList.innerHTML = jobs.slice(0, 10).map(job => `
<div class="job-item">
<div class="job-info">
<div class="job-name">${job.file_info?.original_name || 'Unknown'}</div>
<div class="job-status">
Started: ${job.started_at ? new Date(job.started_at).toLocaleString() : 'Not started'}
${job.error_message ? `• Error: ${job.error_message}` : ''}
</div>
</div>
<span class="status-badge status-${job.status}">
${job.status}
</span>
</div>
`).join('');
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => notification.classList.add('show'), 100);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => document.body.removeChild(notification), 300);
}, 3000);
}
}
// Initialize when page loads
document.addEventListener('DOMContentLoaded', () => {
new DocumentManager();
});
</script>
</body>
</html>