// Vision Record System - Frontend Logic var API_BASE = ''; var currentImageId = null; var refreshTimer = null; // Initialize document.addEventListener('DOMContentLoaded', function() { loadConfig(); refreshAll(); }); // Toast 提示(自动消失) function showToast(message, duration) { duration = duration || 2000; var toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = message; document.body.appendChild(toast); setTimeout(function() { toast.classList.add('fade-out'); setTimeout(function() { toast.remove(); }, 300); }, duration); } // 点击模态框背景关闭 function closeModalOnBackground(event, modalId) { if (event.target.id === modalId) { document.getElementById(modalId).classList.remove('active'); } } function loadConfig() { fetch(API_BASE + '/api/config') .then(function(res) { return res.json(); }) .then(function(config) { var interval = config.refresh_interval || 5; startAutoRefresh(interval); document.getElementById('refresh-info').textContent = 'Refresh: ' + interval + 's'; }) .catch(function(e) { console.error('Load config failed:', e); startAutoRefresh(5); }); } function startAutoRefresh(intervalSeconds) { if (refreshTimer) { clearInterval(refreshTimer); } refreshTimer = setInterval(refreshAll, intervalSeconds * 1000); } function refreshAll() { loadStatus(); loadImages(); loadEvents(); } // Status function loadStatus() { fetch(API_BASE + '/api/status') .then(function(res) { return res.json(); }) .then(function(data) { updateUI(data); }) .catch(function(e) { console.error('Load status failed:', e); }); } function updateUI(data) { var startBtn = document.getElementById('start-btn'); var stopBtn = document.getElementById('stop-btn'); if (data.scheduler.running) { startBtn.disabled = true; stopBtn.disabled = false; document.getElementById('scheduler-status').innerHTML = 'Status: Running'; } else { startBtn.disabled = false; stopBtn.disabled = true; document.getElementById('scheduler-status').innerHTML = 'Status: Stopped'; } document.getElementById('capture-count').textContent = 'Capture count: ' + data.scheduler.capture_count; document.getElementById('total-images').textContent = data.stats.total_images; document.getElementById('analyzed-images').textContent = data.stats.analyzed_images; document.getElementById('total-events').textContent = data.stats.total_events; // Event type filter var eventFilter = document.getElementById('event-filter'); eventFilter.innerHTML = ''; data.stats.event_types.forEach(function(item) { var opt = document.createElement('option'); opt.value = item.type; opt.textContent = item.type + ' (' + item.count + ')'; eventFilter.appendChild(opt); }); } // Control function startScheduler() { fetch(API_BASE + '/api/scheduler/start', {method: 'POST'}) .then(function(res) { return res.json(); }) .then(function(data) { if (data.success) { showToast('Started!', 1500); refreshAll(); } else { showToast('Failed: ' + data.error, 3000); } }) .catch(function(e) { showToast('Error: ' + e.message, 3000); }); } function stopScheduler() { fetch(API_BASE + '/api/scheduler/stop', {method: 'POST'}) .then(function(res) { return res.json(); }) .then(function(data) { if (data.success) { showToast('Stopped!', 1500); refreshAll(); } }) .catch(function(e) { showToast('Error: ' + e.message, 3000); }); } function captureNow() { fetch(API_BASE + '/api/capture', {method: 'POST'}) .then(function(res) { return res.json(); }) .then(function(data) { if (data.success) { showToast('Captured! ID: ' + data.image_id, 1500); refreshAll(); } else { showToast('Failed: ' + data.error, 3000); } }) .catch(function(e) { showToast('Error: ' + e.message, 3000); }); } function analyzeUnanalyzed() { fetch(API_BASE + '/api/analyze/unanalyzed', {method: 'POST'}) .then(function(res) { return res.json(); }) .then(function(data) { showToast('Analyzed ' + data.results.length + ' images', 1500); refreshAll(); }) .catch(function(e) { showToast('Error: ' + e.message, 3000); }); } // Images function loadImages() { var filter = document.getElementById('image-filter').value; var analyzedOnly = filter === 'analyzed'; fetch(API_BASE + '/api/images?limit=50&analyzed_only=' + analyzedOnly) .then(function(res) { return res.json(); }) .then(function(data) { renderImages(data.images); }) .catch(function(e) { console.error('Load images failed:', e); }); } function renderImages(images) { var list = document.getElementById('images-list'); list.innerHTML = ''; if (images.length === 0) { list.innerHTML = '

