Spaces:
Runtime error
Runtime error
| /** | |
| * 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 += ` | |
| <div class="contextual-hint-title"> | |
| <i class="${hint.icon}"></i> | |
| <h5>${hint.title}</h5> | |
| </div> | |
| <div class="contextual-hint-content">${hint.content}</div> | |
| <div class="contextual-hint-actions"> | |
| <button class="hint-button hint-button-primary">${hint.buttonText}</button> | |
| <span class="hint-dont-show">Don't show again</span> | |
| </div> | |
| `; | |
| // 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); | |
| }); |