782 lines
30 KiB
JavaScript
782 lines
30 KiB
JavaScript
// 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) {
|
||
// 更新状态文本
|
||
if (data.scheduler.running) {
|
||
document.getElementById('scheduler-status').innerHTML = '状态: <span style="color:#4CAF50;">运行中</span>';
|
||
} else {
|
||
document.getElementById('scheduler-status').innerHTML = '状态: <span style="color:#f44336;">已停止</span>';
|
||
}
|
||
|
||
// 更新切换按钮状态
|
||
updateToggleButton(data.scheduler.running);
|
||
|
||
document.getElementById('capture-count').textContent = '拍照次数: ' + 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;
|
||
|
||
// 事件类型筛选
|
||
var eventFilter = document.getElementById('event-filter');
|
||
eventFilter.innerHTML = '<option value="all">全部类型</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 toggleScheduler() {
|
||
var btn = document.getElementById('toggle-btn');
|
||
|
||
if (btn.classList.contains('stopped')) {
|
||
// 当前已停止,启动
|
||
fetch('/api/scheduler/start', {method: 'POST'})
|
||
.then(function(res) { return res.json(); })
|
||
.then(function(data) {
|
||
if (data.success) {
|
||
btn.classList.remove('stopped');
|
||
btn.classList.add('running');
|
||
btn.innerHTML = '▶ 运行中';
|
||
btn.style.background = '#4CAF50';
|
||
showToast('已启动!', 1500);
|
||
refreshAll();
|
||
} else {
|
||
showToast('启动失败: ' + data.error, 3000);
|
||
}
|
||
})
|
||
.catch(function(e) { showToast('错误: ' + e.message, 3000); });
|
||
} else {
|
||
// 当前运行中,停止
|
||
fetch('/api/scheduler/stop', {method: 'POST'})
|
||
.then(function(res) { return res.json(); })
|
||
.then(function(data) {
|
||
if (data.success) {
|
||
btn.classList.remove('running');
|
||
btn.classList.add('stopped');
|
||
btn.innerHTML = '⏹ 已停止';
|
||
btn.style.background = '#f44336';
|
||
showToast('已停止!', 1500);
|
||
refreshAll();
|
||
}
|
||
})
|
||
.catch(function(e) { showToast('错误: ' + e.message, 3000); });
|
||
}
|
||
}
|
||
|
||
function updateToggleButton(running) {
|
||
var btn = document.getElementById('toggle-btn');
|
||
|
||
if (running) {
|
||
btn.classList.remove('stopped');
|
||
btn.classList.add('running');
|
||
btn.innerHTML = '▶ 运行中';
|
||
btn.style.background = '#4CAF50';
|
||
} else {
|
||
btn.classList.remove('running');
|
||
btn.classList.add('stopped');
|
||
btn.innerHTML = '⏹ 已停止';
|
||
btn.style.background = '#f44336';
|
||
}
|
||
}
|
||
|
||
function captureNow() {
|
||
fetch('/api/capture', {method: 'POST'})
|
||
.then(function(res) { return res.json(); })
|
||
.then(function(data) {
|
||
if (data.success) {
|
||
showToast('拍照成功!ID: ' + data.image_id, 1500);
|
||
refreshAll();
|
||
} else {
|
||
showToast('拍照失败: ' + data.error, 3000);
|
||
}
|
||
})
|
||
.catch(function(e) { showToast('错误: ' + e.message, 3000); });
|
||
}
|
||
|
||
function analyzeUnanalyzed() {
|
||
fetch('/api/analyze/unanalyzed', {method: 'POST'})
|
||
.then(function(res) { return res.json(); })
|
||
.then(function(data) {
|
||
showToast('分析了 ' + data.results.length + ' 张图片', 1500);
|
||
refreshAll();
|
||
})
|
||
.catch(function(e) { showToast('错误: ' + 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';
|
||
|
||
// Check for person indices in events
|
||
var personIndices = [];
|
||
if (img.events && img.events.length > 0) {
|
||
img.events.forEach(function(event) {
|
||
var match = event.description.match(/#(\d+)/g);
|
||
if (match) {
|
||
match.forEach(function(m) {
|
||
if (personIndices.indexOf(m) === -1) {
|
||
personIndices.push(m);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
var indicesDisplay = personIndices.length > 0 ?
|
||
'<span class="image-person-indices">' + personIndices.slice(0, 3).join(' ') + '</span>' : '';
|
||
|
||
item.innerHTML = '<span class="image-number">#' + img.id + '</span>' +
|
||
'<span class="image-time">' + time + '</span>' +
|
||
'<span class="image-status">' + status + '</span>' +
|
||
indicesDisplay +
|
||
'<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';
|
||
|
||
// 显示人员序号
|
||
var personIndices = [];
|
||
localEvents.forEach(function(event) {
|
||
if (event.person_index) {
|
||
personIndices.push('#' + event.person_index);
|
||
}
|
||
});
|
||
|
||
var indicesDisplay = personIndices.length > 0 ?
|
||
' <span class="person-indices">[' + personIndices.join(', ') + ']</span>' : '';
|
||
|
||
localSection.innerHTML = '<h4 class="section-title local">Local Analysis (' + localEvents.length + ')' + indicesDisplay + '</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');
|
||
}
|
||
|
||
// Stats Management
|
||
function openStatsModal() {
|
||
loadDailyStats();
|
||
loadHistoryStats();
|
||
document.getElementById('stats-modal').classList.add('active');
|
||
}
|
||
|
||
function closeStatsModal() {
|
||
document.getElementById('stats-modal').classList.remove('active');
|
||
}
|
||
|
||
function loadDailyStats() {
|
||
var dateSelect = document.getElementById('stats-date-select');
|
||
var dateValue = dateSelect.value;
|
||
|
||
var targetDate;
|
||
if (dateValue === 'yesterday') {
|
||
var yesterday = new Date();
|
||
yesterday.setDate(yesterday.getDate() - 1);
|
||
targetDate = yesterday.toISOString().split('T')[0];
|
||
} else {
|
||
targetDate = new Date().toISOString().split('T')[0];
|
||
}
|
||
|
||
fetch(API_BASE + '/api/stats/daily?date=' + targetDate)
|
||
.then(function(res) { return res.json(); })
|
||
.then(function(data) {
|
||
renderDailyStats(data);
|
||
})
|
||
.catch(function(e) { console.error('Load stats failed:', e); });
|
||
}
|
||
|
||
function renderDailyStats(data) {
|
||
// Summary
|
||
var summaryDiv = document.getElementById('stats-summary');
|
||
summaryDiv.innerHTML = '<div class="stats-summary">' +
|
||
'<span>Date: ' + data.date + '</span>' +
|
||
'<span>Images: ' + data.total_images + '</span>' +
|
||
'<span>Events: ' + data.total_events + '</span>' +
|
||
'</div>';
|
||
|
||
// Person count timeline chart (using simple CSS bars)
|
||
var timeline = data.timeline;
|
||
var chartCanvas = document.getElementById('person-chart');
|
||
|
||
if (timeline.length === 0) {
|
||
chartCanvas.innerHTML = '<p style="color:#888;text-align:center;">No data</p>';
|
||
return;
|
||
}
|
||
|
||
// Build simple bar chart
|
||
var chartHtml = '<div class="timeline-chart">';
|
||
|
||
var maxCount = Math.max.apply(Math, timeline.map(function(t) { return t.person_count || 0 }));
|
||
maxCount = Math.max(maxCount, 1);
|
||
|
||
timeline.slice(-20).forEach(function(t) {
|
||
var timeLabel = new Date(t.time).toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit'});
|
||
var count = t.person_count || 0;
|
||
var height = (count / maxCount) * 100;
|
||
|
||
chartHtml += '<div class="timeline-bar">' +
|
||
'<div class="bar-fill" style="height:' + height + '%"></div>' +
|
||
'<div class="bar-label">' + timeLabel + '</div>' +
|
||
'<div class="bar-value">' + count + '</div>' +
|
||
'</div>';
|
||
});
|
||
|
||
chartHtml += '</div>';
|
||
chartCanvas.innerHTML = chartHtml;
|
||
|
||
// Event types distribution
|
||
var eventTypesDiv = document.getElementById('event-types-chart');
|
||
eventTypesDiv.innerHTML = '';
|
||
|
||
if (data.event_types.length === 0) {
|
||
eventTypesDiv.innerHTML = '<p style="color:#888;">No events</p>';
|
||
return;
|
||
}
|
||
|
||
var maxEventCount = Math.max.apply(Math, data.event_types.map(function(e) { return e.count }));
|
||
|
||
data.event_types.forEach(function(et) {
|
||
var width = (et.count / maxEventCount) * 100;
|
||
var barHtml = '<div class="event-type-bar">' +
|
||
'<span class="event-type-label">' + et.type + '</span>' +
|
||
'<div class="event-type-fill" style="width:' + width + '%">' +
|
||
'<span class="event-type-count">' + et.count + '</span>' +
|
||
'</div></div>';
|
||
eventTypesDiv.innerHTML += barHtml;
|
||
});
|
||
}
|
||
|
||
function loadHistoryStats() {
|
||
fetch(API_BASE + '/api/stats/history?days=7')
|
||
.then(function(res) { return res.json(); })
|
||
.then(function(data) {
|
||
renderHistoryStats(data);
|
||
})
|
||
.catch(function(e) { console.error('Load history failed:', e); });
|
||
}
|
||
|
||
function renderHistoryStats(data) {
|
||
var historyDiv = document.getElementById('history-chart');
|
||
|
||
if (data.history.length === 0) {
|
||
historyDiv.innerHTML = '<p style="color:#888;">No history data</p>';
|
||
return;
|
||
}
|
||
|
||
var maxImages = Math.max.apply(Math, data.history.map(function(h) { return h.total_images }));
|
||
maxImages = Math.max(maxImages, 1);
|
||
|
||
var html = '<div class="history-chart">';
|
||
|
||
data.history.reverse().forEach(function(h) {
|
||
var dateLabel = h.date.slice(5); // MM-DD
|
||
var height = (h.total_images / maxImages) * 100;
|
||
|
||
html += '<div class="history-bar">' +
|
||
'<div class="bar-fill images" style="height:' + height + '%"></div>' +
|
||
'<div class="bar-label">' + dateLabel + '</div>' +
|
||
'<div class="bar-value">' + h.total_images + '</div>' +
|
||
'</div>';
|
||
});
|
||
|
||
html += '</div>';
|
||
historyDiv.innerHTML = html;
|
||
}
|
||
|
||
// Persons Management
|
||
function openPersonsModal() {
|
||
loadPersonsList();
|
||
document.getElementById('persons-modal').classList.add('active');
|
||
}
|
||
|
||
function closePersonsModal() {
|
||
document.getElementById('persons-modal').classList.remove('active');
|
||
}
|
||
|
||
function loadPersonsList() {
|
||
fetch(API_BASE + '/api/persons')
|
||
.then(function(res) { return res.json(); })
|
||
.then(function(data) {
|
||
var statsDiv = document.getElementById('persons-stats');
|
||
var listDiv = document.getElementById('persons-list');
|
||
|
||
// 统计信息
|
||
statsDiv.innerHTML = '<div class="stats-summary">' +
|
||
'<span>Total: ' + data.stats.total_persons + '</span>' +
|
||
'<span>Detected: ' + data.stats.total_detections + '</span>' +
|
||
'<span>Known: ' + data.stats.known_persons_detected + '</span>' +
|
||
'<span>New: ' + data.stats.new_persons_added + '</span>' +
|
||
'</div>';
|
||
|
||
// 人员列表
|
||
if (data.persons.length === 0) {
|
||
listDiv.innerHTML = '<p style="color:#888;text-align:center;">No persons recorded yet</p>';
|
||
return;
|
||
}
|
||
|
||
listDiv.innerHTML = '';
|
||
data.persons.forEach(function(person) {
|
||
var item = document.createElement('div');
|
||
item.className = 'person-item';
|
||
|
||
var firstSeen = new Date(person.first_seen).toLocaleString();
|
||
var lastSeen = new Date(person.last_seen).toLocaleString();
|
||
|
||
// 构建人员图片URL
|
||
var faceImgHtml = '<div class="person-face-placeholder">No Image</div>';
|
||
if (person.face_path) {
|
||
faceImgHtml = '<img class="person-face-img" src="/api/persons/' + person.person_id + '/face" alt="' + person.name + '" onerror="this.parentElement.innerHTML=\'<div class="person-face-placeholder">No Image</div>\'">';
|
||
}
|
||
|
||
item.innerHTML = '<div class="person-face">' + faceImgHtml + '</div>' +
|
||
'<div class="person-content">' +
|
||
'<div class="person-info">' +
|
||
'<span class="person-name">' + person.name + '</span>' +
|
||
'<span class="person-id">' + person.person_id + '</span>' +
|
||
'</div>' +
|
||
'<div class="person-stats">' +
|
||
'<span>Visits: ' + person.visit_count + '</span>' +
|
||
'<span>First: ' + firstSeen + '</span>' +
|
||
'<span>Last: ' + lastSeen + '</span>' +
|
||
'</div>' +
|
||
'<div class="person-actions">' +
|
||
'<button onclick="renamePerson(\'' + person.person_id + '\')" class="btn-small">Rename</button>' +
|
||
'<button onclick="deletePerson(\'' + person.person_id + '\')" class="btn-small btn-danger">Delete</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
|
||
listDiv.appendChild(item);
|
||
});
|
||
})
|
||
.catch(function(e) { console.error('Load persons failed:', e); });
|
||
}
|
||
|
||
function renamePerson(personId) {
|
||
var newName = prompt('Enter new name:');
|
||
if (!newName) return;
|
||
|
||
fetch(API_BASE + '/api/persons/' + personId + '/rename?name=' + encodeURIComponent(newName), {
|
||
method: 'POST'
|
||
})
|
||
.then(function(res) { return res.json(); })
|
||
.then(function(data) {
|
||
if (data.success) {
|
||
showToast('Renamed to ' + newName, 1500);
|
||
loadPersonsList();
|
||
}
|
||
})
|
||
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
|
||
}
|
||
|
||
function deletePerson(personId) {
|
||
if (!confirm('Delete this person?')) return;
|
||
|
||
fetch(API_BASE + '/api/persons/' + personId, {method: 'DELETE'})
|
||
.then(function(res) { return res.json(); })
|
||
.then(function(data) {
|
||
if (data.success) {
|
||
showToast('Person deleted', 1500);
|
||
loadPersonsList();
|
||
}
|
||
})
|
||
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
|
||
}
|
||
|
||
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;
|
||
|
||
// Person Detection (YOLO)
|
||
document.getElementById('setting-use-yolo').checked = config.use_yolo !== false;
|
||
document.getElementById('setting-yolo-confidence').value = config.yolo_min_confidence || 0.3;
|
||
|
||
// Person Identification
|
||
document.getElementById('setting-use-face-rec').checked = config.use_face_recognition !== false;
|
||
document.getElementById('setting-use-mediapipe').checked = config.use_mediapipe_face !== false;
|
||
document.getElementById('setting-use-color-hist').checked = config.use_color_histogram !== false;
|
||
document.getElementById('setting-match-threshold').value = config.face_match_threshold || 0.6;
|
||
|
||
// Confirmation Settings
|
||
document.getElementById('setting-confirm-frames').value = config.confirm_frames || 3;
|
||
document.getElementById('setting-leave-frames').value = config.leave_frames || 2;
|
||
|
||
// Vision API settings
|
||
document.getElementById('setting-use-vision-api').checked = config.use_vision_api === true;
|
||
document.getElementById('setting-vision-trigger').value = config.vision_api_trigger || 'person_change';
|
||
|
||
// API config
|
||
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),
|
||
|
||
// Person Detection (YOLO)
|
||
use_yolo: document.getElementById('setting-use-yolo').checked,
|
||
yolo_min_confidence: parseFloat(document.getElementById('setting-yolo-confidence').value),
|
||
|
||
// Person Identification
|
||
use_face_recognition: document.getElementById('setting-use-face-rec').checked,
|
||
use_mediapipe_face: document.getElementById('setting-use-mediapipe').checked,
|
||
use_color_histogram: document.getElementById('setting-use-color-hist').checked,
|
||
face_match_threshold: parseFloat(document.getElementById('setting-match-threshold').value),
|
||
|
||
// Confirmation Settings
|
||
confirm_frames: parseInt(document.getElementById('setting-confirm-frames').value),
|
||
leave_frames: parseInt(document.getElementById('setting-leave-frames').value),
|
||
|
||
// Vision API
|
||
use_vision_api: document.getElementById('setting-use-vision-api').checked,
|
||
vision_api_trigger: document.getElementById('setting-vision-trigger').value,
|
||
|
||
// API config
|
||
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) {
|
||
showToast('Settings saved!', 1500);
|
||
closeSettingsModal();
|
||
loadConfig();
|
||
refreshAll();
|
||
}
|
||
})
|
||
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
|
||
}
|
||
|
||
function browseImagesDir() {
|
||
alert('Please enter the path manually.\nExample: D:\\vision-images\nImages will be saved to date subfolders automatically.');
|
||
} |