Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Legal Dashboard - Scraping & Rating System</title> | |
| <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.css" rel="stylesheet"> | |
| <!-- Load API Client and Core System --> | |
| <script src="js/api-client.js"></script> | |
| <script src="js/core.js"></script> | |
| <script src="js/notifications.js"></script> | |
| <script src="js/scraping-control.js"></script> | |
| <style> | |
| :root { | |
| --primary-color: #2c3e50; | |
| --secondary-color: #3498db; | |
| --success-color: #27ae60; | |
| --warning-color: #f39c12; | |
| --danger-color: #e74c3c; | |
| --light-bg: #f8f9fa; | |
| --dark-bg: #343a40; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| } | |
| .navbar-custom { | |
| background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); | |
| } | |
| .navbar-brand { | |
| font-weight: bold; | |
| font-size: 1.5rem; | |
| } | |
| .card { | |
| border: none; | |
| border-radius: 15px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.1); | |
| transition: transform 0.3s ease; | |
| } | |
| .card:hover { | |
| transform: translateY(-5px); | |
| } | |
| .card-header { | |
| background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); | |
| color: white; | |
| border-radius: 15px 15px 0 0 ; | |
| font-weight: bold; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--secondary-color), var(--primary-color)); | |
| border: none; | |
| border-radius: 25px; | |
| padding: 10px 25px; | |
| font-weight: bold; | |
| } | |
| .btn-success { | |
| background: linear-gradient(135deg, var(--success-color), #2ecc71); | |
| border: none; | |
| border-radius: 25px; | |
| } | |
| .btn-warning { | |
| background: linear-gradient(135deg, var(--warning-color), #f1c40f); | |
| border: none; | |
| border-radius: 25px; | |
| } | |
| .btn-danger { | |
| background: linear-gradient(135deg, var(--danger-color), #c0392b); | |
| border: none; | |
| border-radius: 25px; | |
| } | |
| .progress { | |
| height: 25px; | |
| border-radius: 15px; | |
| background-color: #e9ecef; | |
| } | |
| .progress-bar { | |
| border-radius: 15px; | |
| font-weight: bold; | |
| } | |
| .stats-card { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border-radius: 15px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .stats-number { | |
| font-size: 2.5rem; | |
| font-weight: bold; | |
| } | |
| .stats-label { | |
| font-size: 0.9rem; | |
| opacity: 0.8; | |
| } | |
| .table { | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| .table thead th { | |
| background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); | |
| color: white; | |
| border: none; | |
| font-weight: bold; | |
| } | |
| .badge { | |
| border-radius: 20px; | |
| padding: 8px 15px; | |
| font-weight: bold; | |
| } | |
| .alert { | |
| border-radius: 15px; | |
| border: none; | |
| } | |
| .form-control, .form-select { | |
| border-radius: 10px; | |
| border: 2px solid #e9ecef; | |
| transition: border-color 0.3s ease; | |
| } | |
| .form-control:focus, .form-select:focus { | |
| border-color: var(--secondary-color); | |
| box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); | |
| } | |
| .modal-content { | |
| border-radius: 15px; | |
| border: none; | |
| } | |
| .modal-header { | |
| background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); | |
| color: white; | |
| border-radius: 15px 15px 0 0; | |
| } | |
| .loading-spinner { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid var(--secondary-color); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .notification { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 9999; | |
| border-radius: 10px; | |
| padding: 15px 20px; | |
| color: white; | |
| font-weight: bold; | |
| transform: translateX(400px); | |
| transition: transform 0.3s ease; | |
| } | |
| .notification.show { | |
| transform: translateX(0); | |
| } | |
| .notification.success { | |
| background: linear-gradient(135deg, var(--success-color), #2ecc71); | |
| } | |
| .notification.warning { | |
| background: linear-gradient(135deg, var(--warning-color), #f1c40f); | |
| } | |
| .notification.error { | |
| background: linear-gradient(135deg, var(--danger-color), #c0392b); | |
| } | |
| .chart-container { | |
| position: relative; | |
| height: 300px; | |
| margin: 20px 0; | |
| } | |
| .job-status { | |
| padding: 10px 15px; | |
| border-radius: 20px; | |
| font-weight: bold; | |
| font-size: 0.9rem; | |
| } | |
| .status-pending { background-color: #f8f9fa; color: #6c757d; } | |
| .status-processing { background-color: #fff3cd; color: #856404; } | |
| .status-completed { background-color: #d1ecf1; color: #0c5460; } | |
| .status-failed { background-color: #f8d7da; color: #721c24; } | |
| .rating-badge { | |
| padding: 5px 10px; | |
| border-radius: 15px; | |
| font-weight: bold; | |
| font-size: 0.8rem; | |
| } | |
| .rating-excellent { background-color: #d4edda; color: #155724; } | |
| .rating-good { background-color: #d1ecf1; color: #0c5460; } | |
| .rating-average { background-color: #fff3cd; color: #856404; } | |
| .rating-poor { background-color: #f8d7da; color: #721c24; } | |
| .rating-unrated { background-color: #e2e3e5; color: #383d41; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Navigation --> | |
| <nav class="navbar navbar-expand-lg navbar-dark navbar-custom"> | |
| <div class="container-fluid"> | |
| <a class="navbar-brand" href="#"> | |
| <i class="fas fa-spider me-2"></i> | |
| Legal Dashboard - Scraping & Rating | |
| </a> | |
| <div class="navbar-nav ms-auto"> | |
| <a class="nav-link" href="#" onclick="showNotification('System is running smoothly', 'success')"> | |
| <i class="fas fa-heartbeat me-1"></i> | |
| System Health | |
| </a> | |
| </div> | |
| </div> | |
| </nav> | |
| <div class="container-fluid mt-4"> | |
| <!-- Statistics Cards --> | |
| <div class="row mb-4"> | |
| <div class="col-md-3"> | |
| <div class="stats-card text-center"> | |
| <div class="stats-number" id="totalItems">0</div> | |
| <div class="stats-label">Total Items Scraped</div> | |
| </div> | |
| </div> | |
| <div class="col-md-3"> | |
| <div class="stats-card text-center"> | |
| <div class="stats-number" id="activeJobs">0</div> | |
| <div class="stats-label">Active Jobs</div> | |
| </div> | |
| </div> | |
| <div class="col-md-3"> | |
| <div class="stats-card text-center"> | |
| <div class="stats-number" id="avgRating">0.0</div> | |
| <div class="stats-label">Average Rating</div> | |
| </div> | |
| </div> | |
| <div class="col-md-3"> | |
| <div class="stats-card text-center"> | |
| <div class="stats-number" id="totalRated">0</div> | |
| <div class="stats-label">Items Rated</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <!-- Scraping Control Panel --> | |
| <div class="col-lg-4"> | |
| <div class="card mb-4"> | |
| <div class="card-header"> | |
| <i class="fas fa-spider me-2"></i> | |
| Scraping Control Panel | |
| </div> | |
| <div class="card-body"> | |
| <form id="scrapingForm"> | |
| <div class="mb-3"> | |
| <label for="urls" class="form-label">URLs to Scrape</label> | |
| <textarea class="form-control" id="urls" rows="4" placeholder="Enter URLs (one per line) Example: https://example.com/page1 https://example.com/page2"></textarea> | |
| </div> | |
| <div class="mb-3"> | |
| <label for="strategy" class="form-label">Scraping Strategy</label> | |
| <select class="form-select" id="strategy"> | |
| <option value="general">General</option> | |
| <option value="legal_documents">Legal Documents</option> | |
| <option value="news_articles">News Articles</option> | |
| <option value="academic_papers">Academic Papers</option> | |
| <option value="government_sites">Government Sites</option> | |
| <option value="custom">Custom</option> | |
| </select> | |
| </div> | |
| <div class="mb-3"> | |
| <label for="keywords" class="form-label">Keywords (optional)</label> | |
| <input type="text" class="form-control" id="keywords" placeholder="Enter keywords separated by commas"> | |
| </div> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <div class="mb-3"> | |
| <label for="maxDepth" class="form-label">Max Depth</label> | |
| <input type="number" class="form-control" id="maxDepth" value="1" min="1" max="5"> | |
| </div> | |
| </div> | |
| <div class="col-md-6"> | |
| <div class="mb-3"> | |
| <label for="delay" class="form-label">Delay (seconds)</label> | |
| <input type="number" class="form-control" id="delay" value="1.0" min="0.1" max="10.0" step="0.1"> | |
| </div> | |
| </div> | |
| </div> | |
| <button type="submit" class="btn btn-primary w-100" id="startScrapingBtn"> | |
| <i class="fas fa-play me-2"></i> | |
| Start Scraping Job | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Rating Controls --> | |
| <div class="card mb-4"> | |
| <div class="card-header"> | |
| <i class="fas fa-star me-2"></i> | |
| Rating Controls | |
| </div> | |
| <div class="card-body"> | |
| <button type="button" class="btn btn-success w-100 mb-2" onclick="rateAllItems()"> | |
| <i class="fas fa-star me-2"></i> | |
| Rate All Unrated Items | |
| </button> | |
| <button type="button" class="btn btn-warning w-100 mb-2" onclick="getLowQualityItems()"> | |
| <i class="fas fa-exclamation-triangle me-2"></i> | |
| Get Low Quality Items | |
| </button> | |
| <button type="button" class="btn btn-info w-100" onclick="refreshStatistics()"> | |
| <i class="fas fa-sync-alt me-2"></i> | |
| Refresh Statistics | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Active Jobs --> | |
| <div class="col-lg-8"> | |
| <div class="card mb-4"> | |
| <div class="card-header d-flex justify-content-between align-items-center"> | |
| <span><i class="fas fa-tasks me-2"></i>Active Scraping Jobs</span> | |
| <button type="button" class="btn btn-sm btn-outline-light" onclick="refreshJobs()" aria-label="Refresh jobs"> | |
| <i class="fas fa-sync-alt"></i> | |
| </button> | |
| </div> | |
| <div class="card-body"> | |
| <div id="jobsContainer"> | |
| <div class="text-center text-muted"> | |
| <i class="fas fa-spinner fa-spin fa-2x mb-2"></i> | |
| <p>Loading jobs...</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Charts --> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="fas fa-chart-pie me-2"></i> | |
| Rating Distribution | |
| </div> | |
| <div class="card-body"> | |
| <div class="chart-container"> | |
| <canvas id="ratingChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-6"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="fas fa-chart-bar me-2"></i> | |
| Language Distribution | |
| </div> | |
| <div class="card-body"> | |
| <div class="chart-container"> | |
| <canvas id="languageChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Scraped Items Table --> | |
| <div class="row mt-4"> | |
| <div class="col-12"> | |
| <div class="card"> | |
| <div class="card-header d-flex justify-content-between align-items-center"> | |
| <span><i class="fas fa-list me-2"></i>Scraped Items</span> | |
| <div> | |
| <button type="button" class="btn btn-sm btn-outline-light me-2" onclick="refreshItems()" aria-label="Refresh items"> | |
| <i class="fas fa-sync-alt"></i> | |
| </button> | |
| <select class="form-select form-select-sm d-inline-block w-auto" id="itemFilter"> | |
| <option value="">All Items</option> | |
| <option value="completed">Completed</option> | |
| <option value="failed">Failed</option> | |
| <option value="rated">Rated</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="card-body"> | |
| <div class="table-responsive"> | |
| <table class="table table-hover"> | |
| <thead> | |
| <tr> | |
| <th scope="col">Title</th> | |
| <th scope="col">URL</th> | |
| <th scope="col">Status</th> | |
| <th scope="col">Rating</th> | |
| <th scope="col">Language</th> | |
| <th scope="col">Word Count</th> | |
| <th scope="col">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="itemsTableBody"> | |
| <tr> | |
| <td colspan="7" class="text-center text-muted"> | |
| <i class="fas fa-spinner fa-spin me-2"></i> | |
| Loading items... | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Notification Container --> | |
| <div id="notificationContainer"></div> | |
| <!-- Scripts --> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></script> | |
| <script> | |
| // Global variables | |
| let ratingChart = null; | |
| let languageChart = null; | |
| let refreshInterval = null; | |
| // Initialize dashboard | |
| document.addEventListener('DOMContentLoaded', function() { | |
| loadDashboard(); | |
| startAutoRefresh(); | |
| }); | |
| // Load dashboard data | |
| async function loadDashboard() { | |
| try { | |
| await Promise.all([ | |
| loadStatistics(), | |
| loadJobs(), | |
| loadItems(), | |
| loadCharts() | |
| ]); | |
| } catch (error) { | |
| console.error('Error loading dashboard:', error); | |
| showNotification('Error loading dashboard data', 'error'); | |
| } | |
| } | |
| // Load statistics | |
| async function loadStatistics() { | |
| try { | |
| const [scrapingStats, ratingSummary] = await Promise.all([ | |
| fetch('/api/scrape/statistics').then(r => r.json()), | |
| fetch('/api/rating/summary').then(r => r.json()) | |
| ]); | |
| document.getElementById('totalItems').textContent = scrapingStats.total_items || 0; | |
| document.getElementById('activeJobs').textContent = scrapingStats.active_jobs || 0; | |
| document.getElementById('avgRating').textContent = (ratingSummary.average_score || 0).toFixed(2); | |
| document.getElementById('totalRated').textContent = ratingSummary.total_rated || 0; | |
| } catch (error) { | |
| console.error('Error loading statistics:', error); | |
| } | |
| } | |
| // Load jobs | |
| async function loadJobs() { | |
| try { | |
| const response = await fetch('/api/scrape/status'); | |
| const jobs = await response.json(); | |
| const container = document.getElementById('jobsContainer'); | |
| if (jobs.length === 0) { | |
| container.innerHTML = '<div class="text-center text-muted"><p>No active jobs</p></div>'; | |
| return; | |
| } | |
| let html = ''; | |
| jobs.forEach(job => { | |
| const progress = job.progress * 100; | |
| html += ` | |
| <div class="card mb-3"> | |
| <div class="card-body"> | |
| <div class="d-flex justify-content-between align-items-center mb-2"> | |
| <h6 class="card-title mb-0">Job ${job.job_id}</h6> | |
| <span class="job-status status-${job.status}">${job.status}</span> | |
| </div> | |
| <div class="progress mb-2"> | |
| <div class="progress-bar" role="progressbar" style="width: ${progress}%" | |
| aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100"> | |
| ${progress.toFixed(1)}% | |
| </div> | |
| </div> | |
| <div class="row text-center"> | |
| <div class="col-4"> | |
| <small class="text-muted">Total</small><br> | |
| <strong>${job.total_items}</strong> | |
| </div> | |
| <div class="col-4"> | |
| <small class="text-muted">Completed</small><br> | |
| <strong class="text-success">${job.completed_items}</strong> | |
| </div> | |
| <div class="col-4"> | |
| <small class="text-muted">Failed</small><br> | |
| <strong class="text-danger">${job.failed_items}</strong> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| container.innerHTML = html; | |
| } catch (error) { | |
| console.error('Error loading jobs:', error); | |
| document.getElementById('jobsContainer').innerHTML = | |
| '<div class="alert alert-danger">Error loading jobs</div>'; | |
| } | |
| } | |
| // Load items | |
| async function loadItems() { | |
| try { | |
| const response = await fetch('/api/scrape/items?limit=50'); | |
| const items = await response.json(); | |
| const tbody = document.getElementById('itemsTableBody'); | |
| let html = ''; | |
| items.forEach(item => { | |
| const ratingClass = getRatingClass(item.rating_score); | |
| const ratingText = item.rating_score > 0 ? item.rating_score.toFixed(2) : 'Unrated'; | |
| html += ` | |
| <tr> | |
| <td> | |
| <strong>${item.title || 'No Title'}</strong> | |
| <br><small class="text-muted">${item.domain}</small> | |
| </td> | |
| <td> | |
| <a href="${item.url}" target="_blank" class="text-decoration-none"> | |
| ${item.url.substring(0, 50)}... | |
| </a> | |
| </td> | |
| <td> | |
| <span class="job-status status-${item.processing_status}"> | |
| ${item.processing_status} | |
| </span> | |
| </td> | |
| <td> | |
| <span class="rating-badge ${ratingClass}"> | |
| ${ratingText} | |
| </span> | |
| </td> | |
| <td> | |
| <span class="badge bg-info">${item.language}</span> | |
| </td> | |
| <td> | |
| <span class="badge bg-secondary">${item.word_count}</span> | |
| </td> | |
| <td> | |
| <button class="btn btn-sm btn-outline-primary me-1" onclick="viewItem('${item.id}')"> | |
| <i class="fas fa-eye"></i> | |
| </button> | |
| <button class="btn btn-sm btn-outline-success" onclick="rateItem('${item.id}')"> | |
| <i class="fas fa-star"></i> | |
| </button> | |
| </td> | |
| </tr> | |
| `; | |
| }); | |
| tbody.innerHTML = html; | |
| } catch (error) { | |
| console.error('Error loading items:', error); | |
| document.getElementById('itemsTableBody').innerHTML = | |
| '<tr><td colspan="7" class="text-center text-danger">Error loading items</td></tr>'; | |
| } | |
| } | |
| // Load charts | |
| async function loadCharts() { | |
| try { | |
| const [scrapingStats, ratingSummary] = await Promise.all([ | |
| fetch('/api/scrape/statistics').then(r => r.json()), | |
| fetch('/api/rating/summary').then(r => r.json()) | |
| ]); | |
| // Rating distribution chart | |
| const ratingCtx = document.getElementById('ratingChart').getContext('2d'); | |
| if (ratingChart) ratingChart.destroy(); | |
| ratingChart = new Chart(ratingCtx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: Object.keys(ratingSummary.rating_level_distribution || {}), | |
| datasets: [{ | |
| data: Object.values(ratingSummary.rating_level_distribution || {}), | |
| backgroundColor: ['#28a745', '#17a2b8', '#ffc107', '#dc3545', '#6c757d'] | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| position: 'bottom' | |
| } | |
| } | |
| } | |
| }); | |
| // Language distribution chart | |
| const languageCtx = document.getElementById('languageChart').getContext('2d'); | |
| if (languageChart) languageChart.destroy(); | |
| languageChart = new Chart(languageCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: Object.keys(scrapingStats.language_distribution || {}), | |
| datasets: [{ | |
| label: 'Items by Language', | |
| data: Object.values(scrapingStats.language_distribution || {}), | |
| backgroundColor: '#3498db' | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| } | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Error loading charts:', error); | |
| } | |
| } | |
| // Form submission | |
| document.getElementById('scrapingForm').addEventListener('submit', async function(e) { | |
| e.preventDefault(); | |
| const urls = document.getElementById('urls').value.split('\n').filter(url => url.trim()); | |
| const strategy = document.getElementById('strategy').value; | |
| const keywords = document.getElementById('keywords').value.split(',').filter(k => k.trim()); | |
| const maxDepth = parseInt(document.getElementById('maxDepth').value); | |
| const delay = parseFloat(document.getElementById('delay').value); | |
| if (urls.length === 0) { | |
| showNotification('Please enter at least one URL', 'warning'); | |
| return; | |
| } | |
| const startBtn = document.getElementById('startScrapingBtn'); | |
| const originalText = startBtn.innerHTML; | |
| startBtn.innerHTML = '<span class="loading-spinner me-2"></span>Starting...'; | |
| startBtn.disabled = true; | |
| try { | |
| const response = await fetch('/api/scrape', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| urls: urls, | |
| strategy: strategy, | |
| keywords: keywords.length > 0 ? keywords : null, | |
| max_depth: maxDepth, | |
| delay_between_requests: delay | |
| }) | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| showNotification('Scraping job started successfully', 'success'); | |
| document.getElementById('scrapingForm').reset(); | |
| loadJobs(); | |
| } else { | |
| showNotification(result.detail || 'Failed to start scraping job', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error starting scraping job:', error); | |
| showNotification('Error starting scraping job', 'error'); | |
| } finally { | |
| startBtn.innerHTML = originalText; | |
| startBtn.disabled = false; | |
| } | |
| }); | |
| // Rate all items | |
| async function rateAllItems() { | |
| try { | |
| const response = await fetch('/api/rating/rate-all', { | |
| method: 'POST' | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| showNotification(`Rated ${result.rated_count} items successfully`, 'success'); | |
| loadStatistics(); | |
| loadItems(); | |
| } else { | |
| showNotification(result.detail || 'Failed to rate items', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error rating items:', error); | |
| showNotification('Error rating items', 'error'); | |
| } | |
| } | |
| // Get low quality items | |
| async function getLowQualityItems() { | |
| try { | |
| const response = await fetch('/api/rating/low-quality'); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| showNotification(`Found ${result.total_items} low quality items`, 'warning'); | |
| // You could display these in a modal or separate section | |
| } else { | |
| showNotification(result.detail || 'Failed to get low quality items', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error getting low quality items:', error); | |
| showNotification('Error getting low quality items', 'error'); | |
| } | |
| } | |
| // Rate specific item | |
| async function rateItem(itemId) { | |
| try { | |
| const response = await fetch(`/api/rating/rate/${itemId}`, { | |
| method: 'POST' | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| showNotification(`Item ${itemId} rated successfully`, 'success'); | |
| loadItems(); | |
| } else { | |
| showNotification(result.detail || 'Failed to rate item', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error rating item:', error); | |
| showNotification('Error rating item', 'error'); | |
| } | |
| } | |
| // View item details | |
| function viewItem(itemId) { | |
| // This could open a modal with item details | |
| showNotification(`Viewing item ${itemId}`, 'info'); | |
| } | |
| // Refresh functions | |
| function refreshStatistics() { | |
| loadStatistics(); | |
| showNotification('Statistics refreshed', 'success'); | |
| } | |
| function refreshJobs() { | |
| loadJobs(); | |
| showNotification('Jobs refreshed', 'success'); | |
| } | |
| function refreshItems() { | |
| loadItems(); | |
| showNotification('Items refreshed', 'success'); | |
| } | |
| // Auto refresh | |
| function startAutoRefresh() { | |
| refreshInterval = setInterval(() => { | |
| loadStatistics(); | |
| loadJobs(); | |
| }, 10000); // Refresh every 10 seconds | |
| } | |
| // Utility functions | |
| function getRatingClass(score) { | |
| if (score >= 0.8) return 'rating-excellent'; | |
| if (score >= 0.6) return 'rating-good'; | |
| if (score >= 0.4) return 'rating-average'; | |
| if (score >= 0.2) return 'rating-poor'; | |
| return 'rating-unrated'; | |
| } | |
| function showNotification(message, type = 'info') { | |
| const container = document.getElementById('notificationContainer'); | |
| const notification = document.createElement('div'); | |
| notification.className = `notification ${type}`; | |
| notification.innerHTML = ` | |
| <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'warning' ? 'exclamation-triangle' : type === 'error' ? 'times-circle' : 'info-circle'} me-2"></i> | |
| ${message} | |
| `; | |
| container.appendChild(notification); | |
| setTimeout(() => { | |
| notification.classList.add('show'); | |
| }, 100); | |
| setTimeout(() => { | |
| notification.classList.remove('show'); | |
| setTimeout(() => { | |
| container.removeChild(notification); | |
| }, 300); | |
| }, 5000); | |
| } | |
| // Cleanup on page unload | |
| window.addEventListener('beforeunload', function() { | |
| if (refreshInterval) { | |
| clearInterval(refreshInterval); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |