Replace popup menu with full Hermes chat modal

Replaced simple skills popup with comprehensive modal interface featuring chat area, model/voice selection, action buttons (Read Page, TTS, Refresh Models), file upload, and TTS playback for responses. Modal uses ChunkFive branding and gradient styling.
This commit is contained in:
Russell Ballestrini 2025-06-29 18:46:33 -04:00
parent af67a7be99
commit 7698a63362

View file

@ -201,7 +201,9 @@ function addRefreshModelsButton() {
// NEW: Helper function to get the selected model from the dropdown.
function getSelectedModel() {
const dropdown = document.getElementById('model-selection');
// Check modal dropdown first
const modalDropdown = document.getElementById('modal-model-selection');
const dropdown = modalDropdown || document.getElementById('model-selection');
if (!dropdown) return MODEL;
const selectedUniqueId = dropdown.value;
const cacheItem = localStorage.getItem('modelRegistryCache');
@ -220,7 +222,9 @@ function getSelectedModel() {
// Utility to determine which endpoint to use based on the selected model
function getSelectedModelEndpoint() {
const dropdown = document.getElementById('model-selection');
// Check modal dropdown first
const modalDropdown = document.getElementById('modal-model-selection');
const dropdown = modalDropdown || document.getElementById('model-selection');
if (!dropdown) {
console.warn("Model selection dropdown not found, falling back to default endpoint.");
return VLLM_ENDPOINTS[0].url;
@ -1053,87 +1057,410 @@ function createFloatingAIButton() {
transition: all 0.3s ease;
`;
// Create the skills menu
const skillsMenu = document.createElement('div');
skillsMenu.id = 'ai-skills-menu';
skillsMenu.style.cssText = `
position: fixed;
bottom: 90px;
right: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
padding: 16px;
min-width: 200px;
z-index: 999;
display: none;
border: 1px solid #e0e0e0;
`;
// Skills buttons
const skills = [
{ name: 'Chat', icon: '💬', action: () => scrollToChatBox() },
{ name: 'Read Page', icon: '📖', action: readPageWithHermes },
{ name: 'TTS Anything', icon: '🔊', action: openTTSModal },
{ name: 'Upload File', icon: '📁', action: () => document.getElementById('file-input')?.click() },
{ name: 'Refresh Models', icon: '🔄', action: () => document.querySelector('#refresh-models-container button')?.click() }
];
skills.forEach(skill => {
const skillButton = document.createElement('button');
skillButton.style.cssText = `
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 12px;
margin: 4px 0;
border: none;
background: transparent;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s ease;
`;
skillButton.innerHTML = `<span style="font-size: 18px;">${skill.icon}</span> ${skill.name}`;
skillButton.onmouseover = () => skillButton.style.background = '#f5f5f5';
skillButton.onmouseout = () => skillButton.style.background = 'transparent';
skillButton.onclick = () => {
skill.action();
skillsMenu.style.display = 'none';
};
skillsMenu.appendChild(skillButton);
});
// Toggle menu visibility
let menuVisible = false;
floatingButton.onclick = () => {
menuVisible = !menuVisible;
skillsMenu.style.display = menuVisible ? 'block' : 'none';
floatingButton.style.transform = menuVisible ? 'rotate(180deg)' : 'rotate(0deg)';
};
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!floatingButton.contains(e.target) && !skillsMenu.contains(e.target)) {
skillsMenu.style.display = 'none';
menuVisible = false;
floatingButton.style.transform = 'rotate(0deg)';
}
});
// Hover effects
floatingButton.onmouseenter = () => {
floatingButton.style.transform = 'scale(1.1)';
floatingButton.style.boxShadow = '0 6px 16px rgba(0,0,0,0.4)';
};
floatingButton.onmouseleave = () => {
if (!menuVisible) floatingButton.style.transform = 'scale(1)';
floatingButton.style.transform = 'scale(1)';
floatingButton.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
};
// Open modal on click
floatingButton.onclick = () => openHermesModal();
document.body.appendChild(floatingButton);
document.body.appendChild(skillsMenu);
}
// Function to open Hermes modal
function openHermesModal() {
// Create modal backdrop
const modalBackdrop = document.createElement('div');
modalBackdrop.id = 'hermes-modal-backdrop';
modalBackdrop.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
`;
// Create modal container
const modal = document.createElement('div');
modal.id = 'hermes-modal';
modal.style.cssText = `
background: white;
border-radius: 16px;
width: 90%;
max-width: 800px;
height: 80%;
max-height: 600px;
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
overflow: hidden;
`;
// Create modal header
const header = document.createElement('div');
header.style.cssText = `
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
`;
const title = document.createElement('h2');
title.textContent = 'Hermes AI Chat';
title.style.cssText = `
margin: 0;
font-family: 'ChunkFiveRegular', monospace;
font-size: 20px;
`;
const closeButton = document.createElement('button');
closeButton.textContent = '×';
closeButton.style.cssText = `
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
`;
closeButton.onclick = () => document.body.removeChild(modalBackdrop);
header.appendChild(title);
header.appendChild(closeButton);
// Create controls section
const controls = document.createElement('div');
controls.style.cssText = `
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
`;
// Add model selection dropdown
const modelLabel = document.createElement('label');
modelLabel.textContent = 'Model: ';
modelLabel.style.fontWeight = 'bold';
const modelSelect = document.createElement('select');
modelSelect.id = 'modal-model-selection';
modelSelect.style.cssText = `
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
`;
// Copy options from existing model dropdown
const existingDropdown = document.getElementById('model-selection');
if (existingDropdown) {
Array.from(existingDropdown.options).forEach(option => {
const newOption = document.createElement('option');
newOption.value = option.value;
newOption.textContent = option.textContent;
newOption.selected = option.selected;
modelSelect.appendChild(newOption);
});
}
// Add voice selection
const voiceLabel = document.createElement('label');
voiceLabel.textContent = 'Voice: ';
voiceLabel.style.fontWeight = 'bold';
const voiceSelect = document.createElement('select');
voiceSelect.id = 'modal-voice-selection';
voiceSelect.style.cssText = `
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
`;
const voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
voices.forEach(voice => {
const option = document.createElement('option');
option.value = voice;
option.textContent = voice;
if (voice === 'alloy') option.selected = true;
voiceSelect.appendChild(option);
});
// Add action buttons
const actionButtons = document.createElement('div');
actionButtons.style.cssText = `
display: flex;
gap: 8px;
flex-wrap: wrap;
`;
const readPageBtn = document.createElement('button');
readPageBtn.textContent = '📖 Read Page';
readPageBtn.style.cssText = 'padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;';
readPageBtn.onclick = readPageWithHermes;
const ttsBtn = document.createElement('button');
ttsBtn.textContent = '🔊 TTS';
ttsBtn.style.cssText = 'padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;';
ttsBtn.onclick = openTTSModal;
const refreshBtn = document.createElement('button');
refreshBtn.textContent = '🔄 Refresh';
refreshBtn.style.cssText = 'padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;';
refreshBtn.onclick = async () => {
// Clear cache and refresh models
localStorage.removeItem('modelRegistryCache');
localStorage.removeItem('vllmEndpointsHash');
const models = await fetchModelsFromEndpoints();
// Update both dropdowns
[modelSelect, document.getElementById('model-selection')].forEach(dropdown => {
if (dropdown) {
dropdown.innerHTML = '';
models.forEach(model => {
const option = document.createElement('option');
option.value = model.uniqueId;
option.textContent = `${model.endpointId} | ${model.modelName}`;
dropdown.appendChild(option);
});
}
});
};
actionButtons.appendChild(readPageBtn);
actionButtons.appendChild(ttsBtn);
actionButtons.appendChild(refreshBtn);
controls.appendChild(modelLabel);
controls.appendChild(modelSelect);
controls.appendChild(voiceLabel);
controls.appendChild(voiceSelect);
controls.appendChild(actionButtons);
// Create chat area
const chatArea = document.createElement('div');
chatArea.style.cssText = `
flex: 1;
padding: 20px;
overflow-y: auto;
border-bottom: 1px solid #e0e0e0;
`;
const chatBox = document.createElement('div');
chatBox.id = 'modal-chat-box';
chatBox.style.cssText = `
height: 100%;
overflow-y: auto;
`;
chatArea.appendChild(chatBox);
// Create input area
const inputArea = document.createElement('div');
inputArea.style.cssText = `
padding: 16px 20px;
display: flex;
gap: 12px;
align-items: center;
`;
const messageInput = document.createElement('input');
messageInput.type = 'text';
messageInput.id = 'modal-user-input';
messageInput.placeholder = 'Ask Hermes anything...';
messageInput.style.cssText = `
flex: 1;
padding: 12px;
border: 1px solid #ccc;
border-radius: 8px;
font-size: 14px;
`;
const sendButton = document.createElement('button');
sendButton.textContent = 'Send';
sendButton.style.cssText = `
padding: 12px 24px;
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
`;
// File upload
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.id = 'modal-file-input';
fileInput.style.display = 'none';
const fileButton = document.createElement('button');
fileButton.textContent = '📁';
fileButton.style.cssText = `
padding: 12px;
background: #f5f5f5;
border: 1px solid #ccc;
border-radius: 8px;
cursor: pointer;
`;
fileButton.onclick = () => fileInput.click();
// Handle file upload
fileInput.onchange = async () => {
if (fileInput.files.length > 0) {
const file = fileInput.files[0];
try {
showProgressIndicator('Uploading & Processing file...');
const response = await uploadFile(file);
await integrateMegafarceResponseToModal(response, chatBox);
hideProgressIndicator();
fileInput.value = '';
} catch (error) {
console.error('File upload error:', error);
alert('Failed to upload the file.');
hideProgressIndicator();
}
}
};
// Handle send message
const handleModalInput = async () => {
const userInput = messageInput.value.trim();
if (!userInput) return;
messageInput.value = '';
chatBox.innerHTML += `<p><strong>You:</strong> ${userInput}</p>`;
const aiResponseParagraph = document.createElement('p');
aiResponseParagraph.innerHTML = '<strong>AI:</strong> ';
chatBox.appendChild(aiResponseParagraph);
const responseContent = document.createElement('span');
aiResponseParagraph.appendChild(responseContent);
let accumulatedContent = '';
for await (const chunk of sendMessage(userInput)) {
accumulatedContent += chunk;
const parsedChunk = marked.parse(accumulatedContent);
responseContent.innerHTML = parsedChunk;
responseContent.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
chatBox.scrollTop = chatBox.scrollHeight;
// Add TTS button for response
const ttsResponseBtn = document.createElement('button');
ttsResponseBtn.textContent = '🔊 Play Response';
ttsResponseBtn.style.cssText = 'margin: 5px; padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;';
let responseAudio = null;
let responseBlob = null;
ttsResponseBtn.onclick = async () => {
if (!responseAudio) {
ttsResponseBtn.textContent = 'Processing...';
ttsResponseBtn.disabled = true;
const selectedVoice = voiceSelect.value;
const result = await speakText(accumulatedContent, selectedVoice, 0.9);
responseAudio = result.audio;
responseBlob = result.blob;
ttsResponseBtn.textContent = 'Pause';
ttsResponseBtn.disabled = false;
responseAudio.play();
// Add download button
const downloadBtn = document.createElement('button');
downloadBtn.textContent = '💾 Download';
downloadBtn.style.cssText = 'margin: 5px; padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;';
downloadBtn.onclick = () => {
const a = document.createElement('a');
a.href = URL.createObjectURL(responseBlob);
a.download = `hermes-response-${Date.now()}.mp3`;
a.click();
};
chatBox.appendChild(downloadBtn);
} else {
if (responseAudio.paused) {
responseAudio.play();
ttsResponseBtn.textContent = 'Pause';
} else {
responseAudio.pause();
ttsResponseBtn.textContent = 'Play';
}
}
};
chatBox.appendChild(ttsResponseBtn);
};
sendButton.onclick = handleModalInput;
messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleModalInput();
}
});
inputArea.appendChild(fileButton);
inputArea.appendChild(fileInput);
inputArea.appendChild(messageInput);
inputArea.appendChild(sendButton);
// Assemble modal
modal.appendChild(header);
modal.appendChild(controls);
modal.appendChild(chatArea);
modal.appendChild(inputArea);
modalBackdrop.appendChild(modal);
// Close on backdrop click
modalBackdrop.onclick = (e) => {
if (e.target === modalBackdrop) {
document.body.removeChild(modalBackdrop);
}
};
document.body.appendChild(modalBackdrop);
messageInput.focus();
}
// Helper function to integrate file upload response to modal
async function integrateMegafarceResponseToModal(response, chatBox) {
const content = response.content || response.result || "No content received.";
chatHistory.push({ role: "system", content: `Uploaded Context: ${content}` });
const systemMessage = document.createElement('p');
systemMessage.innerHTML = `<strong>System:</strong> ${marked.parse(content)}`;
systemMessage.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
chatBox.appendChild(systemMessage);
chatBox.scrollTop = chatBox.scrollHeight;
}
// Helper function to scroll to chat box