|
|
<!DOCTYPE html> |
|
|
<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 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 class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> |
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
|
|
|
<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"> |
|
|
|
|
|
</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"> |
|
|
|
|
|
</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> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
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); |
|
|
|
|
|
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)]; |
|
|
|
|
|
|
|
|
let confidence; |
|
|
if (Math.random() > 0.1) { |
|
|
confidence = Math.floor(Math.random() * 20) + 75; |
|
|
} else { |
|
|
confidence = Math.floor(Math.random() * 50) + 30; |
|
|
} |
|
|
|
|
|
let feedback = feedbackOptions[Math.floor(Math.random() * feedbackOptions.length)]; |
|
|
let userFeedback; |
|
|
|
|
|
|
|
|
if (Math.random() < 0.15) { |
|
|
feedback = 'overridden'; |
|
|
} |
|
|
|
|
|
if (feedback === 'confirmed') { |
|
|
userFeedback = aiPrediction; |
|
|
} else { |
|
|
|
|
|
userFeedback = ripeness.filter(r => r !== aiPrediction)[Math.floor(Math.random() * (ripeness.length - 1))]; |
|
|
|
|
|
|
|
|
if (confidence < 60 && Math.random() < 0.7) { |
|
|
feedback = 'overridden'; |
|
|
userFeedback = ripeness.filter(r => r !== aiPrediction)[Math.floor(Math.random() * (ripeness.length - 1))]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const timestamp = new Date( |
|
|
startDate.getTime() + |
|
|
Math.random() * 60 * 24 * 60 * 60 * 1000 + |
|
|
Math.random() * 8 * 60 * 60 * 1000 |
|
|
); |
|
|
|
|
|
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)], |
|
|
|
|
|
harvest_date: new Date( |
|
|
timestamp.getTime() - |
|
|
Math.random() * 7 * 24 * 60 * 60 * 1000 |
|
|
).toISOString(), |
|
|
temperature: Math.round((20 + Math.random() * 15) * 10) / 10, |
|
|
humidity: Math.round((60 + Math.random() * 30) * 10) / 10, |
|
|
weight: Math.round((100 + Math.random() * 400) * 10) / 10 |
|
|
}); |
|
|
} |
|
|
|
|
|
return data; |
|
|
} |
|
|
|
|
|
|
|
|
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']; |
|
|
} |
|
|
|
|
|
|
|
|
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'; |
|
|
} |
|
|
|
|
|
|
|
|
function getRipenessClass(ripeness) { |
|
|
return `ripeness-${ripeness}`; |
|
|
} |
|
|
|
|
|
function getRipenessText(ripeness) { |
|
|
return ripeness.charAt(0).toUpperCase() + ripeness.slice(1); |
|
|
} |
|
|
|
|
|
|
|
|
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]; |
|
|
} |
|
|
|
|
|
|
|
|
function getConfidenceClass(confidence) { |
|
|
if (confidence >= 80) { |
|
|
return 'high-confidence'; |
|
|
} else if (confidence >= 60) { |
|
|
return 'medium-confidence'; |
|
|
} else { |
|
|
return 'low-confidence'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function formatDate(dateString) { |
|
|
return moment(dateString).format('MMM D, YYYY h:mm A'); |
|
|
} |
|
|
|
|
|
|
|
|
function initDashboard() { |
|
|
|
|
|
const allData = generateMockData(200); |
|
|
|
|
|
|
|
|
let currentPage = 1; |
|
|
let itemsPerPage = 10; |
|
|
let filteredData = [...allData]; |
|
|
let sortColumn = null; |
|
|
let sortDirection = 'asc'; |
|
|
let currentDetail = null; |
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
let accuracyChart, confidenceChart, classificationChart, mismatchChart; |
|
|
|
|
|
|
|
|
const dateRangePicker = document.getElementById('dateRangePicker'); |
|
|
dateRangePicker.addEventListener('input', applyFiltersFunction); |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
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); |
|
|
}); |
|
|
|
|
|
|
|
|
paginationText.textContent = `Showing ${startIndex + 1} to ${endIndex} of ${filteredData.length} results`; |
|
|
|
|
|
|
|
|
renderPagination(); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.view-detail').forEach(btn => { |
|
|
btn.addEventListener('click', function() { |
|
|
const id = this.getAttribute('data-id'); |
|
|
showDetail(id); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function renderPagination() { |
|
|
pageNumbers.innerHTML = ''; |
|
|
const totalPages = Math.ceil(filteredData.length / itemsPerPage); |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function applyFiltersFunction() { |
|
|
currentPage = 1; |
|
|
filteredData = [...allData]; |
|
|
|
|
|
|
|
|
if (fruitTypeFilter.value) { |
|
|
filteredData = filteredData.filter(item => item.fruit === fruitTypeFilter.value); |
|
|
} |
|
|
|
|
|
|
|
|
if (predictionFilter.value) { |
|
|
filteredData = filteredData.filter(item => item.ai_prediction === predictionFilter.value); |
|
|
} |
|
|
|
|
|
|
|
|
if (feedbackFilter.value) { |
|
|
filteredData = filteredData.filter(item => item.feedback_status === feedbackFilter.value); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
if (sortColumn) { |
|
|
filteredData.sort((a, b) => { |
|
|
let aValue = a[sortColumn]; |
|
|
let bValue = b[sortColumn]; |
|
|
|
|
|
|
|
|
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; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
updateMetrics(); |
|
|
|
|
|
|
|
|
renderCharts(); |
|
|
|
|
|
|
|
|
renderTable(); |
|
|
} |
|
|
|
|
|
|
|
|
function resetFiltersFunction() { |
|
|
fruitTypeFilter.value = ''; |
|
|
predictionFilter.value = ''; |
|
|
feedbackFilter.value = ''; |
|
|
confidenceFilter.value = ''; |
|
|
dateRangePicker.value = ''; |
|
|
searchInput.value = ''; |
|
|
sortColumn = null; |
|
|
sortDirection = 'asc'; |
|
|
|
|
|
|
|
|
document.querySelectorAll('.sort i').forEach(icon => { |
|
|
icon.classList.remove('fa-sort-up', 'fa-sort-down'); |
|
|
icon.classList.add('fa-sort'); |
|
|
}); |
|
|
|
|
|
applyFiltersFunction(); |
|
|
} |
|
|
|
|
|
|
|
|
function showDetail(id) { |
|
|
const item = allData.find(item => item.id === id); |
|
|
if (!item) return; |
|
|
|
|
|
currentDetail = item; |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
const detailImage = document.getElementById('detailImage'); |
|
|
detailImage.innerHTML = ''; |
|
|
const img = document.createElement('img'); |
|
|
img.src = item.image; |
|
|
img.alt = item.fruit; |
|
|
img.className = 'w-full h-full object-cover'; |
|
|
detailImage.appendChild(img); |
|
|
|
|
|
|
|
|
detailModal.classList.remove('hidden'); |
|
|
} |
|
|
|
|
|
|
|
|
function updateMetrics() { |
|
|
totalClassifications.textContent = filteredData.length; |
|
|
|
|
|
|
|
|
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}%`; |
|
|
|
|
|
|
|
|
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}%`; |
|
|
|
|
|
|
|
|
const avgConfidenceValue = filteredData.length > 0 |
|
|
? Math.round(filteredData.reduce((sum, item) => sum + item.confidence, 0) / filteredData.length) |
|
|
: 0; |
|
|
avgConfidence.textContent = `${avgConfidenceValue}%`; |
|
|
} |
|
|
|
|
|
|
|
|
function renderCharts() { |
|
|
|
|
|
if (accuracyChart) accuracyChart.destroy(); |
|
|
if (confidenceChart) confidenceChart.destroy(); |
|
|
if (classificationChart) classificationChart.destroy(); |
|
|
if (mismatchChart) mismatchChart.destroy(); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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}%`; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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' |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const mismatchCtx = document.getElementById('mismatchChart').getContext('2d'); |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
document.querySelectorAll('.sort').forEach(header => { |
|
|
header.addEventListener('click', function() { |
|
|
const column = this.getAttribute('data-sort'); |
|
|
const icon = this.querySelector('i'); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.sort i').forEach(otherIcon => { |
|
|
if (otherIcon !== icon) { |
|
|
otherIcon.classList.remove('fa-sort-up', 'fa-sort-down'); |
|
|
otherIcon.classList.add('fa-sort'); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
if (sortColumn === column) { |
|
|
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; |
|
|
} else { |
|
|
sortColumn = column; |
|
|
sortDirection = 'asc'; |
|
|
} |
|
|
|
|
|
|
|
|
icon.classList.remove('fa-sort'); |
|
|
icon.classList.add(sortDirection === 'asc' ? 'fa-sort-up' : 'fa-sort-down'); |
|
|
|
|
|
applyFiltersFunction(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
applyFilters.addEventListener('click', applyFiltersFunction); |
|
|
resetFilters.addEventListener('click', resetFiltersFunction); |
|
|
searchInput.addEventListener('keyup', function(e) { |
|
|
if (e.key === 'Enter') { |
|
|
applyFiltersFunction(); |
|
|
} |
|
|
}); |
|
|
exportBtn.addEventListener('click', exportData); |
|
|
|
|
|
|
|
|
detailModal.addEventListener('click', function(e) { |
|
|
if (e.target === this) { |
|
|
detailModal.classList.add('hidden'); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelector('#detailModal button[onclick]').addEventListener('click', function() { |
|
|
detailModal.classList.add('hidden'); |
|
|
}); |
|
|
|
|
|
|
|
|
applyFiltersFunction(); |
|
|
|
|
|
|
|
|
dateRangePicker.value = `${moment().subtract(30, 'days').format('MM/DD/YYYY')} - ${moment().format('MM/DD/YYYY')}`; |
|
|
} |
|
|
|
|
|
|
|
|
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> |