From fa32cf5ee79afd41c8a84502dec1e1d0b81cd0c2 Mon Sep 17 00:00:00 2001 From: jedarden Date: Sat, 11 Apr 2026 00:17:33 -0400 Subject: [PATCH] feat: implement post-feedback explanations for false positive detections - Add renderFeedbackExplanation() function to display detailed explanations - Include contributing link name, threshold exceed ratio, and timestamp - Add diagnostic info (root cause and advice) when available - Add expandable UI with toggle arrow - Add CSS styles for explanation section - Show correction note about system learning from feedback When user marks detection as FALSE_POSITIVE, show explanation: 'The system detected motion here because: [link]'s signal exceeded threshold by Nx at [time]. Could be caused by: [root cause or 'ambient RF interference']. We've noted this and will apply a correction.' Files: dashboard/js/feedback.js Acceptance: explanation shown after any FALSE_POSITIVE feedback; contains contributing link name; shows diagnostic result or default message. --- dashboard/js/feedback.js | 267 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 263 insertions(+), 4 deletions(-) diff --git a/dashboard/js/feedback.js b/dashboard/js/feedback.js index 4bd9b60..aab1f10 100644 --- a/dashboard/js/feedback.js +++ b/dashboard/js/feedback.js @@ -368,17 +368,27 @@ // Create inline response element var inline = document.createElement('div'); inline.className = 'feedback-inline-response feedback-inline-' + (response.type || 'info'); - inline.innerHTML = '\ + + var content = '\
' + this.escapeHTML(response.title || 'Feedback recorded') + '
\
' + this.escapeHTML(response.message || '') + '
\ - \ '; + // Add explanation section if available (for FALSE_POSITIVE feedback) + if (response.explainability && response.type === 'adjustment') { + content += this.renderFeedbackExplanation(response.explainability); + } + + content += ''; + + inline.innerHTML = content; + // Add to timeline or body var timeline = document.querySelector('.timeline-events') || document.body; timeline.insertBefore(inline, timeline.firstChild); - // Auto-dismiss after 8 seconds + // Auto-dismiss after 15 seconds for explanations (longer to read) + var dismissTime = response.explainability ? 15000 : 8000; setTimeout(function() { if (inline.parentNode) { inline.classList.add('feedback-inline-fadeout'); @@ -388,7 +398,119 @@ } }, 500); } - }, 8000); + }, dismissTime); + }, + + /** + * Render the feedback explanation for FALSE_POSITIVE detections + */ + renderFeedbackExplanation: function(explainability) { + var contributingLinks = explainability.contributing_links || []; + var primaryLink = contributingLinks.length > 0 ? contributingLinks[0] : null; + var diagnosis = explainability.diagnosis || null; + + var explanationHTML = '\ +
\ +
\ + ?\ + Why did this happen?\ + \ +
\ +
\ + '; + + if (primaryLink) { + var linkName = this.formatLinkID(primaryLink.link_id); + var deltaRMS = primaryLink.delta_rms ? primaryLink.delta_rms.toFixed(4) : 'N/A'; + var threshold = 0.02; // Standard threshold + var ratio = primaryLink.delta_rms ? (primaryLink.delta_rms / threshold).toFixed(1) : 'N/A'; + var timestamp = explainability.timestamp_ms ? new Date(explainability.timestamp_ms).toLocaleTimeString() : 'unknown'; + + explanationHTML += '\ +

\ + The system detected motion here because:\ + ' + linkName + '\'s signal (deltaRMS: ' + deltaRMS + ') exceeded the motion threshold by\ + ' + ratio + 'x at ' + timestamp + '.\ +

\ + '; + + // Add diagnostic info if available + if (diagnosis) { + explanationHTML += '\ +
\ + Possible cause:\ + ' + this.escapeHTML(diagnosis.detail || diagnosis.title || 'Ambient RF interference') + '\ + '; + + if (diagnosis.advice) { + explanationHTML += '\ +
\ + What to do:\ + ' + this.escapeHTML(diagnosis.advice) + '\ +
\ + '; + } + + explanationHTML += '
'; + } else { + explanationHTML += '\ +

\ + Possible cause: Ambient RF interference or environmental changes.\ +

\ + '; + } + + // Add additional contributing links if any + if (contributingLinks.length > 1) { + var linkNames = contributingLinks.slice(1).map(function(l) { + return this.formatLinkID(l.link_id); + }.bind(this)).join(', '); + + explanationHTML += '\ + \ + '; + } + + // Add correction note + explanationHTML += '\ +

\ + We\'ve noted this feedback and will apply corrections to improve future detection accuracy.\ +

\ + '; + } else { + explanationHTML += '\ +

