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:
parent
af67a7be99
commit
7698a63362
1 changed files with 400 additions and 73 deletions
473
uncloseai.js
473
uncloseai.js
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue