Download index.html from mrwhy06/monitor: direct link, hf CLI and curl.
- Browser
- Download file 69 kB
-
https://huggingface.co/spaces/mrwhy06/monitor/resolve/main/index.html
- Command line
-
hf download hf://spaces/mrwhy06/monitor/index.html
-
curl -L -o index.html https://huggingface.co/spaces/mrwhy06/monitor/resolve/main/index.html
69 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Fruit Ripeness Classification Dashboard</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
| <style> | |
| .ripeness-unripe { | |
| background-color: #fef9c3; | |
| color: #ca8a04; | |
| } | |
| .ripeness-ripe { | |
| background-color: #dcfce7; | |
| color: #15803d; | |
| } | |
| .ripeness-overripe { | |
| background-color: #fee2e2; | |
| color: #b91c1c; | |
| } | |
| .confidence-bar { | |
| position: relative; | |
| height: 8px; | |
| background-color: #e5e7eb; | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| .confidence-fill { | |
| position: absolute; | |
| height: 100%; | |
| left: 0; | |
| top: 0; | |
| border-radius: 4px; | |
| } | |
| .high-confidence { | |
| background-color: #10b981; | |
| } | |
| .medium-confidence { | |
| background-color: #f59e0b; | |
| } | |
| .low-confidence { | |
| background-color: #ef4444; | |
| } | |
| .mismatch-highlight { | |
| border-left: 4px solid #ef4444; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { background-color: white; } | |
| 50% { background-color: #fee2e2; } | |
| 100% { background-color: white; } | |
| } | |
| .scrollbar-hide::-webkit-scrollbar { | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <!-- Header --> | |
| <header class="bg-white shadow"> | |
| <div class="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8 flex justify-between items-center"> | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0 flex items-center"> | |
| <i class="fas fa-apple-alt text-3xl text-green-600 mr-2"></i> | |
| <span class="text-xl font-bold text-gray-900">FruitAI Monitor</span> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button id="exportBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium flex items-center"> | |
| <i class="fas fa-file-export mr-2"></i> Export Data | |
| </button> | |
| <div class="relative"> | |
| <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> | |
| <i class="fas fa-calendar text-gray-500"></i> | |
| </div> | |
| <input type="text" id="dateRangePicker" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5" placeholder="Select date range"> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> | |
| <!-- Summary Cards --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> | |
| <div class="bg-white rounded-lg shadow p-4"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <h3 class="text-gray-500 text-sm font-medium">Total Classifications</h3> | |
| <p id="totalClassifications" class="text-2xl font-bold text-gray-900">0</p> | |
| </div> | |
| <div class="p-3 rounded-full bg-blue-100 text-blue-600"> | |
| <i class="fas fa-list-ol text-lg"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-4"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <h3 class="text-gray-500 text-sm font-medium">Accuracy Rate</h3> | |
| <p id="accuracyRate" class="text-2xl font-bold text-gray-900">0%</p> | |
| </div> | |
| <div class="p-3 rounded-full bg-green-100 text-green-600"> | |
| <i class="fas fa-check-circle text-lg"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-4"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <h3 class="text-gray-500 text-sm font-medium">Mismatch Rate</h3> | |
| <p id="mismatchRate" class="text-2xl font-bold text-gray-900">0%</p> | |
| </div> | |
| <div class="p-3 rounded-full bg-red-100 text-red-600"> | |
| <i class="fas fa-times-circle text-lg"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-4"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <h3 class="text-gray-500 text-sm font-medium">Avg Confidence</h3> | |
| <p id="avgConfidence" class="text-2xl font-bold text-gray-900">0%</p> | |
| </div> | |
| <div class="p-3 rounded-full bg-yellow-100 text-yellow-600"> | |
| <i class="fas fa-chart-line text-lg"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Charts Section --> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> | |
| <div class="bg-white p-4 rounded-lg shadow"> | |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Accuracy Over Time</h3> | |
| <canvas id="accuracyChart" height="300"></canvas> | |
| </div> | |
| <div class="bg-white p-4 rounded-lg shadow"> | |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Confidence Distribution</h3> | |
| <canvas id="confidenceChart" height="300"></canvas> | |
| </div> | |
| <div class="bg-white p-4 rounded-lg shadow"> | |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Classification Distribution</h3> | |
| <canvas id="classificationChart" height="300"></canvas> | |
| </div> | |
| <div class="bg-white p-4 rounded-lg shadow"> | |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Mismatch Analysis</h3> | |
| <canvas id="mismatchChart" height="300"></canvas> | |
| </div> | |
| </div> | |
| <!-- Filters Section --> | |
| <div class="bg-white rounded-lg shadow p-4 mb-6"> | |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Filters</h3> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4"> | |
| <div> | |
| <label for="fruitTypeFilter" class="block text-sm font-medium text-gray-700 mb-1">Fruit Type</label> | |
| <select id="fruitTypeFilter" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"> | |
| <option value="">All Fruits</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="predictionFilter" class="block text-sm font-medium text-gray-700 mb-1">Prediction</label> | |
| <select id="predictionFilter" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"> | |
| <option value="">All</option> | |
| <option value="unripe">Unripe</option> | |
| <option value="ripe">Ripe</option> | |
| <option value="overripe">Overripe</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="feedbackFilter" class="block text-sm font-medium text-gray-700 mb-1">Feedback</label> | |
| <select id="feedbackFilter" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"> | |
| <option value="">All</option> | |
| <option value="confirmed">Confirmed</option> | |
| <option value="overridden">Overridden</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="confidenceFilter" class="block text-sm font-medium text-gray-700 mb-1">Confidence</label> | |
| <select id="confidenceFilter" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"> | |
| <option value="">All</option> | |
| <option value="high">High (≥80%)</option> | |
| <option value="medium">Medium (60-79%)</option> | |
| <option value="low">Low (<60%)</option> | |
| </select> | |
| </div> | |
| <div class="flex items-end"> | |
| <button id="applyFilters" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium w-full flex items-center justify-center"> | |
| <i class="fas fa-filter mr-2"></i> Apply Filters | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Data Table --> | |
| <div class="bg-white rounded-lg shadow overflow-hidden"> | |
| <div class="flex justify-between items-center p-4 border-b"> | |
| <h3 class="text-lg font-medium text-gray-900">Classification Results</h3> | |
| <div class="flex space-x-2"> | |
| <div class="relative"> | |
| <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> | |
| <i class="fas fa-search text-gray-400"></i> | |
| </div> | |
| <input type="text" id="searchInput" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5" placeholder="Search..."> | |
| </div> | |
| <button id="resetFilters" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-md text-sm font-medium flex items-center"> | |
| <i class="fas fa-redo mr-2"></i> Reset | |
| </button> | |
| </div> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="fruit"> | |
| <div class="flex items-center"> | |
| Fruit <i class="fas fa-sort ml-1 text-gray-400"></i> | |
| </div> | |
| </th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="ai_prediction"> | |
| <div class="flex items-center"> | |
| AI Prediction <i class="fas fa-sort ml-1 text-gray-400"></i> | |
| </div> | |
| </th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="confidence"> | |
| <div class="flex items-center"> | |
| Confidence <i class="fas fa-sort ml-1 text-gray-400"></i> | |
| </div> | |
| </th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="user_feedback"> | |
| <div class="flex items-center"> | |
| User Feedback <i class="fas fa-sort ml-1 text-gray-400"></i> | |
| </div> | |
| </th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="feedback_status"> | |
| <div class="flex items-center"> | |
| Status <i class="fas fa-sort ml-1 text-gray-400"></i> | |
| </div> | |
| </th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="timestamp"> | |
| <div class="flex items-center"> | |
| Timestamp <i class="fas fa-sort ml-1 text-gray-400"></i> | |
| </div> | |
| </th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="resultsTable" class="bg-white divide-y divide-gray-200"> | |
| <!-- Results will be populated here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6"> | |
| <div class="flex-1 flex justify-between sm:hidden"> | |
| <button id="prevPageMobile" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> | |
| Previous | |
| </button> | |
| <button id="nextPageMobile" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> | |
| Next | |
| </button> | |
| </div> | |
| <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"> | |
| <div> | |
| <p id="paginationText" class="text-sm text-gray-700"> | |
| Showing <span class="font-medium">1</span> to <span class="font-medium">10</span> of <span class="font-medium">200</span> results | |
| </p> | |
| </div> | |
| <div> | |
| <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination"> | |
| <button id="firstPage" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"> | |
| <span class="sr-only">First</span> | |
| <i class="fas fa-angle-double-left"></i> | |
| </button> | |
| <button id="prevPage" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"> | |
| <span class="sr-only">Previous</span> | |
| <i class="fas fa-angle-left"></i> | |
| </button> | |
| <div id="pageNumbers" class="flex items-center"> | |
| <!-- Page numbers will be inserted here --> | |
| </div> | |
| <button id="nextPage" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"> | |
| <span class="sr-only">Next</span> | |
| <i class="fas fa-angle-right"></i> | |
| </button> | |
| <button id="lastPage" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"> | |
| <span class="sr-only">Last</span> | |
| <i class="fas fa-angle-double-right"></i> | |
| </button> | |
| </nav> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Detail Modal --> | |
| <div id="detailModal" class="fixed z-10 inset-0 overflow-y-auto hidden" aria-labelledby="modal-title" role="dialog" aria-modal="true"> | |
| <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | |
| <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div> | |
| <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> | |
| <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full"> | |
| <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | |
| <div class="sm:flex sm:items-start"> | |
| <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full"> | |
| <h3 class="text-lg leading-6 font-medium text-gray-900" id="modalTitle">Classification Details</h3> | |
| <div class="mt-2"> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div> | |
| <div id="detailImage" class="w-full h-48 bg-gray-200 rounded-md overflow-hidden flex items-center justify-center"> | |
| <i class="fas fa-image text-gray-400 text-4xl"></i> | |
| </div> | |
| <div class="mt-2 text-sm"> | |
| <p><span class="font-medium">Classification ID:</span> <span id="detailId"></span></p> | |
| <p><span class="font-medium">Uploaded by:</span> <span id="detailUploadedBy"></span></p> | |
| <p><span class="font-medium">Location:</span> <span id="detailLocation"></span></p> | |
| <p><span class="font-medium">Harvested on:</span> <span id="detailHarvestDate"></span></p> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="bg-gray-50 p-3 rounded-md"> | |
| <h4 class="font-medium text-gray-900 mb-2">AI Analysis</h4> | |
| <div class="mb-2"> | |
| <span class="font-medium">Prediction:</span> <span id="detailAiPrediction" class="px-2 py-1 rounded-full text-xs font-medium"></span> | |
| </div> | |
| <div class="mb-2"> | |
| <div class="flex justify-between mb-1"> | |
| <span class="font-medium">Confidence:</span> | |
| <span id="detailConfidence" class="font-medium"></span> | |
| </div> | |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> | |
| <div id="detailConfidenceBar" class="h-2.5 rounded-full"></div> | |
| </div> | |
| </div> | |
| <div class="mb-2"> | |
| <span class="font-medium">Temperature:</span> <span id="detailTemperature"></span>°C | |
| </div> | |
| <div class="mb-2"> | |
| <span class="font-medium">Humidity:</span> <span id="detailHumidity"></span>% | |
| </div> | |
| <div> | |
| <span class="font-medium">Weight:</span> <span id="detailWeight"></span>g | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="bg-gray-50 p-3 rounded-md mb-3"> | |
| <h4 class="font-medium text-gray-900 mb-2">Human Feedback</h4> | |
| <div class="mb-2"> | |
| <span class="font-medium">Feedback:</span> <span id="detailUserFeedback" class="px-2 py-1 rounded-full text-xs font-medium"></span> | |
| </div> | |
| <div class="mb-2"> | |
| <span class="font-medium">Status:</span> <span id="detailFeedbackStatus" class="px-2 py-1 rounded-full text-xs font-medium"></span> | |
| </div> | |
| <div class="mb-2"> | |
| <span class="font-medium">Timestamp:</span> <span id="detailTimestamp"></span> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 p-3 rounded-md"> | |
| <h4 class="font-medium text-gray-900 mb-2">Notes</h4> | |
| <p id="detailNotes" class="text-sm italic"></p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-4 grid grid-cols-2 gap-4"> | |
| <button type="button" id="editBtn" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none sm:text-sm"> | |
| <i class="fas fa-edit mr-2"></i> Edit Feedback | |
| </button> | |
| <button type="button" class="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:text-sm" onclick="document.getElementById('detailModal').classList.add('hidden')"> | |
| <i class="fas fa-times mr-2"></i> Close | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Enhanced mock data generation with more variety | |
| function generateMockData(count = 200) { | |
| const fruits = [ | |
| 'apple', 'mango', 'orange' | |
| ]; | |
| const ripeness = ['unripe', 'ripe', 'overripe']; | |
| const feedbackOptions = ['confirmed', 'overridden']; | |
| const locations = [ | |
| 'Farm A', 'Farm B', 'Farm C', 'Farm D', 'Farm E', | |
| 'Orchard X', 'Greenhouse Y', 'Plantation Z', | |
| 'Organic Valley', 'Mountain View Farms' | |
| ]; | |
| const users = [ | |
| 'User1', 'User2', 'User3', 'User4', 'User5', | |
| 'InspectorA', 'QualityB', 'TesterC', 'AgentD', 'ManagerE' | |
| ]; | |
| const notesOptions = [ | |
| "Color grading matches maturity level", | |
| "Slight bruising detected", | |
| "Perfect specimen", | |
| "Irregular shape but good quality", | |
| "Sun-exposed side shows advanced ripeness", | |
| "Harvested slightly early", | |
| "Optimal sweetness level", | |
| "Needs immediate processing", | |
| "Excellent for long-term storage", | |
| "Best for juicing" | |
| ]; | |
| const data = []; | |
| const startDate = new Date(); | |
| startDate.setDate(startDate.getDate() - 60); // 2 months range | |
| for (let i = 0; i < count; i++) { | |
| const fruit = fruits[Math.floor(Math.random() * fruits.length)]; | |
| const aiPrediction = ripeness[Math.floor(Math.random() * ripeness.length)]; | |
| // More realistic confidence distribution | |
| let confidence; | |
| if (Math.random() > 0.1) { // 90% high confidence | |
| confidence = Math.floor(Math.random() * 20) + 75; // 75-95% | |
| } else { | |
| confidence = Math.floor(Math.random() * 50) + 30; // 30-80% | |
| } | |
| let feedback = feedbackOptions[Math.floor(Math.random() * feedbackOptions.length)]; | |
| let userFeedback; | |
| // Generate some mismatches (about 15%) | |
| if (Math.random() < 0.15) { | |
| feedback = 'overridden'; | |
| } | |
| if (feedback === 'confirmed') { | |
| userFeedback = aiPrediction; | |
| } else { | |
| // When overridden, choose a different ripeness level | |
| userFeedback = ripeness.filter(r => r !== aiPrediction)[Math.floor(Math.random() * (ripeness.length - 1))]; | |
| // When confidence is low, increase chance of override | |
| if (confidence < 60 && Math.random() < 0.7) { | |
| feedback = 'overridden'; | |
| userFeedback = ripeness.filter(r => r !== aiPrediction)[Math.floor(Math.random() * (ripeness.length - 1))]; | |
| } | |
| } | |
| // Create a more natural timestamp distribution | |
| const timestamp = new Date( | |
| startDate.getTime() + | |
| Math.random() * 60 * 24 * 60 * 60 * 1000 + // up to 60 days | |
| Math.random() * 8 * 60 * 60 * 1000 // random time in workday | |
| ); | |
| data.push({ | |
| id: `FR-${('0000' + i).slice(-4)}-${timestamp.getFullYear().toString().slice(-2)}`, | |
| fruit: fruit, | |
| image: getFruitImage(fruit), | |
| ai_prediction: aiPrediction, | |
| confidence: confidence, | |
| user_feedback: userFeedback, | |
| feedback_status: feedback, | |
| timestamp: timestamp.toISOString(), | |
| location: locations[Math.floor(Math.random() * locations.length)], | |
| uploaded_by: users[Math.floor(Math.random() * users.length)], | |
| notes: notesOptions[Math.floor(Math.random() * notesOptions.length)], | |
| // Additional metrics for detailed analysis | |
| harvest_date: new Date( | |
| timestamp.getTime() - | |
| Math.random() * 7 * 24 * 60 * 60 * 1000 // 0-7 days before classification | |
| ).toISOString(), | |
| temperature: Math.round((20 + Math.random() * 15) * 10) / 10, // 20-35°C | |
| humidity: Math.round((60 + Math.random() * 30) * 10) / 10, // 60-90% | |
| weight: Math.round((100 + Math.random() * 400) * 10) / 10 // 100-500g | |
| }); | |
| } | |
| return data; | |
| } | |
| // Expanded fruit images | |
| function getFruitImage(fruit) { | |
| const images = { | |
| apple: 'https://images.unsplash.com/photo-1568702846914-96b305d2aaeb?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| banana: 'https://images.unsplash.com/photo-1603833665858-e61bb17a7218?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| mango: 'https://images.unsplash.com/photo-1553279768-865429fa0078?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| orange: 'https://images.unsplash.com/photo-1547514701-42782101795e?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| strawberry: 'https://images.unsplash.com/photo-1464965911861-746a04b4bca6?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| peach: 'https://images.unsplash.com/photo-1559181567-c3190ca9959b?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| pear: 'https://images.unsplash.com/photo-1530893609605-72d7600779ae?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| grape: 'https://images.unsplash.com/photo-1517587171378-24a6fba3c2af?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| kiwi: 'https://images.unsplash.com/photo-1598283027164-78bd17e81663?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| pineapple: 'https://images.unsplash.com/photo-1490885578164-de435ba9c64b?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| watermelon: 'https://images.unsplash.com/photo-1571575173700-afb9492e6a50?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| blueberry: 'https://images.unsplash.com/photo-1498557850523-fd3d118b962e?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| raspberry: 'https://images.unsplash.com/photo-1518633626590-c51b881e2c96?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| blackberry: 'https://images.unsplash.com/photo-1493925415034-d6f1a3f436b1?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', | |
| cherry: 'https://images.unsplash.com/photo-1533158313479-c24d2f16f3b5?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80' | |
| }; | |
| return images[fruit] || images['apple']; // default to apple if fruit not found | |
| } | |
| // Update fruit color mapping | |
| function getFruitColor(fruit) { | |
| const colors = { | |
| apple: '#db2777', | |
| banana: '#f59e0b', | |
| mango: '#ea580c', | |
| orange: '#ea580c', | |
| strawberry: '#e11d48', | |
| peach: '#f97316', | |
| pear: '#84cc16', | |
| grape: '#7e22ce', | |
| kiwi: '#22c55e', | |
| pineapple: '#facc15', | |
| watermelon: '#ef4444', | |
| blueberry: '#3b82f6', | |
| raspberry: '#ec4899', | |
| blackberry: '#6b7280', | |
| cherry: '#b91c1c' | |
| }; | |
| return colors[fruit] || '#6b7280'; | |
| } | |
| // Ripeness styling | |
| function getRipenessClass(ripeness) { | |
| return `ripeness-${ripeness}`; | |
| } | |
| function getRipenessText(ripeness) { | |
| return ripeness.charAt(0).toUpperCase() + ripeness.slice(1); | |
| } | |
| // Status styling | |
| function getStatusClass(status) { | |
| return { | |
| 'confirmed': 'bg-green-100 text-green-800', | |
| 'overridden': 'bg-red-100 text-red-800' | |
| }[status]; | |
| } | |
| function getStatusText(status) { | |
| return { | |
| 'confirmed': 'Confirmed', | |
| 'overridden': 'Overridden' | |
| }[status]; | |
| } | |
| // Confidence styling | |
| function getConfidenceClass(confidence) { | |
| if (confidence >= 80) { | |
| return 'high-confidence'; | |
| } else if (confidence >= 60) { | |
| return 'medium-confidence'; | |
| } else { | |
| return 'low-confidence'; | |
| } | |
| } | |
| // Format date | |
| function formatDate(dateString) { | |
| return moment(dateString).format('MMM D, YYYY h:mm A'); | |
| } | |
| // Initialize dashboard | |
| function initDashboard() { | |
| // Generate mock data | |
| const allData = generateMockData(200); | |
| // Initialize state | |
| let currentPage = 1; | |
| let itemsPerPage = 10; | |
| let filteredData = [...allData]; | |
| let sortColumn = null; | |
| let sortDirection = 'asc'; | |
| let currentDetail = null; | |
| // DOM elements | |
| const resultsTable = document.getElementById('resultsTable'); | |
| const paginationText = document.getElementById('paginationText'); | |
| const pageNumbers = document.getElementById('pageNumbers'); | |
| const fruitTypeFilter = document.getElementById('fruitTypeFilter'); | |
| const predictionFilter = document.getElementById('predictionFilter'); | |
| const feedbackFilter = document.getElementById('feedbackFilter'); | |
| const confidenceFilter = document.getElementById('confidenceFilter'); | |
| const applyFilters = document.getElementById('applyFilters'); | |
| const resetFilters = document.getElementById('resetFilters'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const exportBtn = document.getElementById('exportBtn'); | |
| const totalClassifications = document.getElementById('totalClassifications'); | |
| const accuracyRate = document.getElementById('accuracyRate'); | |
| const mismatchRate = document.getElementById('mismatchRate'); | |
| const avgConfidence = document.getElementById('avgConfidence'); | |
| const detailModal = document.getElementById('detailModal'); | |
| // Charts | |
| let accuracyChart, confidenceChart, classificationChart, mismatchChart; | |
| // Initialize date range picker | |
| const dateRangePicker = document.getElementById('dateRangePicker'); | |
| dateRangePicker.addEventListener('input', applyFiltersFunction); | |
| // Populate fruit type filter | |
| const uniqueFruits = [...new Set(allData.map(item => item.fruit))]; | |
| uniqueFruits.forEach(fruit => { | |
| const option = document.createElement('option'); | |
| option.value = fruit; | |
| option.textContent = fruit.charAt(0).toUpperCase() + fruit.slice(1); | |
| fruitTypeFilter.appendChild(option); | |
| }); | |
| // Render table | |
| function renderTable() { | |
| const startIndex = (currentPage - 1) * itemsPerPage; | |
| const endIndex = Math.min(startIndex + itemsPerPage, filteredData.length); | |
| const currentData = filteredData.slice(startIndex, endIndex); | |
| resultsTable.innerHTML = ''; | |
| if (currentData.length === 0) { | |
| resultsTable.innerHTML = ` | |
| <tr> | |
| <td colspan="8" class="px-6 py-4 text-center text-gray-500"> | |
| No results found matching your filters. | |
| </td> | |
| </tr> | |
| `; | |
| return; | |
| } | |
| currentData.forEach(item => { | |
| const row = document.createElement('tr'); | |
| // Highlight mismatches | |
| if (item.feedback_status === 'overridden') { | |
| row.classList.add('mismatch-highlight'); | |
| } | |
| row.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="flex items-center"> | |
| <div class="w-4 h-4 rounded-full mr-2" style="background-color: ${getFruitColor(item.fruit)}"></div> | |
| <div class="text-sm font-medium text-gray-900 capitalize">${item.fruit}</div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="px-2 py-1 text-xs font-medium rounded-full capitalize ${getRipenessClass(item.ai_prediction)}"> | |
| ${getRipenessText(item.ai_prediction)} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="w-10 h-10 rounded-md overflow-hidden"> | |
| <img src="${item.image}" alt="${item.fruit}" class="w-full h-full object-cover"> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0 mr-2 text-sm font-medium">${item.confidence}%</div> | |
| <div class="w-full max-w-xs"> | |
| <div class="confidence-bar"> | |
| <div class="confidence-fill ${getConfidenceClass(item.confidence)}" style="width: ${item.confidence}%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="px-2 py-1 text-xs font-medium rounded-full capitalize ${getRipenessClass(item.user_feedback)}"> | |
| ${getRipenessText(item.user_feedback)} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="px-2 py-1 text-xs font-medium rounded-full ${getStatusClass(item.feedback_status)}"> | |
| ${getStatusText(item.feedback_status)} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> | |
| ${formatDate(item.timestamp)} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> | |
| <button class="text-indigo-600 hover:text-indigo-900 mr-2 view-detail" data-id="${item.id}"> | |
| <i class="fas fa-eye"></i> | |
| </button> | |
| </td> | |
| `; | |
| resultsTable.appendChild(row); | |
| }); | |
| // Update pagination info | |
| paginationText.textContent = `Showing ${startIndex + 1} to ${endIndex} of ${filteredData.length} results`; | |
| // Update pagination buttons | |
| renderPagination(); | |
| // Add event listeners to detail buttons | |
| document.querySelectorAll('.view-detail').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| const id = this.getAttribute('data-id'); | |
| showDetail(id); | |
| }); | |
| }); | |
| } | |
| // Render pagination buttons | |
| function renderPagination() { | |
| pageNumbers.innerHTML = ''; | |
| const totalPages = Math.ceil(filteredData.length / itemsPerPage); | |
| // Always show first page | |
| if (currentPage > 3) { | |
| const pageItem = document.createElement('button'); | |
| pageItem.textContent = '1'; | |
| pageItem.className = `relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium ${currentPage === 1 ? 'bg-blue-50 text-blue-600' : 'bg-white text-gray-500 hover:bg-gray-50'}`; | |
| pageItem.addEventListener('click', () => { | |
| currentPage = 1; | |
| renderTable(); | |
| }); | |
| pageNumbers.appendChild(pageItem); | |
| if (currentPage > 4) { | |
| const ellipsis = document.createElement('span'); | |
| ellipsis.className = 'relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700'; | |
| ellipsis.textContent = '...'; | |
| pageNumbers.appendChild(ellipsis); | |
| } | |
| } | |
| // Show surrounding pages | |
| const startPage = Math.max(1, currentPage - 2); | |
| const endPage = Math.min(totalPages, currentPage + 2); | |
| for (let i = startPage; i <= endPage; i++) { | |
| const pageItem = document.createElement('button'); | |
| pageItem.textContent = i; | |
| pageItem.className = `relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium ${currentPage === i ? 'bg-blue-50 text-blue-600' : 'bg-white text-gray-500 hover:bg-gray-50'}`; | |
| pageItem.addEventListener('click', () => { | |
| currentPage = i; | |
| renderTable(); | |
| }); | |
| pageNumbers.appendChild(pageItem); | |
| } | |
| // Always show last page | |
| if (currentPage < totalPages - 2) { | |
| if (currentPage < totalPages - 3) { | |
| const ellipsis = document.createElement('span'); | |
| ellipsis.className = 'relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700'; | |
| ellipsis.textContent = '...'; | |
| pageNumbers.appendChild(ellipsis); | |
| } | |
| const pageItem = document.createElement('button'); | |
| pageItem.textContent = totalPages; | |
| pageItem.className = `relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium ${currentPage === totalPages ? 'bg-blue-50 text-blue-600' : 'bg-white text-gray-500 hover:bg-gray-50'}`; | |
| pageItem.addEventListener('click', () => { | |
| currentPage = totalPages; | |
| renderTable(); | |
| }); | |
| pageNumbers.appendChild(pageItem); | |
| } | |
| } | |
| // Apply filters | |
| function applyFiltersFunction() { | |
| currentPage = 1; | |
| filteredData = [...allData]; | |
| // Fruit type filter | |
| if (fruitTypeFilter.value) { | |
| filteredData = filteredData.filter(item => item.fruit === fruitTypeFilter.value); | |
| } | |
| // Prediction filter | |
| if (predictionFilter.value) { | |
| filteredData = filteredData.filter(item => item.ai_prediction === predictionFilter.value); | |
| } | |
| // Feedback filter | |
| if (feedbackFilter.value) { | |
| filteredData = filteredData.filter(item => item.feedback_status === feedbackFilter.value); | |
| } | |
| // Confidence filter | |
| if (confidenceFilter.value) { | |
| filteredData = filteredData.filter(item => { | |
| if (confidenceFilter.value === 'high') return item.confidence >= 80; | |
| if (confidenceFilter.value === 'medium') return item.confidence >= 60 && item.confidence < 80; | |
| if (confidenceFilter.value === 'low') return item.confidence < 60; | |
| return true; | |
| }); | |
| } | |
| // Date range filter (simplified for this example) | |
| if (dateRangePicker.value) { | |
| const [startDateStr, endDateStr] = dateRangePicker.value.split(' - '); | |
| const startDate = new Date(startDateStr); | |
| const endDate = new Date(endDateStr); | |
| filteredData = filteredData.filter(item => { | |
| const itemDate = new Date(item.timestamp); | |
| return itemDate >= startDate && itemDate <= endDate; | |
| }); | |
| } | |
| // Search text | |
| if (searchInput.value) { | |
| const searchTerm = searchInput.value.toLowerCase(); | |
| filteredData = filteredData.filter(item => | |
| item.fruit.toLowerCase().includes(searchTerm) || | |
| item.ai_prediction.toLowerCase().includes(searchTerm) || | |
| item.user_feedback.toLowerCase().includes(searchTerm) || | |
| item.location.toLowerCase().includes(searchTerm) || | |
| item.uploaded_by.toLowerCase().includes(searchTerm) || | |
| item.id.toLowerCase().includes(searchTerm) | |
| ); | |
| } | |
| // Sorting | |
| if (sortColumn) { | |
| filteredData.sort((a, b) => { | |
| let aValue = a[sortColumn]; | |
| let bValue = b[sortColumn]; | |
| // Special handling for dates | |
| if (sortColumn === 'timestamp') { | |
| aValue = new Date(a.timestamp).getTime(); | |
| bValue = new Date(b.timestamp).getTime(); | |
| } | |
| if (typeof aValue === 'string') aValue = aValue.toLowerCase(); | |
| if (typeof bValue === 'string') bValue = bValue.toLowerCase(); | |
| if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1; | |
| if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1; | |
| return 0; | |
| }); | |
| } | |
| // Update metrics | |
| updateMetrics(); | |
| // Render charts | |
| renderCharts(); | |
| // Render table with filtered data | |
| renderTable(); | |
| } | |
| // Reset filters | |
| function resetFiltersFunction() { | |
| fruitTypeFilter.value = ''; | |
| predictionFilter.value = ''; | |
| feedbackFilter.value = ''; | |
| confidenceFilter.value = ''; | |
| dateRangePicker.value = ''; | |
| searchInput.value = ''; | |
| sortColumn = null; | |
| sortDirection = 'asc'; | |
| // Reset sort indicators | |
| document.querySelectorAll('.sort i').forEach(icon => { | |
| icon.classList.remove('fa-sort-up', 'fa-sort-down'); | |
| icon.classList.add('fa-sort'); | |
| }); | |
| applyFiltersFunction(); | |
| } | |
| // Show detail modal | |
| function showDetail(id) { | |
| const item = allData.find(item => item.id === id); | |
| if (!item) return; | |
| currentDetail = item; | |
| // Update modal content | |
| document.getElementById('modalTitle').textContent = `${item.fruit.charAt(0).toUpperCase() + item.fruit.slice(1)} Classification`; | |
| document.getElementById('detailId').textContent = item.id; | |
| document.getElementById('detailAiPrediction').textContent = getRipenessText(item.ai_prediction); | |
| document.getElementById('detailAiPrediction').className = `px-2 py-1 rounded-full text-xs font-medium capitalize ${getRipenessClass(item.ai_prediction)}`; | |
| document.getElementById('detailConfidence').textContent = `${item.confidence}%`; | |
| document.getElementById('detailConfidenceBar').className = `h-2.5 rounded-full ${getConfidenceClass(item.confidence)}`; | |
| document.getElementById('detailConfidenceBar').style.width = `${item.confidence}%`; | |
| document.getElementById('detailUserFeedback').textContent = getRipenessText(item.user_feedback); | |
| document.getElementById('detailUserFeedback').className = `px-2 py-1 rounded-full text-xs font-medium capitalize ${getRipenessClass(item.user_feedback)}`; | |
| document.getElementById('detailFeedbackStatus').textContent = getStatusText(item.feedback_status); | |
| document.getElementById('detailFeedbackStatus').className = `px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(item.feedback_status)}`; | |
| document.getElementById('detailTimestamp').textContent = formatDate(item.timestamp); | |
| document.getElementById('detailUploadedBy').textContent = item.uploaded_by; | |
| document.getElementById('detailLocation').textContent = item.location; | |
| document.getElementById('detailHarvestDate').textContent = formatDate(item.harvest_date); | |
| document.getElementById('detailTemperature').textContent = item.temperature; | |
| document.getElementById('detailHumidity').textContent = item.humidity; | |
| document.getElementById('detailWeight').textContent = item.weight; | |
| document.getElementById('detailNotes').textContent = item.notes; | |
| // Update image | |
| const detailImage = document.getElementById('detailImage'); | |
| detailImage.innerHTML = ''; // Clear previous content | |
| const img = document.createElement('img'); | |
| img.src = item.image; | |
| img.alt = item.fruit; | |
| img.className = 'w-full h-full object-cover'; | |
| detailImage.appendChild(img); | |
| // Show modal | |
| detailModal.classList.remove('hidden'); | |
| } | |
| // Update metrics | |
| function updateMetrics() { | |
| totalClassifications.textContent = filteredData.length; | |
| // Calculate accuracy rate (percentage of confirmed feedback) | |
| const confirmedCount = filteredData.filter(item => item.feedback_status === 'confirmed').length; | |
| const accuracy = filteredData.length > 0 ? Math.round((confirmedCount / filteredData.length) * 100) : 0; | |
| accuracyRate.textContent = `${accuracy}%`; | |
| // Calculate mismatch rate (percentage of overridden feedback) | |
| const mismatchCount = filteredData.filter(item => item.feedback_status === 'overridden').length; | |
| const mismatchRateValue = filteredData.length > 0 ? Math.round((mismatchCount / filteredData.length) * 100) : 0; | |
| mismatchRate.textContent = `${mismatchRateValue}%`; | |
| // Calculate average confidence | |
| const avgConfidenceValue = filteredData.length > 0 | |
| ? Math.round(filteredData.reduce((sum, item) => sum + item.confidence, 0) / filteredData.length) | |
| : 0; | |
| avgConfidence.textContent = `${avgConfidenceValue}%`; | |
| } | |
| // Render charts | |
| function renderCharts() { | |
| // Destroy existing charts if they exist | |
| if (accuracyChart) accuracyChart.destroy(); | |
| if (confidenceChart) confidenceChart.destroy(); | |
| if (classificationChart) classificationChart.destroy(); | |
| if (mismatchChart) mismatchChart.destroy(); | |
| // Group data by week for trend analysis | |
| const weeklyGroups = {}; | |
| filteredData.forEach(item => { | |
| const week = moment(item.timestamp).startOf('week').format('MMM D'); | |
| if (!weeklyGroups[week]) { | |
| weeklyGroups[week] = { | |
| total: 0, | |
| confirmed: 0, | |
| overridden: 0 | |
| }; | |
| } | |
| weeklyGroups[week].total++; | |
| if (item.feedback_status === 'confirmed') { | |
| weeklyGroups[week].confirmed++; | |
| } else { | |
| weeklyGroups[week].overridden++; | |
| } | |
| }); | |
| const weeks = Object.keys(weeklyGroups); | |
| const confirmedData = weeks.map(week => weeklyGroups[week].confirmed); | |
| const overriddenData = weeks.map(week => weeklyGroups[week].overridden); | |
| // Accuracy Over Time Chart | |
| const accuracyCtx = document.getElementById('accuracyChart').getContext('2d'); | |
| accuracyChart = new Chart(accuracyCtx, { | |
| type: 'line', | |
| data: { | |
| labels: weeks, | |
| datasets: [ | |
| { | |
| label: 'Accuracy Rate (%)', | |
| data: weeks.map(week => { | |
| const group = weeklyGroups[week]; | |
| return group.total > 0 ? Math.round((group.confirmed / group.total) * 100) : 0; | |
| }), | |
| borderColor: '#10b981', | |
| backgroundColor: 'rgba(16, 185, 129, 0.1)', | |
| borderWidth: 2, | |
| fill: true, | |
| tension: 0.4 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| plugins: { | |
| legend: { | |
| display: true, | |
| position: 'top' | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| label: function(context) { | |
| return `${context.dataset.label}: ${context.raw}%`; | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| max: 100, | |
| ticks: { | |
| callback: function(value) { | |
| return `${value}%`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // Confidence Distribution Chart | |
| const confidenceCtx = document.getElementById('confidenceChart').getContext('2d'); | |
| confidenceChart = new Chart(confidenceCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: ['<60%', '60-69%', '70-79%', '80-89%', '90%+'], | |
| datasets: [ | |
| { | |
| label: 'Confirmed', | |
| data: [ | |
| filteredData.filter(item => item.feedback_status === 'confirmed' && item.confidence < 60).length, | |
| filteredData.filter(item => item.feedback_status === 'confirmed' && item.confidence >= 60 && item.confidence < 70).length, | |
| filteredData.filter(item => item.feedback_status === 'confirmed' && item.confidence >= 70 && item.confidence < 80).length, | |
| filteredData.filter(item => item.feedback_status === 'confirmed' && item.confidence >= 80 && item.confidence < 90).length, | |
| filteredData.filter(item => item.feedback_status === 'confirmed' && item.confidence >= 90).length | |
| ], | |
| backgroundColor: '#10b981', | |
| borderColor: '#10b981', | |
| borderWidth: 1 | |
| }, | |
| { | |
| label: 'Overridden', | |
| data: [ | |
| filteredData.filter(item => item.feedback_status === 'overridden' && item.confidence < 60).length, | |
| filteredData.filter(item => item.feedback_status === 'overridden' && item.confidence >= 60 && item.confidence < 70).length, | |
| filteredData.filter(item => item.feedback_status === 'overridden' && item.confidence >= 70 && item.confidence < 80).length, | |
| filteredData.filter(item => item.feedback_status === 'overridden' && item.confidence >= 80 && item.confidence < 90).length, | |
| filteredData.filter(item => item.feedback_status === 'overridden' && item.confidence >= 90).length | |
| ], | |
| backgroundColor: '#ef4444', | |
| borderColor: '#ef4444', | |
| borderWidth: 1 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| plugins: { | |
| legend: { | |
| display: true, | |
| position: 'top' | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| stacked: false | |
| }, | |
| y: { | |
| stacked: false | |
| } | |
| } | |
| } | |
| }); | |
| // Classification Distribution Chart | |
| const classificationCtx = document.getElementById('classificationChart').getContext('2d'); | |
| classificationChart = new Chart(classificationCtx, { | |
| type: 'pie', | |
| data: { | |
| labels: ['Unripe', 'Ripe', 'Overripe'], | |
| datasets: [ | |
| { | |
| data: [ | |
| filteredData.filter(item => item.ai_prediction === 'unripe').length, | |
| filteredData.filter(item => item.ai_prediction === 'ripe').length, | |
| filteredData.filter(item => item.ai_prediction === 'overripe').length | |
| ], | |
| backgroundColor: ['#f59e0b', '#10b981', '#ef4444'], | |
| borderColor: ['#f59e0b', '#10b981', '#ef4444'], | |
| borderWidth: 1 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| plugins: { | |
| legend: { | |
| display: true, | |
| position: 'right' | |
| } | |
| } | |
| } | |
| }); | |
| // Mismatch Analysis Chart | |
| const mismatchCtx = document.getElementById('mismatchChart').getContext('2d'); | |
| // Count mismatches by fruit type | |
| const mismatchByFruit = {}; | |
| filteredData.filter(item => item.feedback_status === 'overridden').forEach(item => { | |
| if (!mismatchByFruit[item.fruit]) { | |
| mismatchByFruit[item.fruit] = 0; | |
| } | |
| mismatchByFruit[item.fruit]++; | |
| }); | |
| const fruits = Object.keys(mismatchByFruit).map(fruit => fruit.charAt(0).toUpperCase() + fruit.slice(1)); | |
| const mismatchCounts = Object.values(mismatchByFruit); | |
| mismatchChart = new Chart(mismatchCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: fruits, | |
| datasets: [ | |
| { | |
| label: 'Mismatch Count', | |
| data: mismatchCounts, | |
| backgroundColor: fruits.map(fruit => getFruitColor(fruit.toLowerCase())), | |
| borderColor: fruits.map(fruit => getFruitColor(fruit.toLowerCase())), | |
| borderWidth: 1 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Export data | |
| function exportData() { | |
| const dataStr = JSON.stringify(filteredData, null, 2); | |
| const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); | |
| const exportFileDefaultName = `fruit-ai-export-${moment().format('YYYY-MM-DD')}.json`; | |
| const linkElement = document.createElement('a'); | |
| linkElement.setAttribute('href', dataUri); | |
| linkElement.setAttribute('download', exportFileDefaultName); | |
| linkElement.click(); | |
| } | |
| // Sorting functionality | |
| document.querySelectorAll('.sort').forEach(header => { | |
| header.addEventListener('click', function() { | |
| const column = this.getAttribute('data-sort'); | |
| const icon = this.querySelector('i'); | |
| // Reset other sort icons | |
| document.querySelectorAll('.sort i').forEach(otherIcon => { | |
| if (otherIcon !== icon) { | |
| otherIcon.classList.remove('fa-sort-up', 'fa-sort-down'); | |
| otherIcon.classList.add('fa-sort'); | |
| } | |
| }); | |
| // Toggle sort direction | |
| if (sortColumn === column) { | |
| sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; | |
| } else { | |
| sortColumn = column; | |
| sortDirection = 'asc'; | |
| } | |
| // Update icon | |
| icon.classList.remove('fa-sort'); | |
| icon.classList.add(sortDirection === 'asc' ? 'fa-sort-up' : 'fa-sort-down'); | |
| applyFiltersFunction(); | |
| }); | |
| }); | |
| // Pagination buttons | |
| document.getElementById('firstPage').addEventListener('click', () => { | |
| currentPage = 1; | |
| renderTable(); | |
| }); | |
| document.getElementById('prevPage').addEventListener('click', () => { | |
| if (currentPage > 1) { | |
| currentPage--; | |
| renderTable(); | |
| } | |
| }); | |
| document.getElementById('nextPage').addEventListener('click', () => { | |
| if (currentPage < Math.ceil(filteredData.length / itemsPerPage)) { | |
| currentPage++; | |
| renderTable(); | |
| } | |
| }); | |
| document.getElementById('lastPage').addEventListener('click', () => { | |
| currentPage = Math.ceil(filteredData.length / itemsPerPage); | |
| renderTable(); | |
| }); | |
| document.getElementById('prevPageMobile').addEventListener('click', () => { | |
| if (currentPage > 1) { | |
| currentPage--; | |
| renderTable(); | |
| } | |
| }); | |
| document.getElementById('nextPageMobile').addEventListener('click', () => { | |
| if (currentPage < Math.ceil(filteredData.length / itemsPerPage)) { | |
| currentPage++; | |
| renderTable(); | |
| } | |
| }); | |
| // Event listeners | |
| applyFilters.addEventListener('click', applyFiltersFunction); | |
| resetFilters.addEventListener('click', resetFiltersFunction); | |
| searchInput.addEventListener('keyup', function(e) { | |
| if (e.key === 'Enter') { | |
| applyFiltersFunction(); | |
| } | |
| }); | |
| exportBtn.addEventListener('click', exportData); | |
| // Close modal when clicking outside | |
| detailModal.addEventListener('click', function(e) { | |
| if (e.target === this) { | |
| detailModal.classList.add('hidden'); | |
| } | |
| }); | |
| // Close modal with X button | |
| document.querySelector('#detailModal button[onclick]').addEventListener('click', function() { | |
| detailModal.classList.add('hidden'); | |
| }); | |
| // Initialize dashboard | |
| applyFiltersFunction(); | |
| // Simulate date range picker functionality | |
| dateRangePicker.value = `${moment().subtract(30, 'days').format('MM/DD/YYYY')} - ${moment().format('MM/DD/YYYY')}`; | |
| } | |
| // Initialize the dashboard | |
| document.addEventListener('DOMContentLoaded', initDashboard); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=mrwhy06/monitor" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
| </html> |