/** * Feedback UI Components for Detection Accuracy * Provides thumbs-up/down buttons, feedback forms, and missed detection reporting */ (function() { 'use strict'; var Feedback = { // State pendingFeedback: null, feedbackPanelVisible: false, // Event types EventTypes: { BLOB_DETECTION: 'blob_detection', ZONE_TRANSITION: 'zone_transition', FALL_ALERT: 'fall_alert', ANOMALY: 'anomaly' }, // Feedback types FeedbackTypes: { TRUE_POSITIVE: 'TRUE_POSITIVE', FALSE_POSITIVE: 'FALSE_POSITIVE', FALSE_NEGATIVE: 'FALSE_NEGATIVE', WRONG_IDENTITY: 'WRONG_IDENTITY', WRONG_ZONE: 'WRONG_ZONE' }, /** * Initialize the feedback module */ init: function() { this.createFeedbackPanel(); this.createMissedDetectionButton(); console.log('[Feedback] Module initialized'); }, /** * Create the feedback panel (hidden by default) */ createFeedbackPanel: function() { var panel = document.createElement('div'); panel.id = 'feedback-panel'; panel.className = 'feedback-panel'; panel.style.display = 'none'; panel.innerHTML = '\
\ Report Feedback\ \
\
\
\ \ \
\
\ \ \ \ \
\
'; document.body.appendChild(panel); // Add styles this.addStyles(); }, /** * Create the "Report missed detection" button */ createMissedDetectionButton: function() { var btn = document.createElement('button'); btn.id = 'missed-detection-btn'; btn.className = 'missed-detection-btn'; btn.innerHTML = '⚠ Report missed detection'; btn.onclick = function() { Feedback.showMissedDetectionForm(); }; document.body.appendChild(btn); }, /** * Show feedback panel for a specific event (thumbs-down clicked) */ showFeedbackPanel: function(eventID, eventType, eventTime, details) { this.pendingFeedback = { eventID: eventID, eventType: eventType, eventTime: eventTime, details: details || {} }; var panel = document.getElementById('feedback-panel'); if (!panel) return; // Update event info panel.querySelector('.feedback-event-type').textContent = this.formatEventType(eventType); panel.querySelector('.feedback-event-time').textContent = eventTime ? new Date(eventTime).toLocaleString() : 'Now'; // Reset form var radios = panel.querySelectorAll('input[name="feedback-type"]'); radios.forEach(function(r) { r.checked = false; }); document.getElementById('feedback-notes').value = ''; // Show panel panel.style.display = 'block'; this.feedbackPanelVisible = true; }, /** * Hide the feedback panel */ hideFeedbackPanel: function() { var panel = document.getElementById('feedback-panel'); if (panel) { panel.style.display = 'none'; } this.pendingFeedback = null; this.feedbackPanelVisible = false; }, /** * Submit feedback to the server */ submitFeedback: function() { if (!this.pendingFeedback) return; var panel = document.getElementById('feedback-panel'); var selected = panel.querySelector('input[name="feedback-type"]:checked'); if (!selected) { alert('Please select what was wrong with this detection.'); return; } var feedbackType = selected.value; var notes = document.getElementById('feedback-notes').value; var details = Object.assign({}, this.pendingFeedback.details); if (notes) { details.notes = notes; } this.sendFeedback( this.pendingFeedback.eventID, this.pendingFeedback.eventType, feedbackType, details ); this.hideFeedbackPanel(); }, /** * Submit thumbs-up (true positive) feedback */ submitThumbsUp: function(eventID, eventType, details) { this.sendFeedback(eventID, eventType, this.FeedbackTypes.TRUE_POSITIVE, details || {}); }, /** * Send feedback to the API */ sendFeedback: function(eventID, eventType, feedbackType, details) { var data = { event_id: eventID || '', event_type: eventType, feedback_type: feedbackType, details: details }; fetch('/api/learning/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(function(res) { return res.json(); }) .then(function(result) { if (result.success) { console.log('[Feedback] Submitted:', feedbackType, 'for event', eventID); if (window.SpaxelApp && window.SpaxelApp.showToast) { window.SpaxelApp.showToast('Thank you for your feedback!', 'success'); } } }) .catch(function(err) { console.error('[Feedback] Failed to submit:', err); }); }, /** * Show missed detection form */ showMissedDetectionForm: function() { var modal = document.createElement('div'); modal.id = 'missed-detection-modal'; modal.className = 'missed-detection-modal'; modal.innerHTML = '\
\
\

Report Missed Detection

\ \
\
\
\ \ \
\
\ \ \
\
\ \
\ \ \ \
\
\
\ \ \
\
\ \ \
\
\
'; document.body.appendChild(modal); // Set default time to now var timeInput = document.getElementById('missed-time'); var now = new Date(); timeInput.value = now.toISOString().slice(0, 16); // Populate zones this.populateZoneSelector(document.getElementById('missed-zone')); }, /** * Close the missed detection modal */ closeMissedDetectionModal: function() { var modal = document.getElementById('missed-detection-modal'); if (modal) { modal.remove(); } }, /** * Submit missed detection report */ submitMissedDetection: function() { var timeStr = document.getElementById('missed-time').value; var zoneID = document.getElementById('missed-zone').value; var posX = parseFloat(document.getElementById('missed-x').value) || 0; var posY = parseFloat(document.getElementById('missed-y').value) || 0; var posZ = parseFloat(document.getElementById('missed-z').value) || 0; var notes = document.getElementById('missed-notes').value; var details = { zone_id: zoneID, position_x: posX, position_y: posY, position_z: posZ, user_reported: true }; if (notes) { details.notes = notes; } this.sendFeedback( '', // No event ID for missed detections this.EventTypes.BLOB_DETECTION, this.FeedbackTypes.FALSE_NEGATIVE, details ); this.closeMissedDetectionModal(); }, /** * Populate zone selector from API */ populateZoneSelector: function(select) { if (!select) return; fetch('/api/zones') .then(function(res) { return res.json(); }) .then(function(zones) { zones = zones || []; zones.forEach(function(zone) { var opt = document.createElement('option'); opt.value = zone.id; opt.textContent = zone.name || zone.id; select.appendChild(opt); }); }) .catch(function(err) { console.error('[Feedback] Failed to load zones:', err); }); }, /** * Create thumbs up/down buttons for an event */ createFeedbackButtons: function(eventID, eventType, eventTime, details) { var container = document.createElement('div'); container.className = 'feedback-buttons'; var thumbsUp = document.createElement('button'); thumbsUp.className = 'feedback-btn-icon feedback-thumbs-up'; thumbsUp.innerHTML = '👍'; thumbsUp.title = 'Correct detection'; thumbsUp.onclick = function(e) { e.stopPropagation(); Feedback.submitThumbsUp(eventID, eventType, details); }; var thumbsDown = document.createElement('button'); thumbsDown.className = 'feedback-btn-icon feedback-thumbs-down'; thumbsDown.innerHTML = '👎'; thumbsDown.title = 'Incorrect detection'; thumbsDown.onclick = function(e) { e.stopPropagation(); Feedback.showFeedbackPanel(eventID, eventType, eventTime, details); }; container.appendChild(thumbsUp); container.appendChild(thumbsDown); return container; }, /** * Format event type for display */ formatEventType: function(eventType) { var types = { 'blob_detection': 'Detection', 'zone_transition': 'Zone Change', 'fall_alert': 'Fall Alert', 'anomaly': 'Anomaly' }; return types[eventType] || eventType; }, /** * Add CSS styles for feedback UI */ addStyles: function() { if (document.getElementById('feedback-styles')) return; var style = document.createElement('style'); style.id = 'feedback-styles'; style.textContent = '\ .feedback-panel {\ position: fixed;\ bottom: 100px;\ right: 20px;\ width: 320px;\ background: rgba(0, 0, 0, 0.95);\ border-radius: 8px;\ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);\ z-index: 200;\ font-size: 13px;\ }\ .feedback-header {\ display: flex;\ justify-content: space-between;\ align-items: center;\ padding: 12px 16px;\ border-bottom: 1px solid rgba(255, 255, 255, 0.1);\ }\ .feedback-title {\ font-weight: 600;\ color: #eee;\ }\ .feedback-close {\ background: none;\ border: none;\ color: #888;\ font-size: 20px;\ cursor: pointer;\ }\ .feedback-close:hover { color: #fff; }\ .feedback-content {\ padding: 16px;\ }\ .feedback-event-info {\ display: flex;\ justify-content: space-between;\ margin-bottom: 12px;\ font-size: 11px;\ color: #888;\ }\ .feedback-question {\ font-size: 13px;\ color: #ccc;\ margin-bottom: 10px;\ }\ .feedback-options {\ display: flex;\ flex-direction: column;\ gap: 8px;\ margin-bottom: 16px;\ }\ .feedback-option {\ display: flex;\ align-items: center;\ gap: 8px;\ cursor: pointer;\ padding: 6px 8px;\ border-radius: 4px;\ transition: background 0.2s;\ }\ .feedback-option:hover {\ background: rgba(255, 255, 255, 0.05);\ }\ .feedback-option input {\ margin: 0;\ }\ .feedback-option span {\ color: #bbb;\ font-size: 12px;\ }\ .feedback-notes {\ margin-bottom: 16px;\ }\ .feedback-notes label {\ display: block;\ font-size: 11px;\ color: #888;\ margin-bottom: 4px;\ }\ .feedback-notes textarea {\ width: 100%;\ height: 60px;\ background: rgba(255, 255, 255, 0.08);\ border: 1px solid rgba(255, 255, 255, 0.15);\ border-radius: 4px;\ color: #eee;\ font-size: 12px;\ padding: 8px;\ resize: none;\ box-sizing: border-box;\ }\ .feedback-actions {\ display: flex;\ justify-content: flex-end;\ gap: 8px;\ }\ .feedback-btn {\ padding: 6px 14px;\ border-radius: 4px;\ font-size: 12px;\ cursor: pointer;\ border: none;\ }\ .feedback-btn-cancel {\ background: rgba(255, 255, 255, 0.1);\ color: #ccc;\ }\ .feedback-btn-submit {\ background: #4fc3f7;\ color: #1a1a2e;\ font-weight: 500;\ }\ .feedback-btn-icon {\ background: rgba(255, 255, 255, 0.1);\ border: none;\ width: 28px;\ height: 28px;\ border-radius: 4px;\ cursor: pointer;\ font-size: 14px;\ display: flex;\ align-items: center;\ justify-content: center;\ transition: background 0.2s;\ }\ .feedback-btn-icon:hover {\ background: rgba(255, 255, 255, 0.2);\ }\ .feedback-thumbs-up:hover {\ background: rgba(76, 175, 80, 0.3);\ }\ .feedback-thumbs-down:hover {\ background: rgba(244, 67, 54, 0.3);\ }\ .feedback-buttons {\ display: inline-flex;\ gap: 4px;\ }\ .missed-detection-btn {\ position: fixed;\ bottom: 20px;\ right: 440px;\ background: rgba(255, 167, 38, 0.2);\ border: 1px solid rgba(255, 167, 38, 0.5);\ color: #ffa726;\ padding: 6px 12px;\ border-radius: 4px;\ font-size: 11px;\ cursor: pointer;\ z-index: 100;\ transition: background 0.2s;\ }\ .missed-detection-btn:hover {\ background: rgba(255, 167, 38, 0.3);\ }\ .missed-detection-modal {\ position: fixed;\ top: 0;\ left: 0;\ right: 0;\ bottom: 0;\ background: rgba(0, 0, 0, 0.8);\ display: flex;\ align-items: center;\ justify-content: center;\ z-index: 300;\ }\ .missed-detection-card {\ background: #1e1e3a;\ border-radius: 12px;\ padding: 24px;\ width: 400px;\ max-width: 90%;\ }\ .missed-detection-header {\ display: flex;\ justify-content: space-between;\ align-items: center;\ margin-bottom: 16px;\ }\ .missed-detection-header h2 {\ font-size: 16px;\ color: #eee;\ margin: 0;\ }\ .modal-close {\ background: none;\ border: none;\ color: #888;\ font-size: 24px;\ cursor: pointer;\ }\ .modal-close:hover { color: #fff; }\ .missed-detection-form .form-group {\ margin-bottom: 14px;\ }\ .missed-detection-form label {\ display: block;\ font-size: 12px;\ color: #888;\ margin-bottom: 4px;\ }\ .missed-detection-form input,\ .missed-detection-form select,\ .missed-detection-form textarea {\ width: 100%;\ padding: 8px 10px;\ background: rgba(255, 255, 255, 0.08);\ border: 1px solid rgba(255, 255, 255, 0.15);\ border-radius: 4px;\ color: #eee;\ font-size: 13px;\ box-sizing: border-box;\ }\ .position-inputs {\ display: flex;\ gap: 8px;\ }\ .position-inputs input {\ flex: 1;\ }\ .missed-detection-form textarea {\ height: 60px;\ resize: none;\ }\ .form-actions {\ display: flex;\ justify-content: flex-end;\ gap: 10px;\ margin-top: 20px;\ }\ .btn {\ padding: 8px 16px;\ border-radius: 4px;\ font-size: 13px;\ cursor: pointer;\ border: none;\ }\ .btn-secondary {\ background: rgba(255, 255, 255, 0.1);\ color: #ccc;\ }\ .btn-primary {\ background: #4fc3f7;\ color: #1a1a2e;\ font-weight: 500;\ }'; document.head.appendChild(style); } }; // Expose globally window.Feedback = Feedback; // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { Feedback.init(); }); } else { Feedback.init(); } })();