maximus-im / contextual_hints.js
lattmamb's picture
Upload 47 files
8beb2b1
/**
* 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);
});