|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>ThermoScan AI | Industrial Temperature Monitoring</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<script> |
|
|
tailwind.config = { |
|
|
theme: { |
|
|
extend: { |
|
|
colors: { |
|
|
industrial: { |
|
|
50: '#f0f9ff', |
|
|
100: '#e0f2fe', |
|
|
200: '#bae6fd', |
|
|
300: '#7dd3fc', |
|
|
400: '#38bdf8', |
|
|
500: '#0ea5e9', |
|
|
600: '#0284c7', |
|
|
700: '#0369a1', |
|
|
800: '#075985', |
|
|
900: '#0c4a6e', |
|
|
}, |
|
|
danger: { |
|
|
500: '#ef4444', |
|
|
600: '#dc2626', |
|
|
}, |
|
|
warning: { |
|
|
500: '#f59e0b', |
|
|
600: '#d97706', |
|
|
}, |
|
|
success: { |
|
|
500: '#10b981', |
|
|
600: '#059669', |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
</script> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;600;700&display=swap'); |
|
|
|
|
|
body { |
|
|
font-family: 'Roboto Mono', monospace; |
|
|
background: linear-gradient(135deg, #1a2a3a 0%, #0f172a 100%); |
|
|
color: #e2e8f0; |
|
|
min-height: 100vh; |
|
|
} |
|
|
|
|
|
.dashboard-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); |
|
|
gap: 1.5rem; |
|
|
} |
|
|
|
|
|
.industrial-card { |
|
|
background: rgba(15, 23, 42, 0.7); |
|
|
border: 1px solid rgba(56, 189, 248, 0.2); |
|
|
border-radius: 0.75rem; |
|
|
backdrop-filter: blur(10px); |
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.industrial-card:hover { |
|
|
transform: translateY(-5px); |
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
|
|
|
.camera-feed { |
|
|
border: 2px dashed rgba(56, 189, 248, 0.5); |
|
|
border-radius: 0.5rem; |
|
|
background: rgba(15, 23, 42, 0.5); |
|
|
} |
|
|
|
|
|
.temperature-display { |
|
|
font-size: 5rem; |
|
|
font-weight: 700; |
|
|
text-shadow: 0 0 10px rgba(56, 189, 248, 0.7); |
|
|
transition: all 0.5s ease; |
|
|
} |
|
|
|
|
|
.status-indicator { |
|
|
width: 12px; |
|
|
height: 12px; |
|
|
border-radius: 50%; |
|
|
display: inline-block; |
|
|
margin-right: 8px; |
|
|
} |
|
|
|
|
|
.status-normal { background-color: #10b981; } |
|
|
.status-warning { background-color: #f59e0b; } |
|
|
.status-danger { background-color: #ef4444; } |
|
|
|
|
|
.history-item { |
|
|
border-left: 3px solid #38bdf8; |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
|
|
|
.history-item:hover { |
|
|
background: rgba(56, 189, 248, 0.1); |
|
|
transform: translateX(5px); |
|
|
} |
|
|
|
|
|
.gauge { |
|
|
position: relative; |
|
|
width: 200px; |
|
|
height: 200px; |
|
|
} |
|
|
|
|
|
.gauge-circle { |
|
|
fill: none; |
|
|
stroke: rgba(30, 41, 59, 0.8); |
|
|
stroke-width: 10; |
|
|
} |
|
|
|
|
|
.gauge-progress { |
|
|
fill: none; |
|
|
stroke: #38bdf8; |
|
|
stroke-width: 10; |
|
|
stroke-linecap: round; |
|
|
transform: rotate(-90deg); |
|
|
transform-origin: 50% 50%; |
|
|
transition: stroke-dasharray 0.5s ease; |
|
|
} |
|
|
|
|
|
.pulse { |
|
|
animation: pulse 2s infinite; |
|
|
} |
|
|
|
|
|
@keyframes pulse { |
|
|
0% { box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.7); } |
|
|
70% { box-shadow: 0 0 0 10px rgba(56, 189, 248, 0); } |
|
|
100% { box-shadow: 0 0 0 0 rgba(56, 189, 248, 0); } |
|
|
} |
|
|
|
|
|
.glow { |
|
|
text-shadow: 0 0 10px rgba(56, 189, 248, 0.7); |
|
|
} |
|
|
|
|
|
|
|
|
.modal-overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
bottom: 0; |
|
|
background: rgba(15, 23, 42, 0.9); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
z-index: 1000; |
|
|
} |
|
|
|
|
|
.modal-content { |
|
|
background: #1e293b; |
|
|
border-radius: 0.5rem; |
|
|
padding: 1.5rem; |
|
|
width: 90%; |
|
|
max-width: 500px; |
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); |
|
|
} |
|
|
|
|
|
.modal-actions { |
|
|
display: flex; |
|
|
justify-content: flex-end; |
|
|
gap: 0.75rem; |
|
|
margin-top: 1rem; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="min-h-screen p-4 md:p-8"> |
|
|
<div class="max-w-7xl mx-auto"> |
|
|
|
|
|
<header class="flex flex-col md:flex-row justify-between items-center mb-8 md:mb-12"> |
|
|
<div class="flex items-center mb-4 md:mb-0"> |
|
|
<div class="bg-industrial-600 p-3 rounded-lg mr-4"> |
|
|
<i class="fas fa-industry text-3xl text-industrial-200"></i> |
|
|
</div> |
|
|
<div> |
|
|
<h1 class="text-2xl md:text-3xl font-bold text-white">ThermoScan<span class="text-industrial-400">AI</span></h1> |
|
|
<p class="text-industrial-300 text-sm">Industrial Machine Temperature Monitoring</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="flex items-center space-x-4"> |
|
|
<div class="hidden md:block"> |
|
|
<div class="flex items-center"> |
|
|
<span class="status-indicator status-normal"></span> |
|
|
<span class="text-industrial-300">System Status: <span class="text-success-500 font-medium">Operational</span></span> |
|
|
</div> |
|
|
<div class="text-xs text-industrial-400 mt-1">Connected to Gemini 2.5 Flash API</div> |
|
|
</div> |
|
|
<button id="settingsBtn" class="bg-industrial-600 hover:bg-industrial-500 text-white px-4 py-2 rounded-lg transition flex items-center"> |
|
|
<i class="fas fa-cog mr-2"></i> Settings |
|
|
</button> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8"> |
|
|
|
|
|
<div class="lg:col-span-2"> |
|
|
<div class="industrial-card h-full"> |
|
|
<div class="p-4 border-b border-industrial-700 flex justify-between items-center"> |
|
|
<h2 class="text-xl font-bold text-white">Machine Camera Feed</h2> |
|
|
<div class="flex space-x-2"> |
|
|
<button id="captureBtn" class="bg-industrial-500 hover:bg-industrial-400 text-white px-3 py-1 rounded text-sm flex items-center"> |
|
|
<i class="fas fa-camera mr-1"></i> Capture & Analyze |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="p-4"> |
|
|
<div class="camera-feed h-96 flex items-center justify-center relative"> |
|
|
<video id="cameraFeed" class="w-full h-full object-contain" autoplay playsinline></video> |
|
|
<canvas id="captureCanvas" class="hidden"></canvas> |
|
|
</div> |
|
|
|
|
|
<div class="mt-4 text-center text-industrial-300 text-sm"> |
|
|
<i class="fas fa-microchip mr-1"></i> Using Gemini 2.5 Flash OCR |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="industrial-card"> |
|
|
<div class="p-4 border-b border-industrial-700"> |
|
|
<h2 class="text-xl font-bold text-white">Temperature Dashboard</h2> |
|
|
</div> |
|
|
<div class="p-4"> |
|
|
<div class="flex flex-col items-center mb-6"> |
|
|
<div class="text-industrial-300 mb-2">Current Temperature</div> |
|
|
<div id="currentTemp" class="temperature-display text-industrial-200">--°C</div> |
|
|
<div id="tempStatus" class="mt-2 px-3 py-1 rounded-full bg-industrial-700 text-industrial-300 text-sm"> |
|
|
<span class="status-indicator"></span> No data |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid grid-cols-2 gap-4 mb-6"> |
|
|
<div class="industrial-card bg-industrial-800 p-4 rounded-lg"> |
|
|
<div class="text-industrial-400 text-sm mb-1">Maximum</div> |
|
|
<div id="maxTemp" class="text-2xl font-bold text-white">--°C</div> |
|
|
</div> |
|
|
<div class="industrial-card bg-industrial-800 p-4 rounded-lg"> |
|
|
<div class="text-industrial-400 text-sm mb-1">Minimum</div> |
|
|
<div id="minTemp" class="text-2xl font-bold text-white">--°C</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="flex justify-center mb-4"> |
|
|
<div class="gauge"> |
|
|
<svg width="200" height="200" viewBox="0 0 200 200"> |
|
|
<circle class="gauge-circle" cx="100" cy="100" r="90" /> |
|
|
<circle id="gaugeProgress" class="gauge-progress" cx="100" cy="100" r="90" |
|
|
stroke-dasharray="0 565" /> |
|
|
</svg> |
|
|
<div class="absolute inset-0 flex items-center justify-center"> |
|
|
<div id="gaugeValue" class="text-3xl font-bold text-white">--</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="text-center text-industrial-400 text-sm"> |
|
|
<i class="fas fa-thermometer-half mr-1"></i> Normal Range: 20°C - 35°C |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="industrial-card mb-8"> |
|
|
<div class="p-4 border-b border-industrial-700"> |
|
|
<h2 class="text-xl font-bold text-white">Temperature History</h2> |
|
|
</div> |
|
|
<div class="p-4"> |
|
|
<div class="flex justify-between mb-4"> |
|
|
<div class="text-industrial-300"> |
|
|
Last 20 readings |
|
|
</div> |
|
|
<div class="flex space-x-2"> |
|
|
<button class="bg-industrial-700 hover:bg-industrial-600 text-white px-3 py-1 rounded text-sm"> |
|
|
<i class="fas fa-download mr-1"></i> Export Data |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="overflow-x-auto"> |
|
|
<table class="min-w-full divide-y divide-industrial-700"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-industrial-400 uppercase tracking-wider">Timestamp</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-industrial-400 uppercase tracking-wider">Temperature</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-industrial-400 uppercase tracking-wider">Status</th> |
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-industrial-400 uppercase tracking-wider">Actions</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="historyBody" class="divide-y divide-industrial-800"> |
|
|
|
|
|
<tr> |
|
|
<td colspan="4" class="px-4 py-8 text-center text-industrial-500"> |
|
|
No temperature data recorded yet |
|
|
</td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="dashboard-grid mb-8"> |
|
|
<div class="industrial-card"> |
|
|
<div class="p-4 border-b border-industrial-700"> |
|
|
<h2 class="text-xl font-bold text-white">OCR Status</h2> |
|
|
</div> |
|
|
<div class="p-4"> |
|
|
<div class="flex items-center mb-4"> |
|
|
<div class="mr-4"> |
|
|
<div class="bg-industrial-700 rounded-full p-3"> |
|
|
<i class="fas fa-eye text-industrial-300 text-2xl"></i> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<div class="text-industrial-300">Gemini OCR Engine</div> |
|
|
<div class="text-white font-bold text-lg">Operational</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-industrial-800 rounded-lg p-3"> |
|
|
<div class="text-industrial-400 text-sm mb-1">Last OCR Result</div> |
|
|
<div id="lastOcrResult" class="text-industrial-200 font-mono">Waiting for first capture...</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="industrial-card"> |
|
|
<div class="p-4 border-b border-industrial-700"> |
|
|
<h2 class="text-xl font-bold text-white">System Alerts</h2> |
|
|
</div> |
|
|
<div class="p-4"> |
|
|
<div class="flex items-center mb-4"> |
|
|
<div class="mr-4"> |
|
|
<div class="bg-industrial-700 rounded-full p-3"> |
|
|
<i class="fas fa-bell text-industrial-300 text-2xl"></i> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<div class="text-industrial-300">Alert Status</div> |
|
|
<div class="text-white font-bold text-lg">No Active Alerts</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-industrial-800 rounded-lg p-3"> |
|
|
<div class="text-industrial-400 text-sm mb-1">Notification Settings</div> |
|
|
<div class="text-industrial-200">Email alerts enabled for temperatures above 35°C</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="industrial-card"> |
|
|
<div class="p-4 border-b border-industrial-700"> |
|
|
<h2 class="text-xl font-bold text-white">API Status</h2> |
|
|
</div> |
|
|
<div class="p-4"> |
|
|
<div class="flex items-center mb-4"> |
|
|
<div class="mr-4"> |
|
|
<div class="bg-industrial-700 rounded-full p-3"> |
|
|
<i class="fas fa-plug text-industrial-300 text-2xl"></i> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<div class="text-industrial-300">Gemini API</div> |
|
|
<div class="text-white font-bold text-lg">Connected</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-industrial-800 rounded-lg p-3"> |
|
|
<div class="text-industrial-400 text-sm mb-1">Rate Limit Status</div> |
|
|
<div class="text-industrial-200">10 requests/min available (1 used)</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<footer class="text-center text-industrial-500 text-sm pt-6 border-t border-industrial-800"> |
|
|
<p>ThermoScanAI - Industrial Machine Temperature Monitoring System | Using Gemini 2.5 Flash OCR</p> |
|
|
<p class="mt-2">© 2023 Industrial AI Solutions. All rights reserved.</p> |
|
|
</footer> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
const cameraFeed = document.getElementById('cameraFeed'); |
|
|
const cameraPlaceholder = document.getElementById('cameraPlaceholder'); |
|
|
const captureCanvas = document.getElementById('captureCanvas'); |
|
|
const startBtn = document.getElementById('startBtn'); |
|
|
const stopBtn = document.getElementById('stopBtn'); |
|
|
const countdownEl = document.getElementById('countdown'); |
|
|
const currentTempEl = document.getElementById('currentTemp'); |
|
|
const maxTempEl = document.getElementById('maxTemp'); |
|
|
const minTempEl = document.getElementById('minTemp'); |
|
|
const tempStatusEl = document.getElementById('tempStatus'); |
|
|
const historyBody = document.getElementById('historyBody'); |
|
|
const gaugeProgress = document.getElementById('gaugeProgress'); |
|
|
const gaugeValue = document.getElementById('gaugeValue'); |
|
|
const lastOcrResult = document.getElementById('lastOcrResult'); |
|
|
|
|
|
|
|
|
let apiKey = localStorage.getItem('geminiApiKey') || ''; |
|
|
const apiKeyModal = document.createElement('div'); |
|
|
apiKeyModal.className = 'fixed inset-0 bg-industrial-900 bg-opacity-90 flex items-center justify-center z-50 hidden'; |
|
|
apiKeyModal.innerHTML = ` |
|
|
<div class="bg-industrial-800 rounded-lg p-6 max-w-md w-full"> |
|
|
<h3 class="text-xl font-bold mb-4">Enter Gemini API Key</h3> |
|
|
<input type="password" id="apiKeyInput" placeholder="Your Gemini API Key" |
|
|
class="w-full bg-industrial-700 border border-industrial-600 rounded p-3 mb-4 text-white"> |
|
|
<div class="flex justify-end space-x-3"> |
|
|
<button id="cancelApiKey" class="px-4 py-2 rounded bg-industrial-600 hover:bg-industrial-500"> |
|
|
Cancel |
|
|
</button> |
|
|
<button id="saveApiKey" class="px-4 py-2 rounded bg-industrial-500 hover:bg-industrial-400"> |
|
|
Save Key |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
document.body.appendChild(apiKeyModal); |
|
|
|
|
|
|
|
|
let monitoringInterval; |
|
|
let countdownInterval; |
|
|
let countdown = 10; |
|
|
let temperatureHistory = []; |
|
|
let maxTemp = null; |
|
|
let minTemp = null; |
|
|
let stream = null; |
|
|
let hasValidApiKey = false; |
|
|
|
|
|
|
|
|
async function init() { |
|
|
|
|
|
document.getElementById('captureBtn').addEventListener('click', captureAndProcess); |
|
|
updateGauge(0); |
|
|
|
|
|
|
|
|
document.getElementById('captureBtn').addEventListener('click', async function firstCapture() { |
|
|
try { |
|
|
if (!stream) { |
|
|
stream = await navigator.mediaDevices.getUserMedia({ |
|
|
video: { |
|
|
facingMode: 'environment', |
|
|
width: { ideal: 1280 }, |
|
|
height: { ideal: 720 } |
|
|
} |
|
|
}); |
|
|
cameraFeed.srcObject = stream; |
|
|
} |
|
|
} catch (err) { |
|
|
console.error("Error accessing camera:", err); |
|
|
cameraFeed.parentElement.innerHTML = ` |
|
|
<div class="text-center text-industrial-300 p-4"> |
|
|
<i class="fas fa-video-slash text-4xl mb-2"></i> |
|
|
<p>Could not access camera. Please check permissions.</p> |
|
|
<button onclick="window.location.reload()" class="mt-2 bg-industrial-600 hover:bg-industrial-500 text-white px-4 py-2 rounded-lg"> |
|
|
Try Again |
|
|
</button> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
document.getElementById('captureBtn').removeEventListener('click', firstCapture); |
|
|
}, { once: true }); |
|
|
|
|
|
|
|
|
document.getElementById('settingsBtn').addEventListener('click', () => { |
|
|
apiKeyModal.classList.remove('hidden'); |
|
|
document.getElementById('apiKeyInput').value = apiKey; |
|
|
}); |
|
|
|
|
|
document.getElementById('saveApiKey').addEventListener('click', () => { |
|
|
apiKey = document.getElementById('apiKeyInput').value.trim(); |
|
|
localStorage.setItem('geminiApiKey', apiKey); |
|
|
apiKeyModal.classList.add('hidden'); |
|
|
hasValidApiKey = apiKey.length > 0; |
|
|
}); |
|
|
|
|
|
document.getElementById('cancelApiKey').addEventListener('click', () => { |
|
|
apiKeyModal.classList.add('hidden'); |
|
|
}); |
|
|
|
|
|
hasValidApiKey = apiKey.length > 0; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function updateCountdown() { |
|
|
countdownEl.textContent = countdown; |
|
|
|
|
|
if (countdown <= 0) { |
|
|
countdown = 10; |
|
|
} |
|
|
|
|
|
countdownInterval = setTimeout(() => { |
|
|
countdown--; |
|
|
updateCountdown(); |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
|
|
|
function captureAndProcess() { |
|
|
if (!stream) return; |
|
|
|
|
|
|
|
|
const context = captureCanvas.getContext('2d'); |
|
|
captureCanvas.width = cameraFeed.videoWidth; |
|
|
captureCanvas.height = cameraFeed.videoHeight; |
|
|
context.drawImage(cameraFeed, 0, 0, captureCanvas.width, captureCanvas.height); |
|
|
|
|
|
|
|
|
const imageData = captureCanvas.toDataURL('image/jpeg').split(',')[1]; |
|
|
|
|
|
|
|
|
processWithGemini(imageData); |
|
|
} |
|
|
|
|
|
|
|
|
async function processWithGemini(imageData) { |
|
|
if (!hasValidApiKey) { |
|
|
lastOcrResult.textContent = "API Key not configured"; |
|
|
currentTempEl.textContent = "--°C"; |
|
|
gaugeValue.textContent = "--"; |
|
|
return; |
|
|
} |
|
|
|
|
|
lastOcrResult.textContent = "Processing image..."; |
|
|
|
|
|
try { |
|
|
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=${apiKey}`, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: JSON.stringify({ |
|
|
contents: [{ |
|
|
parts: [{ |
|
|
text: "Extract the numerical temperature value from this image. Only return the number, nothing else." |
|
|
}, { |
|
|
inlineData: { |
|
|
mimeType: "image/jpeg", |
|
|
data: imageData |
|
|
} |
|
|
}] |
|
|
}] |
|
|
}) |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.candidates && data.candidates[0].content.parts[0].text) { |
|
|
const result = data.candidates[0].content.parts[0].text; |
|
|
const tempMatch = result.match(/\d+(\.\d+)?/); |
|
|
|
|
|
if (tempMatch) { |
|
|
const temperature = parseFloat(tempMatch[0]); |
|
|
lastOcrResult.textContent = `Detected temperature: ${temperature}°C`; |
|
|
updateTemperature(temperature); |
|
|
} else { |
|
|
lastOcrResult.textContent = "No temperature detected"; |
|
|
currentTempEl.textContent = "--°C"; |
|
|
gaugeValue.textContent = "--"; |
|
|
} |
|
|
} else { |
|
|
lastOcrResult.textContent = "Failed to process image"; |
|
|
currentTempEl.textContent = "--°C"; |
|
|
gaugeValue.textContent = "--"; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error("Gemini API error:", error); |
|
|
lastOcrResult.textContent = "API Error - Check console"; |
|
|
currentTempEl.textContent = "--°C"; |
|
|
gaugeValue.textContent = "--"; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateTemperature(temp) { |
|
|
|
|
|
currentTempEl.textContent = `${temp}°C`; |
|
|
|
|
|
|
|
|
updateGauge(temp); |
|
|
|
|
|
|
|
|
updateStatus(temp); |
|
|
|
|
|
|
|
|
temperatureHistory.push({ |
|
|
temp: temp, |
|
|
timestamp: new Date().toLocaleTimeString(), |
|
|
status: getStatus(temp) |
|
|
}); |
|
|
|
|
|
|
|
|
if (temperatureHistory.length > 20) { |
|
|
temperatureHistory.shift(); |
|
|
} |
|
|
|
|
|
|
|
|
if (maxTemp === null || temp > maxTemp) { |
|
|
maxTemp = temp; |
|
|
maxTempEl.textContent = `${maxTemp}°C`; |
|
|
} |
|
|
|
|
|
if (minTemp === null || temp < minTemp) { |
|
|
minTemp = temp; |
|
|
minTempEl.textContent = `${minTemp}°C`; |
|
|
} |
|
|
|
|
|
|
|
|
updateHistoryTable(); |
|
|
} |
|
|
|
|
|
|
|
|
function updateGauge(temp) { |
|
|
|
|
|
const percentage = Math.min(Math.max((temp / 50) * 100, 0), 100); |
|
|
const dashValue = (565 * percentage) / 100; |
|
|
|
|
|
gaugeProgress.style.strokeDasharray = `${dashValue} 565`; |
|
|
gaugeValue.textContent = `${temp}°C`; |
|
|
|
|
|
|
|
|
if (temp > 35) { |
|
|
gaugeProgress.style.stroke = '#ef4444'; |
|
|
} else if (temp > 30) { |
|
|
gaugeProgress.style.stroke = '#f59e0b'; |
|
|
} else { |
|
|
gaugeProgress.style.stroke = '#38bdf8'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateStatus(temp) { |
|
|
const statusIndicator = tempStatusEl.querySelector('.status-indicator'); |
|
|
statusIndicator.className = 'status-indicator'; |
|
|
|
|
|
if (temp > 35) { |
|
|
tempStatusEl.innerHTML = '<span class="status-indicator status-danger"></span> CRITICAL TEMPERATURE'; |
|
|
tempStatusEl.className = 'mt-2 px-3 py-1 rounded-full bg-danger-900 text-danger-200 text-sm'; |
|
|
currentTempEl.classList.add('text-danger-500'); |
|
|
currentTempEl.classList.remove('text-industrial-200', 'text-warning-500'); |
|
|
} else if (temp > 30) { |
|
|
tempStatusEl.innerHTML = '<span class="status-indicator status-warning"></span> HIGH TEMPERATURE'; |
|
|
tempStatusEl.className = 'mt-2 px-3 py-1 rounded-full bg-warning-900 text-warning-200 text-sm'; |
|
|
currentTempEl.classList.add('text-warning-500'); |
|
|
currentTempEl.classList.remove('text-industrial-200', 'text-danger-500'); |
|
|
} else { |
|
|
tempStatusEl.innerHTML = '<span class="status-indicator status-normal"></span> NORMAL'; |
|
|
tempStatusEl.className = 'mt-2 px-3 py-1 rounded-full bg-industrial-800 text-industrial-300 text-sm'; |
|
|
currentTempEl.classList.add('text-industrial-200'); |
|
|
currentTempEl.classList.remove('text-warning-500', 'text-danger-500'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function getStatus(temp) { |
|
|
if (temp > 35) return 'critical'; |
|
|
if (temp > 30) return 'warning'; |
|
|
return 'normal'; |
|
|
} |
|
|
|
|
|
|
|
|
function updateHistoryTable() { |
|
|
if (temperatureHistory.length === 0) { |
|
|
historyBody.innerHTML = ` |
|
|
<tr> |
|
|
<td colspan="4" class="px-4 py-8 text-center text-industrial-500"> |
|
|
No temperature data recorded yet |
|
|
</td> |
|
|
</tr> |
|
|
`; |
|
|
return; |
|
|
} |
|
|
|
|
|
let historyHTML = ''; |
|
|
temperatureHistory.slice().reverse().forEach(reading => { |
|
|
let statusClass = ''; |
|
|
let statusText = ''; |
|
|
|
|
|
switch(reading.status) { |
|
|
case 'critical': |
|
|
statusClass = 'text-danger-500'; |
|
|
statusText = 'Critical'; |
|
|
break; |
|
|
case 'warning': |
|
|
statusClass = 'text-warning-500'; |
|
|
statusText = 'Warning'; |
|
|
break; |
|
|
default: |
|
|
statusClass = 'text-success-500'; |
|
|
statusText = 'Normal'; |
|
|
} |
|
|
|
|
|
historyHTML += ` |
|
|
<tr class="history-item"> |
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-industrial-300">${reading.timestamp}</td> |
|
|
<td class="px-4 py-3 whitespace-nowrap"> |
|
|
<div class="text-lg font-bold ${reading.status === 'critical' ? 'text-danger-500' : reading.status === 'warning' ? 'text-warning-500' : 'text-industrial-200'}"> |
|
|
${reading.temp}°C |
|
|
</div> |
|
|
</td> |
|
|
<td class="px-4 py-3 whitespace-nowrap"> |
|
|
<span class="${statusClass} font-medium">${statusText}</span> |
|
|
</td> |
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm"> |
|
|
<button class="text-industrial-400 hover:text-industrial-300 mr-2"> |
|
|
<i class="fas fa-chart-line"></i> |
|
|
</button> |
|
|
<button class="text-industrial-400 hover:text-industrial-300"> |
|
|
<i class="fas fa-info-circle"></i> |
|
|
</button> |
|
|
</td> |
|
|
</tr> |
|
|
`; |
|
|
}); |
|
|
|
|
|
historyBody.innerHTML = historyHTML; |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', init); |
|
|
</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=pksaheb/temperature-monitoring" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |