/** * Contextual Hint System * * A subtle, non-intrusive tooltip system providing just-in-time guidance and learning * for the Quantum NLP Framework. This system detects when a user might need help * understanding a feature and provides relevant information without disrupting workflow. */ class ContextualHintSystem { constructor() { this.hints = {}; this.activeHint = null; this.hintContainer = null; this.shownHints = []; this.initialize(); } /** * Initialize the hint system */ initialize() { // Load previously shown hints this.loadShownHints(); // Register predefined hints this.registerPredefinedHints(); // Initialize scroll listener for detecting visible elements this.initScrollListener(); // Create container for hints if it doesn't exist if (!document.getElementById('contextual-hints-container')) { this.hintContainer = document.createElement('div'); this.hintContainer.id = 'contextual-hints-container'; document.body.appendChild(this.hintContainer); } else { this.hintContainer = document.getElementById('contextual-hints-container'); } // Listen for ESC key to dismiss hints document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.activeHint) { this.dismissActiveHint(); } }); // Listen for clicks outside hint to dismiss document.addEventListener('click', (e) => { if (this.activeHint && !this.activeHint.contains(e.target)) { // Check if the click was not on the target element either const activeHintId = this.activeHint.getAttribute('data-hint-id'); const targetElement = document.querySelector(`[data-hint="${activeHintId}"]`); if (!targetElement || !targetElement.contains(e.target)) { this.dismissActiveHint(); } } }); } /** * Register a hint with the system * @param {string} id - Unique identifier for the hint * @param {object} options - Hint options */ registerHint(id, options) { this.hints[id] = { title: options.title || 'Hint', content: options.content || '', position: options.position || 'bottom', important: options.important || false, maxShows: options.maxShows || 3, icon: options.icon || 'fas fa-lightbulb', selector: options.selector || null, particles: options.particles || false, trigger: options.trigger || 'auto', // auto, manual, hover buttonText: options.buttonText || 'Got it', onShown: options.onShown || null, onDismiss: options.onDismiss || null }; // Attach triggers for this hint if necessary if (options.selector) { this.attachHintTrigger(id); } } /** * Register all predefined hints for the application */ registerPredefinedHints() { // Quantum Dimensions hint this.registerHint('quantum-dimensions', { title: 'Quantum Dimensions', content: 'Increase dimensions for deeper, multi-layered analysis of your text. Higher dimensions explore more interconnected thought paths.', position: 'top', selector: '#depth', icon: 'fas fa-layer-group', trigger: 'hover' }); // OpenAI integration hint this.registerHint('openai-integration', { title: 'AI Enhancement', content: 'Enable this to use OpenAI for generating human-like responses based on quantum analysis results. Requires API key in settings.', position: 'right', selector: '#use_ai', icon: 'fas fa-robot', important: !document.body.classList.contains('has-openai-key') }); // Analyze button hint this.registerHint('analyze-button', { title: 'Quantum Analysis', content: 'Start the quantum-inspired recursive thinking process to analyze your text through multiple dimensions.', position: 'top', selector: '#analyze-btn', particles: true, maxShows: 2 }); // Quantum Score hint this.registerHint('quantum-score', { title: 'Quantum Score', content: 'This score represents the confidence and coherence of the quantum analysis across all dimensions.', position: 'left', selector: '.quantum-score-visualization', icon: 'fas fa-chart-line', trigger: 'manual' }); // Zap Integrations hint this.registerHint('zap-integrations', { title: 'ZAP Integrations', content: 'Connect the Quantum Framework to other services and applications to extend its capabilities.', position: 'bottom', selector: 'a[href="/zap-integrations"]', icon: 'fas fa-bolt', trigger: 'hover' }); // Automation Workflow hint this.registerHint('automation-workflow', { title: 'Automation Workflow', content: 'View and configure automated tasks and workflows using the quantum framework.', position: 'bottom', selector: 'a[href="/automation-workflow"]', icon: 'fas fa-cogs', trigger: 'hover' }); // Named Entities hint this.registerHint('named-entities', { title: 'Named Entities', content: 'These are specific objects identified in your text like people, organizations, locations, and more.', position: 'right', selector: '.quantum-entity-item', icon: 'fas fa-fingerprint', trigger: 'manual' }); } /** * Attach a trigger to show a hint when interacting with an element * @param {string} hintId - The ID of the hint to trigger */ attachHintTrigger(hintId) { const hint = this.hints[hintId]; if (!hint || !hint.selector) return; // Find all matching elements const elements = document.querySelectorAll(hint.selector); if (elements.length === 0) return; elements.forEach(element => { // Add data attribute to mark the element as a hint target element.setAttribute('data-hint', hintId); element.classList.add('hint-target'); // Attach event listeners based on trigger type if (hint.trigger === 'hover') { element.addEventListener('mouseenter', () => { this.considerShowingHint(hintId, element); element.classList.add('hint-highlight'); }); element.addEventListener('mouseleave', () => { element.classList.remove('hint-highlight'); }); } else if (hint.trigger === 'auto') { // For auto triggers, we'll check visibility in the scroll listener // and show the hint when appropriate } // For manual triggers, the hint will be shown programmatically }); } /** * Consider whether to show a hint based on whether it's been shown before * @param {string} hintId - The ID of the hint to consider showing * @param {Element} target - The target element for the hint */ considerShowingHint(hintId, target) { // Don't show if another hint is active if (this.activeHint) return; const hint = this.hints[hintId]; if (!hint) return; // Count how many times this hint has been shown const timesShown = this.shownHints.filter(id => id === hintId).length; // If it's been shown fewer times than the max, show it if (timesShown < hint.maxShows) { this.showHint(hintId, target); } } /** * Show a hint for a specific element * @param {string} hintId - The ID of the hint to show * @param {Element} targetElement - The element to attach the hint to */ showHint(hintId, targetElement) { const hint = this.hints[hintId]; if (!hint) return; // Dismiss any active hint this.dismissActiveHint(); // Create the hint element const hintElement = document.createElement('div'); hintElement.className = `contextual-hint position-${hint.position}`; hintElement.setAttribute('data-hint-id', hintId); if (hint.important) { hintElement.classList.add('important'); } if (hint.particles) { hintElement.classList.add('has-particles'); } // Add LED tracer effect const ledTracer = document.createElement('div'); ledTracer.className = 'led-tracer'; hintElement.appendChild(ledTracer); // Add content hintElement.innerHTML += `
${hint.title}
${hint.content}
Don't show again
`; // Add particles if enabled if (hint.particles) { const particlesContainer = document.createElement('div'); particlesContainer.className = 'hint-particles'; // Add several particles for (let i = 0; i < 8; i++) { const particle = document.createElement('div'); particle.className = 'hint-particle'; particle.style.top = `${Math.random() * 100}%`; particle.style.left = `${Math.random() * 100}%`; particle.style.animationDelay = `${Math.random() * 2}s`; particlesContainer.appendChild(particle); } hintElement.appendChild(particlesContainer); } // Add to DOM this.hintContainer.appendChild(hintElement); // Position relative to target this.positionHint(hintElement, targetElement, hint.position); // Show with animation setTimeout(() => { hintElement.classList.add('active'); }, 10); // Set as active hint this.activeHint = hintElement; // Record that this hint has been shown this.markHintAsShown(hintId); // Attach event listeners to buttons const dismissButton = hintElement.querySelector('.hint-button'); dismissButton.addEventListener('click', () => { this.dismissActiveHint(); // Run onDismiss callback if provided if (typeof hint.onDismiss === 'function') { hint.onDismiss(); } }); const dontShowAgain = hintElement.querySelector('.hint-dont-show'); dontShowAgain.addEventListener('click', () => { // Add to shown hints enough times to reach maxShows for (let i = timesShown; i < hint.maxShows; i++) { this.markHintAsShown(hintId); } this.dismissActiveHint(); }); // Call onShown callback if provided if (typeof hint.onShown === 'function') { hint.onShown(); } } /** * Position a hint element relative to its target * @param {Element} hintElement - The hint element * @param {Element} targetElement - The target element * @param {string} position - The position (top, bottom, left, right) */ positionHint(hintElement, targetElement, position) { if (!targetElement) return; const targetRect = targetElement.getBoundingClientRect(); const hintRect = hintElement.getBoundingClientRect(); let top, left; switch (position) { case 'top': top = targetRect.top - hintRect.height - 15; left = targetRect.left + (targetRect.width / 2) - (hintRect.width / 2); hintElement.querySelector('::before').style.left = '50%'; break; case 'bottom': top = targetRect.bottom + 15; left = targetRect.left + (targetRect.width / 2) - (hintRect.width / 2); hintElement.querySelector('::before').style.left = '50%'; break; case 'left': top = targetRect.top + (targetRect.height / 2) - (hintRect.height / 2); left = targetRect.left - hintRect.width - 15; hintElement.querySelector('::before').style.top = '50%'; break; case 'right': top = targetRect.top + (targetRect.height / 2) - (hintRect.height / 2); left = targetRect.right + 15; hintElement.querySelector('::before').style.top = '50%'; break; } // Adjust if the hint would be off-screen const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; if (left < 10) left = 10; if (left + hintRect.width > viewportWidth - 10) { left = viewportWidth - hintRect.width - 10; } if (top < 10) top = 10; if (top + hintRect.height > viewportHeight - 10) { top = viewportHeight - hintRect.height - 10; } // Set the position hintElement.style.top = `${top}px`; hintElement.style.left = `${left}px`; } /** * Dismiss the currently active hint */ dismissActiveHint() { if (this.activeHint) { this.activeHint.classList.remove('active'); // Remove from DOM after animation completes setTimeout(() => { if (this.activeHint && this.activeHint.parentNode) { this.activeHint.parentNode.removeChild(this.activeHint); } this.activeHint = null; }, 300); } } /** * Mark a hint as having been shown * @param {string} hintId - The ID of the hint */ markHintAsShown(hintId) { this.shownHints.push(hintId); this.saveShownHints(); } /** * Check if a hint has been shown the maximum number of times * @param {string} hintId - The ID of the hint * @returns {boolean} Whether the hint has been shown max times */ hasHintBeenShown(hintId) { const hint = this.hints[hintId]; if (!hint) return true; const timesShown = this.shownHints.filter(id => id === hintId).length; return timesShown >= hint.maxShows; } /** * Load the list of shown hints from localStorage */ loadShownHints() { const savedHints = localStorage.getItem('shownHints'); if (savedHints) { try { this.shownHints = JSON.parse(savedHints); } catch (e) { this.shownHints = []; } } } /** * Save the list of shown hints to localStorage */ saveShownHints() { localStorage.setItem('shownHints', JSON.stringify(this.shownHints)); } /** * Reset all shown hints so they'll be shown again */ resetShownHints() { this.shownHints = []; localStorage.removeItem('shownHints'); } /** * Initialize the scroll listener for detecting visible elements */ initScrollListener() { // Check initially this.checkVisibleElementsForHints(); // Check on scroll window.addEventListener('scroll', () => { this.checkVisibleElementsForHints(); }); // Check on resize window.addEventListener('resize', () => { this.checkVisibleElementsForHints(); }); // Check after a short delay (DOM may still be loading) setTimeout(() => { this.checkVisibleElementsForHints(); }, 1000); } /** * Check for elements with auto hints that are visible in the viewport */ checkVisibleElementsForHints() { // Don't check if a hint is already active if (this.activeHint) return; // Look for elements with auto hints that are in the viewport for (const hintId in this.hints) { const hint = this.hints[hintId]; if (hint.trigger !== 'auto') continue; if (this.hasHintBeenShown(hintId)) continue; const elements = document.querySelectorAll(`[data-hint="${hintId}"]`); for (const element of elements) { if (this.isElementInViewport(element)) { this.considerShowingHint(hintId, element); break; } } } } /** * Check if an element is in the viewport * @param {Element} el - The element to check * @returns {boolean} Whether the element is in the viewport */ isElementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } } // Initialize the hint system when the DOM is ready document.addEventListener('DOMContentLoaded', () => { window.contextualHintSystem = new ContextualHintSystem(); // Manually show hints for elements that may not be detected automatically setTimeout(() => { const quantumScoreElements = document.querySelectorAll('.quantum-score-visualization'); if (quantumScoreElements.length > 0) { window.contextualHintSystem.considerShowingHint('quantum-score', quantumScoreElements[0]); } const entityItems = document.querySelectorAll('.quantum-entity-item'); if (entityItems.length > 0) { window.contextualHintSystem.considerShowingHint('named-entities', entityItems[0]); } }, 2000); });