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); | |
}); |