Skip to content

Instantly share code, notes, and snippets.

@keon
Created March 19, 2025 22:46
Show Gist options
  • Save keon/26f52515ba8d201ba0fa95ec7bbcba80 to your computer and use it in GitHub Desktop.
Save keon/26f52515ba8d201ba0fa95ec7bbcba80 to your computer and use it in GitHub Desktop.
// Function to create and manage the multi-step guidance and chat
function createSupportSystem() {
// Add styles for all components
const style = document.createElement('style');
style.textContent = `
/* General tooltip/modal styles */
.guidance-tooltip {
position: fixed;
background-color: #0099CC;
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
font-family: 'Styrene', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
z-index: 9999;
animation: pulse 2s infinite alternate;
width: 280px;
}
.guidance-tooltip-arrow {
position: absolute;
width: 0;
height: 0;
}
.guidance-tooltip-title {
font-weight: bold;
font-size: 16px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
}
.guidance-tooltip-close {
cursor: pointer;
font-size: 18px;
}
.guidance-tooltip-content {
margin-bottom: 12px;
line-height: 1.4;
}
.guidance-tooltip-btn {
background-color: white;
color: #0099CC;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s;
}
.guidance-tooltip-btn:hover {
background-color: #f0f0f0;
}
/* Chat interface styles */
#support-chat-button {
position: fixed;
bottom: 20px;
right: 20px;
width: 60px;
height: 60px;
background-color: #0099CC;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 9998;
transition: all 0.3s ease;
animation: pulse 2s infinite alternate;
}
#support-chat-icon {
color: white;
font-size: 24px;
}
#support-chat-window {
position: fixed;
bottom: 90px;
right: 20px;
width: 320px;
height: 400px;
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: none;
flex-direction: column;
overflow: hidden;
z-index: 9997;
font-family: 'Styrene', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#support-chat-header {
background-color: #0099CC;
color: white;
padding: 12px 16px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
#support-chat-close {
cursor: pointer;
font-size: 18px;
}
#support-chat-messages {
flex: 1;
padding: 12px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
background-color: #f7f7f7;
}
.chat-message {
max-width: 80%;
padding: 8px 12px;
border-radius: 16px;
margin-bottom: 4px;
word-break: break-word;
}
.user-message {
align-self: flex-end;
background-color: #0099CC;
color: white;
border-bottom-right-radius: 4px;
}
.assistant-message {
align-self: flex-start;
background-color: #eaeaea;
color: #333;
border-bottom-left-radius: 4px;
}
#support-chat-input-container {
padding: 12px;
border-top: 1px solid #eaeaea;
display: flex;
gap: 8px;
background-color: white;
}
#support-chat-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 16px;
outline: none;
font-family: inherit;
}
#support-chat-submit {
background-color: #0099CC;
color: white;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
/* Highlight styles */
.menu-highlight {
background-color: rgba(0, 153, 204, 0.2) !important;
border-left: 3px solid #0099CC !important;
position: relative;
animation: highlight-pulse 2s infinite alternate;
}
.buy-credits-highlight {
position: relative;
animation: highlight-button 2s infinite alternate;
box-shadow: 0 0 0 4px rgba(0, 153, 204, 0.4) !important;
}
/* Animation keyframes */
@keyframes pulse {
0% { box-shadow: 0 4px 12px rgba(0, 153, 204, 0.4); }
100% { box-shadow: 0 4px 20px rgba(0, 153, 204, 0.8); }
}
@keyframes highlight-pulse {
0% { background-color: rgba(0, 153, 204, 0.1) !important; }
100% { background-color: rgba(0, 153, 204, 0.3) !important; }
}
@keyframes highlight-button {
0% { transform: scale(1); box-shadow: 0 0 0 2px rgba(0, 153, 204, 0.4) !important; }
100% { transform: scale(1.05); box-shadow: 0 0 0 4px rgba(0, 153, 204, 0.7) !important; }
}
/* Overlay styles */
.guidance-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9990;
pointer-events: none;
}
.guidance-spotlight {
position: absolute;
border-radius: 8px;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7);
pointer-events: auto;
}
`;
document.head.appendChild(style);
// Create chat interface
function createChatInterface() {
// Create chat button
const chatButton = document.createElement('div');
chatButton.id = 'support-chat-button';
chatButton.innerHTML = `<div id="support-chat-icon">💬</div>`;
document.body.appendChild(chatButton);
// Create chat window
const chatWindow = document.createElement('div');
chatWindow.id = 'support-chat-window';
chatWindow.innerHTML = `
<div id="support-chat-header">
<div>Support Chat</div>
<div id="support-chat-close">×</div>
</div>
<div id="support-chat-messages">
<div class="chat-message assistant-message">Hello! I'm your support assistant. How can I help you today?</div>
</div>
<div id="support-chat-input-container">
<input type="text" id="support-chat-input" placeholder="Type your message...">
<button id="support-chat-submit">➤</button>
</div>
`;
document.body.appendChild(chatWindow);
// Toggle chat window
chatButton.addEventListener('click', () => {
const isVisible = chatWindow.style.display === 'flex';
chatWindow.style.display = isVisible ? 'none' : 'flex';
});
// Close chat window
document.getElementById('support-chat-close').addEventListener('click', () => {
chatWindow.style.display = 'none';
});
// Send message on submit button click
document.getElementById('support-chat-submit').addEventListener('click', handleChatSubmit);
// Send message on Enter key
document.getElementById('support-chat-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleChatSubmit();
}
});
}
// Handle chat message submission
function handleChatSubmit() {
const inputField = document.getElementById('support-chat-input');
const message = inputField.value.trim();
if (message) {
// Add user message to chat
addChatMessage(message, true);
// Clear input field
inputField.value = '';
// Process the message
processUserMessage(message);
}
}
// Add a message to the chat window
function addChatMessage(content, isUser) {
const messagesContainer = document.getElementById('support-chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message ${isUser ? 'user-message' : 'assistant-message'}`;
messageDiv.textContent = content;
messagesContainer.appendChild(messageDiv);
// Scroll to the bottom
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Process user message and determine response
function processUserMessage(message) {
// Check if message contains the trigger phrase
if (message.toLowerCase().includes('my api stopped working')) {
setTimeout(() => {
addChatMessage("I found the issue. Your API access is likely suspended due to low credit balance. Let me help you fix this.", false);
setTimeout(() => {
addChatMessage("I'll guide you to the Billing page where you can add more credits to resume your API access.", false);
// Start the guidance process after a short delay
setTimeout(startGuidance, 1000);
}, 1000);
}, 1000);
} else {
// Generic responses for other messages
setTimeout(() => {
addChatMessage("I'm here to help. If you're experiencing issues with your API, please let me know by saying 'my API stopped working'.", false);
}, 500);
}
}
// Create an overlay with a spotlight
function createOverlay(targetElement) {
// Remove any existing overlay
const existingOverlay = document.getElementById('guidance-overlay');
if (existingOverlay) existingOverlay.remove();
// Create new overlay
const overlay = document.createElement('div');
overlay.id = 'guidance-overlay';
overlay.className = 'guidance-overlay';
document.body.appendChild(overlay);
if (targetElement) {
const rect = targetElement.getBoundingClientRect();
// Create spotlight
const spotlight = document.createElement('div');
spotlight.className = 'guidance-spotlight';
spotlight.style.top = `${rect.top - 5}px`;
spotlight.style.left = `${rect.left - 5}px`;
spotlight.style.width = `${rect.width + 10}px`;
spotlight.style.height = `${rect.height + 10}px`;
overlay.appendChild(spotlight);
}
return overlay;
}
// Create and show a tooltip
function showTooltip(targetElement, position, title, content, buttonText, buttonAction) {
// Remove any existing tooltip
const existingTooltip = document.getElementById('guidance-tooltip');
if (existingTooltip) existingTooltip.remove();
// Create tooltip
const tooltip = document.createElement('div');
tooltip.id = 'guidance-tooltip';
tooltip.className = 'guidance-tooltip';
tooltip.innerHTML = `
<div class="guidance-tooltip-title">
<span>${title}</span>
<span class="guidance-tooltip-close">×</span>
</div>
<div class="guidance-tooltip-content">${content}</div>
${buttonText ? `<button class="guidance-tooltip-btn">${buttonText}</button>` : ''}
`;
document.body.appendChild(tooltip);
// Position the tooltip relative to the target
const rect = targetElement.getBoundingClientRect();
const arrow = document.createElement('div');
arrow.className = 'guidance-tooltip-arrow';
tooltip.appendChild(arrow);
switch (position) {
case 'right':
tooltip.style.left = `${rect.right + 15}px`;
tooltip.style.top = `${rect.top + rect.height/2 - tooltip.offsetHeight/2}px`;
arrow.style.left = '-8px';
arrow.style.top = '50%';
arrow.style.transform = 'translateY(-50%)';
arrow.style.borderTop = '8px solid transparent';
arrow.style.borderBottom = '8px solid transparent';
arrow.style.borderRight = '8px solid #0099CC';
break;
case 'bottom':
tooltip.style.left = `${rect.left + rect.width/2 - tooltip.offsetWidth/2}px`;
tooltip.style.top = `${rect.bottom + 15}px`;
arrow.style.top = '-8px';
arrow.style.left = '50%';
arrow.style.transform = 'translateX(-50%)';
arrow.style.borderLeft = '8px solid transparent';
arrow.style.borderRight = '8px solid transparent';
arrow.style.borderBottom = '8px solid #0099CC';
break;
case 'top':
tooltip.style.left = `${rect.left + rect.width/2 - tooltip.offsetWidth/2}px`;
tooltip.style.top = `${rect.top - tooltip.offsetHeight - 15}px`;
arrow.style.bottom = '-8px';
arrow.style.left = '50%';
arrow.style.transform = 'translateX(-50%)';
arrow.style.borderLeft = '8px solid transparent';
arrow.style.borderRight = '8px solid transparent';
arrow.style.borderTop = '8px solid #0099CC';
break;
case 'left':
tooltip.style.left = `${rect.left - tooltip.offsetWidth - 15}px`;
tooltip.style.top = `${rect.top + rect.height/2 - tooltip.offsetHeight/2}px`;
arrow.style.right = '-8px';
arrow.style.top = '50%';
arrow.style.transform = 'translateY(-50%)';
arrow.style.borderTop = '8px solid transparent';
arrow.style.borderBottom = '8px solid transparent';
arrow.style.borderLeft = '8px solid #0099CC';
break;
}
// Add event listeners
tooltip.querySelector('.guidance-tooltip-close').addEventListener('click', () => {
tooltip.remove();
const overlay = document.getElementById('guidance-overlay');
if (overlay) overlay.remove();
// Remove any highlights
document.querySelectorAll('.menu-highlight').forEach(el => {
el.classList.remove('menu-highlight');
});
document.querySelectorAll('.buy-credits-highlight').forEach(el => {
el.classList.remove('buy-credits-highlight');
});
});
if (buttonText) {
tooltip.querySelector('.guidance-tooltip-btn').addEventListener('click', () => {
buttonAction();
});
}
return tooltip;
}
// Highlight the Billing menu item
function highlightBillingMenu() {
const billingMenuItems = Array.from(document.querySelectorAll('a'))
.filter(a => a.textContent.trim() === 'Billing');
if (billingMenuItems.length > 0) {
const billingMenuItem = billingMenuItems[0];
// Highlight the menu item
billingMenuItem.classList.add('menu-highlight');
// Create overlay with spotlight on the menu item
createOverlay(billingMenuItem);
// Show tooltip next to the menu item
showTooltip(
billingMenuItem,
'right',
'Low Credit Balance',
'Your API has stopped working because your credit balance is too low. Click on Billing to add more credits.',
'Continue',
() => {
// Simulate click on billing menu
billingMenuItem.click();
// Wait for page to load, then proceed to next step
setTimeout(highlightBuyCreditsButton, 1000);
}
);
// Scroll menu into view if needed
billingMenuItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
// Highlight the Buy Credits button
function highlightBuyCreditsButton() {
// Remove old overlay and highlights
const overlay = document.getElementById('guidance-overlay');
if (overlay) overlay.remove();
document.querySelectorAll('.menu-highlight').forEach(el => {
el.classList.remove('menu-highlight');
});
// Find the Buy Credits button
setTimeout(() => {
const buyCreditsButton = Array.from(document.querySelectorAll('button'))
.find(btn => btn.textContent.trim() === 'Buy credits');
if (buyCreditsButton) {
// Highlight the button
buyCreditsButton.classList.add('buy-credits-highlight');
// Create overlay with spotlight on the button
createOverlay(buyCreditsButton);
// Show tooltip next to the button
showTooltip(
buyCreditsButton,
'bottom',
'Purchase More Credits',
'Your current balance is only <strong>$17.16</strong>. You need to add more credits to reactivate your API access.',
'Got it',
() => {
// Remove the tooltip and overlay
const tooltip = document.getElementById('guidance-tooltip');
if (tooltip) tooltip.remove();
const overlay = document.getElementById('guidance-overlay');
if (overlay) overlay.remove();
// Remove highlights
buyCreditsButton.classList.remove('buy-credits-highlight');
// Add a final message to the chat
addChatMessage("Once you've added more credits, your API access will be restored automatically. Let me know if you need any further assistance!", false);
}
);
// Scroll button into view if needed
buyCreditsButton.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 500);
}
// Start the guidance process
function startGuidance() {
// Check if we're already on the billing page
const isBillingPage = window.location.pathname.includes('/settings/billing');
if (isBillingPage) {
// Skip directly to highlighting the buy credits button
highlightBuyCreditsButton();
} else {
// Start with highlighting the billing menu
highlightBillingMenu();
}
}
// Create the chat interface
createChatInterface();
}
// Run the function
(function() {
// Try several times to make sure it runs after everything is loaded
setTimeout(createSupportSystem, 1000);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment