monitor / index.html
mrwhy06's picture
remove banana
cbb4b2c verified
<!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 -->
<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">&#8203;</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>