No images

'; return; } images.forEach(function(img) { var item = document.createElement('div'); item.className = 'image-item ' + (img.analyzed ? 'analyzed' : 'unanalyzed'); item.onclick = function() { openImageModal(img.id); }; var time = new Date(img.timestamp).toLocaleString(); var status = img.analyzed ? 'Analyzed' : 'Unanalyzed'; var events = img.events_summary || 'No events'; item.innerHTML = '#' + img.id + '' + '' + time + '' + '' + status + '' + '' + events + ''; list.appendChild(item); }); } function openImageModal(imageId) { currentImageId = imageId; fetch(API_BASE + '/api/images/' + imageId) .then(function(res) { return res.json(); }) .then(function(data) { var filename = data.path.split('/').pop().split('\\').pop(); document.getElementById('modal-image').src = '/images/' + filename; document.getElementById('modal-title').textContent = 'Image #' + data.id; document.getElementById('modal-time').textContent = 'Time: ' + new Date(data.timestamp).toLocaleString(); var eventsDiv = document.getElementById('modal-events'); eventsDiv.innerHTML = ''; if (data.events && data.events.length > 0) { // 分组显示 var localEvents = []; var aiEvents = []; data.events.forEach(function(event) { if (event.event_type.indexOf('(本地)') >= 0) { localEvents.push(event); } else if (event.event_type.indexOf('(AI)') >= 0) { aiEvents.push(event); } else { localEvents.push(event); } }); // 本地分析结果 if (localEvents.length > 0) { var localSection = document.createElement('div'); localSection.className = 'modal-events-section'; localSection.innerHTML = '

Local Analysis (' + localEvents.length + ')

'; localEvents.forEach(function(event) { var div = document.createElement('div'); div.className = 'modal-event source-local'; var eventType = event.event_type.replace('(本地)', '').trim(); div.innerHTML = '' + eventType + ' ' + '' + event.confidence + '' + '
' + event.description + '
'; localSection.appendChild(div); }); eventsDiv.appendChild(localSection); } // AI分析结果 if (aiEvents.length > 0) { var aiSection = document.createElement('div'); aiSection.className = 'modal-events-section'; aiSection.innerHTML = '

AI Analysis (' + aiEvents.length + ')

'; aiEvents.forEach(function(event) { var div = document.createElement('div'); div.className = 'modal-event source-ai'; var eventType = event.event_type.replace('(AI)', '').trim(); div.innerHTML = '' + eventType + ' ' + '' + event.confidence + '' + '
' + event.description + '
'; aiSection.appendChild(div); }); eventsDiv.appendChild(aiSection); } } else { eventsDiv.innerHTML = '

No events recorded

'; } document.getElementById('image-modal').classList.add('active'); }) .catch(function(e) { console.error('Load image failed:', e); }); } function closeImageModal() { document.getElementById('image-modal').classList.remove('active'); currentImageId = null; } function analyzeCurrentImage() { if (!currentImageId) return; fetch(API_BASE + '/api/analyze/' + currentImageId, {method: 'POST'}) .then(function(res) { return res.json(); }) .then(function(data) { if (data.success) { alert('Analyzed!'); openImageModal(currentImageId); refreshAll(); } else { alert('Failed: ' + data.error); } }) .catch(function(e) { alert('Error: ' + e.message); }); } function deleteCurrentImage() { if (!currentImageId) return; if (!confirm('Delete this image?')) return; fetch(API_BASE + '/api/images/' + currentImageId, {method: 'DELETE'}) .then(function(res) { return res.json(); }) .then(function(data) { if (data.success) { closeImageModal(); refreshAll(); } }) .catch(function(e) { alert('Error: ' + e.message); }); } // Events function loadEvents() { var filter = document.getElementById('event-filter').value; var eventType = filter === 'all' ? '' : filter; var url = API_BASE + '/api/events?limit=50'; if (eventType) { url += '&event_type=' + encodeURIComponent(eventType); } fetch(url) .then(function(res) { return res.json(); }) .then(function(data) { renderEvents(data.events); }) .catch(function(e) { console.error('Load events failed:', e); }); } function renderEvents(events) { var list = document.getElementById('events-list'); list.innerHTML = ''; if (events.length === 0) { list.innerHTML = '

