spaxel/dashboard/js/tooltip.js
jedarden b583990d43 feat: implement guided troubleshooting with proactive contextual help
Implemented Component 36 of the plan, providing proactive contextual help
and post-feedback explanations for Spaxel users.

Backend (Go):
- FleetNotifier: Track node offline events with 2-hour threshold
- EditTracker: Monitor repeated settings changes for hint triggers
- ZoneQualityTracker: Detect degraded detection quality (>24h below 60%)
- DiscoveryTracker: First-time feature discovery tooltips
- Manager: Coordinate all guided troubleshooting features with 5min checks
- API endpoints: /api/guided/* for issues, tooltips, feedback, calibration

Frontend (JavaScript):
- tooltip.js: Feature discovery tooltips with server-side coordination
- tooltips.js: Sequential tooltip tour manager for first-time users
- troubleshoot.js: Troubleshooting manager for quality/offline events
- guided-help.js: Step-by-step guidance content

Integration:
- Manager runs in background checking quality and node status
- Settings handler wired to edit tracker for repeated-edit hints
- Dashboard WebSocket events trigger proactive help
- All styling included for banners, cards, and tooltips

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 04:25:31 -04:00

202 lines
6.7 KiB
JavaScript

/**
* Spaxel First-Time Feature Discovery Toololtips
*
* Shows brief, non-intrusive tooltips when users access features for the first time.
* Tooltips are shown once per feature and remembered in localStorage.
*/
(function () {
'use strict';
// Feature IDs that trigger tooltips
var FEATURES = {
TRIGGER_VOLUMES: 'trigger_volumes',
COVERAGE_PAINTING: 'coverage_painting',
TIME_TRAVEL: 'time_travel',
FRESNEL_ZONES: 'fresnel_zones',
PERSON_IDENTITY: 'person_identity',
AUTOMATION_BUILDER: 'automation_builder',
};
// Cooldown duration (24 hours)
var COOLDOWN_MS = 24 * 60 * 60 * 1000;
/**
* Check if a tooltip should be shown for a feature.
* Returns a promise that resolves to {show: boolean, tooltip?: object}.
*/
function shouldShow(featureId) {
// Check localStorage cooldown
var cooldownKey = 'spaxel_tooltip_shown_' + featureId;
var lastShown = localStorage.getItem(cooldownKey);
if (lastShown) {
var elapsed = Date.now() - parseInt(lastShown, 10);
if (elapsed < COOLDOWN_MS) {
return Promise.resolve({ show: false });
}
}
// Check with server
return fetch('/api/guided/tooltip/' + featureId)
.then(function(res) {
if (!res.ok) {
if (res.status === 404) {
// No tooltip configured for this feature
return { show: false };
}
throw new Error('Failed to check tooltip: ' + res.status);
}
return res.json();
})
.then(function(data) {
return data.show ? { show: true, tooltip: data } : { show: false };
})
.catch(function(err) {
console.error('[Tooltip] Failed to check tooltip:', err);
return { show: false };
});
}
/**
* Mark a tooltip as shown (dismissed).
*/
function markShown(featureId) {
var cooldownKey = 'spaxel_tooltip_shown_' + featureId;
localStorage.setItem(cooldownKey, Date.now().toString());
// Also notify server
return fetch('/api/guided/tooltip/' + featureId + '/dismiss', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}).catch(function(err) {
console.error('[Tooltip] Failed to dismiss tooltip:', err);
});
}
/**
* Show a tooltip for a feature.
* @param {string} featureId - The feature ID
* @param {HTMLElement} target - The target element to anchor the tooltip to
* @param {string} position - Position: 'top', 'bottom', 'left', or 'right'
*/
function show(featureId, target, position) {
if (!target) return;
shouldShow(featureId).then(function(result) {
if (!result.show) return;
var tooltip = result.tooltip;
var el = createTooltipElement(tooltip, position);
positionTooltip(el, target, position || 'bottom');
// Auto-dismiss after 10 seconds or on click
var dismissTimer = setTimeout(function() {
dismiss(el, featureId);
}, 10000);
el.addEventListener('click', function() {
clearTimeout(dismissTimer);
dismiss(el, featureId);
});
});
}
/**
* Create the tooltip DOM element.
*/
function createTooltipElement(tooltip, position) {
var el = document.createElement('div');
el.className = 'spaxel-tooltip spaxel-tooltip-' + (position || 'bottom');
el.innerHTML =
'<div class="spaxel-tooltip-content">' +
'<div class="spaxel-tooltip-title">' + escapeHtml(tooltip.title || 'Tip') + '</div>' +
'<div class="spaxel-tooltip-text">' + escapeHtml(tooltip.description || '') + '</div>' +
'<div class="spaxel-tooltip-close"></div>' +
'</div>' +
'<div class="spaxel-tooltip-arrow spaxel-tooltip-arrow-' + (position || 'bottom') + '"></div>';
document.body.appendChild(el);
return el;
}
/**
* Position the tooltip relative to the target element.
*/
function positionTooltip(tooltip, target, position) {
var targetRect = target.getBoundingClientRect();
var tooltipRect = tooltip.getBoundingClientRect();
var top, left;
switch (position) {
case 'top':
top = targetRect.top - tooltipRect.height - 10;
left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
break;
case 'bottom':
top = targetRect.bottom + 10;
left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
break;
case 'left':
top = targetRect.top + (targetRect.height - tooltipRect.height) / 2;
left = targetRect.left - tooltipRect.width - 10;
break;
case 'right':
top = targetRect.top + (targetRect.height - tooltipRect.height) / 2;
left = targetRect.right + 10;
break;
default:
top = targetRect.bottom + 10;
left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
}
// Constrain to viewport
var viewportWidth = window.innerWidth;
var viewportHeight = window.innerHeight;
if (left < 10) left = 10;
if (left + tooltipRect.width > viewportWidth - 10) {
left = viewportWidth - tooltipRect.width - 10;
}
if (top < 10) top = 10;
if (top + tooltipRect.height > viewportHeight - 10) {
top = viewportHeight - tooltipRect.height - 10;
}
tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px';
}
/**
* Dismiss and remove the tooltip.
*/
function dismiss(el, featureId) {
el.classList.add('spaxel-tooltip-hiding');
setTimeout(function() {
if (el.parentNode) {
el.parentNode.removeChild(el);
}
if (featureId) {
markShown(featureId);
}
}, 300);
}
/**
* Escape HTML to prevent XSS.
*/
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// Public API
window.SpaxelTooltip = {
show: show,
shouldShow: shouldShow,
markShown: markShown,
FEATURES: FEATURES,
};
console.log('[Spaxel] Tooltip manager loaded');
})();