|
<!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> |
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></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 1.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 id="exportDataBtn" 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> |
|
<button id="stopRecordingBtn" class="bg-danger-600 hover:bg-danger-500 text-white px-3 py-1 rounded text-sm ml-2 hidden"> |
|
<i class="fas fa-stop mr-1"></i> Stop Recording |
|
</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="industrial-card mb-8"> |
|
<div class="p-4 border-b border-industrial-700"> |
|
<h2 class="text-xl font-bold text-white">Temperature Trend</h2> |
|
</div> |
|
<div class="p-4"> |
|
<canvas id="liveChart" height="300"></canvas> |
|
</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> |
|
|
|
|
|
<div class="industrial-card mb-8"> |
|
<div class="p-4 border-b border-industrial-700"> |
|
<h2 class="text-xl font-bold text-white">Debug Console</h2> |
|
</div> |
|
<div class="p-4"> |
|
<div id="debugConsole" class="bg-black text-green-400 font-mono text-sm p-4 rounded h-64 overflow-y-auto"></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 1.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); |
|
} |
|
|
|
function startPeriodicCapture(intervalSeconds = 10) { |
|
logDebug(`Starting periodic capture every ${intervalSeconds} seconds`); |
|
return setInterval(() => { |
|
logDebug("Auto-capturing image..."); |
|
captureAndProcess(); |
|
}, intervalSeconds * 1000); |
|
} |
|
|
|
function stopPeriodicCapture(intervalId) { |
|
clearInterval(intervalId); |
|
logDebug("Stopped periodic capture"); |
|
} |
|
|
|
|
|
function logDebug(message) { |
|
const debugConsole = document.getElementById('debugConsole'); |
|
const timestamp = new Date().toLocaleTimeString(); |
|
const logEntry = document.createElement('div'); |
|
logEntry.innerHTML = `[${timestamp}] ${message}`; |
|
debugConsole.appendChild(logEntry); |
|
debugConsole.scrollTop = debugConsole.scrollHeight; |
|
|
|
|
|
console.log(`[ThermoScan] ${message}`); |
|
|
|
|
|
if (debugConsole.children.length > 100) { |
|
debugConsole.removeChild(debugConsole.children[0]); |
|
} |
|
} |
|
|
|
async function processWithGemini(imageData) { |
|
if (!hasValidApiKey) { |
|
const msg = "API Key not configured"; |
|
lastOcrResult.textContent = msg; |
|
logDebug(msg); |
|
currentTempEl.textContent = "--°C"; |
|
gaugeValue.textContent = "--"; |
|
return; |
|
} |
|
lastOcrResult.textContent = "Processing image..."; |
|
logDebug("Starting image processing with Gemini API"); |
|
logDebug(`Image data size: ${Math.round(imageData.length / 1024)}KB`); |
|
|
|
try { |
|
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`; |
|
logDebug(`Sending request to: ${apiUrl}`); |
|
|
|
const requestBody = { |
|
contents: [{ |
|
parts: [{ |
|
text: "Extract the numerical temperature value from this image. Only return the number, nothing else." |
|
}, { |
|
inlineData: { |
|
mimeType: "image/jpeg", |
|
data: imageData |
|
} |
|
}] |
|
}] |
|
}; |
|
|
|
logDebug("Request payload prepared"); |
|
logDebug(`Prompt: "${requestBody.contents[0].parts[0].text}"`); |
|
|
|
const response = await fetch(apiUrl, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
contents: [{ |
|
parts: [{ |
|
text: "Analyze this image of an industrial machine temperature display. Extract only the numerical temperature value. Return just the number with no additional text or symbols." |
|
}, { |
|
inlineData: { |
|
mimeType: "image/jpeg", |
|
data: imageData |
|
} |
|
}] |
|
}], |
|
generationConfig: { |
|
temperature: 0.1, |
|
topP: 0.1, |
|
topK: 1 |
|
} |
|
}) |
|
}); |
|
const data = await response.json(); |
|
logDebug(`API response status: ${response.status}`); |
|
logDebug(`Full response: ${JSON.stringify(data, null, 2)}`); |
|
|
|
if (data.candidates && data.candidates[0].content.parts[0].text) { |
|
const result = data.candidates[0].content.parts[0].text; |
|
logDebug(`Raw API response text: "${result}"`); |
|
|
|
let temperature; |
|
const tempMatch = result.match(/-?\d+(\.\d+)?/); |
|
|
|
if (tempMatch) { |
|
temperature = parseFloat(tempMatch[0]); |
|
|
|
if (isNaN(temperature)) { |
|
const cleaned = result.replace(/[^\d.-]/g, ''); |
|
temperature = parseFloat(cleaned); |
|
} |
|
const msg = `Detected temperature: ${temperature}°C`; |
|
lastOcrResult.textContent = msg; |
|
logDebug(msg); |
|
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); |
|
const msg = `API Error: ${error.message}`; |
|
lastOcrResult.textContent = msg; |
|
logDebug(msg); |
|
currentTempEl.textContent = "--°C"; |
|
gaugeValue.textContent = "--"; |
|
} |
|
} |
|
|
|
|
|
let lastWebhookTimestamp = 0; |
|
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(); |
|
updateLiveChart(temp, new Date().toLocaleTimeString()); |
|
|
|
|
|
const now = Date.now(); |
|
const criticalThreshold = 50; |
|
const webhookCooldown = 30000; |
|
|
|
if (temp > criticalThreshold && now - lastWebhookTimestamp > webhookCooldown) { |
|
lastWebhookTimestamp = now; |
|
logDebug(`Triggering webhook for critical temperature: ${temp}°C`); |
|
|
|
const sendWebhook = (attempt = 1) => { |
|
const webhookUrl = 'https://n8n-1r4e.onrender.com/webhook-test/fb1185d8-51fc-4938-bd86-0222ceb2d3af'; |
|
const payload = { |
|
temperature: temp, |
|
timestamp: new Date().toISOString(), |
|
status: getStatus(temp), |
|
message: `⚠️ Critical temperature (${temp}°C) detected. Immediate action required.`, |
|
deviceInfo: navigator.userAgent, |
|
attempt: attempt |
|
}; |
|
|
|
logDebug(`Sending webhook attempt ${attempt} to: ${webhookUrl}`); |
|
logDebug(`Payload: ${JSON.stringify(payload)}`); |
|
|
|
fetch(webhookUrl, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'X-ThermoScan-Signature': 'your-secret-key-here' |
|
}, |
|
body: JSON.stringify(payload) |
|
}) |
|
.then(async res => { |
|
if (!res.ok) { |
|
const errorText = await res.text(); |
|
throw new Error(`HTTP ${res.status}: ${errorText}`); |
|
} |
|
return res.json(); |
|
}) |
|
.then(data => { |
|
logDebug(`Webhook successful. Response: ${JSON.stringify(data)}`); |
|
showAlert('Critical temperature alert sent', 'success'); |
|
}) |
|
.catch(err => { |
|
logDebug(`Webhook attempt ${attempt} failed: ${err.message}`); |
|
if (attempt < 3) { |
|
const retryDelay = 5000 * attempt; |
|
logDebug(`Retrying in ${retryDelay/1000} seconds...`); |
|
setTimeout(() => sendWebhook(attempt + 1), retryDelay); |
|
} else { |
|
logDebug('Max retry attempts reached'); |
|
showAlert('Failed to send critical alert after 3 attempts', 'error'); |
|
} |
|
}); |
|
}; |
|
|
|
sendWebhook(); |
|
} else if (temp > criticalThreshold) { |
|
const timeLeft = Math.ceil((webhookCooldown - (now - lastWebhookTimestamp)) / 1000); |
|
logDebug(`Webhook cooldown active. ${timeLeft}s remaining until next alert can be sent`); |
|
} |
|
} |
|
|
|
|
|
|
|
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" onclick="showTemperatureChart()"> |
|
<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; |
|
} |
|
|
|
|
|
function exportData() { |
|
if (temperatureHistory.length === 0) { |
|
logDebug("No data to export"); |
|
return; |
|
} |
|
let csvContent = "Timestamp,Temperature,Status\n"; |
|
temperatureHistory.forEach(reading => { |
|
csvContent += `${reading.timestamp},${reading.temp},${reading.status}\n`; |
|
}); |
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); |
|
const url = URL.createObjectURL(blob); |
|
const link = document.createElement('a'); |
|
link.setAttribute('href', url); |
|
link.setAttribute('download', `thermoscan_data_${new Date().toISOString().slice(0,10)}.csv`); |
|
link.style.visibility = 'hidden'; |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
logDebug("Data exported as CSV"); |
|
} |
|
|
|
let temperatureChart = null; |
|
function showTemperatureChart() { |
|
const modal = document.getElementById('chartModal'); |
|
modal.classList.remove('hidden'); |
|
|
|
const ctx = document.getElementById('temperatureChart').getContext('2d'); |
|
|
|
|
|
if (temperatureChart) { |
|
temperatureChart.destroy(); |
|
} |
|
|
|
|
|
const labels = temperatureHistory.map(reading => reading.timestamp); |
|
const data = temperatureHistory.map(reading => reading.temp); |
|
const statusColors = temperatureHistory.map(reading => { |
|
switch(reading.status) { |
|
case 'critical': return '#ef4444'; |
|
case 'warning': return '#f59e0b'; |
|
default: return '#10b981'; |
|
} |
|
}); |
|
|
|
temperatureChart = new Chart(ctx, { |
|
type: 'line', |
|
data: { |
|
labels: labels, |
|
datasets: [{ |
|
label: 'Temperature (°C)', |
|
data: data, |
|
borderColor: '#38bdf8', |
|
backgroundColor: 'rgba(56, 189, 248, 0.1)', |
|
borderWidth: 2, |
|
pointBackgroundColor: statusColors, |
|
pointRadius: 5, |
|
pointHoverRadius: 7, |
|
tension: 0.1, |
|
fill: true |
|
}] |
|
}, |
|
options: { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
scales: { |
|
y: { |
|
beginAtZero: false, |
|
grid: { |
|
color: 'rgba(30, 41, 59, 0.5)' |
|
}, |
|
ticks: { |
|
color: '#94a3b8' |
|
} |
|
}, |
|
x: { |
|
grid: { |
|
color: 'rgba(30, 41, 59, 0.5)' |
|
}, |
|
ticks: { |
|
color: '#94a3b8' |
|
} |
|
} |
|
}, |
|
plugins: { |
|
legend: { |
|
labels: { |
|
color: '#e2e8f0' |
|
} |
|
}, |
|
tooltip: { |
|
backgroundColor: '#1e293b', |
|
titleColor: '#e2e8f0', |
|
bodyColor: '#e2e8f0', |
|
borderColor: '#334155', |
|
borderWidth: 1 |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
function exportChartAsImage() { |
|
if (!temperatureChart) return; |
|
|
|
const link = document.createElement('a'); |
|
link.download = `temperature_chart_${new Date().toISOString().slice(0,10)}.png`; |
|
link.href = temperatureChart.toBase64Image(); |
|
link.click(); |
|
} |
|
|
|
let liveChart = null; |
|
let chartData = { |
|
labels: [], |
|
datasets: [{ |
|
label: 'Temperature (°C)', |
|
data: [], |
|
borderColor: '#38bdf8', |
|
backgroundColor: 'rgba(56, 189, 248, 0.1)', |
|
borderWidth: 2, |
|
pointRadius: 3, |
|
tension: 0.1, |
|
fill: true |
|
}] |
|
}; |
|
|
|
function initLiveChart() { |
|
const ctx = document.getElementById('liveChart').getContext('2d'); |
|
liveChart = new Chart(ctx, { |
|
type: 'line', |
|
data: chartData, |
|
options: { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
animation: { |
|
duration: 1000, |
|
easing: 'linear' |
|
}, |
|
scales: { |
|
y: { |
|
beginAtZero: false, |
|
grid: { |
|
color: 'rgba(30, 41, 59, 0.5)' |
|
}, |
|
ticks: { |
|
color: '#94a3b8' |
|
} |
|
}, |
|
x: { |
|
grid: { |
|
color: 'rgba(30, 41, 59, 0.5)' |
|
}, |
|
ticks: { |
|
color: '#94a3b8', |
|
maxRotation: 45, |
|
minRotation: 45 |
|
} |
|
} |
|
}, |
|
plugins: { |
|
legend: { |
|
labels: { |
|
color: '#e2e8f0' |
|
} |
|
}, |
|
tooltip: { |
|
backgroundColor: '#1e293b', |
|
titleColor: '#e2e8f0', |
|
bodyColor: '#e2e8f0', |
|
borderColor: '#334155', |
|
borderWidth: 1 |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
|
|
function updateLiveChart(temp, timestamp) { |
|
|
|
chartData.labels.push(timestamp); |
|
chartData.datasets[0].data.push(temp); |
|
|
|
|
|
if (chartData.labels.length > 20) { |
|
chartData.labels.shift(); |
|
chartData.datasets[0].data.shift(); |
|
} |
|
|
|
|
|
liveChart.update(); |
|
} |
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
initLiveChart(); |
|
init(); |
|
|
|
|
|
let captureInterval; |
|
const captureBtn = document.getElementById('captureBtn'); |
|
const stopRecordingBtn = document.getElementById('stopRecordingBtn'); |
|
|
|
captureBtn.addEventListener('click', () => { |
|
if (captureInterval) { |
|
stopPeriodicCapture(captureInterval); |
|
captureInterval = null; |
|
captureBtn.innerHTML = '<i class="fas fa-camera mr-1"></i> Capture & Analyze'; |
|
stopRecordingBtn.classList.add('hidden'); |
|
} else { |
|
captureInterval = startPeriodicCapture(10); |
|
captureBtn.innerHTML = '<i class="fas fa-pause mr-1"></i> Pause Recording'; |
|
stopRecordingBtn.classList.remove('hidden'); |
|
} |
|
}); |
|
|
|
stopRecordingBtn.addEventListener('click', () => { |
|
if (captureInterval) { |
|
stopPeriodicCapture(captureInterval); |
|
captureInterval = null; |
|
captureBtn.innerHTML = '<i class="fas fa-camera mr-1"></i> Capture & Analyze'; |
|
stopRecordingBtn.classList.add('hidden'); |
|
} |
|
}); |
|
|
|
document.getElementById('exportDataBtn').addEventListener('click', exportData); |
|
|
|
|
|
document.getElementById('closeChartModal').addEventListener('click', () => { |
|
document.getElementById('chartModal').classList.add('hidden'); |
|
}); |
|
|
|
document.getElementById('exportChartBtn').addEventListener('click', exportChartAsImage); |
|
}); |
|
</script> |
|
|
|
|
|
<div id="chartModal" class="modal-overlay hidden"> |
|
<div class="modal-content"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h3 class="text-xl font-bold">Temperature History Chart</h3> |
|
<button id="closeChartModal" class="text-industrial-400 hover:text-industrial-300"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
</div> |
|
<div class="bg-industrial-800 p-4 rounded-lg"> |
|
<canvas id="temperatureChart" height="300"></canvas> |
|
</div> |
|
<div class="modal-actions"> |
|
<button id="exportChartBtn" class="bg-industrial-600 hover:bg-industrial-500 text-white px-4 py-2 rounded-lg"> |
|
<i class="fas fa-download mr-2"></i> Export as Image |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
<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> |