\ + The system detected motion based on signal patterns across multiple links.\ + We\'ve noted this feedback to improve accuracy.\ +

\ + '; + } + + explanationHTML += '\ +
\ +
\ + '; + + return explanationHTML; + }, + + /** + * Format a link ID for display + */ + formatLinkID: function(linkID) { + if (!linkID) { + return 'Unknown Link'; + } + // Format MAC address pairs nicely + var parts = linkID.split(':'); + if (parts.length === 2) { + var mac1 = parts[0].substring(0, 8); // First 8 chars of first MAC + var mac2 = parts[1].substring(0, 8); // First 8 chars of second MAC + return mac1 + ' → ' + mac2; + } + return linkID.substring(0, 16) + '...'; }, /** @@ -753,6 +875,143 @@ margin: 0;\ padding: 0;\ }\ + }\ + .feedback-explanation {\ + margin-top: 12px;\ + padding-top: 12px;\ + border-top: 1px solid rgba(255, 255, 255, 0.1);\ + }\ + .explanation-toggle {\ + display: flex;\ + align-items: center;\ + gap: 8px;\ + cursor: pointer;\ + user-select: none;\ + padding: 6px 8px;\ + background: rgba(255, 255, 255, 0.05);\ + border-radius: 4px;\ + transition: background 0.2s;\ + }\ + .explanation-toggle:hover {\ + background: rgba(255, 255, 255, 0.08);\ + }\ + .explanation-icon {\ + display: flex;\ + align-items: center;\ + justify-content: center;\ + width: 20px;\ + height: 20px;\ + background: rgba(79, 195, 247, 0.2);\ + color: #4fc3f7;\ + border-radius: 50%;\ + font-weight: 600;\ + font-size: 12px;\ + }\ + .explanation-label {\ + flex: 1;\ + font-size: 12px;\ + font-weight: 500;\ + color: #4fc3f7;\ + }\ + .explanation-arrow {\ + font-size: 10px;\ + color: #888;\ + transition: transform 0.2s;\ + }\ + .explanation-toggle.expanded .explanation-arrow {\ + transform: rotate(180deg);\ + }\ + .explanation-content {\ + display: none;\ + margin-top: 8px;\ + padding: 10px;\ + background: rgba(79, 195, 247, 0.05);\ + border-radius: 4px;\ + font-size: 11px;\ + }\ + .explanation-toggle.expanded + .explanation-content {\ + display: block;\ + animation: explanationSlideIn 0.2s ease-out;\ + }\ + @keyframes explanationSlideIn {\ + from {\ + opacity: 0;\ + max-height: 0;\ + }\ + to {\ + opacity: 1;\ + max-height: 500px;\ + }\ + }\ + .explanation-text {\ + color: #bbb;\ + line-height: 1.4;\ + margin: 0 0 8px 0;\ + }\ + .explanation-text strong {\ + color: #4fc3f7;\ + font-weight: 600;\ + }\ + .explanation-root-cause {\ + color: #bbb;\ + line-height: 1.4;\ + margin: 0 0 8px 0;\ + }\ + .explanation-root-cause strong {\ + color: #ffa726;\ + }\ + .explanation-diagnosis {\ + margin: 8px 0;\ + padding: 8px;\ + background: rgba(255, 167, 38, 0.08);\ + border-left: 2px solid #ffa726;\ + border-radius: 3px;\ + }\ + .diagnosis-label {\ + display: block;\ + font-size: 10px;\ + text-transform: uppercase;\ + color: #ffa726;\ + font-weight: 600;\ + margin-bottom: 3px;\ + }\ + .diagnosis-detail {\ + display: block;\ + color: #ccc;\ + line-height: 1.3;\ + margin-bottom: 4px;\ + }\ + .diagnosis-advice {\ + margin-top: 6px;\ + padding-top: 6px;\ + border-top: 1px solid rgba(255, 255, 255, 0.1);\ + }\ + .advice-label {\ + display: block;\ + font-size: 10px;\ + text-transform: uppercase;\ + color: #4caf50;\ + font-weight: 600;\ + margin-bottom: 2px;\ + }\ + .advice-text {\ + color: #bbb;\ + line-height: 1.3;\ + }\ + .explanation-additional-links {\ + margin: 8px 0;\ + padding-top: 8px;\ + border-top: 1px solid rgba(255, 255, 255, 0.1);\ + font-size: 10px;\ + color: #888;\ + }\ + .explanation-correction {\ + margin: 8px 0 0 0;\ + padding: 6px 8px;\ + background: rgba(76, 175, 80, 0.08);\ + border-radius: 3px;\ + font-size: 10px;\ + color: #a5d6a7;\ }'; document.head.appendChild(style); }