Files
vision-record/web/static/app.js

458 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = '<option value="all">All types</option>';
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 = '<p style="color:#888;text-align:center;">No images</p>';
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 = '<span class="image-number">#' + img.id + '</span>' +
'<span class="image-time">' + time + '</span>' +
'<span class="image-status">' + status + '</span>' +
'<span class="image-events-summary">' + events + '</span>';
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 = '<h4 class="section-title local">Local Analysis (' + localEvents.length + ') </h4>';
localEvents.forEach(function(event) {
var div = document.createElement('div');
div.className = 'modal-event source-local';
var eventType = event.event_type.replace('(本地)', '').trim();
div.innerHTML = '<span class="event-type">' + eventType + '</span> ' +
'<span class="event-confidence">' + event.confidence + '</span>' +
'<div>' + event.description + '</div>';
localSection.appendChild(div);
});
eventsDiv.appendChild(localSection);
}
// AI分析结果
if (aiEvents.length > 0) {
var aiSection = document.createElement('div');
aiSection.className = 'modal-events-section';
aiSection.innerHTML = '<h4 class="section-title ai">AI Analysis (' + aiEvents.length + ') </h4>';
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 = '<span class="event-type">' + eventType + '</span> ' +
'<span class="event-confidence">' + event.confidence + '</span>' +
'<div>' + event.description + '</div>';
aiSection.appendChild(div);
});
eventsDiv.appendChild(aiSection);
}
} else {
eventsDiv.innerHTML = '<p style="color:#888;">No events recorded</p>';
}
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 = '<p style="color:#888;text-align:center;">No events</p>';
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 = '<span class="header-label local">Local Analysis</span><span class="header-count">' + localEvents.length + ' events</span>';
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 = '<span class="header-label ai">AI Analysis</span><span class="header-count">' + aiEvents.length + ' events</span>';
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 = '<div class="event-header">' +
'<span class="event-type">' + eventType + '</span>' +
'<span class="event-source">' + (source === 'local' ? 'Local' : 'AI') + '</span>' +
'<span class="event-time">' + time + '</span></div>' +
'<div class="event-desc">' + event.description + '</div>' +
'<div class="event-confidence">' + event.confidence + '</div>';
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.');
}