No events

'; return; } // 分组显示:本地分析和AI分析 var localEvents = []; var aiEvents = []; events.forEach(function(event) { if (event.event_type.indexOf('(本地)') >= 0 || event.event_type.indexOf('(local)') >= 0) { localEvents.push(event); } else if (event.event_type.indexOf('(AI)') >= 0) { aiEvents.push(event); } else { localEvents.push(event); } }); // 显示本地分析结果 if (localEvents.length > 0) { var localHeader = document.createElement('div'); localHeader.className = 'events-header'; localHeader.innerHTML = 'Local Analysis' + localEvents.length + ' events'; list.appendChild(localHeader); localEvents.forEach(function(event) { var card = createEventCard(event, 'local'); list.appendChild(card); }); } // 显示AI分析结果 if (aiEvents.length > 0) { var aiHeader = document.createElement('div'); aiHeader.className = 'events-header'; aiHeader.innerHTML = 'AI Analysis' + aiEvents.length + ' events'; list.appendChild(aiHeader); aiEvents.forEach(function(event) { var card = createEventCard(event, 'ai'); list.appendChild(card); }); } } function createEventCard(event, source) { var card = document.createElement('div'); card.className = 'event-card type-' + event.event_type.replace('(本地)', '').replace('(AI)', '').trim() + ' source-' + source; card.onclick = function() { openImageModal(event.image_id); }; var time = new Date(event.timestamp).toLocaleString(); var eventType = event.event_type.replace('(本地)', '').replace('(AI)', '').trim(); card.innerHTML = '
' + '' + eventType + '' + '' + (source === 'local' ? 'Local' : 'AI') + '' + '' + time + '
' + '
' + event.description + '
' + '
' + event.confidence + '
'; return card; } // Filter listeners document.getElementById('image-filter').addEventListener('change', loadImages); document.getElementById('event-filter').addEventListener('change', loadEvents); // Settings function openSettingsModal() { loadSettingsForm(); document.getElementById('settings-modal').classList.add('active'); } function closeSettingsModal() { document.getElementById('settings-modal').classList.remove('active'); } function loadSettingsForm() { fetch(API_BASE + '/api/config') .then(function(res) { return res.json(); }) .then(function(config) { document.getElementById('setting-images-dir').value = config.images_dir || ''; document.getElementById('setting-interval').value = config.capture_interval || 60; document.getElementById('setting-camera-index').value = config.camera_index || 0; document.getElementById('setting-auto-analyze').checked = config.auto_analyze !== false; document.getElementById('setting-display-limit').value = config.display_limit || 20; document.getElementById('setting-refresh-interval').value = config.refresh_interval || 5; document.getElementById('setting-api-url').value = config.vision_api_url || ''; document.getElementById('setting-api-key').value = config.vision_api_key || ''; document.getElementById('setting-model').value = config.vision_model || ''; }) .catch(function(e) { console.error('Load settings failed:', e); }); } function saveSettings() { var settings = { images_dir: document.getElementById('setting-images-dir').value, capture_interval: parseInt(document.getElementById('setting-interval').value), camera_index: parseInt(document.getElementById('setting-camera-index').value), auto_analyze: document.getElementById('setting-auto-analyze').checked, display_limit: parseInt(document.getElementById('setting-display-limit').value), refresh_interval: parseInt(document.getElementById('setting-refresh-interval').value), vision_api_url: document.getElementById('setting-api-url').value, vision_api_key: document.getElementById('setting-api-key').value, vision_model: document.getElementById('setting-model').value }; fetch(API_BASE + '/api/config', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(settings) }) .then(function(res) { return res.json(); }) .then(function(data) { if (data.success) { alert('Settings saved!'); closeSettingsModal(); // Reload config to apply new refresh interval loadConfig(); refreshAll(); } }) .catch(function(e) { alert('Error: ' + e.message); }); } function browseImagesDir() { alert('Please enter the path manually.\nExample: D:\\vision-images\nImages will be saved to date subfolders automatically.'); }