2590 lines
77 KiB
JavaScript
2590 lines
77 KiB
JavaScript
// Main Hermes AI modal functionality - COMPLETE WORKING VERSION
|
|
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
|
|
import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js";
|
|
import { API_KEY, setSystemMessageAppend, TTS_API_URL } from "./config.js";
|
|
import { extractWebpageContent } from "./content.js";
|
|
import { detectPageLanguage } from "./language-detection.js";
|
|
import {
|
|
detectCurrentTheme,
|
|
getThemeColors,
|
|
initializeChunkFiveFont,
|
|
} from "./ui-themes.js";
|
|
import {
|
|
getUIText,
|
|
getUserLanguagePreference,
|
|
setUserLanguagePreference,
|
|
} from "./ui-translations.js";
|
|
import { NATIVE_LANGUAGE_NAMES } from "./translation.js";
|
|
|
|
// Get USE_CUSTOM_STYLING from window or default
|
|
const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false;
|
|
|
|
// Import required functions dynamically to avoid circular dependencies
|
|
async function speakChatText(text, voice, speed) {
|
|
const { speakTextDirect } = await import("./tts.js");
|
|
return await speakTextDirect(text, voice, speed);
|
|
}
|
|
|
|
async function* sendMessage(message) {
|
|
try {
|
|
const chatModule = await import("./chat.js");
|
|
if (!chatModule.sendMessage) {
|
|
console.error("sendMessage not found in chat.js module:", chatModule);
|
|
throw new Error("sendMessage function not found in chat.js");
|
|
}
|
|
yield* chatModule.sendMessage(message);
|
|
} catch (error) {
|
|
console.error("Error in sendMessage wrapper:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function* sendMessageWithCustomHistory(history) {
|
|
const { sendMessageWithCustomHistory } = await import("./chat.js");
|
|
yield* sendMessageWithCustomHistory(history);
|
|
}
|
|
|
|
async function getChatHistory() {
|
|
const { getChatHistory } = await import("./chat.js");
|
|
return getChatHistory();
|
|
}
|
|
|
|
async function updateChatHistory(history) {
|
|
const { updateChatHistory } = await import("./chat.js");
|
|
return updateChatHistory(history);
|
|
}
|
|
|
|
async function clearConversationHistory() {
|
|
const { clearConversationHistory } = await import("./storage.js");
|
|
return clearConversationHistory();
|
|
}
|
|
|
|
async function loadConversationHistory() {
|
|
const { loadConversationHistory } = await import("./storage.js");
|
|
return loadConversationHistory();
|
|
}
|
|
|
|
async function saveConversationHistory(history) {
|
|
const { saveConversationHistory } = await import("./storage.js");
|
|
return saveConversationHistory(history);
|
|
}
|
|
|
|
async function fetchModelsFromEndpoints() {
|
|
const { fetchModelsFromEndpoints } = await import("./models.js");
|
|
return fetchModelsFromEndpoints();
|
|
}
|
|
|
|
async function getSelectedModel() {
|
|
const { getSelectedModel } = await import("./models.js");
|
|
return getSelectedModel();
|
|
}
|
|
|
|
// Helper function to add copy buttons to code blocks
|
|
function addCodeBlockCopyButtons(element) {
|
|
const codeBlocks = element.querySelectorAll("pre code");
|
|
codeBlocks.forEach((codeBlock) => {
|
|
const pre = codeBlock.parentElement;
|
|
if (pre.tagName.toLowerCase() === "pre") {
|
|
// Make the pre element relative for positioning
|
|
pre.style.position = "relative";
|
|
|
|
// Create copy button
|
|
const copyBtn = document.createElement("button");
|
|
copyBtn.textContent = "📋";
|
|
copyBtn.title = "Copy code";
|
|
copyBtn.style.cssText = `
|
|
position: absolute;
|
|
top: 8px;
|
|
right: 8px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
opacity: 0.8;
|
|
transition: opacity 0.2s;
|
|
z-index: 10;
|
|
`;
|
|
|
|
copyBtn.onmouseenter = () => {
|
|
copyBtn.style.opacity = "1";
|
|
};
|
|
copyBtn.onmouseleave = () => {
|
|
copyBtn.style.opacity = "0.8";
|
|
};
|
|
|
|
copyBtn.onclick = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(codeBlock.textContent);
|
|
copyBtn.textContent = "✓";
|
|
setTimeout(() => {
|
|
copyBtn.textContent = "📋";
|
|
}, 2000);
|
|
} catch (error) {
|
|
console.error("Failed to copy code:", error);
|
|
}
|
|
};
|
|
|
|
pre.appendChild(copyBtn);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Function to refresh modal UI when language changes
|
|
function refreshModalUI() {
|
|
// Refresh all elements with data-i18n attributes in the modal
|
|
const modal = document.getElementById("uncloseai-embedded-modal");
|
|
if (!modal) return;
|
|
|
|
const elementsWithI18n = modal.querySelectorAll("[data-i18n]");
|
|
elementsWithI18n.forEach((element) => {
|
|
const key = element.getAttribute("data-i18n");
|
|
const translatedText = getUIText(key);
|
|
|
|
if (element.tagName === "INPUT" && element.type === "text") {
|
|
element.placeholder = translatedText;
|
|
} else if (element.tagName === "BUTTON" || element.tagName === "LABEL") {
|
|
// Preserve any icons/emojis at the start
|
|
const currentText = element.textContent;
|
|
const iconMatch = currentText.match(/^[^\w\s]+\s*/);
|
|
const icon = iconMatch ? iconMatch[0] : '';
|
|
element.textContent = icon + translatedText;
|
|
} else {
|
|
element.textContent = translatedText;
|
|
}
|
|
});
|
|
|
|
// Update any specific elements that need special handling
|
|
const titleElement = modal.querySelector("h1");
|
|
if (titleElement) {
|
|
titleElement.innerHTML = `<span style="font-family: 'ChunkFiveRegular', monospace;">uncloseai.</span> ${getUIText("hermesTitle")}`;
|
|
}
|
|
|
|
console.log("Modal UI language refreshed");
|
|
}
|
|
|
|
async function openUncloseaiEmbeddedModalNew() {
|
|
// Ensure ChunkFive font is loaded and text colors are enforced
|
|
initializeChunkFiveFont();
|
|
|
|
// Get theme colors
|
|
const theme = detectCurrentTheme();
|
|
const colors = getThemeColors(theme);
|
|
|
|
const modal = document.createElement("dialog");
|
|
modal.id = "uncloseai-embedded-modal";
|
|
modal.setAttribute("data-theme", theme);
|
|
|
|
// Mobile-first: full screen on mobile, centered on desktop
|
|
const isMobile = window.innerWidth <= 768;
|
|
|
|
modal.style.cssText = `
|
|
position: fixed;
|
|
margin: 0;
|
|
padding: 0;
|
|
border: none;
|
|
background: ${colors.modalBackground};
|
|
color: ${colors.primaryText};
|
|
z-index: 2000;
|
|
${
|
|
isMobile
|
|
? `
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
max-width: 100vw;
|
|
max-height: 100vh;
|
|
border-radius: 0;
|
|
`
|
|
: `
|
|
width: 90vw;
|
|
max-width: 800px;
|
|
height: 90vh;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
border-radius: 16px;
|
|
box-shadow: 0 20px 40px ${colors.shadowColor};
|
|
`
|
|
}
|
|
`;
|
|
|
|
// Simple container with flex layout
|
|
const container = document.createElement("div");
|
|
container.style.cssText = `
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
`;
|
|
|
|
// Header
|
|
const header = document.createElement("header");
|
|
header.style.cssText = `
|
|
flex-shrink: 0;
|
|
padding: 16px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
box-shadow: 0 2px 4px ${colors.lightShadow};
|
|
`;
|
|
|
|
const titleContainer = document.createElement("div");
|
|
titleContainer.style.cssText =
|
|
"flex: 1; display: flex; flex-direction: column; gap: 4px;";
|
|
|
|
const title = document.createElement("h2");
|
|
title.innerHTML =
|
|
"🤖 <span style=\"font-family: 'ChunkFiveRegular', monospace; color: white;\">uncloseai.</span>";
|
|
title.style.cssText = `
|
|
margin: 0;
|
|
font-size: ${isMobile ? "18px" : "20px"};
|
|
font-weight: 600;
|
|
color: white;
|
|
`;
|
|
|
|
const subtitle = document.createElement("div");
|
|
const pageTitle = document.title || window.location.hostname;
|
|
subtitle.innerHTML = `
|
|
<div style="font-size: 12px; opacity: 0.9; font-weight: normal; line-height: 1.3; color: white;">
|
|
<span style="font-family: 'ChunkFiveRegular', monospace; color: white;">uncloseai.</span> ${getUIText("hermesIntro")}<br>
|
|
<span style="opacity: 0.8;">${getUIText("discussingPage")}: ${pageTitle}</span>
|
|
</div>
|
|
`;
|
|
|
|
titleContainer.appendChild(title);
|
|
titleContainer.appendChild(subtitle);
|
|
|
|
const closeBtn = document.createElement("button");
|
|
closeBtn.textContent = "✕";
|
|
closeBtn.style.cssText = `
|
|
background: rgba(255,255,255,0.2);
|
|
border: none;
|
|
color: white;
|
|
font-size: 20px;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: background 0.2s;
|
|
`;
|
|
closeBtn.onmouseover = () =>
|
|
(closeBtn.style.background = "rgba(255,255,255,0.3)");
|
|
closeBtn.onmouseout = () =>
|
|
(closeBtn.style.background = "rgba(255,255,255,0.2)");
|
|
closeBtn.onclick = () => {
|
|
modal.close();
|
|
document.body.removeChild(modal);
|
|
uncloseaiEmbeddedModalOpen = false;
|
|
};
|
|
|
|
// Add settings/menu button
|
|
const menuBtn = document.createElement("button");
|
|
menuBtn.textContent = "⚙️";
|
|
menuBtn.style.cssText = `
|
|
background: rgba(255,255,255,0.2);
|
|
border: none;
|
|
color: white;
|
|
font-size: 18px;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: background 0.2s;
|
|
margin-right: 8px;
|
|
`;
|
|
menuBtn.onmouseover = () =>
|
|
(menuBtn.style.background = "rgba(255,255,255,0.3)");
|
|
menuBtn.onmouseout = () =>
|
|
(menuBtn.style.background = "rgba(255,255,255,0.2)");
|
|
|
|
const headerRight = document.createElement("div");
|
|
headerRight.style.cssText = "display: flex; gap: 8px; align-items: center;";
|
|
headerRight.appendChild(menuBtn);
|
|
headerRight.appendChild(closeBtn);
|
|
|
|
header.appendChild(titleContainer);
|
|
header.appendChild(headerRight);
|
|
|
|
// Chat area
|
|
const chatArea = document.createElement("div");
|
|
chatArea.style.cssText = `
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px;
|
|
background: ${colors.contentBackground};
|
|
-webkit-overflow-scrolling: touch;
|
|
`;
|
|
|
|
const chatBox = document.createElement("div");
|
|
chatBox.id = "modal-chat-box";
|
|
chatBox.style.cssText = `
|
|
max-width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
`;
|
|
chatArea.appendChild(chatBox);
|
|
|
|
// Settings panel (collapsible)
|
|
const settingsPanel = document.createElement("div");
|
|
settingsPanel.style.cssText = `
|
|
flex-shrink: 0;
|
|
background: ${colors.panelBackground};
|
|
border-top: 1px solid ${colors.dividerColor};
|
|
border-bottom: 1px solid ${colors.dividerColor};
|
|
padding: 16px;
|
|
display: none;
|
|
max-height: 40vh;
|
|
overflow-y: auto;
|
|
`;
|
|
|
|
// Model selection
|
|
const modelSection = document.createElement("div");
|
|
modelSection.style.cssText = "margin-bottom: 16px;";
|
|
|
|
const modelLabel = document.createElement("label");
|
|
modelLabel.textContent = getUIText("modelLabel");
|
|
modelLabel.style.cssText = `display: block; margin-bottom: 8px; font-weight: 600; color: ${colors.primaryText};`;
|
|
|
|
const modelSelect = document.createElement("select");
|
|
modelSelect.style.cssText = `
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
background: ${colors.inputBackground};
|
|
color: ${USE_CUSTOM_STYLING ? 'black' : colors.primaryText};
|
|
`;
|
|
|
|
// Store models for selection handler
|
|
let loadedModels = [];
|
|
|
|
// Load models
|
|
const loadModels = async () => {
|
|
try {
|
|
console.log("Loading models for Hermes modal...");
|
|
const models = await fetchModelsFromEndpoints();
|
|
console.log("Models loaded:", models);
|
|
loadedModels = models; // Store for selection handler
|
|
modelSelect.innerHTML = "";
|
|
|
|
if (models && models.length > 0) {
|
|
models.forEach((model) => {
|
|
const option = document.createElement("option");
|
|
option.value = model.uniqueId;
|
|
option.textContent = `${model.endpointId} | ${model.modelName}`;
|
|
modelSelect.appendChild(option);
|
|
});
|
|
|
|
// Load saved model from localStorage or use current selection
|
|
const savedModel = localStorage.getItem("selectedModel");
|
|
const savedEndpoint = localStorage.getItem("selectedEndpoint");
|
|
const currentModel = getSelectedModel();
|
|
|
|
if (savedModel && savedEndpoint) {
|
|
const savedOption = Array.from(modelSelect.options).find(
|
|
(opt) =>
|
|
opt.textContent.includes(savedModel) &&
|
|
opt.textContent.includes(savedEndpoint),
|
|
);
|
|
if (savedOption) {
|
|
modelSelect.value = savedOption.value;
|
|
console.log(
|
|
"Restored saved model:",
|
|
savedModel,
|
|
"from endpoint:",
|
|
savedEndpoint,
|
|
);
|
|
}
|
|
} else if (currentModel) {
|
|
const currentOption = Array.from(modelSelect.options).find((opt) =>
|
|
opt.textContent.includes(currentModel),
|
|
);
|
|
if (currentOption) {
|
|
modelSelect.value = currentOption.value;
|
|
}
|
|
}
|
|
} else {
|
|
const option = document.createElement("option");
|
|
option.textContent = "No models available";
|
|
option.disabled = true;
|
|
modelSelect.appendChild(option);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load models:", error);
|
|
const option = document.createElement("option");
|
|
option.textContent = "Error loading models";
|
|
option.disabled = true;
|
|
modelSelect.appendChild(option);
|
|
}
|
|
};
|
|
|
|
// Handle model selection changes
|
|
modelSelect.onchange = () => {
|
|
const selectedUniqueId = modelSelect.value;
|
|
const selectedModel = loadedModels.find(
|
|
(model) => model.uniqueId === selectedUniqueId,
|
|
);
|
|
if (selectedModel) {
|
|
// Update the global model selection
|
|
localStorage.setItem("selectedModel", selectedModel.modelName);
|
|
localStorage.setItem("selectedEndpoint", selectedModel.endpointId);
|
|
console.log("Model changed to:", selectedModel);
|
|
}
|
|
};
|
|
|
|
modelSection.appendChild(modelLabel);
|
|
modelSection.appendChild(modelSelect);
|
|
|
|
// Voice selection for TTS
|
|
const voiceSection = document.createElement("div");
|
|
voiceSection.style.cssText = "margin-bottom: 16px;";
|
|
|
|
const voiceLabel = document.createElement("label");
|
|
voiceLabel.textContent = getUIText("voiceLabel");
|
|
voiceLabel.style.cssText = `display: block; margin-bottom: 8px; font-weight: 600; color: ${colors.primaryText};`;
|
|
|
|
const voiceSelect = document.createElement("select");
|
|
voiceSelect.id = "hermes-voice-select";
|
|
voiceSelect.style.cssText = `
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
background: ${colors.inputBackground};
|
|
color: ${USE_CUSTOM_STYLING ? 'black' : colors.primaryText};
|
|
`;
|
|
|
|
const voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"];
|
|
voices.forEach((voice, index) => {
|
|
const option = document.createElement("option");
|
|
option.value = voice;
|
|
option.textContent = voice.charAt(0).toUpperCase() + voice.slice(1);
|
|
voiceSelect.appendChild(option);
|
|
});
|
|
|
|
// Load saved voice from localStorage or default to alloy
|
|
const savedVoice = localStorage.getItem("selectedVoice") || "alloy";
|
|
voiceSelect.value = savedVoice;
|
|
console.log("Voices loaded:", voices.length, "voices, selected:", savedVoice);
|
|
|
|
// Save voice selection to localStorage when changed
|
|
voiceSelect.onchange = () => {
|
|
localStorage.setItem("selectedVoice", voiceSelect.value);
|
|
console.log("Voice changed to:", voiceSelect.value);
|
|
};
|
|
|
|
voiceSection.appendChild(voiceLabel);
|
|
voiceSection.appendChild(voiceSelect);
|
|
|
|
// Language preference section
|
|
const languageSection = document.createElement("div");
|
|
languageSection.style.cssText = "margin-bottom: 16px;";
|
|
|
|
const languageLabel = document.createElement("label");
|
|
languageLabel.textContent = getUIText("languageLabel");
|
|
languageLabel.style.cssText = `display: block; margin-bottom: 8px; font-weight: 600; color: ${colors.primaryText};`;
|
|
|
|
// Create language selector
|
|
const { SUPPORTED_LANGUAGES, NATIVE_LANGUAGE_NAMES } = await import(
|
|
"./translation.js"
|
|
);
|
|
const currentLang = getUserLanguagePreference();
|
|
|
|
const languageSelect = document.createElement("select");
|
|
languageSelect.id = "hermes-language-select";
|
|
languageSelect.style.cssText = `
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
background: ${colors.inputBackground};
|
|
color: ${USE_CUSTOM_STYLING ? 'black' : colors.primaryText};
|
|
`;
|
|
|
|
// Add options for each supported language
|
|
Object.entries(SUPPORTED_LANGUAGES).forEach(([code, englishName]) => {
|
|
const option = document.createElement("option");
|
|
option.value = code;
|
|
const nativeName = NATIVE_LANGUAGE_NAMES[code];
|
|
option.textContent = `${nativeName} (${englishName})`;
|
|
|
|
if (code === currentLang) {
|
|
option.selected = true;
|
|
}
|
|
|
|
languageSelect.appendChild(option);
|
|
});
|
|
|
|
// Handle language change
|
|
languageSelect.onchange = () => {
|
|
const newLang = languageSelect.value;
|
|
setUserLanguagePreference(newLang);
|
|
|
|
// Show confirmation
|
|
const selectedOption = languageSelect.options[languageSelect.selectedIndex];
|
|
console.log(`Language changed to: ${selectedOption.textContent}`);
|
|
|
|
// Refresh all UI text in the modal
|
|
refreshModalUI();
|
|
|
|
// Notify user to refresh for full effect
|
|
setTimeout(() => {
|
|
const langName = SUPPORTED_LANGUAGES[newLang];
|
|
alert(getUIText("languageChanged", { lang: langName }));
|
|
}, 100);
|
|
};
|
|
|
|
languageSection.appendChild(languageLabel);
|
|
languageSection.appendChild(languageSelect);
|
|
|
|
// Action buttons
|
|
const actionsSection = document.createElement("div");
|
|
actionsSection.style.cssText = "margin-bottom: 16px;";
|
|
|
|
const actionsLabel = document.createElement("div");
|
|
actionsLabel.textContent = getUIText("quickActions");
|
|
actionsLabel.style.cssText =
|
|
"margin-bottom: 8px; font-weight: 600; color: #495057;";
|
|
|
|
const actionsGrid = document.createElement("div");
|
|
actionsGrid.style.cssText = `
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
gap: 8px;
|
|
`;
|
|
|
|
const actions = [
|
|
{ text: getUIText("refreshModels"), action: loadModels },
|
|
{
|
|
text: getUIText("clearChat"),
|
|
action: async () => {
|
|
if (confirm(getUIText("clearChatConfirm"))) {
|
|
chatBox.innerHTML = "";
|
|
await clearConversationHistory();
|
|
|
|
// Also clear the chat history in the chat.js module
|
|
const currentHistory = await getChatHistory();
|
|
const systemMsg = currentHistory.find((msg) => msg.role === "system");
|
|
const newHistory = systemMsg ? [systemMsg] : [];
|
|
await updateChatHistory(newHistory);
|
|
console.log("Chat history cleared, reset to system message only");
|
|
|
|
// Re-add intro message
|
|
await addIntroMessage();
|
|
}
|
|
},
|
|
},
|
|
];
|
|
|
|
actions.forEach(({ text, action }) => {
|
|
const btn = document.createElement("button");
|
|
btn.textContent = text;
|
|
btn.style.cssText = `
|
|
padding: 8px 12px;
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.buttonBorder};
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
color: ${colors.primaryText};
|
|
transition: all 0.2s;
|
|
white-space: nowrap;
|
|
`;
|
|
btn.onmouseover = () => {
|
|
btn.style.background = colors.buttonHover;
|
|
btn.style.borderColor = colors.buttonBorder;
|
|
};
|
|
btn.onmouseout = () => {
|
|
btn.style.background = colors.buttonBackground;
|
|
btn.style.borderColor = colors.buttonBorder;
|
|
};
|
|
btn.onclick = action;
|
|
actionsGrid.appendChild(btn);
|
|
});
|
|
|
|
actionsSection.appendChild(actionsLabel);
|
|
actionsSection.appendChild(actionsGrid);
|
|
|
|
settingsPanel.appendChild(modelSection);
|
|
settingsPanel.appendChild(voiceSection);
|
|
settingsPanel.appendChild(languageSection);
|
|
settingsPanel.appendChild(actionsSection);
|
|
|
|
// Load and save settings panel state
|
|
let settingsOpen = localStorage.getItem("hermesSettingsOpen") === "true";
|
|
settingsPanel.style.display = settingsOpen ? "block" : "none";
|
|
menuBtn.style.background = settingsOpen
|
|
? "rgba(255,255,255,0.3)"
|
|
: "rgba(255,255,255,0.2)";
|
|
|
|
menuBtn.onclick = () => {
|
|
settingsOpen = !settingsOpen;
|
|
settingsPanel.style.display = settingsOpen ? "block" : "none";
|
|
menuBtn.style.background = settingsOpen
|
|
? "rgba(255,255,255,0.3)"
|
|
: "rgba(255,255,255,0.2)";
|
|
localStorage.setItem("hermesSettingsOpen", settingsOpen.toString());
|
|
};
|
|
|
|
// Quick action buttons
|
|
const controls = document.createElement("div");
|
|
controls.style.cssText = `
|
|
flex-shrink: 0;
|
|
padding: 8px 16px;
|
|
background: ${colors.panelBackground};
|
|
border-top: 1px solid ${colors.dividerColor};
|
|
border-bottom: 1px solid ${colors.dividerColor};
|
|
display: flex;
|
|
gap: 8px;
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
`;
|
|
|
|
// HTML to Markdown converter function
|
|
function htmlToMarkdown(html) {
|
|
// Create a temporary element to parse HTML
|
|
const temp = document.createElement("div");
|
|
temp.innerHTML = html;
|
|
|
|
// Remove script and style elements
|
|
temp
|
|
.querySelectorAll("script, style, noscript")
|
|
.forEach((el) => el.remove());
|
|
|
|
const markdown = "";
|
|
|
|
function processNode(node) {
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
return node.textContent.trim();
|
|
}
|
|
|
|
if (node.nodeType !== Node.ELEMENT_NODE) return "";
|
|
|
|
const tag = node.tagName.toLowerCase();
|
|
const children = Array.from(node.childNodes).map(processNode).join("");
|
|
|
|
switch (tag) {
|
|
case "h1":
|
|
return `# ${children}\n\n`;
|
|
case "h2":
|
|
return `## ${children}\n\n`;
|
|
case "h3":
|
|
return `### ${children}\n\n`;
|
|
case "h4":
|
|
return `#### ${children}\n\n`;
|
|
case "h5":
|
|
return `##### ${children}\n\n`;
|
|
case "h6":
|
|
return `###### ${children}\n\n`;
|
|
case "p":
|
|
return `${children}\n\n`;
|
|
case "br":
|
|
return "\n";
|
|
case "strong":
|
|
case "b":
|
|
return `**${children}**`;
|
|
case "em":
|
|
case "i":
|
|
return `*${children}*`;
|
|
case "code":
|
|
return `\`${children}\``;
|
|
case "pre":
|
|
return `\`\`\`\n${children}\n\`\`\`\n\n`;
|
|
case "a": {
|
|
const href = node.getAttribute("href");
|
|
return href ? `[${children}](${href})` : children;
|
|
}
|
|
case "img": {
|
|
const src = node.getAttribute("src");
|
|
const alt = node.getAttribute("alt") || "";
|
|
return src ? `` : "";
|
|
}
|
|
case "ul":
|
|
case "ol":
|
|
return `${children}\n`;
|
|
case "li":
|
|
return `- ${children}\n`;
|
|
case "blockquote":
|
|
return `> ${children}\n\n`;
|
|
default:
|
|
return children;
|
|
}
|
|
}
|
|
|
|
return processNode(temp).trim();
|
|
}
|
|
|
|
let readPageAudio = null;
|
|
let isReadingPage = false;
|
|
let readPageContainer = null;
|
|
|
|
const controlActions = [
|
|
{
|
|
text: getUIText("readPage"),
|
|
action: async (button) => {
|
|
// If already playing, pause
|
|
if (readPageAudio && !readPageAudio.paused) {
|
|
readPageAudio.pause();
|
|
button.textContent = getUIText("readPage");
|
|
isReadingPage = false;
|
|
return;
|
|
}
|
|
|
|
// If paused, resume
|
|
if (
|
|
readPageAudio?.paused &&
|
|
readPageAudio.currentTime > 0
|
|
) {
|
|
readPageAudio.play();
|
|
button.textContent = "⏸️ Pause";
|
|
isReadingPage = true;
|
|
return;
|
|
}
|
|
|
|
// Prevent multiple simultaneous requests
|
|
if (isReadingPage) return;
|
|
|
|
// Start new reading
|
|
button.textContent = getUIText("processingText");
|
|
button.disabled = true;
|
|
isReadingPage = true;
|
|
|
|
try {
|
|
// Get page content
|
|
const { extractWebpageContent } = await import("./content.js");
|
|
const pageContent = extractWebpageContent();
|
|
|
|
// Generate TTS
|
|
const { speakText } = await import("./tts.js");
|
|
const result = await speakText(pageContent, "alloy", 1.0);
|
|
readPageAudio = result.audio;
|
|
|
|
// Set up audio event handlers
|
|
readPageAudio.onplay = () => {
|
|
button.textContent = "⏸️ Pause";
|
|
isReadingPage = true;
|
|
};
|
|
|
|
readPageAudio.onpause = () => {
|
|
button.textContent = "▶️ Resume";
|
|
};
|
|
|
|
readPageAudio.onended = () => {
|
|
button.textContent = getUIText("readPage");
|
|
isReadingPage = false;
|
|
readPageAudio = null;
|
|
// Remove download button container if it exists
|
|
if (readPageContainer) {
|
|
readPageContainer.remove();
|
|
readPageContainer = null;
|
|
}
|
|
};
|
|
|
|
// Create download button next to the read button
|
|
if (!readPageContainer) {
|
|
readPageContainer = document.createElement("div");
|
|
readPageContainer.style.cssText = `
|
|
display: inline-flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
`;
|
|
|
|
// Move the read button into the container
|
|
const parent = button.parentElement;
|
|
parent.insertBefore(readPageContainer, button);
|
|
readPageContainer.appendChild(button);
|
|
|
|
// Add download button
|
|
const downloadBtn = document.createElement("button");
|
|
downloadBtn.textContent = "💾";
|
|
downloadBtn.title = "Download audio";
|
|
downloadBtn.style.cssText = `
|
|
padding: 8px 16px;
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.buttonBorder};
|
|
border-radius: 20px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
white-space: nowrap;
|
|
color: ${colors.primaryText} !important;
|
|
transition: all 0.2s;
|
|
`;
|
|
downloadBtn.onmouseover = () => {
|
|
downloadBtn.style.background = colors.buttonHover;
|
|
downloadBtn.style.borderColor = colors.buttonBorder;
|
|
};
|
|
downloadBtn.onmouseout = () => {
|
|
downloadBtn.style.background = colors.buttonBackground;
|
|
downloadBtn.style.borderColor = colors.buttonBorder;
|
|
};
|
|
downloadBtn.onclick = () => {
|
|
if (readPageAudio) {
|
|
const url = readPageAudio.src;
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "page-audio.mp3";
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
};
|
|
|
|
readPageContainer.appendChild(downloadBtn);
|
|
}
|
|
|
|
// Auto-play the generated audio
|
|
try {
|
|
await readPageAudio.play();
|
|
} catch (playError) {
|
|
button.textContent = getUIText("readPage");
|
|
isReadingPage = false;
|
|
}
|
|
} catch (error) {
|
|
alert(getUIText("failedToReadPage", { error: error.message }));
|
|
button.textContent = getUIText("readPage");
|
|
isReadingPage = false;
|
|
} finally {
|
|
button.disabled = false;
|
|
}
|
|
},
|
|
},
|
|
{
|
|
text: getUIText("ttsAnything"),
|
|
action: async (btn) => {
|
|
if (typeof window.openTTSModal === 'function') {
|
|
window.openTTSModal();
|
|
} else {
|
|
// Fallback: dynamically import and call
|
|
try {
|
|
const { openTTSModal } = await import("./tts-modal.js");
|
|
openTTSModal();
|
|
} catch (error) {
|
|
console.error("Failed to open TTS modal:", error);
|
|
alert("TTS modal is not available at the moment.");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
text: getUIText("translationModal"),
|
|
action: async (btn) => {
|
|
if (typeof window.openTranslateModal === 'function') {
|
|
window.openTranslateModal();
|
|
} else {
|
|
// Fallback: dynamically import and call
|
|
try {
|
|
const { openTranslateModal } = await import("./translate-modal.js");
|
|
openTranslateModal();
|
|
} catch (error) {
|
|
console.error("Failed to open translate modal:", error);
|
|
alert("Translation modal is not available at the moment.");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
text: getUIText("smartTranslate"),
|
|
action: async (btn) => {
|
|
// Create dropdown instead of opening modal
|
|
if (
|
|
btn.nextElementSibling?.classList.contains("translate-dropdown")
|
|
) {
|
|
// Toggle existing dropdown
|
|
btn.nextElementSibling.remove();
|
|
return;
|
|
}
|
|
|
|
// Detect page language
|
|
btn.textContent = getUIText("detectingLanguage");
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
const currentLang = await detectPageLanguage();
|
|
btn.textContent = getUIText("smartTranslate");
|
|
btn.disabled = false;
|
|
|
|
// Import supported languages
|
|
const { SUPPORTED_LANGUAGES, translateCurrentPage } = await import(
|
|
"./translation.js"
|
|
);
|
|
|
|
// Create dropdown
|
|
const dropdown = document.createElement("div");
|
|
dropdown.className = "translate-dropdown";
|
|
dropdown.style.cssText = `
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
background: ${colors.panelBackground};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px ${colors.shadowColor};
|
|
z-index: 1000;
|
|
min-width: 200px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
margin-top: 4px;
|
|
`;
|
|
|
|
// Add current language indicator
|
|
const currentLangDiv = document.createElement("div");
|
|
currentLangDiv.style.cssText = `
|
|
padding: 8px 12px;
|
|
font-size: 12px;
|
|
color: ${colors.mutedText};
|
|
border-bottom: 1px solid ${colors.dividerColor};
|
|
background: ${colors.contentBackground};
|
|
`;
|
|
const currentLangName = SUPPORTED_LANGUAGES[currentLang] || "Unknown";
|
|
currentLangDiv.textContent = `Current: ${currentLangName} (${currentLang})`;
|
|
dropdown.appendChild(currentLangDiv);
|
|
|
|
// Add translation options
|
|
Object.entries(SUPPORTED_LANGUAGES).forEach(([code, name]) => {
|
|
// Skip current language
|
|
if (code === currentLang) return;
|
|
|
|
const option = document.createElement("div");
|
|
option.style.cssText = `
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
color: ${colors.primaryText};
|
|
font-size: 14px;
|
|
`;
|
|
option.textContent = `${name} (${code})`;
|
|
|
|
option.onmouseenter = () => {
|
|
option.style.background = colors.buttonHover;
|
|
};
|
|
option.onmouseleave = () => {
|
|
option.style.background = "transparent";
|
|
};
|
|
|
|
option.onclick = async () => {
|
|
try {
|
|
// Show loading
|
|
option.textContent = `Translating to ${name}...`;
|
|
option.style.pointerEvents = "none";
|
|
|
|
// Translate the page
|
|
const translatedHtml = await translateCurrentPage(code);
|
|
|
|
// Open in new tab
|
|
const newWindow = window.open("", "_blank");
|
|
newWindow.document.write(translatedHtml);
|
|
newWindow.document.close();
|
|
newWindow.document.title = `${document.title} (${name})`;
|
|
|
|
// Close dropdown
|
|
dropdown.remove();
|
|
} catch (error) {
|
|
alert(`Translation failed: ${error.message}`);
|
|
option.textContent = `${name} (${code})`;
|
|
option.style.pointerEvents = "auto";
|
|
}
|
|
};
|
|
|
|
dropdown.appendChild(option);
|
|
});
|
|
|
|
// Position dropdown relative to button
|
|
btn.style.position = "relative";
|
|
btn.parentElement.appendChild(dropdown);
|
|
|
|
// Close dropdown when clicking outside
|
|
const closeDropdown = (e) => {
|
|
if (!dropdown.contains(e.target) && e.target !== btn) {
|
|
dropdown.remove();
|
|
document.removeEventListener("click", closeDropdown);
|
|
}
|
|
};
|
|
setTimeout(
|
|
() => document.addEventListener("click", closeDropdown),
|
|
100,
|
|
);
|
|
} catch (error) {
|
|
btn.textContent = getUIText("smartTranslate");
|
|
btn.disabled = false;
|
|
alert(getUIText("languageDetectionFailed", { error: error.message }));
|
|
}
|
|
},
|
|
},
|
|
{
|
|
text: getUIText("fullChatRaw"),
|
|
action: async () => {
|
|
try {
|
|
const { getChatHistory } = await import("./chat.js");
|
|
const history = await getChatHistory();
|
|
let rawContent = "";
|
|
history.forEach((msg) => {
|
|
if (msg.role === "user") {
|
|
rawContent += `**You:** ${msg.content}\n\n`;
|
|
} else if (msg.role === "assistant") {
|
|
rawContent += `**AI:** ${msg.content}\n\n`;
|
|
}
|
|
});
|
|
await navigator.clipboard.writeText(rawContent);
|
|
alert(getUIText("fullChatCopied"));
|
|
} catch (error) {
|
|
alert(getUIText("failedToCopy", { error: error.message }));
|
|
}
|
|
},
|
|
},
|
|
{
|
|
text: getUIText("fullChatHTML"),
|
|
action: async () => {
|
|
try {
|
|
const { getChatHistory } = await import("./chat.js");
|
|
const history = await getChatHistory();
|
|
let htmlContent =
|
|
'<div style="font-family: system-ui, -apple-system, sans-serif;">';
|
|
history.forEach((msg) => {
|
|
if (msg.role === "user") {
|
|
htmlContent += `<div style="margin-bottom: 16px;"><strong>You:</strong><br>${marked.parse(msg.content)}</div>`;
|
|
} else if (msg.role === "assistant") {
|
|
htmlContent += `<div style="margin-bottom: 16px;"><strong>AI:</strong><br>${marked.parse(msg.content)}</div>`;
|
|
}
|
|
});
|
|
htmlContent += "</div>";
|
|
await navigator.clipboard.writeText(htmlContent);
|
|
alert(getUIText("fullChatHTMLCopied"));
|
|
} catch (error) {
|
|
alert(getUIText("failedToCopy", { error: error.message }));
|
|
}
|
|
},
|
|
},
|
|
{
|
|
text: getUIText("fullPageRaw"),
|
|
action: async () => {
|
|
try {
|
|
// Get the entire page HTML
|
|
const pageHtml = document.documentElement.outerHTML;
|
|
// Convert to markdown
|
|
const markdown = htmlToMarkdown(pageHtml);
|
|
await navigator.clipboard.writeText(markdown);
|
|
alert(getUIText("fullPageCopied"));
|
|
} catch (error) {
|
|
alert(getUIText("failedToCopyPage", { error: error.message }));
|
|
}
|
|
},
|
|
},
|
|
];
|
|
|
|
controlActions.forEach(({ text, action }) => {
|
|
const btn = document.createElement("button");
|
|
btn.textContent = text;
|
|
btn.style.cssText = `
|
|
padding: 8px 16px;
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.buttonBorder};
|
|
border-radius: 20px;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
font-size: 14px;
|
|
color: ${colors.primaryText} !important;
|
|
transition: all 0.2s;
|
|
`;
|
|
btn.onmouseover = () => {
|
|
btn.style.background = colors.buttonHover;
|
|
btn.style.borderColor = colors.buttonBorder;
|
|
};
|
|
btn.onmouseout = () => {
|
|
btn.style.background = colors.buttonBackground;
|
|
btn.style.borderColor = colors.buttonBorder;
|
|
};
|
|
btn.onclick = () => action(btn);
|
|
controls.appendChild(btn);
|
|
});
|
|
|
|
// Assemble modal structure
|
|
container.appendChild(header);
|
|
container.appendChild(settingsPanel);
|
|
container.appendChild(chatArea);
|
|
container.appendChild(controls);
|
|
|
|
// Input area
|
|
const inputArea = document.createElement("div");
|
|
inputArea.style.cssText = `
|
|
flex-shrink: 0;
|
|
padding: 16px;
|
|
background: ${colors.panelBackground};
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-end;
|
|
box-shadow: 0 -2px 10px ${colors.lightShadow};
|
|
`;
|
|
|
|
const input = document.createElement("textarea");
|
|
input.id = "modal-user-input";
|
|
input.placeholder = getUIText("typePlaceholder");
|
|
input.style.cssText = `
|
|
flex: 1;
|
|
min-height: 44px;
|
|
max-height: 120px;
|
|
padding: 12px;
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 24px;
|
|
resize: none;
|
|
font-size: 16px;
|
|
font-family: inherit;
|
|
line-height: 1.4;
|
|
outline: none;
|
|
background: ${colors.inputBackground};
|
|
color: ${USE_CUSTOM_STYLING ? 'black' : colors.primaryText};
|
|
transition: border-color 0.2s;
|
|
`;
|
|
input.onfocus = () => (input.style.borderColor = colors.focusBorder);
|
|
input.onblur = () => (input.style.borderColor = colors.borderColor);
|
|
|
|
// Auto-resize textarea
|
|
input.oninput = () => {
|
|
input.style.height = "auto";
|
|
input.style.height = `${Math.min(input.scrollHeight, 120)}px`;
|
|
};
|
|
|
|
const sendBtn = document.createElement("button");
|
|
sendBtn.textContent = "→";
|
|
sendBtn.style.cssText = `
|
|
width: 44px;
|
|
height: 44px;
|
|
padding: 0;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: transform 0.2s;
|
|
flex-shrink: 0;
|
|
`;
|
|
sendBtn.onmouseover = () => (sendBtn.style.transform = "scale(1.05)");
|
|
sendBtn.onmouseout = () => (sendBtn.style.transform = "scale(1)");
|
|
|
|
// Handle message sending
|
|
const sendMessageHandler = async () => {
|
|
const message = input.value.trim();
|
|
if (!message) return;
|
|
|
|
input.value = "";
|
|
input.style.height = "44px";
|
|
|
|
// Add user message
|
|
const userMsg = document.createElement("div");
|
|
userMsg.className = "user-message";
|
|
userMsg.style.cssText = `
|
|
align-self: flex-end;
|
|
background: ${colors.userMessageBg};
|
|
color: ${colors.userMessageText};
|
|
padding: 12px 16px;
|
|
border-radius: 18px 18px 4px 18px;
|
|
max-width: 80%;
|
|
word-wrap: break-word;
|
|
`;
|
|
|
|
// Add message text and actions inside the bubble
|
|
const messageText = document.createElement("div");
|
|
messageText.textContent = message;
|
|
messageText.style.cssText = "margin-bottom: 8px;";
|
|
|
|
const userMessageActions = document.createElement("div");
|
|
userMessageActions.style.cssText = `
|
|
display: flex;
|
|
gap: 8px;
|
|
opacity: 0.8;
|
|
justify-content: flex-end;
|
|
`;
|
|
|
|
const userTtsBtn = document.createElement("button");
|
|
userTtsBtn.textContent = "🔊";
|
|
userTtsBtn.title = "Read aloud";
|
|
userTtsBtn.style.cssText = `
|
|
background: ${colors.actionBg};
|
|
border: 1px solid ${colors.actionBorder};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.userMessageText};
|
|
transition: all 0.2s;
|
|
`;
|
|
userTtsBtn.onmouseenter = () => {
|
|
userTtsBtn.style.background = colors.actionBgHover;
|
|
};
|
|
userTtsBtn.onmouseleave = () => {
|
|
userTtsBtn.style.background = colors.actionBg;
|
|
};
|
|
|
|
// Store audio for pause/resume functionality
|
|
let userAudio = null;
|
|
let isPlaying = false;
|
|
|
|
userTtsBtn.onclick = async () => {
|
|
const modalVoiceSelect = document.getElementById("hermes-voice-select");
|
|
const selectedVoice = modalVoiceSelect?.value || "alloy";
|
|
|
|
// If audio is already playing, pause it
|
|
if (userAudio && !userAudio.paused) {
|
|
userAudio.pause();
|
|
userTtsBtn.textContent = "🔊";
|
|
isPlaying = false;
|
|
return;
|
|
}
|
|
|
|
// If audio exists and is paused, resume it
|
|
if (userAudio?.paused && userAudio.currentTime > 0) {
|
|
userAudio.play();
|
|
userTtsBtn.textContent = "⏸️";
|
|
isPlaying = true;
|
|
return;
|
|
}
|
|
|
|
// Generate new audio
|
|
const originalText = userTtsBtn.textContent;
|
|
userTtsBtn.textContent = "⏳";
|
|
userTtsBtn.disabled = true;
|
|
|
|
try {
|
|
const result = await speakChatText(message, selectedVoice, 1.0);
|
|
userAudio = result.audio;
|
|
|
|
// Add download button if not already present
|
|
if (!userMessageActions.querySelector('.download-btn')) {
|
|
const downloadBtn = document.createElement("button");
|
|
downloadBtn.textContent = "💾";
|
|
downloadBtn.title = "Download MP3";
|
|
downloadBtn.className = "download-btn";
|
|
downloadBtn.style.cssText = `
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.userMessageText};
|
|
transition: all 0.2s;
|
|
margin-left: 4px;
|
|
`;
|
|
downloadBtn.onclick = async () => {
|
|
try {
|
|
const { generateTitleForTTS } = await import("./tts.js");
|
|
const filename = await generateTitleForTTS(message);
|
|
const a = document.createElement("a");
|
|
a.href = result.blobUrl;
|
|
a.download = `${filename}.mp3`;
|
|
a.click();
|
|
} catch (error) {
|
|
console.error("Error generating filename:", error);
|
|
const a = document.createElement("a");
|
|
a.href = result.blobUrl;
|
|
a.download = "user-message.mp3";
|
|
a.click();
|
|
}
|
|
};
|
|
// Insert download button right after the play button (before delete button)
|
|
const userDeleteBtn = userMessageActions.querySelector('button[title="Delete message"]');
|
|
if (userDeleteBtn) {
|
|
userMessageActions.insertBefore(downloadBtn, userDeleteBtn);
|
|
} else {
|
|
userMessageActions.appendChild(downloadBtn);
|
|
}
|
|
}
|
|
|
|
// Set up audio event handlers
|
|
userAudio.onplay = () => {
|
|
userTtsBtn.textContent = "⏸️";
|
|
isPlaying = true;
|
|
};
|
|
|
|
userAudio.onpause = () => {
|
|
userTtsBtn.textContent = "🔊";
|
|
isPlaying = false;
|
|
};
|
|
|
|
userAudio.onended = () => {
|
|
userTtsBtn.textContent = "🔊";
|
|
isPlaying = false;
|
|
userAudio = null;
|
|
};
|
|
|
|
// Auto-play the generated audio
|
|
try {
|
|
await userAudio.play();
|
|
} catch (playError) {
|
|
userTtsBtn.textContent = "🔊";
|
|
}
|
|
} catch (error) {
|
|
alert(`User TTS failed: ${error.message}`);
|
|
userTtsBtn.textContent = originalText;
|
|
} finally {
|
|
userTtsBtn.disabled = false;
|
|
}
|
|
};
|
|
|
|
const userDeleteBtn = document.createElement("button");
|
|
userDeleteBtn.textContent = "🗑️";
|
|
userDeleteBtn.title = "Delete message";
|
|
userDeleteBtn.style.cssText = `
|
|
background: ${colors.actionBg};
|
|
border: 1px solid ${colors.actionBorder};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.userMessageText};
|
|
transition: all 0.2s;
|
|
`;
|
|
userDeleteBtn.onmouseenter = () => {
|
|
userDeleteBtn.style.background = colors.actionBgHover;
|
|
};
|
|
userDeleteBtn.onmouseleave = () => {
|
|
userDeleteBtn.style.background = colors.actionBg;
|
|
};
|
|
userDeleteBtn.onclick = async () => {
|
|
// Remove from chat history and localStorage
|
|
const { loadConversationHistory, saveConversationHistory } = await import("./storage.js");
|
|
const { updateChatHistory } = await import("./chat.js");
|
|
|
|
const currentHistory = await loadConversationHistory();
|
|
const updatedHistory = currentHistory.filter(
|
|
(msg) => !(msg.role === "user" && msg.content === message),
|
|
);
|
|
|
|
// Update both localStorage and chat.js module
|
|
await saveConversationHistory(updatedHistory);
|
|
await updateChatHistory(updatedHistory);
|
|
|
|
userMsg.remove();
|
|
};
|
|
|
|
userMessageActions.appendChild(userTtsBtn);
|
|
userMessageActions.appendChild(userDeleteBtn);
|
|
userMsg.appendChild(messageText);
|
|
userMsg.appendChild(userMessageActions);
|
|
chatBox.appendChild(userMsg);
|
|
|
|
// Add AI response placeholder
|
|
const aiMsg = document.createElement("div");
|
|
aiMsg.style.cssText = `
|
|
align-self: flex-start;
|
|
background: ${colors.aiMessageBg};
|
|
color: ${colors.aiMessageText};
|
|
padding: 12px 16px;
|
|
border-radius: 18px 18px 18px 4px;
|
|
max-width: 80%;
|
|
box-shadow: 0 1px 2px ${colors.lightShadow};
|
|
`;
|
|
aiMsg.innerHTML = `<em style="color: ${colors.mutedText};">Thinking...</em>`;
|
|
chatBox.appendChild(aiMsg);
|
|
|
|
chatArea.scrollTop = chatArea.scrollHeight;
|
|
|
|
try {
|
|
let response = "";
|
|
for await (const chunk of sendMessage(message)) {
|
|
response += chunk;
|
|
aiMsg.innerHTML = marked.parse(response);
|
|
addCodeBlockCopyButtons(aiMsg);
|
|
}
|
|
|
|
// Add the AI response to chat history manually since sendMessage generator doesn't do it
|
|
const { getChatHistory, updateChatHistory } = await import("./chat.js");
|
|
const currentHistory = await getChatHistory();
|
|
currentHistory.push({ role: "assistant", content: response });
|
|
await updateChatHistory(currentHistory);
|
|
|
|
// Save conversation history after successful response
|
|
await saveConversationHistory(currentHistory);
|
|
console.log(
|
|
"Conversation history saved:",
|
|
currentHistory.length,
|
|
"messages",
|
|
);
|
|
console.log(
|
|
"Saved history content:",
|
|
currentHistory.map((msg) => ({
|
|
role: msg.role,
|
|
content: `${msg.content.substring(0, 50)}...`,
|
|
})),
|
|
);
|
|
|
|
// Add TTS and delete buttons to AI message
|
|
const messageActions = document.createElement("div");
|
|
messageActions.style.cssText = `
|
|
margin-top: 8px;
|
|
display: flex;
|
|
gap: 8px;
|
|
opacity: 0.7;
|
|
`;
|
|
|
|
const ttsBtn = document.createElement("button");
|
|
ttsBtn.textContent = "🔊";
|
|
ttsBtn.title = "Read aloud";
|
|
ttsBtn.style.cssText = `
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.primaryText};
|
|
transition: all 0.2s;
|
|
`;
|
|
// Store audio for pause/resume functionality
|
|
let currentAudio = null;
|
|
let isPlaying = false;
|
|
|
|
ttsBtn.onclick = async () => {
|
|
// Find voice selection from the modal by ID
|
|
const modalVoiceSelect = document.getElementById("hermes-voice-select");
|
|
const selectedVoice = modalVoiceSelect?.value || "alloy";
|
|
|
|
// If audio is already playing, pause it
|
|
if (currentAudio && !currentAudio.paused) {
|
|
currentAudio.pause();
|
|
ttsBtn.textContent = "🔊";
|
|
isPlaying = false;
|
|
console.log("🔊 CHAT TTS: Paused");
|
|
return;
|
|
}
|
|
|
|
// If audio exists and is paused, resume it
|
|
if (
|
|
currentAudio?.paused &&
|
|
currentAudio.currentTime > 0
|
|
) {
|
|
currentAudio.play();
|
|
ttsBtn.textContent = "⏸️";
|
|
isPlaying = true;
|
|
console.log("🔊 CHAT TTS: Resumed");
|
|
return;
|
|
}
|
|
|
|
// Generate new audio
|
|
console.log("🔊 CHAT TTS: Generating new audio");
|
|
const originalText = ttsBtn.textContent;
|
|
ttsBtn.textContent = "⏳";
|
|
ttsBtn.disabled = true;
|
|
|
|
try {
|
|
const result = await speakChatText(response, selectedVoice, 1.0);
|
|
currentAudio = result.audio;
|
|
|
|
// Add download button if not already present
|
|
if (!messageActions.querySelector('.download-btn')) {
|
|
const downloadBtn = document.createElement("button");
|
|
downloadBtn.textContent = "💾";
|
|
downloadBtn.title = "Download MP3";
|
|
downloadBtn.className = "download-btn";
|
|
downloadBtn.style.cssText = `
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.primaryText};
|
|
transition: all 0.2s;
|
|
margin-left: 4px;
|
|
`;
|
|
downloadBtn.onclick = async () => {
|
|
try {
|
|
const { generateTitleForTTS } = await import("./tts.js");
|
|
const filename = await generateTitleForTTS(response);
|
|
const a = document.createElement("a");
|
|
a.href = result.blobUrl;
|
|
a.download = `${filename}.mp3`;
|
|
a.click();
|
|
} catch (error) {
|
|
console.error("Error generating filename:", error);
|
|
const a = document.createElement("a");
|
|
a.href = result.blobUrl;
|
|
a.download = "ai-response.mp3";
|
|
a.click();
|
|
}
|
|
};
|
|
// Insert download button right after the play button (before copy buttons)
|
|
const copyRawBtn = messageActions.querySelector('button[title="Copy raw response"]');
|
|
if (copyRawBtn) {
|
|
messageActions.insertBefore(downloadBtn, copyRawBtn);
|
|
} else {
|
|
messageActions.appendChild(downloadBtn);
|
|
}
|
|
}
|
|
|
|
// Set up audio event handlers
|
|
currentAudio.onplay = () => {
|
|
ttsBtn.textContent = "⏸️";
|
|
isPlaying = true;
|
|
};
|
|
|
|
currentAudio.onpause = () => {
|
|
ttsBtn.textContent = "🔊";
|
|
isPlaying = false;
|
|
};
|
|
|
|
currentAudio.onended = () => {
|
|
ttsBtn.textContent = "🔊";
|
|
isPlaying = false;
|
|
currentAudio = null;
|
|
};
|
|
|
|
// Auto-play the generated audio
|
|
try {
|
|
await currentAudio.play();
|
|
console.log("🔊 CHAT TTS: Playing new audio");
|
|
} catch (playError) {
|
|
console.log("🔊 CHAT TTS: Auto-play blocked:", playError.message);
|
|
ttsBtn.textContent = "🔊";
|
|
}
|
|
} catch (error) {
|
|
console.error("🔊 CHAT TTS: Failed with error:", error);
|
|
alert(`Chat TTS failed: ${error.message}`);
|
|
ttsBtn.textContent = originalText;
|
|
} finally {
|
|
ttsBtn.disabled = false;
|
|
}
|
|
};
|
|
|
|
const deleteBtn = document.createElement("button");
|
|
deleteBtn.textContent = "🗑️";
|
|
deleteBtn.title = "Delete message";
|
|
deleteBtn.style.cssText = `
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.primaryText};
|
|
transition: all 0.2s;
|
|
`;
|
|
deleteBtn.onclick = async () => {
|
|
// Remove both user and AI messages from history and localStorage
|
|
const { loadConversationHistory, saveConversationHistory } = await import("./storage.js");
|
|
const { updateChatHistory } = await import("./chat.js");
|
|
|
|
const currentHistory = await loadConversationHistory();
|
|
const updatedHistory = currentHistory.filter(
|
|
(msg) => !(
|
|
(msg.role === "user" && msg.content === message) ||
|
|
(msg.role === "assistant" && msg.content === response)
|
|
)
|
|
);
|
|
|
|
// Update both localStorage and chat.js module
|
|
await saveConversationHistory(updatedHistory);
|
|
await updateChatHistory(updatedHistory);
|
|
|
|
userMsg.remove();
|
|
aiMsg.remove();
|
|
};
|
|
|
|
// Copy Raw (Markdown) button
|
|
const copyRawBtn = document.createElement("button");
|
|
copyRawBtn.textContent = "📋";
|
|
copyRawBtn.title = "Copy raw (Markdown)";
|
|
copyRawBtn.style.cssText = `
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.primaryText};
|
|
transition: all 0.2s;
|
|
`;
|
|
copyRawBtn.onclick = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(response);
|
|
const originalText = copyRawBtn.textContent;
|
|
copyRawBtn.textContent = "✓";
|
|
setTimeout(() => {
|
|
copyRawBtn.textContent = originalText;
|
|
}, 1000);
|
|
} catch (error) {
|
|
alert(`Failed to copy: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// Copy HTML button
|
|
const copyHtmlBtn = document.createElement("button");
|
|
copyHtmlBtn.textContent = "📄";
|
|
copyHtmlBtn.title = "Copy HTML";
|
|
copyHtmlBtn.style.cssText = `
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.primaryText};
|
|
transition: all 0.2s;
|
|
`;
|
|
copyHtmlBtn.onclick = async () => {
|
|
try {
|
|
const htmlContent = marked.parse(response);
|
|
await navigator.clipboard.writeText(htmlContent);
|
|
const originalText = copyHtmlBtn.textContent;
|
|
copyHtmlBtn.textContent = "✓";
|
|
setTimeout(() => {
|
|
copyHtmlBtn.textContent = originalText;
|
|
}, 1000);
|
|
} catch (error) {
|
|
alert(`Failed to copy: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
messageActions.appendChild(ttsBtn);
|
|
messageActions.appendChild(copyRawBtn);
|
|
messageActions.appendChild(copyHtmlBtn);
|
|
messageActions.appendChild(deleteBtn);
|
|
aiMsg.appendChild(messageActions);
|
|
} catch (error) {
|
|
aiMsg.innerHTML = `<span style="color: #dc3545;">Error: ${error.message}</span>`;
|
|
}
|
|
|
|
chatArea.scrollTop = chatArea.scrollHeight;
|
|
};
|
|
|
|
sendBtn.onclick = sendMessageHandler;
|
|
input.onkeydown = (e) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessageHandler();
|
|
}
|
|
};
|
|
|
|
inputArea.appendChild(input);
|
|
inputArea.appendChild(sendBtn);
|
|
container.appendChild(inputArea);
|
|
modal.appendChild(container);
|
|
|
|
document.body.appendChild(modal);
|
|
modal.showModal();
|
|
|
|
modal.addEventListener("click", (e) => {
|
|
if (e.target === modal) {
|
|
modal.close();
|
|
document.body.removeChild(modal);
|
|
uncloseaiEmbeddedModalOpen = false;
|
|
}
|
|
});
|
|
|
|
// Theme change monitoring
|
|
const updateModalTheme = () => {
|
|
const newTheme = detectCurrentTheme();
|
|
const newColors = getThemeColors(newTheme);
|
|
modal.setAttribute("data-theme", newTheme);
|
|
|
|
// Update modal background and main color
|
|
modal.style.background = newColors.modalBackground;
|
|
modal.style.color = newColors.primaryText;
|
|
|
|
// Update chat area background
|
|
chatArea.style.background = newColors.contentBackground;
|
|
|
|
// Update settings panel background
|
|
settingsPanel.style.background = newColors.panelBackground;
|
|
settingsPanel.style.borderTopColor = newColors.dividerColor;
|
|
settingsPanel.style.borderBottomColor = newColors.dividerColor;
|
|
|
|
// Update input area
|
|
inputArea.style.background = newColors.panelBackground;
|
|
input.style.background = newColors.inputBackground;
|
|
input.style.color = newColors.primaryText;
|
|
input.style.borderColor = newColors.borderColor;
|
|
|
|
// Update controls panel
|
|
controls.style.background = newColors.panelBackground;
|
|
controls.style.borderTopColor = newColors.dividerColor;
|
|
controls.style.borderBottomColor = newColors.dividerColor;
|
|
|
|
// Update all control buttons
|
|
controls.querySelectorAll("button").forEach((btn) => {
|
|
btn.style.background = newColors.buttonBackground;
|
|
btn.style.borderColor = newColors.buttonBorder;
|
|
btn.style.color = `${newColors.primaryText} !important`;
|
|
|
|
// Re-attach hover handlers with new colors
|
|
btn.onmouseover = () => {
|
|
btn.style.background = newColors.buttonHover;
|
|
btn.style.borderColor = newColors.buttonBorder;
|
|
};
|
|
btn.onmouseout = () => {
|
|
btn.style.background = newColors.buttonBackground;
|
|
btn.style.borderColor = newColors.buttonBorder;
|
|
};
|
|
});
|
|
|
|
// Update all message bubbles
|
|
chatBox
|
|
.querySelectorAll('div[style*="align-self: flex-end"]')
|
|
.forEach((userMsg) => {
|
|
userMsg.style.background = newColors.userMessageBg;
|
|
userMsg.style.color = newColors.userMessageText;
|
|
});
|
|
|
|
chatBox
|
|
.querySelectorAll('div[style*="align-self: flex-start"]')
|
|
.forEach((aiMsg) => {
|
|
aiMsg.style.background = newColors.aiMessageBg;
|
|
aiMsg.style.color = newColors.aiMessageText;
|
|
aiMsg.style.boxShadow = `0 1px 2px ${newColors.lightShadow}`;
|
|
});
|
|
};
|
|
|
|
// Set up theme change listeners
|
|
const themeObserver = new MutationObserver(updateModalTheme);
|
|
themeObserver.observe(document.documentElement, {
|
|
attributes: true,
|
|
attributeFilter: ["data-theme"],
|
|
});
|
|
|
|
if (window.matchMedia) {
|
|
window
|
|
.matchMedia("(prefers-color-scheme: dark)")
|
|
.addEventListener("change", updateModalTheme);
|
|
}
|
|
|
|
// Clean up listeners when modal is closed
|
|
const originalClose = modal.close.bind(modal);
|
|
modal.close = () => {
|
|
themeObserver.disconnect();
|
|
if (window.matchMedia) {
|
|
window
|
|
.matchMedia("(prefers-color-scheme: dark)")
|
|
.removeEventListener("change", updateModalTheme);
|
|
}
|
|
originalClose();
|
|
};
|
|
|
|
input.focus();
|
|
|
|
// Load models on startup
|
|
loadModels();
|
|
|
|
// Load conversation history
|
|
const loadHistory = async () => {
|
|
try {
|
|
const history = await loadConversationHistory();
|
|
console.log("Raw history from localStorage:", history);
|
|
console.log("History length:", history ? history.length : 0);
|
|
if (history && history.length > 0) {
|
|
// Set up page context for existing conversation
|
|
const pageContent = extractWebpageContent();
|
|
const pageTitle = document.title || window.location.hostname;
|
|
const conversationContextAppend = `
|
|
|
|
PAGE CONTEXT FOR THIS CONVERSATION:
|
|
You are embedded on the webpage: "${pageTitle}"
|
|
URL: ${window.location.href}
|
|
|
|
FULL PAGE CONTENT:
|
|
${pageContent}
|
|
|
|
You have complete knowledge of this page content and can reference any details, names, topics, or information mentioned on this page. Answer questions about the page content accurately and helpfully.`;
|
|
|
|
setSystemMessageAppend(conversationContextAppend);
|
|
|
|
// Sync the loaded history with the chat.js module
|
|
const { updateChatHistory } = await import("./chat.js");
|
|
const { getSystemMessage } = await import("./config.js");
|
|
|
|
// Create new history with updated system message and loaded conversation
|
|
const newHistory = [
|
|
{ role: "system", content: getSystemMessage() },
|
|
...history,
|
|
];
|
|
|
|
// Update the chat.js module's history
|
|
await updateChatHistory(newHistory);
|
|
console.log(
|
|
"Chat history synced with loaded data:",
|
|
newHistory.length,
|
|
"messages",
|
|
);
|
|
console.log("History to display:", history);
|
|
// Display previous conversation
|
|
history.forEach((msg, index) => {
|
|
console.log(
|
|
`Displaying message ${index}:`,
|
|
msg.role,
|
|
msg.content.substring(0, 50),
|
|
);
|
|
if (msg.role === "user") {
|
|
const userMsg = document.createElement("div");
|
|
userMsg.className = "user-message";
|
|
userMsg.style.cssText = `
|
|
align-self: flex-end;
|
|
background: ${colors.userMessageBg};
|
|
color: ${colors.userMessageText};
|
|
padding: 12px 16px;
|
|
border-radius: 18px 18px 4px 18px;
|
|
max-width: 80%;
|
|
word-wrap: break-word;
|
|
`;
|
|
|
|
// Add message text and actions inside the bubble
|
|
const messageText = document.createElement("div");
|
|
messageText.textContent = msg.content;
|
|
messageText.style.cssText = "margin-bottom: 8px;";
|
|
|
|
const userMessageActions = document.createElement("div");
|
|
userMessageActions.style.cssText = `
|
|
display: flex;
|
|
gap: 8px;
|
|
opacity: 0.8;
|
|
justify-content: flex-end;
|
|
`;
|
|
|
|
const userTtsBtn = document.createElement("button");
|
|
userTtsBtn.textContent = "🔊";
|
|
userTtsBtn.title = "Read aloud";
|
|
userTtsBtn.style.cssText = `
|
|
background: ${colors.actionBg};
|
|
border: 1px solid ${colors.actionBorder};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.userMessageText};
|
|
transition: all 0.2s;
|
|
`;
|
|
userTtsBtn.onmouseenter = () => {
|
|
userTtsBtn.style.background = "rgba(255, 255, 255, 0.3)";
|
|
};
|
|
userTtsBtn.onmouseleave = () => {
|
|
userTtsBtn.style.background = "rgba(255, 255, 255, 0.2)";
|
|
};
|
|
|
|
// Store audio for pause/resume functionality
|
|
let historicalUserAudio = null;
|
|
|
|
userTtsBtn.onclick = async () => {
|
|
const modalVoiceSelect = document.getElementById(
|
|
"hermes-voice-select",
|
|
);
|
|
const selectedVoice = modalVoiceSelect?.value || "alloy";
|
|
|
|
// If audio is already playing, pause it
|
|
if (historicalUserAudio && !historicalUserAudio.paused) {
|
|
historicalUserAudio.pause();
|
|
userTtsBtn.textContent = "🔊";
|
|
return;
|
|
}
|
|
|
|
// If audio exists and is paused, resume it
|
|
if (
|
|
historicalUserAudio &&
|
|
historicalUserAudio.paused &&
|
|
historicalUserAudio.currentTime > 0
|
|
) {
|
|
historicalUserAudio.play();
|
|
userTtsBtn.textContent = "⏸️";
|
|
return;
|
|
}
|
|
|
|
// Generate new audio
|
|
const originalText = userTtsBtn.textContent;
|
|
userTtsBtn.textContent = "⏳";
|
|
userTtsBtn.disabled = true;
|
|
|
|
try {
|
|
const result = await speakChatText(
|
|
msg.content,
|
|
selectedVoice,
|
|
1.0,
|
|
);
|
|
historicalUserAudio = result.audio;
|
|
|
|
// Add download button if not already present
|
|
if (!userMessageActions.querySelector('.download-btn')) {
|
|
const downloadBtn = document.createElement("button");
|
|
downloadBtn.textContent = "💾";
|
|
downloadBtn.title = "Download MP3";
|
|
downloadBtn.className = "download-btn";
|
|
downloadBtn.style.cssText = `
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.userMessageText};
|
|
transition: all 0.2s;
|
|
margin-left: 4px;
|
|
`;
|
|
downloadBtn.onclick = async () => {
|
|
try {
|
|
const { generateTitleForTTS } = await import("./tts.js");
|
|
const filename = await generateTitleForTTS(msg.content);
|
|
const a = document.createElement("a");
|
|
a.href = result.blobUrl;
|
|
a.download = `${filename}.mp3`;
|
|
a.click();
|
|
} catch (error) {
|
|
console.error("Error generating filename:", error);
|
|
const a = document.createElement("a");
|
|
a.href = result.blobUrl;
|
|
a.download = "user-historical-message.mp3";
|
|
a.click();
|
|
}
|
|
};
|
|
// Insert download button right after the play button (before delete button)
|
|
const userDeleteBtn = userMessageActions.querySelector('button[title="Delete message"]');
|
|
if (userDeleteBtn) {
|
|
userMessageActions.insertBefore(downloadBtn, userDeleteBtn);
|
|
} else {
|
|
userMessageActions.appendChild(downloadBtn);
|
|
}
|
|
}
|
|
|
|
// Set up audio event handlers
|
|
historicalUserAudio.onplay = () => {
|
|
userTtsBtn.textContent = "⏸️";
|
|
};
|
|
|
|
historicalUserAudio.onpause = () => {
|
|
userTtsBtn.textContent = "🔊";
|
|
};
|
|
|
|
historicalUserAudio.onended = () => {
|
|
userTtsBtn.textContent = "🔊";
|
|
historicalUserAudio = null;
|
|
};
|
|
|
|
// Auto-play the generated audio
|
|
try {
|
|
await historicalUserAudio.play();
|
|
} catch (playError) {
|
|
userTtsBtn.textContent = "🔊";
|
|
}
|
|
} catch (error) {
|
|
alert(`User TTS failed: ${error.message}`);
|
|
userTtsBtn.textContent = originalText;
|
|
} finally {
|
|
userTtsBtn.disabled = false;
|
|
}
|
|
};
|
|
|
|
const userDeleteBtn = document.createElement("button");
|
|
userDeleteBtn.textContent = "🗑️";
|
|
userDeleteBtn.title = "Delete message";
|
|
userDeleteBtn.style.cssText = `
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.userMessageText};
|
|
transition: all 0.2s;
|
|
`;
|
|
userDeleteBtn.onmouseenter = () => {
|
|
userDeleteBtn.style.background = "rgba(255, 255, 255, 0.3)";
|
|
};
|
|
userDeleteBtn.onmouseleave = () => {
|
|
userDeleteBtn.style.background = "rgba(255, 255, 255, 0.2)";
|
|
};
|
|
userDeleteBtn.onclick = async () => {
|
|
// Remove from chat history by index
|
|
const currentHistory = await loadConversationHistory();
|
|
if (index >= 0 && index < currentHistory.length) {
|
|
currentHistory.splice(index, 1);
|
|
await saveConversationHistory(currentHistory);
|
|
|
|
// Update the chat.js module history to match localStorage
|
|
const { updateChatHistory } = await import("./chat.js");
|
|
const { getSystemMessage } = await import("./config.js");
|
|
const newHistory = [
|
|
{ role: "system", content: getSystemMessage() },
|
|
...currentHistory,
|
|
];
|
|
await updateChatHistory(newHistory);
|
|
}
|
|
|
|
userMsg.remove();
|
|
};
|
|
|
|
userMessageActions.appendChild(userTtsBtn);
|
|
userMessageActions.appendChild(userDeleteBtn);
|
|
userMsg.appendChild(messageText);
|
|
userMsg.appendChild(userMessageActions);
|
|
chatBox.appendChild(userMsg);
|
|
console.log("Added user message to chatBox");
|
|
} else if (msg.role === "assistant") {
|
|
const aiMsg = document.createElement("div");
|
|
aiMsg.style.cssText = `
|
|
align-self: flex-start;
|
|
background: ${colors.aiMessageBg};
|
|
color: ${colors.aiMessageText};
|
|
padding: 12px 16px;
|
|
border-radius: 18px 18px 18px 4px;
|
|
max-width: 80%;
|
|
box-shadow: 0 1px 2px ${colors.lightShadow};
|
|
`;
|
|
aiMsg.innerHTML = marked.parse(msg.content);
|
|
addCodeBlockCopyButtons(aiMsg);
|
|
|
|
// Add TTS and delete buttons to historical AI messages
|
|
const messageActions = document.createElement("div");
|
|
messageActions.style.cssText = `
|
|
margin-top: 8px;
|
|
display: flex;
|
|
gap: 8px;
|
|
opacity: 0.7;
|
|
`;
|
|
|
|
const ttsBtn = document.createElement("button");
|
|
ttsBtn.textContent = "🔊";
|
|
ttsBtn.title = "Read aloud";
|
|
ttsBtn.style.cssText = `
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.primaryText};
|
|
transition: all 0.2s;
|
|
`;
|
|
// Store audio for pause/resume functionality
|
|
let historicalAudio = null;
|
|
|
|
ttsBtn.onclick = async () => {
|
|
const modalVoiceSelect = document.getElementById(
|
|
"hermes-voice-select",
|
|
);
|
|
const selectedVoice = modalVoiceSelect?.value || "alloy";
|
|
|
|
// If audio is already playing, pause it
|
|
if (historicalAudio && !historicalAudio.paused) {
|
|
historicalAudio.pause();
|
|
ttsBtn.textContent = "🔊";
|
|
console.log("🔊 HISTORICAL TTS: Paused");
|
|
return;
|
|
}
|
|
|
|
// If audio exists and is paused, resume it
|
|
if (
|
|
historicalAudio &&
|
|
historicalAudio.paused &&
|
|
historicalAudio.currentTime > 0
|
|
) {
|
|
historicalAudio.play();
|
|
ttsBtn.textContent = "⏸️";
|
|
console.log("🔊 HISTORICAL TTS: Resumed");
|
|
return;
|
|
}
|
|
|
|
// Generate new audio
|
|
console.log("🔊 HISTORICAL TTS: Generating new audio");
|
|
const originalText = ttsBtn.textContent;
|
|
ttsBtn.textContent = "⏳";
|
|
ttsBtn.disabled = true;
|
|
|
|
try {
|
|
const result = await speakChatText(
|
|
msg.content,
|
|
selectedVoice,
|
|
1.0,
|
|
);
|
|
historicalAudio = result.audio;
|
|
|
|
// Add download button if not already present
|
|
if (!messageActions.querySelector('.download-btn')) {
|
|
const downloadBtn = document.createElement("button");
|
|
downloadBtn.textContent = "💾";
|
|
downloadBtn.title = "Download MP3";
|
|
downloadBtn.className = "download-btn";
|
|
downloadBtn.style.cssText = `
|
|
background: none;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all 0.2s;
|
|
margin-left: 4px;
|
|
`;
|
|
downloadBtn.onclick = async () => {
|
|
try {
|
|
const { generateTitleForTTS } = await import("./tts.js");
|
|
const filename = await generateTitleForTTS(msg.content);
|
|
const a = document.createElement("a");
|
|
a.href = result.blobUrl;
|
|
a.download = `${filename}.mp3`;
|
|
a.click();
|
|
} catch (error) {
|
|
console.error("Error generating filename:", error);
|
|
const a = document.createElement("a");
|
|
a.href = result.blobUrl;
|
|
a.download = "historical-message.mp3";
|
|
a.click();
|
|
}
|
|
};
|
|
// Insert download button right after the play button (before copy buttons)
|
|
const copyRawBtn = messageActions.querySelector('button[title="Copy raw response"]');
|
|
if (copyRawBtn) {
|
|
messageActions.insertBefore(downloadBtn, copyRawBtn);
|
|
} else {
|
|
messageActions.appendChild(downloadBtn);
|
|
}
|
|
}
|
|
|
|
// Set up audio event handlers
|
|
historicalAudio.onplay = () => {
|
|
ttsBtn.textContent = "⏸️";
|
|
};
|
|
|
|
historicalAudio.onpause = () => {
|
|
ttsBtn.textContent = "🔊";
|
|
};
|
|
|
|
historicalAudio.onended = () => {
|
|
ttsBtn.textContent = "🔊";
|
|
historicalAudio = null;
|
|
};
|
|
|
|
// Auto-play the generated audio
|
|
try {
|
|
await historicalAudio.play();
|
|
console.log("🔊 HISTORICAL TTS: Playing new audio");
|
|
} catch (playError) {
|
|
console.log(
|
|
"🔊 HISTORICAL TTS: Auto-play blocked:",
|
|
playError.message,
|
|
);
|
|
ttsBtn.textContent = "🔊";
|
|
}
|
|
} catch (error) {
|
|
console.error("🔊 HISTORICAL TTS: Failed with error:", error);
|
|
alert(`Historical TTS failed: ${error.message}`);
|
|
ttsBtn.textContent = originalText;
|
|
} finally {
|
|
ttsBtn.disabled = false;
|
|
}
|
|
};
|
|
|
|
const deleteBtn = document.createElement("button");
|
|
deleteBtn.textContent = "🗑️";
|
|
deleteBtn.title = "Delete message";
|
|
deleteBtn.style.cssText = `
|
|
background: none;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all 0.2s;
|
|
`;
|
|
deleteBtn.onclick = async () => {
|
|
// Remove from chat history and localStorage by index
|
|
const { loadConversationHistory, saveConversationHistory } = await import("./storage.js");
|
|
const { updateChatHistory } = await import("./chat.js");
|
|
|
|
const currentHistory = await loadConversationHistory();
|
|
if (index >= 0 && index < currentHistory.length) {
|
|
currentHistory.splice(index, 1);
|
|
|
|
// Update both localStorage and chat.js module
|
|
await saveConversationHistory(currentHistory);
|
|
await updateChatHistory(currentHistory);
|
|
}
|
|
|
|
aiMsg.remove();
|
|
};
|
|
|
|
// Copy Raw (Markdown) button
|
|
const copyRawBtn = document.createElement("button");
|
|
copyRawBtn.textContent = "📋";
|
|
copyRawBtn.title = "Copy raw (Markdown)";
|
|
copyRawBtn.style.cssText = `
|
|
background: none;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all 0.2s;
|
|
`;
|
|
copyRawBtn.onclick = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(msg.content);
|
|
const originalText = copyRawBtn.textContent;
|
|
copyRawBtn.textContent = "✓";
|
|
setTimeout(() => {
|
|
copyRawBtn.textContent = originalText;
|
|
}, 1000);
|
|
} catch (error) {
|
|
alert(`Failed to copy: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// Copy HTML button
|
|
const copyHtmlBtn = document.createElement("button");
|
|
copyHtmlBtn.textContent = "📄";
|
|
copyHtmlBtn.title = "Copy HTML";
|
|
copyHtmlBtn.style.cssText = `
|
|
background: none;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all 0.2s;
|
|
`;
|
|
copyHtmlBtn.onclick = async () => {
|
|
try {
|
|
const htmlContent = marked.parse(msg.content);
|
|
await navigator.clipboard.writeText(htmlContent);
|
|
const originalText = copyHtmlBtn.textContent;
|
|
copyHtmlBtn.textContent = "✓";
|
|
setTimeout(() => {
|
|
copyHtmlBtn.textContent = originalText;
|
|
}, 1000);
|
|
} catch (error) {
|
|
alert(`Failed to copy: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
messageActions.appendChild(ttsBtn);
|
|
messageActions.appendChild(copyRawBtn);
|
|
messageActions.appendChild(copyHtmlBtn);
|
|
messageActions.appendChild(deleteBtn);
|
|
aiMsg.appendChild(messageActions);
|
|
|
|
chatBox.appendChild(aiMsg);
|
|
console.log("Added AI message to chatBox with TTS buttons");
|
|
}
|
|
});
|
|
chatArea.scrollTop = chatArea.scrollHeight;
|
|
} else {
|
|
// Add intro message if no history
|
|
await addIntroMessage();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load conversation history:", error);
|
|
await addIntroMessage();
|
|
}
|
|
};
|
|
|
|
const addIntroMessage = async () => {
|
|
const introMsg = document.createElement("div");
|
|
introMsg.style.cssText = `
|
|
align-self: flex-start;
|
|
background: white;
|
|
padding: 12px 16px;
|
|
border-radius: 18px 18px 18px 4px;
|
|
max-width: 80%;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
|
color: #495057;
|
|
`;
|
|
|
|
// Show loading message first
|
|
introMsg.innerHTML =
|
|
'<em style="color: #6c757d;">Analyzing page and generating welcome message...</em>';
|
|
chatBox.appendChild(introMsg);
|
|
|
|
try {
|
|
// Get page content for context
|
|
const pageContent = extractWebpageContent();
|
|
const pageTitle = document.title || window.location.hostname;
|
|
|
|
// FIRST: Generate intro message with specialized intro system prompt
|
|
// Get user's preferred language and localize the system prompt
|
|
const userLang = getUserLanguagePreference();
|
|
const languageName = NATIVE_LANGUAGE_NAMES[userLang] || "English";
|
|
|
|
const introSystemPrompt = `${getUIText("systemPromptIntro")}
|
|
|
|
PAGE INFORMATION:
|
|
Title: "${pageTitle}"
|
|
URL: ${window.location.href}
|
|
|
|
FULL PAGE CONTENT:
|
|
${pageContent}
|
|
|
|
Generate a 3-paragraph introduction that:
|
|
${getUIText("systemPromptTask1")}
|
|
${getUIText("systemPromptTask2")}
|
|
${getUIText("systemPromptTask3")}
|
|
|
|
${getUIText("systemPromptInstructions")} ${languageName}.`;
|
|
|
|
const introPrompt = "Generate the welcoming introduction message now.";
|
|
|
|
// Create temporary chat history for intro generation
|
|
const introHistory = [
|
|
{ role: "system", content: introSystemPrompt },
|
|
{ role: "user", content: introPrompt },
|
|
];
|
|
|
|
// Generate intro with specialized system prompt
|
|
let response = "";
|
|
for await (const chunk of sendMessageWithCustomHistory(introHistory)) {
|
|
response += chunk;
|
|
introMsg.innerHTML = marked.parse(response);
|
|
addCodeBlockCopyButtons(introMsg);
|
|
}
|
|
|
|
// SECOND: Set up conversation system prompt for follow-up messages
|
|
const conversationContextAppend = `
|
|
|
|
PAGE CONTEXT FOR THIS CONVERSATION:
|
|
You are embedded on the webpage: "${pageTitle}"
|
|
URL: ${window.location.href}
|
|
|
|
FULL PAGE CONTENT:
|
|
${pageContent}
|
|
|
|
You have complete knowledge of this page content and can reference any details, names, topics, or information mentioned on this page. Answer questions about the page content accurately and helpfully.`;
|
|
|
|
setSystemMessageAppend(conversationContextAppend);
|
|
|
|
// Update the chat.js module with the intro message
|
|
const { updateChatHistory, getChatHistory } = await import("./chat.js");
|
|
const { getSystemMessage } = await import("./config.js");
|
|
|
|
// Create new history with system message and intro
|
|
const newHistory = [
|
|
{ role: "system", content: getSystemMessage() },
|
|
{ role: "assistant", content: response },
|
|
];
|
|
|
|
// Update the chat.js module's history
|
|
await updateChatHistory(newHistory);
|
|
console.log("Added intro message to chat history");
|
|
|
|
// Add TTS and delete buttons to intro message
|
|
const messageActions = document.createElement("div");
|
|
messageActions.style.cssText = `
|
|
margin-top: 8px;
|
|
display: flex;
|
|
gap: 8px;
|
|
opacity: 0.7;
|
|
`;
|
|
|
|
const ttsBtn = document.createElement("button");
|
|
ttsBtn.textContent = "🔊";
|
|
ttsBtn.title = "Read aloud";
|
|
ttsBtn.style.cssText = `
|
|
background: none;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all 0.2s;
|
|
`;
|
|
|
|
// Store audio for pause/resume functionality
|
|
let introAudio = null;
|
|
|
|
ttsBtn.onclick = async () => {
|
|
const modalVoiceSelect = document.getElementById("hermes-voice-select");
|
|
const selectedVoice = modalVoiceSelect?.value || "alloy";
|
|
|
|
// If audio is already playing, pause it
|
|
if (introAudio && !introAudio.paused) {
|
|
introAudio.pause();
|
|
ttsBtn.textContent = "🔊";
|
|
console.log("🔊 INTRO TTS: Paused");
|
|
return;
|
|
}
|
|
|
|
// If audio exists and is paused, resume it
|
|
if (introAudio && introAudio.paused && introAudio.currentTime > 0) {
|
|
introAudio.play();
|
|
ttsBtn.textContent = "⏸️";
|
|
console.log("🔊 INTRO TTS: Resumed");
|
|
return;
|
|
}
|
|
|
|
// Generate new audio
|
|
console.log("🔊 INTRO TTS: Generating new audio");
|
|
const originalText = ttsBtn.textContent;
|
|
ttsBtn.textContent = "⏳";
|
|
ttsBtn.disabled = true;
|
|
|
|
try {
|
|
const result = await speakChatText(response, selectedVoice, 1.0);
|
|
introAudio = result.audio;
|
|
|
|
// Set up audio event handlers
|
|
introAudio.onplay = () => {
|
|
ttsBtn.textContent = "⏸️";
|
|
};
|
|
|
|
introAudio.onpause = () => {
|
|
ttsBtn.textContent = "🔊";
|
|
};
|
|
|
|
introAudio.onended = () => {
|
|
ttsBtn.textContent = "🔊";
|
|
introAudio = null;
|
|
};
|
|
|
|
// Auto-play the generated audio
|
|
try {
|
|
await introAudio.play();
|
|
console.log("🔊 INTRO TTS: Playing new audio");
|
|
} catch (playError) {
|
|
console.log("🔊 INTRO TTS: Auto-play blocked:", playError.message);
|
|
ttsBtn.textContent = "🔊";
|
|
}
|
|
} catch (error) {
|
|
console.error("🔊 INTRO TTS: Failed with error:", error);
|
|
alert(`Intro TTS failed: ${error.message}`);
|
|
ttsBtn.textContent = originalText;
|
|
} finally {
|
|
ttsBtn.disabled = false;
|
|
}
|
|
};
|
|
|
|
const deleteBtn = document.createElement("button");
|
|
deleteBtn.textContent = "🗑️";
|
|
deleteBtn.title = "Delete message";
|
|
deleteBtn.style.cssText = `
|
|
background: none;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all 0.2s;
|
|
`;
|
|
deleteBtn.onclick = async () => {
|
|
// Remove intro message from localStorage
|
|
const { loadConversationHistory, saveConversationHistory } = await import("./storage.js");
|
|
const { updateChatHistory } = await import("./chat.js");
|
|
|
|
const currentHistory = await loadConversationHistory();
|
|
const updatedHistory = currentHistory.filter(
|
|
(msg) => !(msg.role === "assistant" && msg.content === response)
|
|
);
|
|
|
|
// Update both localStorage and chat.js module
|
|
await saveConversationHistory(updatedHistory);
|
|
await updateChatHistory(updatedHistory);
|
|
|
|
introMsg.remove();
|
|
};
|
|
|
|
// Copy Raw (Markdown) button
|
|
const copyRawBtn = document.createElement("button");
|
|
copyRawBtn.textContent = "📋";
|
|
copyRawBtn.title = "Copy raw (Markdown)";
|
|
copyRawBtn.style.cssText = `
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.primaryText};
|
|
transition: all 0.2s;
|
|
`;
|
|
copyRawBtn.onclick = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(response);
|
|
const originalText = copyRawBtn.textContent;
|
|
copyRawBtn.textContent = "✓";
|
|
setTimeout(() => {
|
|
copyRawBtn.textContent = originalText;
|
|
}, 1000);
|
|
} catch (error) {
|
|
alert(`Failed to copy: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// Copy HTML button
|
|
const copyHtmlBtn = document.createElement("button");
|
|
copyHtmlBtn.textContent = "📄";
|
|
copyHtmlBtn.title = "Copy HTML";
|
|
copyHtmlBtn.style.cssText = `
|
|
background: ${colors.buttonBackground};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: ${colors.primaryText};
|
|
transition: all 0.2s;
|
|
`;
|
|
copyHtmlBtn.onclick = async () => {
|
|
try {
|
|
const htmlContent = marked.parse(response);
|
|
await navigator.clipboard.writeText(htmlContent);
|
|
const originalText = copyHtmlBtn.textContent;
|
|
copyHtmlBtn.textContent = "✓";
|
|
setTimeout(() => {
|
|
copyHtmlBtn.textContent = originalText;
|
|
}, 1000);
|
|
} catch (error) {
|
|
alert(`Failed to copy: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
messageActions.appendChild(ttsBtn);
|
|
messageActions.appendChild(copyRawBtn);
|
|
messageActions.appendChild(copyHtmlBtn);
|
|
messageActions.appendChild(deleteBtn);
|
|
introMsg.appendChild(messageActions);
|
|
|
|
// Save this intro as part of the conversation
|
|
await saveConversationHistory(await getChatHistory());
|
|
} catch (error) {
|
|
console.error("Failed to generate contextual intro:", error);
|
|
// Fallback to basic intro
|
|
introMsg.innerHTML = `👋 Hi! I'm Hermes AI. I can help you understand this page, answer questions, or assist with various tasks. How can I help you today?`;
|
|
}
|
|
};
|
|
|
|
await loadHistory();
|
|
}
|
|
|
|
// Function to toggle Hermes modal
|
|
let uncloseaiEmbeddedModalOpen = false;
|
|
|
|
async function toggleUncloseaiEmbeddedModal() {
|
|
if (uncloseaiEmbeddedModalOpen) {
|
|
const existingModal = document.getElementById("uncloseai-embedded-modal");
|
|
if (existingModal) {
|
|
existingModal.close();
|
|
document.body.removeChild(existingModal);
|
|
}
|
|
uncloseaiEmbeddedModalOpen = false;
|
|
} else {
|
|
await openUncloseaiEmbeddedModalNew();
|
|
uncloseaiEmbeddedModalOpen = true;
|
|
}
|
|
}
|
|
|
|
// Export the modal functions
|
|
export { toggleUncloseaiEmbeddedModal, openUncloseaiEmbeddedModalNew };
|
|
|
|
// Export compatibility alias
|
|
export const openUncloseaiEmbeddedModal = toggleUncloseaiEmbeddedModal;
|