diff --git a/js/chat/chat-core.js b/js/chat/chat-core.js index 82fe4df..9eb9aae 100644 --- a/js/chat/chat-core.js +++ b/js/chat/chat-core.js @@ -1,20 +1,20 @@ // ===== network.js ===== async function apiFetch(url, options = {}, { timeoutMs = 45000 } = {}) { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(new DOMException('timeout', 'AbortError')), timeoutMs); - try { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(new DOMException('timeout', 'AbortError')), timeoutMs); + try { const res = await fetch( url, { ...options, signal: controller.signal, cache: 'no-store' } ); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res; - } finally { - clearTimeout(timer); - } + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res; + } finally { + clearTimeout(timer); + } } window.apiFetch = apiFetch; - + // Load global AI instructions from external markdown file window.aiInstructions = ""; window.aiInstructionPromise = fetch("prompts/ai-instruct.md") @@ -64,29 +64,29 @@ function validateUICommand(cmd) { if (action === 'setValue' && (typeof target !== 'string' || typeof value !== 'string')) return false; return true; } - -document.addEventListener("DOMContentLoaded", () => { - - const chatBox = document.getElementById("chat-box"); - const chatInput = document.getElementById("chat-input"); - const sendButton = document.getElementById("send-button"); - const clearChatBtn = document.getElementById("clear-chat"); - const voiceToggleBtn = document.getElementById("voice-toggle"); - const modelSelect = document.getElementById("model-select"); - - let currentSession = Storage.getCurrentSession(); - if (!currentSession) { - currentSession = Storage.createSession("New Chat"); - localStorage.setItem("currentSessionId", currentSession.id); - } - - const synth = window.speechSynthesis; - let voices = []; - let selectedVoice = null; - let isSpeaking = false; - let autoSpeakEnabled = localStorage.getItem("autoSpeakEnabled") === "true"; - let currentlySpeakingMessage = null; - let activeUtterance = null; + +document.addEventListener("DOMContentLoaded", () => { + + const chatBox = document.getElementById("chat-box"); + const chatInput = document.getElementById("chat-input"); + const sendButton = document.getElementById("send-button"); + const clearChatBtn = document.getElementById("clear-chat"); + const voiceToggleBtn = document.getElementById("voice-toggle"); + const modelSelect = document.getElementById("model-select"); + + let currentSession = Storage.getCurrentSession(); + if (!currentSession) { + currentSession = Storage.createSession("New Chat"); + localStorage.setItem("currentSessionId", currentSession.id); + } + + const synth = window.speechSynthesis; + let voices = []; + let selectedVoice = null; + let isSpeaking = false; + let autoSpeakEnabled = localStorage.getItem("autoSpeakEnabled") === "true"; + let currentlySpeakingMessage = null; + let activeUtterance = null; let recognition = null; let isListening = false; let voiceInputBtn = null; @@ -115,74 +115,74 @@ document.addEventListener("DOMContentLoaded", () => { window.updateCapabilityUI = applyCapabilities; ensureCapabilities().then(() => applyCapabilities(modelSelect?.value)); - - function normalize(str) { - return str?.toLowerCase().trim() || ""; - } - - function autoTagVoiceTargets(root = document) { - const selectors = 'button, [role="button"], a, input, select, textarea'; - const elements = root.querySelectorAll(selectors); - for (const el of elements) { - if (el.dataset.voice) continue; - const labels = [ - el.id?.replace(/[-_]/g, ' '), - el.getAttribute('aria-label'), - el.getAttribute('title'), - el.textContent - ].map(normalize).filter(Boolean); - if (!labels.length) continue; - const variants = new Set(); - for (const label of labels) { - variants.add(label); - if (label.endsWith('s')) variants.add(label.slice(0, -1)); - else variants.add(label + 's'); - } - el.dataset.voice = Array.from(variants).join(' '); - } - } - - autoTagVoiceTargets(); - const voiceTagObserver = new MutationObserver(mutations => { - for (const m of mutations) { - for (const node of m.addedNodes) { - if (node.nodeType !== 1) continue; - autoTagVoiceTargets(node); - } - } - }); - voiceTagObserver.observe(document.body, { childList: true, subtree: true }); - - function findElement(phrase) { - const norm = normalize(phrase); - const id = norm.replace(/\s+/g, "-"); - let el = document.getElementById(id) || - document.querySelector(`[data-voice~="${norm}"]`); - - if (!el && norm.endsWith('s')) { - const singular = norm.slice(0, -1); - const singularId = singular.replace(/\s+/g, "-"); - el = document.getElementById(singularId) || - document.querySelector(`[data-voice~="${singular}"]`); - } - - if (el) return el; - - const candidates = Array.from(document.querySelectorAll("*")); - for (const candidate of candidates) { - const texts = [ - candidate.getAttribute("aria-label"), - candidate.getAttribute("title"), - candidate.textContent, - candidate.dataset?.voice - ].map(normalize); - if (texts.some(t => t && (t.includes(norm) || norm.includes(t)))) { - return candidate; - } - } - return null; - } - + + function normalize(str) { + return str?.toLowerCase().trim() || ""; + } + + function autoTagVoiceTargets(root = document) { + const selectors = 'button, [role="button"], a, input, select, textarea'; + const elements = root.querySelectorAll(selectors); + for (const el of elements) { + if (el.dataset.voice) continue; + const labels = [ + el.id?.replace(/[-_]/g, ' '), + el.getAttribute('aria-label'), + el.getAttribute('title'), + el.textContent + ].map(normalize).filter(Boolean); + if (!labels.length) continue; + const variants = new Set(); + for (const label of labels) { + variants.add(label); + if (label.endsWith('s')) variants.add(label.slice(0, -1)); + else variants.add(label + 's'); + } + el.dataset.voice = Array.from(variants).join(' '); + } + } + + autoTagVoiceTargets(); + const voiceTagObserver = new MutationObserver(mutations => { + for (const m of mutations) { + for (const node of m.addedNodes) { + if (node.nodeType !== 1) continue; + autoTagVoiceTargets(node); + } + } + }); + voiceTagObserver.observe(document.body, { childList: true, subtree: true }); + + function findElement(phrase) { + const norm = normalize(phrase); + const id = norm.replace(/\s+/g, "-"); + let el = document.getElementById(id) || + document.querySelector(`[data-voice~="${norm}"]`); + + if (!el && norm.endsWith('s')) { + const singular = norm.slice(0, -1); + const singularId = singular.replace(/\s+/g, "-"); + el = document.getElementById(singularId) || + document.querySelector(`[data-voice~="${singular}"]`); + } + + if (el) return el; + + const candidates = Array.from(document.querySelectorAll("*")); + for (const candidate of candidates) { + const texts = [ + candidate.getAttribute("aria-label"), + candidate.getAttribute("title"), + candidate.textContent, + candidate.dataset?.voice + ].map(normalize); + if (texts.some(t => t && (t.includes(norm) || norm.includes(t)))) { + return candidate; + } + } + return null; + } + function executeCommand(command) { if (typeof command === 'object') { if (!validateUICommand(command)) return false; @@ -385,7 +385,7 @@ document.addEventListener("DOMContentLoaded", () => { return false; } - + const polliTools = window.polliLib?.tools; const toolDefinitions = polliTools ? [ polliTools.functionTool('image', 'Generate an image', { @@ -442,76 +442,476 @@ document.addEventListener("DOMContentLoaded", () => { return {}; }); - async function handleToolJson(raw, { imageUrls, audioUrls }) { - const obj = (window.repairJson || (() => ({ text: raw })))(raw); + async function handleToolJson(raw, { imageUrls, audioUrls }, messageObj = null) { + const textFromSpecs = []; let handled = false; - const texts = []; - - const runTool = async spec => { - const fn = spec && toolbox.get(spec.tool); - if (fn) { - try { - const res = await fn(spec); - if (res?.imageUrl) imageUrls.push(res.imageUrl); - if (res?.audioUrl) audioUrls.push(res.audioUrl); - if (res?.text) texts.push(res.text); - handled = true; - } catch (e) { - console.warn('tool execution failed', e); + const structured = { images: [], audio: [], ui: [], voice: [] }; + + const tryParseJson = (value) => { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + if (!trimmed) return null; + try { + return JSON.parse(trimmed); + } catch (err) { + if (window.repairJson) { + try { + const repaired = window.repairJson(trimmed); + if (repaired && typeof repaired === 'object') { + const keys = Object.keys(repaired); + if (!(keys.length === 1 && repaired.text === trimmed)) { + return repaired; + } + } + } catch (repairErr) { + console.warn('repairJson failed', repairErr); + } } } + return null; }; - if (Array.isArray(obj.tools)) { - for (const t of obj.tools) await runTool(t); - } else if (obj.tool) { - await runTool(obj); - } + const parseCommandString = (value) => { + if (typeof value !== 'string') return value; + const trimmed = value.trim(); + if (!trimmed) return null; + const parsed = tryParseJson(trimmed); + if (parsed && typeof parsed === 'object') return parsed; + const tokens = trimmed.split(' ').map(part => part.trim()).filter(Boolean); + if (!tokens.length) return null; + const [action, ...rest] = tokens; + const target = rest.length ? rest.join(' ') : undefined; + return { action, target }; + }; + + const parseArgPayload = (payload) => { + if (payload == null) return {}; + if (typeof payload === 'string') { + const parsed = tryParseJson(payload); + if (parsed && typeof parsed === 'object') return parsed; + return { prompt: payload }; + } + if (typeof payload === 'object') { + return Array.isArray(payload) ? { values: payload } : { ...payload }; + } + return {}; + }; + + const extractArgs = (source) => { + if (!source || typeof source !== 'object' || Array.isArray(source)) return {}; + const argKeys = ['arguments', 'args', 'parameters', 'payload', 'data', 'input', 'options', 'values']; + for (const key of argKeys) { + if (source[key] != null) { + const parsed = parseArgPayload(source[key]); + if (parsed && Object.keys(parsed).length) return parsed; + } + } + const args = {}; + for (const [key, value] of Object.entries(source)) { + if (value === undefined) continue; + if (['tool', 'name', 'type', 'function', 'tool_calls', 'tools', 'commands', 'command', 'ui', 'image', 'images', 'audio', 'tts', 'voice', 'speak'].includes(key)) continue; + if (['text', 'message', 'response', 'reply', 'description', 'caption'].includes(key)) continue; + args[key] = value; + } + if (source.prompt != null && args.prompt == null) args.prompt = source.prompt; + if (source.text != null && args.text == null) args.text = source.text; + if (source.command && typeof source.command === 'object' && args.command == null) args.command = source.command; + return args; + }; + + const pickString = (candidates) => { + for (const value of candidates) { + if (typeof value === 'string' && value.trim()) return value.trim(); + } + return null; + }; + + const mapToolName = (name) => { + if (!name) return null; + const normalized = String(name).toLowerCase(); + if (normalized.includes('image') || normalized.includes('picture') || normalized.includes('photo') || normalized.includes('draw') || normalized.includes('art')) { + return 'image'; + } + if (normalized.includes('audio') || normalized.includes('speak') || normalized.includes('voice') || normalized.includes('sound') || normalized.includes('speech') || normalized.includes('tts')) { + return 'tts'; + } + if (normalized.includes('ui') || normalized.includes('command') || normalized.includes('action') || normalized.includes('control')) { + return 'ui'; + } + return normalized; + }; + + const executeTool = async (name, rawArgs = {}, original = null) => { + const canonical = mapToolName(name); + if (!canonical) return false; + const fn = toolbox.get(canonical); + if (!fn) return false; + + let args = {}; + if (rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)) { + args = { ...rawArgs }; + } else if (typeof rawArgs === 'string') { + args = { value: rawArgs }; + } + + if (canonical === 'image') { + const prompt = pickString([ + args.prompt, + args.description, + args.text, + args.query, + args.input, + original?.prompt, + original?.description, + original?.text, + typeof original === 'string' ? original : null + ]); + if (!prompt) return false; + args = { ...args, prompt }; + } else if (canonical === 'tts') { + const textValue = pickString([ + args.text, + args.prompt, + args.speech, + args.say, + args.message, + args.content, + original?.text, + original?.speech, + original?.message, + typeof original === 'string' ? original : null + ]); + if (!textValue) return false; + args = { ...args, text: textValue }; + } else if (canonical === 'ui') { + let command = args.command ?? original?.command ?? args; + if (typeof command === 'string') { + command = parseCommandString(command); + } else if (!command || Array.isArray(command)) { + command = { + action: args.action ?? original?.action, + target: args.target ?? original?.target, + value: args.value ?? original?.value + }; + } + if (typeof command === 'string') { + command = parseCommandString(command); + } + if (!command || !validateUICommand(command)) return false; + args = { command }; + structured.ui.push({ command }); + } - const imgPrompts = obj.image ? [obj.image] : Array.isArray(obj.images) ? obj.images : []; - for (const prompt of imgPrompts) { - if (!(window.polliLib && window.polliClient) || !prompt) continue; try { - const blob = await window.polliLib.image(prompt, { width: 512, height: 512, private: true, nologo: true, safe: true }, window.polliClient); - const url = blob?.url ? blob.url : URL.createObjectURL(blob); - imageUrls.push(url); + const result = await fn(args); + if (result?.imageUrl) { + imageUrls.push(result.imageUrl); + structured.images.push({ url: result.imageUrl, prompt: args.prompt ?? null, options: args }); + } + if (result?.audioUrl) { + audioUrls.push(result.audioUrl); + structured.audio.push({ url: result.audioUrl, text: args.text ?? null, options: args }); + } + if (typeof result?.text === 'string') { + textFromSpecs.push(result.text); + } handled = true; + return true; } catch (e) { - console.warn('polliLib image failed', e); + console.warn('tool execution failed', e); + } + return false; + }; + + const processStructuredValue = async (value, parentToolName = null) => { + if (value == null) return; + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + const str = String(value).trim(); + if (!str) return; + if (parentToolName) { + if (parentToolName === 'voice') { + structured.voice.push(str); + return; + } + if (parentToolName === 'image') { + await executeTool('image', { prompt: str }, str); + return; + } + if (parentToolName === 'tts') { + await executeTool('tts', { text: str }, str); + return; + } + if (parentToolName === 'ui') { + await executeTool('ui', { command: str }, str); + return; + } + await executeTool(parentToolName, { value: str }, str); + return; + } + textFromSpecs.push(str); + return; + } + + if (Array.isArray(value)) { + for (const item of value) { + await processStructuredValue(item, parentToolName); + } + return; + } + + if (typeof value !== 'object') return; + + if (parentToolName) { + if (parentToolName === 'voice') { + const voiceText = pickString([ + value.text, + value.message, + value.prompt, + value.say, + value.response + ]); + if (voiceText) structured.voice.push(voiceText); + return; + } + const args = extractArgs(value); + const executed = await executeTool(parentToolName, args, value); + if (executed) return; + } + + if (Array.isArray(value.tool_calls)) { + for (const call of value.tool_calls) { + await processStructuredValue(call); + } + } + + if (Array.isArray(value.tools)) { + for (const tool of value.tools) { + await processStructuredValue(tool); + } + } + + if (Array.isArray(value.commands)) { + for (const cmd of value.commands) { + await processStructuredValue({ tool: 'ui', ...cmd }); + } + } + + if (value.function && value.function.name) { + const args = parseArgPayload(value.function.arguments ?? value.function.args ?? value.function.parameters ?? value.function.payload); + await executeTool(value.function.name, args, value.function); + } + + if (value.name && (value.arguments || value.args || value.parameters)) { + const args = parseArgPayload(value.arguments ?? value.args ?? value.parameters); + await executeTool(value.name, args, value); + } + + if (value.tool) { + if (typeof value.tool === 'object') { + await processStructuredValue(value.tool); + } else { + const args = extractArgs(value); + await executeTool(value.tool, args, value); + } + } else if (value.type) { + await executeTool(value.type, extractArgs(value), value); + } + + if (value.image !== undefined) await processStructuredValue(value.image, 'image'); + if (value.images !== undefined) await processStructuredValue(value.images, 'image'); + if (value.audio !== undefined) await processStructuredValue(value.audio, 'tts'); + if (value.tts !== undefined) await processStructuredValue(value.tts, 'tts'); + if (value.voice !== undefined) await processStructuredValue(value.voice, 'voice'); + if (value.speak !== undefined) await processStructuredValue(value.speak, 'tts'); + if (value.ui !== undefined) await processStructuredValue(value.ui, 'ui'); + if (value.command !== undefined) await processStructuredValue(value.command, 'ui'); + + const textKeys = ['text', 'message', 'response', 'reply', 'caption', 'description']; + for (const key of textKeys) { + if (typeof value[key] === 'string') { + const trimmed = value[key].trim(); + if (trimmed) textFromSpecs.push(trimmed); + } + } + }; + + const extractJsonSections = (input) => { + if (typeof input !== 'string') return []; + const sections = []; + let start = -1; + let depth = 0; + let inString = false; + let escape = false; + for (let i = 0; i < input.length; i++) { + const char = input[i]; + if (start === -1) { + if (char === '{' || char === '[') { + start = i; + depth = 1; + inString = false; + escape = false; + } + continue; + } + if (escape) { + escape = false; + continue; + } + if (char === '\' && inString) { + escape = true; + continue; + } + if (char === '"') { + inString = !inString; + continue; + } + if (inString) continue; + if (char === '{' || char === '[') { + depth += 1; + continue; + } + if (char === '}' || char === ']') { + depth -= 1; + if (depth === 0) { + const snippet = input.slice(start, i + 1); + const parsed = tryParseJson(snippet); + if (parsed !== null) { + sections.push({ start, end: i + 1, value: parsed }); + } + start = -1; + depth = 0; + inString = false; + escape = false; + } else if (depth < 0) { + start = -1; + depth = 0; + } + } } + return sections; + }; + + const handledFenceTypes = new Set(['image', 'audio', 'ui', 'voice', 'video']); + const instructions = []; + let leftoverText = typeof raw === 'string' ? raw : ''; + + if (typeof raw === 'string' && raw.trim()) { + const jsonSections = extractJsonSections(raw); + const segments = []; + let lastIndex = 0; + for (const section of jsonSections) { + if (section.start > lastIndex) { + segments.push({ text: raw.slice(lastIndex, section.start), start: lastIndex }); + } + instructions.push({ start: section.start, value: section.value }); + lastIndex = section.end; + } + if (lastIndex < raw.length) { + segments.push({ text: raw.slice(lastIndex), start: lastIndex }); + } + + const cleanedSegments = []; + for (const segment of segments) { + const text = segment.text; + if (typeof text !== 'string' || text.indexOf('```') === -1) { + cleanedSegments.push(text); + continue; + } + let idx = 0; + let cleaned = ''; + while (idx < text.length) { + const fenceStart = text.indexOf('```', idx); + if (fenceStart === -1) { + cleaned += text.slice(idx); + break; + } + const langLineEnd = text.indexOf('\n', fenceStart + 3); + if (langLineEnd === -1) { + cleaned += text.slice(idx); + break; + } + const lang = text.slice(fenceStart + 3, langLineEnd).trim().toLowerCase(); + const fenceEnd = text.indexOf('```', langLineEnd + 1); + if (fenceEnd === -1) { + cleaned += text.slice(idx); + break; + } + if (handledFenceTypes.has(lang)) { + cleaned += text.slice(idx, fenceStart); + const blockContent = text.slice(langLineEnd + 1, fenceEnd).trim(); + if (blockContent) { + instructions.push({ start: segment.start + fenceStart, value: { fence: lang, content: blockContent } }); + } + idx = fenceEnd + 3; + } else { + cleaned += text.slice(idx, fenceEnd + 3); + idx = fenceEnd + 3; + } + } + cleanedSegments.push(cleaned); + } + leftoverText = cleanedSegments.join(''); } - const audioText = obj.audio || obj.tts; - if (audioText && window.polliLib && window.polliClient) { - try { - const blob = await window.polliLib.tts(audioText, { model: 'openai-audio' }, window.polliClient); - const url = URL.createObjectURL(blob); - audioUrls.push(url); - handled = true; - } catch (e) { - console.warn('polliLib tts failed', e); + if (Array.isArray(messageObj?.tool_calls)) { + let offset = -1; + for (const call of messageObj.tool_calls) { + instructions.push({ start: offset, value: { toolCall: call } }); + offset -= 1; } } - const command = obj.ui || obj.command; - if (command) { - if (validateUICommand(command)) { - try { executeCommand(command); } catch (e) { console.warn('executeCommand failed', e); } - handled = true; - } else { - console.warn('invalid ui command', command); + instructions.sort((a, b) => a.start - b.start); + + for (const entry of instructions) { + const value = entry.value; + if (!value) continue; + if (value.toolCall) { + await processStructuredValue(value.toolCall); + continue; } + if (value.fence) { + const lang = value.fence; + if (lang === 'voice') { + structured.voice.push(value.content); + } else if (lang === 'image') { + await processStructuredValue({ tool: 'image', prompt: value.content }); + } else if (lang === 'audio') { + await processStructuredValue({ tool: 'tts', text: value.content }); + } else if (lang === 'ui') { + await processStructuredValue({ tool: 'ui', command: value.content }); + } else { + await processStructuredValue({ tool: lang, prompt: value.content }); + } + continue; + } + await processStructuredValue(value); } - if (typeof obj.text === 'string') texts.push(obj.text); - const text = texts.join('').trim() || raw; - return { handled, text }; - } + const cleanedLeftover = typeof leftoverText === 'string' + ? leftoverText.replace(/\n{3,}/g, '\n\n').trim() + : ''; + + const textParts = []; + if (cleanedLeftover) textParts.push(cleanedLeftover); + if (textFromSpecs.length) textParts.push(textFromSpecs.join('\n\n').trim()); + const finalText = textParts.join('\n\n').trim(); + const structuredResult = {}; + for (const [key, value] of Object.entries(structured)) { + if (Array.isArray(value)) { + if (value.length) structuredResult[key] = value; + } else if (value) { + structuredResult[key] = value; + } + } + return { handled, text: finalText, structured: structuredResult }; + } function handleVoiceCommand(text) { return executeCommand(text); } - + function setVoiceInputButton(btn) { voiceInputBtn = btn; if (window._chatInternals) { @@ -519,230 +919,230 @@ document.addEventListener("DOMContentLoaded", () => { } if (modelSelect) applyCapabilities(modelSelect.value); } - - function loadVoices() { - return new Promise((resolve) => { - voices = synth.getVoices(); - if (voices.length === 0) { - synth.onvoiceschanged = () => { - voices = synth.getVoices(); - if (voices.length > 0) { - setVoiceOptions(resolve); - } - }; - setTimeout(() => { - if (voices.length === 0) { - voices = synth.getVoices(); - setVoiceOptions(resolve); - } - }, 2000); - } else { - setVoiceOptions(resolve); - } - }); - } - - function setVoiceOptions(resolve) { - const savedVoiceIndex = localStorage.getItem("selectedVoiceIndex"); - if (savedVoiceIndex && voices[savedVoiceIndex]) { - selectedVoice = voices[savedVoiceIndex]; - } else { - selectedVoice = voices.find((v) => v.name === "Google UK English Female") || - voices.find((v) => v.lang === "en-GB" && v.name.toLowerCase().includes("female")) || - voices[0]; - const selectedIndex = voices.indexOf(selectedVoice); - if (selectedIndex >= 0) { - localStorage.setItem("selectedVoiceIndex", selectedIndex); - } - } - populateAllVoiceDropdowns(); - resolve(selectedVoice); - } - - function getVoiceDropdowns() { - const voiceSelect = document.getElementById("voice-select"); - const voiceSelectModal = document.getElementById("voice-select-modal"); - const voiceSelectSettings = document.getElementById("voice-select-settings"); - const voiceSelectVoiceChat = document.getElementById("voice-select-voicechat"); - return [voiceSelect, voiceSelectModal, voiceSelectSettings, voiceSelectVoiceChat]; - } - - function populateAllVoiceDropdowns() { - const dropdowns = getVoiceDropdowns(); - - dropdowns.forEach((dropdown) => { - if (dropdown) { - dropdown.innerHTML = ""; - voices.forEach((voice, index) => { - const option = document.createElement("option"); - option.value = index; - option.textContent = `${voice.name} (${voice.lang})`; - dropdown.appendChild(option); - }); - - const savedVoiceIndex = localStorage.getItem("selectedVoiceIndex"); - if (savedVoiceIndex && voices[savedVoiceIndex]) { - dropdown.value = savedVoiceIndex; - } - - dropdown.addEventListener("change", () => { - selectedVoice = voices[dropdown.value]; - localStorage.setItem("selectedVoiceIndex", dropdown.value); - updateAllVoiceDropdowns(dropdown.value); - showToast(`Voice changed to ${selectedVoice.name}`); - }); - } - }); - } - - function updateAllVoiceDropdowns(selectedIndex) { - const dropdowns = getVoiceDropdowns(); - - dropdowns.forEach((dropdown) => { - if (dropdown && dropdown.value !== selectedIndex) { - dropdown.value = selectedIndex; - } - }); - } - - loadVoices().then(() => { - updateVoiceToggleUI(); - }); - - function toggleAutoSpeak() { - autoSpeakEnabled = !autoSpeakEnabled; - localStorage.setItem("autoSpeakEnabled", autoSpeakEnabled.toString()); - updateVoiceToggleUI(); - showToast(autoSpeakEnabled ? "Auto-speak enabled" : "Auto-speak disabled"); - if (autoSpeakEnabled) { - speakMessage("Voice mode enabled. I'll speak responses out loud."); - } else { - stopSpeaking(); - } - } - - function updateVoiceToggleUI() { - if (voiceToggleBtn) { - voiceToggleBtn.textContent = autoSpeakEnabled ? "🔊 Voice On" : "🔇 Voice Off"; - voiceToggleBtn.style.backgroundColor = autoSpeakEnabled ? "#4CAF50" : ""; - } - } - - function speakMessage(text, onEnd = null) { - if (!synth || !window.SpeechSynthesisUtterance) { - showToast("Speech synthesis not supported in your browser"); - return; - } - - if (isSpeaking) { - synth.cancel(); - isSpeaking = false; - activeUtterance = null; - } - - let speakText = text.replace(/\[CODE\][\s\S]*?\[\/CODE\]/gi, "").replace(/https?:\/\/[^\s)"'<>]+/gi, "").trim(); - - const utterance = new SpeechSynthesisUtterance(speakText); - activeUtterance = utterance; - - if (selectedVoice) { - utterance.voice = selectedVoice; - } else { - loadVoices().then((voice) => { - if (voice) { - utterance.voice = voice; - synth.speak(utterance); - } - }); - return; - } - - utterance.rate = parseFloat(localStorage.getItem("voiceSpeed")) || 0.9; - utterance.pitch = parseFloat(localStorage.getItem("voicePitch")) || 1.0; - utterance.volume = 1.0; - - utterance.onstart = () => { - isSpeaking = true; - currentlySpeakingMessage = speakText; - }; - - utterance.onend = () => { - isSpeaking = false; - currentlySpeakingMessage = null; - activeUtterance = null; - if (onEnd) onEnd(); - }; - - utterance.onerror = (event) => { - isSpeaking = false; - currentlySpeakingMessage = null; - activeUtterance = null; - showToast(`Speech error: ${event.error}`); - if (onEnd) onEnd(); - }; - - try { - synth.speak(utterance); - } catch (err) { - showToast("Error initiating speech synthesis"); - isSpeaking = false; - activeUtterance = null; - } - - const keepAlive = setInterval(() => { - if (!isSpeaking || !activeUtterance) { - clearInterval(keepAlive); - } - }, 10000); - } - - function stopSpeaking() { - if (synth && (isSpeaking || synth.speaking)) { - synth.cancel(); - isSpeaking = false; - currentlySpeakingMessage = null; - activeUtterance = null; - } - } - - function shutUpTTS() { - if (synth) { - synth.cancel(); - isSpeaking = false; - currentlySpeakingMessage = null; - activeUtterance = null; - showToast("TTS stopped"); - } - } - - // Directly handle whatever response shape the API returns without filtering. - - function speakSentences(sentences, index = 0) { - if (index >= sentences.length) { - return; - } - speakMessage(sentences[index], () => speakSentences(sentences, index + 1)); - } - + + function loadVoices() { + return new Promise((resolve) => { + voices = synth.getVoices(); + if (voices.length === 0) { + synth.onvoiceschanged = () => { + voices = synth.getVoices(); + if (voices.length > 0) { + setVoiceOptions(resolve); + } + }; + setTimeout(() => { + if (voices.length === 0) { + voices = synth.getVoices(); + setVoiceOptions(resolve); + } + }, 2000); + } else { + setVoiceOptions(resolve); + } + }); + } + + function setVoiceOptions(resolve) { + const savedVoiceIndex = localStorage.getItem("selectedVoiceIndex"); + if (savedVoiceIndex && voices[savedVoiceIndex]) { + selectedVoice = voices[savedVoiceIndex]; + } else { + selectedVoice = voices.find((v) => v.name === "Google UK English Female") || + voices.find((v) => v.lang === "en-GB" && v.name.toLowerCase().includes("female")) || + voices[0]; + const selectedIndex = voices.indexOf(selectedVoice); + if (selectedIndex >= 0) { + localStorage.setItem("selectedVoiceIndex", selectedIndex); + } + } + populateAllVoiceDropdowns(); + resolve(selectedVoice); + } + + function getVoiceDropdowns() { + const voiceSelect = document.getElementById("voice-select"); + const voiceSelectModal = document.getElementById("voice-select-modal"); + const voiceSelectSettings = document.getElementById("voice-select-settings"); + const voiceSelectVoiceChat = document.getElementById("voice-select-voicechat"); + return [voiceSelect, voiceSelectModal, voiceSelectSettings, voiceSelectVoiceChat]; + } + + function populateAllVoiceDropdowns() { + const dropdowns = getVoiceDropdowns(); + + dropdowns.forEach((dropdown) => { + if (dropdown) { + dropdown.innerHTML = ""; + voices.forEach((voice, index) => { + const option = document.createElement("option"); + option.value = index; + option.textContent = `${voice.name} (${voice.lang})`; + dropdown.appendChild(option); + }); + + const savedVoiceIndex = localStorage.getItem("selectedVoiceIndex"); + if (savedVoiceIndex && voices[savedVoiceIndex]) { + dropdown.value = savedVoiceIndex; + } + + dropdown.addEventListener("change", () => { + selectedVoice = voices[dropdown.value]; + localStorage.setItem("selectedVoiceIndex", dropdown.value); + updateAllVoiceDropdowns(dropdown.value); + showToast(`Voice changed to ${selectedVoice.name}`); + }); + } + }); + } + + function updateAllVoiceDropdowns(selectedIndex) { + const dropdowns = getVoiceDropdowns(); + + dropdowns.forEach((dropdown) => { + if (dropdown && dropdown.value !== selectedIndex) { + dropdown.value = selectedIndex; + } + }); + } + + loadVoices().then(() => { + updateVoiceToggleUI(); + }); + + function toggleAutoSpeak() { + autoSpeakEnabled = !autoSpeakEnabled; + localStorage.setItem("autoSpeakEnabled", autoSpeakEnabled.toString()); + updateVoiceToggleUI(); + showToast(autoSpeakEnabled ? "Auto-speak enabled" : "Auto-speak disabled"); + if (autoSpeakEnabled) { + speakMessage("Voice mode enabled. I'll speak responses out loud."); + } else { + stopSpeaking(); + } + } + + function updateVoiceToggleUI() { + if (voiceToggleBtn) { + voiceToggleBtn.textContent = autoSpeakEnabled ? "🔊 Voice On" : "🔇 Voice Off"; + voiceToggleBtn.style.backgroundColor = autoSpeakEnabled ? "#4CAF50" : ""; + } + } + + function speakMessage(text, onEnd = null) { + if (!synth || !window.SpeechSynthesisUtterance) { + showToast("Speech synthesis not supported in your browser"); + return; + } + + if (isSpeaking) { + synth.cancel(); + isSpeaking = false; + activeUtterance = null; + } + + let speakText = text.replace(/\[CODE\][\s\S]*?\[\/CODE\]/gi, "").replace(/https?:\/\/[^\s)"'<>]+/gi, "").trim(); + + const utterance = new SpeechSynthesisUtterance(speakText); + activeUtterance = utterance; + + if (selectedVoice) { + utterance.voice = selectedVoice; + } else { + loadVoices().then((voice) => { + if (voice) { + utterance.voice = voice; + synth.speak(utterance); + } + }); + return; + } + + utterance.rate = parseFloat(localStorage.getItem("voiceSpeed")) || 0.9; + utterance.pitch = parseFloat(localStorage.getItem("voicePitch")) || 1.0; + utterance.volume = 1.0; + + utterance.onstart = () => { + isSpeaking = true; + currentlySpeakingMessage = speakText; + }; + + utterance.onend = () => { + isSpeaking = false; + currentlySpeakingMessage = null; + activeUtterance = null; + if (onEnd) onEnd(); + }; + + utterance.onerror = (event) => { + isSpeaking = false; + currentlySpeakingMessage = null; + activeUtterance = null; + showToast(`Speech error: ${event.error}`); + if (onEnd) onEnd(); + }; + + try { + synth.speak(utterance); + } catch (err) { + showToast("Error initiating speech synthesis"); + isSpeaking = false; + activeUtterance = null; + } + + const keepAlive = setInterval(() => { + if (!isSpeaking || !activeUtterance) { + clearInterval(keepAlive); + } + }, 10000); + } + + function stopSpeaking() { + if (synth && (isSpeaking || synth.speaking)) { + synth.cancel(); + isSpeaking = false; + currentlySpeakingMessage = null; + activeUtterance = null; + } + } + + function shutUpTTS() { + if (synth) { + synth.cancel(); + isSpeaking = false; + currentlySpeakingMessage = null; + activeUtterance = null; + showToast("TTS stopped"); + } + } + + // Directly handle whatever response shape the API returns without filtering. + + function speakSentences(sentences, index = 0) { + if (index >= sentences.length) { + return; + } + speakMessage(sentences[index], () => speakSentences(sentences, index + 1)); + } + window.sendToPolliLib = async function sendToPolliLib(callback = null, overrideContent = null) { - const currentSession = Storage.getCurrentSession(); - const loadingDiv = document.createElement("div"); - loadingDiv.className = "message ai-message"; - loadingDiv.textContent = "Thinking..."; - chatBox.appendChild(loadingDiv); - chatBox.scrollTop = chatBox.scrollHeight; - + const currentSession = Storage.getCurrentSession(); + const loadingDiv = document.createElement("div"); + loadingDiv.className = "message ai-message"; + loadingDiv.textContent = "Thinking..."; + chatBox.appendChild(loadingDiv); + chatBox.scrollTop = chatBox.scrollHeight; + await window.ensureAIInstructions(); const messages = []; if (window.aiInstructions) { messages.push({ role: "system", content: window.aiInstructions }); } - const memories = Memory.getMemories(); - if (memories?.length) { - messages.push({ role: "system", content: `Relevant memory:\n${memories.join("\n")}\nUse it in your response.` }); - } - + const memories = Memory.getMemories(); + if (memories?.length) { + messages.push({ role: "system", content: `Relevant memory:\n${memories.join("\n")}\nUse it in your response.` }); + } + const HISTORY = 10; const end = currentSession.messages.length - 1; const start = Math.max(0, end - HISTORY); @@ -763,26 +1163,26 @@ document.addEventListener("DOMContentLoaded", () => { const content = typeof m?.content === 'string' ? m.content : (m?.content != null ? String(m.content) : ''); if (role && content) messages.push({ role, content }); } - + const lastUser = overrideContent || currentSession.messages[end]?.content; if (lastUser) { messages.push({ role: "user", content: lastUser }); } - - const modelSelectEl = document.getElementById("model-select"); - const model = modelSelectEl?.value || currentSession.model || Storage.getDefaultModel(); - if (!model) { - loadingDiv.textContent = "Error: No model selected."; - setTimeout(() => loadingDiv.remove(), 3000); - const btn = window._chatInternals?.sendButton || document.getElementById("send-button"); - const input = window._chatInternals?.chatInput || document.getElementById("chat-input"); - if (btn) btn.disabled = false; - if (input) input.disabled = false; - showToast("Please select a model before sending a message."); - if (callback) callback(); - return; - } - + + const modelSelectEl = document.getElementById("model-select"); + const model = modelSelectEl?.value || currentSession.model || Storage.getDefaultModel(); + if (!model) { + loadingDiv.textContent = "Error: No model selected."; + setTimeout(() => loadingDiv.remove(), 3000); + const btn = window._chatInternals?.sendButton || document.getElementById("send-button"); + const input = window._chatInternals?.chatInput || document.getElementById("chat-input"); + if (btn) btn.disabled = false; + if (input) input.disabled = false; + showToast("Please select a model before sending a message."); + if (callback) callback(); + return; + } + try { const capsInfo = capabilities?.text?.[model]; const chatParams = { model, messages }; @@ -815,7 +1215,8 @@ document.addEventListener("DOMContentLoaded", () => { aiContent = messageObj.content || ""; } - const toolRes = await handleToolJson(aiContent, { imageUrls, audioUrls }); + const toolRes = await handleToolJson(aiContent, { imageUrls, audioUrls }, messageObj); + const structuredOutputs = toolRes.structured || {}; aiContent = toolRes.text; const memRegex = /\[memory\]([\s\S]*?)\[\/memory\]/gi; @@ -823,196 +1224,154 @@ document.addEventListener("DOMContentLoaded", () => { while ((m = memRegex.exec(aiContent)) !== null) Memory.addMemoryEntry(m[1].trim()); aiContent = aiContent.replace(memRegex, "").trim(); - if (aiContent) { - const processPatterns = async (patterns, handler) => { - for (const { pattern, group } of patterns) { - const grpIndex = typeof group === 'number' ? group : 1; - const p = pattern.global ? pattern : new RegExp(pattern.source, pattern.flags + 'g'); - const matches = Array.from(aiContent.matchAll(p)); - for (const match of matches) { - const captured = match[grpIndex] && match[grpIndex].trim(); - if (!captured) continue; - try { await handler(captured); } catch (e) { console.warn('pattern handler failed', e); } - } - aiContent = aiContent.replace(p, ''); - } - }; - - await processPatterns(window.imagePatterns || [], async prompt => { - if (!(window.polliLib && window.polliClient)) return; + if (structuredOutputs.voice) { + for (const voiceText of structuredOutputs.voice) { + if (!voiceText) continue; try { - const blob = await window.polliLib.image( - prompt, - { width: 512, height: 512, private: true, nologo: true, safe: true }, - window.polliClient - ); - const url = blob?.url ? blob.url : URL.createObjectURL(blob); - imageUrls.push(url); - } catch (e) { - console.warn('polliLib image failed', e); - } - }); - - await processPatterns(window.audioPatterns || [], async prompt => { - if (!(window.polliLib && window.polliClient)) return; - try { - const blob = await window.polliLib.tts(prompt, { model: 'openai-audio' }, window.polliClient); - const url = URL.createObjectURL(blob); - audioUrls.push(url); - } catch (e) { - console.warn('polliLib tts failed', e); - } - }); - - await processPatterns(window.uiPatterns || [], async command => { - try { executeCommand(command); } catch (e) { console.warn('executeCommand failed', e); } - }); - - await processPatterns(window.videoPatterns || [], async prompt => { - // Video handling to be implemented - }); - - await processPatterns(window.voicePatterns || [], async text => { - try { - const sentences = text.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0); + const sentences = voiceText.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0); speakSentences(sentences); } catch (e) { console.warn('speakSentences failed', e); } - }); + } + } + if (aiContent) { aiContent = aiContent.replace(/\n{3,}/g, '\n\n'); aiContent = aiContent.replace(/\n?---\n?/g, '\n\n---\n\n'); aiContent = aiContent.replace(/\n{3,}/g, '\n\n').trim(); } - window.addNewMessage({ role: "ai", content: aiContent, imageUrls, audioUrls }); + const hasMetadata = Object.values(structuredOutputs).some(value => Array.isArray(value) ? value.length > 0 : !!value); + const metadata = hasMetadata ? structuredOutputs : null; + + window.addNewMessage({ role: "ai", content: aiContent, imageUrls, audioUrls, metadata }); if (autoSpeakEnabled) { const sentences = aiContent.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0); speakSentences(sentences); } else { stopSpeaking(); - } - if (callback) callback(); - } catch (err) { - loadingDiv.textContent = "Error: Failed to get a response."; - setTimeout(() => loadingDiv.remove(), 3000); - console.error("Pollinations error:", err); - if (callback) callback(); - const btn = window._chatInternals?.sendButton || document.getElementById("send-button"); - const input = window._chatInternals?.chatInput || document.getElementById("chat-input"); - if (btn) btn.disabled = false; - if (input) input.disabled = false; - } - }; - - function initSpeechRecognition() { - if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) { - showToast("Speech recognition not supported in this browser"); - return false; - } - - try { - if ("webkitSpeechRecognition" in window) { - recognition = new window.webkitSpeechRecognition(); - } else { - recognition = new window.SpeechRecognition(); - } - - recognition.continuous = true; - recognition.interimResults = true; - recognition.lang = 'en-US'; - - if (window._chatInternals) { - window._chatInternals.recognition = recognition; - } - - recognition.onstart = () => { - isListening = true; - if (voiceInputBtn) { - voiceInputBtn.classList.add("listening"); - voiceInputBtn.innerHTML = ''; - } - }; - - recognition.onresult = (event) => { - let finalTranscript = ""; - let interimTranscript = ""; - - for (let i = event.resultIndex; i < event.results.length; i++) { - const transcript = event.results[i][0].transcript; - if (event.results[i].isFinal) { - const processed = transcript.trim(); - if (!handleVoiceCommand(processed)) { - finalTranscript += processed + " "; - } - } else { - interimTranscript += transcript; - } - } - - if (finalTranscript) { - chatInput.value = (chatInput.value + " " + finalTranscript).trim(); - chatInput.dispatchEvent(new Event("input")); - const btn = window._chatInternals?.sendButton || document.getElementById("send-button"); - if (btn) { - btn.disabled = false; - btn.click(); - } - } - }; - - recognition.onerror = (event) => { - isListening = false; - if (voiceInputBtn) { - voiceInputBtn.classList.remove("listening"); - voiceInputBtn.innerHTML = ''; - } - console.error("Speech recognition error:", event.error); - }; - - recognition.onend = () => { - isListening = false; - if (voiceInputBtn) { - voiceInputBtn.classList.remove("listening"); - voiceInputBtn.innerHTML = ''; - } - }; - - return true; - } catch (error) { - console.error("Error initializing speech recognition:", error); - showToast("Failed to initialize speech recognition"); - return false; - } - } - - function toggleSpeechRecognition() { - if (!recognition && !initSpeechRecognition()) { - showToast("Speech recognition not supported in this browser. Please use Chrome, Edge, or Firefox."); - return; - } - - if (isListening) { - recognition.stop(); - } else { - try { - showToast("Requesting microphone access..."); - recognition.start(); - } catch (error) { - showToast("Could not start speech recognition: " + error.message); - console.error("Speech recognition start error:", error); - } - } - } - - function showToast(message, duration = 3000) { - let toast = document.getElementById("toast-notification"); - if (!toast) { - toast = document.createElement("div"); - toast.id = "toast-notification"; - toast.style.position = "fixed"; - toast.style.top = "5%"; + } + if (callback) callback(); + } catch (err) { + loadingDiv.textContent = "Error: Failed to get a response."; + setTimeout(() => loadingDiv.remove(), 3000); + console.error("Pollinations error:", err); + if (callback) callback(); + const btn = window._chatInternals?.sendButton || document.getElementById("send-button"); + const input = window._chatInternals?.chatInput || document.getElementById("chat-input"); + if (btn) btn.disabled = false; + if (input) input.disabled = false; + } + }; + + function initSpeechRecognition() { + if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) { + showToast("Speech recognition not supported in this browser"); + return false; + } + + try { + if ("webkitSpeechRecognition" in window) { + recognition = new window.webkitSpeechRecognition(); + } else { + recognition = new window.SpeechRecognition(); + } + + recognition.continuous = true; + recognition.interimResults = true; + recognition.lang = 'en-US'; + + if (window._chatInternals) { + window._chatInternals.recognition = recognition; + } + + recognition.onstart = () => { + isListening = true; + if (voiceInputBtn) { + voiceInputBtn.classList.add("listening"); + voiceInputBtn.innerHTML = ''; + } + }; + + recognition.onresult = (event) => { + let finalTranscript = ""; + let interimTranscript = ""; + + for (let i = event.resultIndex; i < event.results.length; i++) { + const transcript = event.results[i][0].transcript; + if (event.results[i].isFinal) { + const processed = transcript.trim(); + if (!handleVoiceCommand(processed)) { + finalTranscript += processed + " "; + } + } else { + interimTranscript += transcript; + } + } + + if (finalTranscript) { + chatInput.value = (chatInput.value + " " + finalTranscript).trim(); + chatInput.dispatchEvent(new Event("input")); + const btn = window._chatInternals?.sendButton || document.getElementById("send-button"); + if (btn) { + btn.disabled = false; + btn.click(); + } + } + }; + + recognition.onerror = (event) => { + isListening = false; + if (voiceInputBtn) { + voiceInputBtn.classList.remove("listening"); + voiceInputBtn.innerHTML = ''; + } + console.error("Speech recognition error:", event.error); + }; + + recognition.onend = () => { + isListening = false; + if (voiceInputBtn) { + voiceInputBtn.classList.remove("listening"); + voiceInputBtn.innerHTML = ''; + } + }; + + return true; + } catch (error) { + console.error("Error initializing speech recognition:", error); + showToast("Failed to initialize speech recognition"); + return false; + } + } + + function toggleSpeechRecognition() { + if (!recognition && !initSpeechRecognition()) { + showToast("Speech recognition not supported in this browser. Please use Chrome, Edge, or Firefox."); + return; + } + + if (isListening) { + recognition.stop(); + } else { + try { + showToast("Requesting microphone access..."); + recognition.start(); + } catch (error) { + showToast("Could not start speech recognition: " + error.message); + console.error("Speech recognition start error:", error); + } + } + } + + function showToast(message, duration = 3000) { + let toast = document.getElementById("toast-notification"); + if (!toast) { + toast = document.createElement("div"); + toast.id = "toast-notification"; + toast.style.position = "fixed"; + toast.style.top = "5%"; toast.style.left = "50%"; toast.style.transform = "translateX(-50%)"; const bodyStyles = getComputedStyle(document.body); @@ -1026,31 +1385,31 @@ document.addEventListener("DOMContentLoaded", () => { toast.style.zIndex = "9999"; toast.style.transition = "opacity 0.3s"; document.body.appendChild(toast); - } - toast.textContent = message; - toast.style.opacity = "1"; - clearTimeout(toast.timeout); - toast.timeout = setTimeout(() => { - toast.style.opacity = "0"; - }, duration); - } - - window._chatInternals = { - chatBox, - chatInput, - sendButton, - clearChatBtn, - voiceToggleBtn, - modelSelect, - currentSession, - synth, - voices, - selectedVoice, - isSpeaking, - autoSpeakEnabled, - currentlySpeakingMessage, - recognition, - isListening, + } + toast.textContent = message; + toast.style.opacity = "1"; + clearTimeout(toast.timeout); + toast.timeout = setTimeout(() => { + toast.style.opacity = "0"; + }, duration); + } + + window._chatInternals = { + chatBox, + chatInput, + sendButton, + clearChatBtn, + voiceToggleBtn, + modelSelect, + currentSession, + synth, + voices, + selectedVoice, + isSpeaking, + autoSpeakEnabled, + currentlySpeakingMessage, + recognition, + isListening, voiceInputBtn, slideshowInterval, setVoiceInputButton, @@ -1058,21 +1417,21 @@ document.addEventListener("DOMContentLoaded", () => { capabilities, toggleAutoSpeak, updateVoiceToggleUI, - speakMessage, - stopSpeaking, - speakSentences, - shutUpTTS, - initSpeechRecognition, - toggleSpeechRecognition, - handleVoiceCommand, - findElement, - executeCommand, - showToast, - loadVoices, - populateAllVoiceDropdowns, - updateAllVoiceDropdowns, - getVoiceDropdowns - }; - -}); - + speakMessage, + stopSpeaking, + speakSentences, + shutUpTTS, + initSpeechRecognition, + toggleSpeechRecognition, + handleVoiceCommand, + findElement, + executeCommand, + showToast, + loadVoices, + populateAllVoiceDropdowns, + updateAllVoiceDropdowns, + getVoiceDropdowns + }; + +}); + diff --git a/js/chat/chat-init.js b/js/chat/chat-init.js index 06d7b5b..af7ee0a 100644 --- a/js/chat/chat-init.js +++ b/js/chat/chat-init.js @@ -1,31 +1,30 @@ -document.addEventListener("DOMContentLoaded", () => { - const { chatBox, chatInput, clearChatBtn, voiceToggleBtn, modelSelect, synth, autoSpeakEnabled, speakMessage, stopSpeaking, showToast, toggleSpeechRecognition, initSpeechRecognition, handleVoiceCommand, speakSentences } = window._chatInternals; - const imagePatterns = window.imagePatterns; - const randomSeed = window.randomSeed; - const generateSessionTitle = messages => { - let title = messages.find(m => m.role === "ai")?.content.replace(/[#_*`]/g, "").trim() || "New Chat"; - return title.length > 50 ? title.substring(0, 50) + "..." : title; - }; - const checkAndUpdateSessionTitle = () => { - const currentSession = Storage.getCurrentSession(); - if (!currentSession.name || currentSession.name === "New Chat") { - const newTitle = generateSessionTitle(currentSession.messages); - if (newTitle && newTitle !== currentSession.name) Storage.renameSession(currentSession.id, newTitle); - } - }; +document.addEventListener("DOMContentLoaded", () => { + const { chatBox, chatInput, clearChatBtn, voiceToggleBtn, modelSelect, synth, autoSpeakEnabled, speakMessage, stopSpeaking, showToast, toggleSpeechRecognition, initSpeechRecognition, handleVoiceCommand, speakSentences } = window._chatInternals; + const randomSeed = window.randomSeed; + const generateSessionTitle = messages => { + let title = messages.find(m => m.role === "ai")?.content.replace(/[#_*`]/g, "").trim() || "New Chat"; + return title.length > 50 ? title.substring(0, 50) + "..." : title; + }; + const checkAndUpdateSessionTitle = () => { + const currentSession = Storage.getCurrentSession(); + if (!currentSession.name || currentSession.name === "New Chat") { + const newTitle = generateSessionTitle(currentSession.messages); + if (newTitle && newTitle !== currentSession.name) Storage.renameSession(currentSession.id, newTitle); + } + }; const appendMessage = ({ role, content, index, imageUrls = [], audioUrls = [] }) => { - const container = document.createElement("div"); - container.classList.add("message"); - container.dataset.index = index; - container.dataset.role = role; - Object.assign(container.style, { - float: role === "user" ? "right" : "left", - clear: "both", - maxWidth: role === "user" ? "40%" : "60%", - marginRight: role === "user" ? "10px" : null, - marginLeft: role !== "user" ? "10px" : null, - }); - container.classList.add(role === "user" ? "user-message" : "ai-message"); + const container = document.createElement("div"); + container.classList.add("message"); + container.dataset.index = index; + container.dataset.role = role; + Object.assign(container.style, { + float: role === "user" ? "right" : "left", + clear: "both", + maxWidth: role === "user" ? "40%" : "60%", + marginRight: role === "user" ? "10px" : null, + marginLeft: role !== "user" ? "10px" : null, + }); + container.classList.add(role === "user" ? "user-message" : "ai-message"); const bubbleContent = document.createElement("div"); bubbleContent.classList.add("message-text"); if (role === "ai") { @@ -48,215 +47,215 @@ document.addEventListener("DOMContentLoaded", () => { } else { bubbleContent.textContent = content; } - container.appendChild(bubbleContent); - const actionsDiv = document.createElement("div"); - actionsDiv.className = "message-actions"; - if (role === "ai") { - const copyBtn = document.createElement("button"); - copyBtn.className = "message-action-btn"; - copyBtn.textContent = "Copy"; - copyBtn.addEventListener("click", () => { - navigator.clipboard.writeText(content) - .then(() => showToast("AI response copied to clipboard")) - .catch(() => showToast("Failed to copy to clipboard")); - }); - actionsDiv.appendChild(copyBtn); - const speakBtn = document.createElement("button"); - speakBtn.className = "message-action-btn speak-message-btn"; - speakBtn.innerHTML = '🔊 Speak'; - speakBtn.addEventListener("click", () => { - stopSpeaking(); - const sentences = content.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0); - speakSentences(sentences); - }); - actionsDiv.appendChild(speakBtn); - const regenBtn = document.createElement("button"); - regenBtn.className = "message-action-btn"; - regenBtn.textContent = "Re-generate"; - regenBtn.addEventListener("click", () => reGenerateAIResponse(index)); - actionsDiv.appendChild(regenBtn); - const editAIBtn = document.createElement("button"); - editAIBtn.className = "message-action-btn"; - editAIBtn.textContent = "Edit"; - editAIBtn.addEventListener("click", () => editMessage(index)); - actionsDiv.appendChild(editAIBtn); - } else { - const editUserBtn = document.createElement("button"); - editUserBtn.className = "message-action-btn"; - editUserBtn.textContent = "Edit"; - editUserBtn.addEventListener("click", () => editMessage(index)); - actionsDiv.appendChild(editUserBtn); - } - container.appendChild(actionsDiv); - bubbleContent.querySelectorAll("pre code").forEach(block => { - const buttonContainer = document.createElement("div"); - Object.assign(buttonContainer.style, { display: "flex", gap: "5px", marginTop: "5px" }); - const codeContent = block.textContent.trim(); - const language = block.className.match(/language-(\w+)/)?.[1] || "text"; - const copyCodeBtn = document.createElement("button"); - copyCodeBtn.className = "message-action-btn"; - copyCodeBtn.textContent = "Copy Code"; - copyCodeBtn.style.fontSize = "12px"; - copyCodeBtn.addEventListener("click", () => { - navigator.clipboard.writeText(codeContent) - .then(() => showToast("Code copied to clipboard")) - .catch(() => showToast("Failed to copy code")); - }); - buttonContainer.appendChild(copyCodeBtn); - const downloadCodeBtn = document.createElement("button"); - downloadCodeBtn.className = "message-action-btn"; - downloadCodeBtn.textContent = "Download"; - downloadCodeBtn.style.fontSize = "12px"; - downloadCodeBtn.addEventListener("click", () => downloadCodeAsTxt(codeContent, language)); - buttonContainer.appendChild(downloadCodeBtn); - block.parentNode.insertAdjacentElement("afterend", buttonContainer); - }); - chatBox.appendChild(container); + container.appendChild(bubbleContent); + const actionsDiv = document.createElement("div"); + actionsDiv.className = "message-actions"; + if (role === "ai") { + const copyBtn = document.createElement("button"); + copyBtn.className = "message-action-btn"; + copyBtn.textContent = "Copy"; + copyBtn.addEventListener("click", () => { + navigator.clipboard.writeText(content) + .then(() => showToast("AI response copied to clipboard")) + .catch(() => showToast("Failed to copy to clipboard")); + }); + actionsDiv.appendChild(copyBtn); + const speakBtn = document.createElement("button"); + speakBtn.className = "message-action-btn speak-message-btn"; + speakBtn.innerHTML = '🔊 Speak'; + speakBtn.addEventListener("click", () => { + stopSpeaking(); + const sentences = content.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0); + speakSentences(sentences); + }); + actionsDiv.appendChild(speakBtn); + const regenBtn = document.createElement("button"); + regenBtn.className = "message-action-btn"; + regenBtn.textContent = "Re-generate"; + regenBtn.addEventListener("click", () => reGenerateAIResponse(index)); + actionsDiv.appendChild(regenBtn); + const editAIBtn = document.createElement("button"); + editAIBtn.className = "message-action-btn"; + editAIBtn.textContent = "Edit"; + editAIBtn.addEventListener("click", () => editMessage(index)); + actionsDiv.appendChild(editAIBtn); + } else { + const editUserBtn = document.createElement("button"); + editUserBtn.className = "message-action-btn"; + editUserBtn.textContent = "Edit"; + editUserBtn.addEventListener("click", () => editMessage(index)); + actionsDiv.appendChild(editUserBtn); + } + container.appendChild(actionsDiv); + bubbleContent.querySelectorAll("pre code").forEach(block => { + const buttonContainer = document.createElement("div"); + Object.assign(buttonContainer.style, { display: "flex", gap: "5px", marginTop: "5px" }); + const codeContent = block.textContent.trim(); + const language = block.className.match(/language-(\w+)/)?.[1] || "text"; + const copyCodeBtn = document.createElement("button"); + copyCodeBtn.className = "message-action-btn"; + copyCodeBtn.textContent = "Copy Code"; + copyCodeBtn.style.fontSize = "12px"; + copyCodeBtn.addEventListener("click", () => { + navigator.clipboard.writeText(codeContent) + .then(() => showToast("Code copied to clipboard")) + .catch(() => showToast("Failed to copy code")); + }); + buttonContainer.appendChild(copyCodeBtn); + const downloadCodeBtn = document.createElement("button"); + downloadCodeBtn.className = "message-action-btn"; + downloadCodeBtn.textContent = "Download"; + downloadCodeBtn.style.fontSize = "12px"; + downloadCodeBtn.addEventListener("click", () => downloadCodeAsTxt(codeContent, language)); + buttonContainer.appendChild(downloadCodeBtn); + block.parentNode.insertAdjacentElement("afterend", buttonContainer); + }); + chatBox.appendChild(container); chatBox.scrollTop = chatBox.scrollHeight; window.highlightUtils?.highlightAllCodeBlocks(chatBox); - }; - const downloadCodeAsTxt = (codeContent, language) => { - const blob = new Blob([codeContent], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `code-${language}-${Date.now()}.txt`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - showToast("Code downloaded as .txt"); - }; - const copyImage = (img, imageId) => { - console.log(`Copying image with ID: ${imageId}`); - if (!img.complete || img.naturalWidth === 0) { - showToast("Image not fully loaded yet. Please try again."); - return; - } - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - canvas.width = img.naturalWidth; - canvas.height = img.naturalHeight; - try { - ctx.drawImage(img, 0, 0); - canvas.toBlob((blob) => { - if (!blob) { - showToast("Failed to copy image: Unable to create blob."); - return; - } - navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]) - .then(() => { - const dataURL = canvas.toDataURL("image/png"); - localStorage.setItem(`lastCopiedImage_${imageId}`, dataURL); - showToast("Image copied to clipboard and saved to local storage"); - }) - .catch(err => { - console.error("Copy image error:", err); - showToast("Failed to copy image: " + err.message); - }); - }, "image/png"); - } catch (err) { - console.error("Copy image error:", err); - showToast("Failed to copy image due to CORS or other error: " + err.message); - } - }; - const downloadImage = (img, imageId) => { - console.log(`Downloading image with ID: ${imageId}`); - if (!img.src) { - showToast("No image source available to download."); - return; - } - fetch(img.src, { mode: "cors" }) - .then(response => { - if (!response.ok) throw new Error("Network response was not ok"); - return response.blob(); - }) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `image-${imageId}-${Date.now()}.png`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - showToast("Image downloaded successfully"); - }) - .catch(err => { - console.error("Download image error:", err); - showToast("Failed to download image: " + err.message); - }); - }; - const refreshImage = (img, imageId) => { - console.log(`Refreshing image with ID: ${imageId}`); - if (!img.src) { - showToast("No image source to refresh."); - return; - } + }; + const downloadCodeAsTxt = (codeContent, language) => { + const blob = new Blob([codeContent], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `code-${language}-${Date.now()}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + showToast("Code downloaded as .txt"); + }; + const copyImage = (img, imageId) => { + console.log(`Copying image with ID: ${imageId}`); + if (!img.complete || img.naturalWidth === 0) { + showToast("Image not fully loaded yet. Please try again."); + return; + } + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + try { + ctx.drawImage(img, 0, 0); + canvas.toBlob((blob) => { + if (!blob) { + showToast("Failed to copy image: Unable to create blob."); + return; + } + navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]) + .then(() => { + const dataURL = canvas.toDataURL("image/png"); + localStorage.setItem(`lastCopiedImage_${imageId}`, dataURL); + showToast("Image copied to clipboard and saved to local storage"); + }) + .catch(err => { + console.error("Copy image error:", err); + showToast("Failed to copy image: " + err.message); + }); + }, "image/png"); + } catch (err) { + console.error("Copy image error:", err); + showToast("Failed to copy image due to CORS or other error: " + err.message); + } + }; + const downloadImage = (img, imageId) => { + console.log(`Downloading image with ID: ${imageId}`); + if (!img.src) { + showToast("No image source available to download."); + return; + } + fetch(img.src, { mode: "cors" }) + .then(response => { + if (!response.ok) throw new Error("Network response was not ok"); + return response.blob(); + }) + .then(blob => { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `image-${imageId}-${Date.now()}.png`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + showToast("Image downloaded successfully"); + }) + .catch(err => { + console.error("Download image error:", err); + showToast("Failed to download image: " + err.message); + }); + }; + const refreshImage = (img, imageId) => { + console.log(`Refreshing image with ID: ${imageId}`); + if (!img.src) { + showToast("No image source to refresh."); + return; + } const urlObj = new URL(img.src); if (!window.polliClient || !window.polliClient.imageBase) { showToast("Image client not ready."); return; } const baseOrigin = new URL(window.polliClient.imageBase).origin; - if (urlObj.origin !== baseOrigin) { - showToast("Can't refresh: not a polliLib image URL."); - return; - } - const newSeed = Math.floor(Math.random() * 1000000); - let prompt = ''; - try { - const parts = urlObj.pathname.split('/'); - const i = parts.indexOf('prompt'); - if (i >= 0 && parts[i+1]) prompt = decodeURIComponent(parts[i+1]); - } catch {} - const width = Number(urlObj.searchParams.get('width')) || img.naturalWidth || 512; - const height = Number(urlObj.searchParams.get('height')) || img.naturalHeight || 512; - const model = urlObj.searchParams.get('model') || (document.getElementById('model-select')?.value || undefined); - let newUrl = img.src; - try { - if (window.polliLib && window.polliClient && prompt) { - newUrl = window.polliLib.mcp.generateImageUrl(window.polliClient, { - prompt, width, height, seed: newSeed, nologo: true, model - }); - } else { - urlObj.searchParams.set('seed', String(newSeed)); - newUrl = urlObj.toString(); - } - } catch (e) { - console.warn('polliLib generateImageUrl failed; falling back to seed swap', e); - urlObj.searchParams.set('seed', String(newSeed)); - newUrl = urlObj.toString(); - } - const loadingDiv = document.createElement("div"); - loadingDiv.className = "ai-image-loading"; - const spinner = document.createElement("div"); - spinner.className = "loading-spinner"; - loadingDiv.appendChild(spinner); - Object.assign(loadingDiv.style, { width: img.width + "px", height: img.height + "px" }); - img.parentNode.insertBefore(loadingDiv, img); - img.style.display = "none"; - img.onload = () => { - loadingDiv.remove(); - img.style.display = "block"; - showToast("Image refreshed with new seed"); - }; - img.onerror = () => { - loadingDiv.innerHTML = "⚠️ Failed to refresh image"; - Object.assign(loadingDiv.style, { display: "flex", justifyContent: "center", alignItems: "center" }); - showToast("Failed to refresh image"); - }; - img.src = newUrl; - }; - const openImageInNewTab = (img, imageId) => { - console.log(`Opening image in new tab with ID: ${imageId}`); - if (!img.src) { - showToast("No image source available to open."); - return; - } - window.open(img.src, "_blank"); - showToast("Image opened in new tab"); - }; + if (urlObj.origin !== baseOrigin) { + showToast("Can't refresh: not a polliLib image URL."); + return; + } + const newSeed = Math.floor(Math.random() * 1000000); + let prompt = ''; + try { + const parts = urlObj.pathname.split('/'); + const i = parts.indexOf('prompt'); + if (i >= 0 && parts[i+1]) prompt = decodeURIComponent(parts[i+1]); + } catch {} + const width = Number(urlObj.searchParams.get('width')) || img.naturalWidth || 512; + const height = Number(urlObj.searchParams.get('height')) || img.naturalHeight || 512; + const model = urlObj.searchParams.get('model') || (document.getElementById('model-select')?.value || undefined); + let newUrl = img.src; + try { + if (window.polliLib && window.polliClient && prompt) { + newUrl = window.polliLib.mcp.generateImageUrl(window.polliClient, { + prompt, width, height, seed: newSeed, nologo: true, model + }); + } else { + urlObj.searchParams.set('seed', String(newSeed)); + newUrl = urlObj.toString(); + } + } catch (e) { + console.warn('polliLib generateImageUrl failed; falling back to seed swap', e); + urlObj.searchParams.set('seed', String(newSeed)); + newUrl = urlObj.toString(); + } + const loadingDiv = document.createElement("div"); + loadingDiv.className = "ai-image-loading"; + const spinner = document.createElement("div"); + spinner.className = "loading-spinner"; + loadingDiv.appendChild(spinner); + Object.assign(loadingDiv.style, { width: img.width + "px", height: img.height + "px" }); + img.parentNode.insertBefore(loadingDiv, img); + img.style.display = "none"; + img.onload = () => { + loadingDiv.remove(); + img.style.display = "block"; + showToast("Image refreshed with new seed"); + }; + img.onerror = () => { + loadingDiv.innerHTML = "⚠️ Failed to refresh image"; + Object.assign(loadingDiv.style, { display: "flex", justifyContent: "center", alignItems: "center" }); + showToast("Failed to refresh image"); + }; + img.src = newUrl; + }; + const openImageInNewTab = (img, imageId) => { + console.log(`Opening image in new tab with ID: ${imageId}`); + if (!img.src) { + showToast("No image source available to open."); + return; + } + window.open(img.src, "_blank"); + showToast("Image opened in new tab"); + }; const createImageElement = (url, msgIndex) => { const imageId = `img-${msgIndex}-${Date.now()}`; localStorage.setItem(`imageId_${msgIndex}`, imageId); @@ -328,58 +327,77 @@ document.addEventListener("DOMContentLoaded", () => { return audio; }; const attachImageButtonListeners = (img, imageId) => { - const imgButtonContainer = document.querySelector(`.image-button-container[data-image-id="${imageId}"]`); - if (!imgButtonContainer) { - console.warn(`No image button container found for image ID: ${imageId}`); - return; - } - console.log(`Attaching image button listeners for image ID: ${imageId}`); - imgButtonContainer.innerHTML = ""; - const copyImgBtn = document.createElement("button"); - copyImgBtn.className = "message-action-btn"; - copyImgBtn.textContent = "Copy Image"; - copyImgBtn.style.pointerEvents = "auto"; - copyImgBtn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log(`Copy Image button clicked for image ID: ${imageId}`); - copyImage(img, imageId); - }); - imgButtonContainer.appendChild(copyImgBtn); - const downloadImgBtn = document.createElement("button"); - downloadImgBtn.className = "message-action-btn"; - downloadImgBtn.textContent = "Download Image"; - downloadImgBtn.style.pointerEvents = "auto"; - downloadImgBtn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log(`Download Image button clicked for image ID: ${imageId}`); - downloadImage(img, imageId); - }); - imgButtonContainer.appendChild(downloadImgBtn); - const refreshImgBtn = document.createElement("button"); - refreshImgBtn.className = "message-action-btn"; - refreshImgBtn.textContent = "Refresh Image"; - refreshImgBtn.style.pointerEvents = "auto"; - refreshImgBtn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log(`Refresh Image button clicked for image ID: ${imageId}`); - refreshImage(img, imageId); - }); - imgButtonContainer.appendChild(refreshImgBtn); - const openImgBtn = document.createElement("button"); - openImgBtn.className = "message-action-btn"; - openImgBtn.textContent = "Open in New Tab"; - openImgBtn.style.pointerEvents = "auto"; - openImgBtn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log(`Open in New Tab button clicked for image ID: ${imageId}`); - openImageInNewTab(img, imageId); - }); - imgButtonContainer.appendChild(openImgBtn); - }; + const imgButtonContainer = document.querySelector(`.image-button-container[data-image-id="${imageId}"]`); + if (!imgButtonContainer) { + console.warn(`No image button container found for image ID: ${imageId}`); + return; + } + console.log(`Attaching image button listeners for image ID: ${imageId}`); + imgButtonContainer.innerHTML = ""; + const copyImgBtn = document.createElement("button"); + copyImgBtn.className = "message-action-btn"; + copyImgBtn.textContent = "Copy Image"; + copyImgBtn.style.pointerEvents = "auto"; + copyImgBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log(`Copy Image button clicked for image ID: ${imageId}`); + copyImage(img, imageId); + }); + imgButtonContainer.appendChild(copyImgBtn); + const downloadImgBtn = document.createElement("button"); + downloadImgBtn.className = "message-action-btn"; + downloadImgBtn.textContent = "Download Image"; + downloadImgBtn.style.pointerEvents = "auto"; + downloadImgBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log(`Download Image button clicked for image ID: ${imageId}`); + downloadImage(img, imageId); + }); + imgButtonContainer.appendChild(downloadImgBtn); + const refreshImgBtn = document.createElement("button"); + refreshImgBtn.className = "message-action-btn"; + refreshImgBtn.textContent = "Refresh Image"; + refreshImgBtn.style.pointerEvents = "auto"; + refreshImgBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log(`Refresh Image button clicked for image ID: ${imageId}`); + refreshImage(img, imageId); + }); + imgButtonContainer.appendChild(refreshImgBtn); + const openImgBtn = document.createElement("button"); + openImgBtn.className = "message-action-btn"; + openImgBtn.textContent = "Open in New Tab"; + openImgBtn.style.pointerEvents = "auto"; + openImgBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log(`Open in New Tab button clicked for image ID: ${imageId}`); + openImageInNewTab(img, imageId); + }); + imgButtonContainer.appendChild(openImgBtn); + }; + function getLatestImagePrompt(defaultPrompt = 'default scene') { + const currentSession = Storage.getCurrentSession(); + const messages = currentSession?.messages || []; + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]; + const imageMeta = msg?.metadata?.images; + if (Array.isArray(imageMeta)) { + for (const info of imageMeta) { + if (info && typeof info.prompt === 'string' && info.prompt.trim()) { + return info.prompt.trim(); + } + } + } + } + const fallback = messages[messages.length - 1]?.content; + if (typeof fallback === 'string' && fallback.trim()) return fallback.trim(); + return defaultPrompt; + } + const renderStoredMessages = messages => { console.log("Rendering stored messages..."); chatBox.innerHTML = ""; @@ -416,9 +434,9 @@ document.addEventListener("DOMContentLoaded", () => { }); window.highlightUtils?.highlightAllCodeBlocks(chatBox); }; - window.addNewMessage = ({ role, content, imageUrls = [], audioUrls = [] }) => { + window.addNewMessage = ({ role, content, imageUrls = [], audioUrls = [], metadata = null }) => { const currentSession = Storage.getCurrentSession(); - currentSession.messages.push({ role, content, imageUrls, audioUrls }); + currentSession.messages.push({ role, content, imageUrls, audioUrls, metadata }); Storage.updateSessionMessages(currentSession.id, currentSession.messages); if (!window.polliClient || !window.polliClient.imageBase) { appendMessage({ role, content, index: currentSession.messages.length - 1, imageUrls, audioUrls }); @@ -438,435 +456,425 @@ document.addEventListener("DOMContentLoaded", () => { }); if (role === "ai") checkAndUpdateSessionTitle(); }; - const editMessage = msgIndex => { - const currentSession = Storage.getCurrentSession(); - const oldMessage = currentSession.messages[msgIndex]; - if (!oldMessage) return; - stopSpeaking(); - const newContent = prompt("Edit this message:", oldMessage.content); - if (newContent === null || newContent === oldMessage.content) return; - if (oldMessage.role === "user") { - currentSession.messages[msgIndex].content = newContent; - currentSession.messages = currentSession.messages.slice(0, msgIndex + 1); - Storage.updateSessionMessages(currentSession.id, currentSession.messages); - renderStoredMessages(currentSession.messages); - const loadingDiv = document.createElement("div"); - loadingDiv.id = `loading-${Date.now()}`; - loadingDiv.classList.add("message", "ai-message"); - Object.assign(loadingDiv.style, { float: "left", clear: "both", maxWidth: "60%", marginLeft: "10px" }); - loadingDiv.textContent = "Generating response..."; - chatBox.appendChild(loadingDiv); - chatBox.scrollTop = chatBox.scrollHeight; - sendToPolliLib(() => { - loadingDiv.remove(); + const editMessage = msgIndex => { + const currentSession = Storage.getCurrentSession(); + const oldMessage = currentSession.messages[msgIndex]; + if (!oldMessage) return; + stopSpeaking(); + const newContent = prompt("Edit this message:", oldMessage.content); + if (newContent === null || newContent === oldMessage.content) return; + if (oldMessage.role === "user") { + currentSession.messages[msgIndex].content = newContent; + currentSession.messages = currentSession.messages.slice(0, msgIndex + 1); + Storage.updateSessionMessages(currentSession.id, currentSession.messages); + renderStoredMessages(currentSession.messages); + const loadingDiv = document.createElement("div"); + loadingDiv.id = `loading-${Date.now()}`; + loadingDiv.classList.add("message", "ai-message"); + Object.assign(loadingDiv.style, { float: "left", clear: "both", maxWidth: "60%", marginLeft: "10px" }); + loadingDiv.textContent = "Generating response..."; + chatBox.appendChild(loadingDiv); + chatBox.scrollTop = chatBox.scrollHeight; + sendToPolliLib(() => { + loadingDiv.remove(); window.highlightUtils?.highlightAllCodeBlocks(chatBox); - }, newContent); - showToast("User message updated and new response generated"); - } else { - currentSession.messages[msgIndex].content = newContent; - Storage.updateSessionMessages(currentSession.id, currentSession.messages); - renderStoredMessages(currentSession.messages); + }, newContent); + showToast("User message updated and new response generated"); + } else { + currentSession.messages[msgIndex].content = newContent; + Storage.updateSessionMessages(currentSession.id, currentSession.messages); + renderStoredMessages(currentSession.messages); window.highlightUtils?.highlightAllCodeBlocks(chatBox); - showToast("AI message updated"); - } - }; - const reGenerateAIResponse = aiIndex => { - console.log(`Re-generating AI response for index: ${aiIndex}`); - const currentSession = Storage.getCurrentSession(); - if (aiIndex < 0 || aiIndex >= currentSession.messages.length || currentSession.messages[aiIndex].role !== "ai") { - showToast("Invalid AI message index for regeneration."); - return; - } - let userIndex = -1; - for (let i = aiIndex - 1; i >= 0; i--) { - if (currentSession.messages[i].role === "user") { - userIndex = i; - break; - } - } - if (userIndex === -1) { - showToast("No preceding user message found to regenerate from."); - return; - } - stopSpeaking(); - const userMessage = currentSession.messages[userIndex].content; - currentSession.messages = currentSession.messages.slice(0, userIndex + 1); - Storage.updateSessionMessages(currentSession.id, currentSession.messages); - renderStoredMessages(currentSession.messages); - const loadingDiv = document.createElement("div"); - loadingDiv.id = `loading-${Date.now()}`; - loadingDiv.classList.add("message", "ai-message"); - Object.assign(loadingDiv.style, { float: "left", clear: "both", maxWidth: "60%", marginLeft: "10px" }); - loadingDiv.textContent = "Regenerating response..."; - chatBox.appendChild(loadingDiv); - chatBox.scrollTop = chatBox.scrollHeight; - const uniqueUserMessage = `${userMessage} [regen-${Date.now()}-${Math.random().toString(36).substring(2)}]`; - console.log(`Sending re-generate request for user message: ${userMessage} (with unique suffix: ${uniqueUserMessage})`); - window.sendToPolliLib(() => { - loadingDiv.remove(); + showToast("AI message updated"); + } + }; + const reGenerateAIResponse = aiIndex => { + console.log(`Re-generating AI response for index: ${aiIndex}`); + const currentSession = Storage.getCurrentSession(); + if (aiIndex < 0 || aiIndex >= currentSession.messages.length || currentSession.messages[aiIndex].role !== "ai") { + showToast("Invalid AI message index for regeneration."); + return; + } + let userIndex = -1; + for (let i = aiIndex - 1; i >= 0; i--) { + if (currentSession.messages[i].role === "user") { + userIndex = i; + break; + } + } + if (userIndex === -1) { + showToast("No preceding user message found to regenerate from."); + return; + } + stopSpeaking(); + const userMessage = currentSession.messages[userIndex].content; + currentSession.messages = currentSession.messages.slice(0, userIndex + 1); + Storage.updateSessionMessages(currentSession.id, currentSession.messages); + renderStoredMessages(currentSession.messages); + const loadingDiv = document.createElement("div"); + loadingDiv.id = `loading-${Date.now()}`; + loadingDiv.classList.add("message", "ai-message"); + Object.assign(loadingDiv.style, { float: "left", clear: "both", maxWidth: "60%", marginLeft: "10px" }); + loadingDiv.textContent = "Regenerating response..."; + chatBox.appendChild(loadingDiv); + chatBox.scrollTop = chatBox.scrollHeight; + const uniqueUserMessage = `${userMessage} [regen-${Date.now()}-${Math.random().toString(36).substring(2)}]`; + console.log(`Sending re-generate request for user message: ${userMessage} (with unique suffix: ${uniqueUserMessage})`); + window.sendToPolliLib(() => { + loadingDiv.remove(); window.highlightUtils?.highlightAllCodeBlocks(chatBox); - checkAndUpdateSessionTitle(); - showToast("Response regenerated successfully"); - }, uniqueUserMessage); - }; - - if (voiceToggleBtn) { - voiceToggleBtn.addEventListener("click", window._chatInternals.toggleAutoSpeak); - window._chatInternals.updateVoiceToggleUI(); - setTimeout(() => { - if (autoSpeakEnabled) { - const testUtterance = new SpeechSynthesisUtterance("Voice check"); - testUtterance.volume = 0.1; - testUtterance.onend = () => {}; - testUtterance.onerror = err => { - window._chatInternals.autoSpeakEnabled = false; - localStorage.setItem("autoSpeakEnabled", "false"); - window._chatInternals.updateVoiceToggleUI(); - showToast("Voice synthesis unavailable. Voice mode disabled."); - }; - synth.speak(testUtterance); - } - }, 5000); - } - if (clearChatBtn) { - clearChatBtn.addEventListener("click", () => { - const currentSession = Storage.getCurrentSession(); - if (confirm("Are you sure you want to clear this chat?")) { - currentSession.messages = []; - Storage.updateSessionMessages(currentSession.id, currentSession.messages); - chatBox.innerHTML = ""; - showToast("Chat cleared"); - } - }); - } - const checkFirstLaunch = () => { - if (localStorage.getItem("firstLaunch") !== "0") return; - const firstLaunchModal = document.getElementById("first-launch-modal"); - if (!firstLaunchModal) return; - firstLaunchModal.classList.remove("hidden"); - const closeModal = () => { - firstLaunchModal.classList.add("hidden"); - localStorage.setItem("firstLaunch", "1"); - }; - document.getElementById("first-launch-close").addEventListener("click", closeModal); - document.getElementById("first-launch-complete").addEventListener("click", closeModal); - document.getElementById("setup-theme").addEventListener("click", () => { - firstLaunchModal.classList.add("hidden"); - document.getElementById("settings-modal").classList.remove("hidden"); - }); - document.getElementById("setup-personalization").addEventListener("click", () => { - firstLaunchModal.classList.add("hidden"); - document.getElementById("personalization-modal").classList.remove("hidden"); - }); - document.getElementById("setup-model").addEventListener("click", () => { - firstLaunchModal.classList.add("hidden"); - document.getElementById("model-select").focus(); - }); - }; - checkFirstLaunch(); - const setupVoiceInputButton = () => { - if (!("webkitSpeechRecognition" in window || "SpeechRecognition" in window)) { - const voiceInputBtn = document.getElementById("voice-input-btn"); - if (voiceInputBtn) { - voiceInputBtn.disabled = true; - voiceInputBtn.title = "Voice input not supported in this browser"; - } - return; - } - const inputButtonsContainer = document.querySelector(".input-buttons-container"); - if (!window._chatInternals.voiceInputBtn && inputButtonsContainer) { - const voiceInputBtn = document.createElement("button"); - voiceInputBtn.id = "voice-input-btn"; - voiceInputBtn.innerHTML = ''; - voiceInputBtn.title = "Voice input"; - inputButtonsContainer.insertBefore(voiceInputBtn, document.getElementById("send-button")); - window._chatInternals.setVoiceInputButton(voiceInputBtn); - voiceInputBtn.addEventListener("click", toggleSpeechRecognition); - } - }; - setupVoiceInputButton(); - if ("webkitSpeechRecognition" in window || "SpeechRecognition" in window) { - try { - toggleSpeechRecognition(); - } catch (err) { - console.error("Automatic speech recognition start failed:", err); - } - } - document.addEventListener("click", e => { - if (e.target.closest(".image-button-container")) { - e.preventDefault(); - e.stopPropagation(); - console.log("Click detected on image-button-container, preventing propagation"); - } - }, true); - - const sendButton = document.getElementById("send-button"); - - const handleSendMessage = () => { - const message = chatInput.value.trim(); - if (!message) return; - - chatInput.value = ""; - chatInput.style.height = "auto"; - window.addNewMessage({ role: "user", content: message }); - // Typed input should always go to the model. Commands are voice-only. - window.sendToPolliLib(() => { - sendButton.disabled = false; - chatInput.disabled = false; - chatInput.focus(); - }); - sendButton.disabled = true; - chatInput.disabled = true; - }; - window._chatInternals.handleSendMessage = handleSendMessage; - chatInput.addEventListener("input", () => { - sendButton.disabled = chatInput.value.trim() === ""; - chatInput.style.height = "auto"; - chatInput.style.height = chatInput.scrollHeight + "px"; - }); - sendButton.addEventListener("click", handleSendMessage); - - // Send on Enter, allow newline with Shift+Enter - chatInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - if (e.shiftKey) return; // allow newline - e.preventDefault(); - // Directly invoke the send handler so the message is processed - // even if the button state would block programmatic clicks. - handleSendMessage(); - } - }); - sendButton.disabled = chatInput.value.trim() === ""; - chatInput.dispatchEvent(new Event("input")); - const initialSession = Storage.getCurrentSession(); - if (initialSession.messages?.length > 0) renderStoredMessages(initialSession.messages); - chatInput.disabled = false; - chatInput.focus(); - const voiceChatModal = document.getElementById("voice-chat-modal"); - const openVoiceChatModalBtn = document.getElementById("open-voice-chat-modal"); - const closeVoiceChatModalBtn = document.getElementById("voice-chat-modal-close"); - const voiceSettingsModal = document.getElementById("voice-settings-modal"); - const openVoiceSettingsModalBtn = document.getElementById("open-voice-settings-modal"); - const voiceChatImage = document.getElementById("voice-chat-image"); - let slideshowInterval = null; - const startVoiceChatSlideshow = () => { - if (slideshowInterval) clearInterval(slideshowInterval); - const currentSession = Storage.getCurrentSession(); - let lastMessage = currentSession.messages.slice(-1)[0]?.content || "default scene"; - let imagePrompt = ""; - for (const { pattern, group } of imagePatterns) { - const match = lastMessage.match(pattern); - if (match) { - imagePrompt = match[group].trim(); - break; - } - } - if (!imagePrompt) { - imagePrompt = lastMessage.replace(/image|picture|show me|generate/gi, "").trim(); - } - imagePrompt = imagePrompt.slice(0, 100) + ", photographic"; - const updateImage = () => { - const seed = randomSeed(); - try { - if (window.polliLib && window.polliClient) { - const url = window.polliLib.mcp.generateImageUrl(window.polliClient, { - prompt: imagePrompt, - width: 512, - height: 512, - seed, - nologo: true - }); - voiceChatImage.src = url; - } else { - voiceChatImage.src = "https://via.placeholder.com/512?text=Image+Unavailable"; - } - } catch (e) { - console.warn('polliLib generateImageUrl failed', e); - voiceChatImage.src = "https://via.placeholder.com/512?text=Image+Unavailable"; - } - }; - updateImage(); - slideshowInterval = setInterval(updateImage, 10000); - }; - const stopVoiceChatSlideshow = () => { - if (slideshowInterval) { - clearInterval(slideshowInterval); - slideshowInterval = null; - } - }; - let voiceBuffer = ""; - let silenceTimeout = null; - const setupCustomSpeechRecognition = () => { - if (!window._chatInternals.recognition) { - const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; - if (!SpeechRecognition) { - showToast("Speech recognition not supported in this browser"); - return false; - } - window._chatInternals.recognition = new SpeechRecognition(); - const recognition = window._chatInternals.recognition; - recognition.continuous = true; - recognition.interimResults = true; - recognition.lang = "en-US"; - recognition.onstart = () => { - window._chatInternals.isListening = true; - showToast("Voice recognition active"); - document.getElementById("voice-chat-start").disabled = true; - document.getElementById("voice-chat-stop").disabled = false; - }; - recognition.onend = () => { - window._chatInternals.isListening = false; - document.getElementById("voice-chat-start").disabled = false; - document.getElementById("voice-chat-stop").disabled = true; - }; - recognition.onerror = event => { - window._chatInternals.isListening = false; - document.getElementById("voice-chat-start").disabled = false; - document.getElementById("voice-chat-stop").disabled = true; - const errors = { - "no-speech": "No speech detected. Please try again.", - "not-allowed": "Microphone access denied. Please allow microphone access in your browser settings.", - "service-not-allowed": "Microphone access denied. Please allow microphone access in your browser settings.", - }; - showToast(errors[event.error] || "Voice recognition error: " + event.error); - }; - recognition.onresult = event => { - let interimTranscript = ""; - let finalTranscript = ""; - for (let i = event.resultIndex; i < event.results.length; i++) { - const transcript = event.results[i][0].transcript; - if (event.results[i].isFinal) { - const processed = transcript.trim(); - if (!handleVoiceCommand(processed)) finalTranscript += processed + " "; - } else { - interimTranscript += transcript; - } - } - voiceBuffer += finalTranscript; - chatInput.value = voiceBuffer + interimTranscript; - if (finalTranscript) { - clearTimeout(silenceTimeout); - silenceTimeout = setTimeout(() => { - if (voiceBuffer.trim()) { - window.addNewMessage({ role: "user", content: voiceBuffer.trim() }); - window.sendToPolliLib(startVoiceChatSlideshow); - voiceBuffer = ""; - chatInput.value = ""; - } - }, 1500); - } - }; - } - return true; - }; - const setupVoiceChatControls = () => { - const modalBody = voiceChatModal.querySelector(".modal-body"); - let voiceSelectChat = modalBody.querySelector("#voice-select-voicechat"); - if (!voiceSelectChat) { - const voiceSelectContainer = document.createElement("div"); - voiceSelectContainer.className = "form-group mb-3"; - const voiceSelectLabel = document.createElement("label"); - voiceSelectLabel.className = "form-label"; - voiceSelectLabel.innerHTML = ' Voice Selection:'; - voiceSelectLabel.htmlFor = "voice-select-voicechat"; - voiceSelectChat = document.createElement("select"); - voiceSelectChat.id = "voice-select-voicechat"; - voiceSelectChat.className = "form-control"; - voiceSelectContainer.appendChild(voiceSelectLabel); - voiceSelectContainer.appendChild(voiceSelectChat); - const insertAfter = modalBody.querySelector("p") || voiceChatImage; - if (insertAfter?.nextSibling) modalBody.insertBefore(voiceSelectContainer, insertAfter.nextSibling); - else modalBody.appendChild(voiceSelectContainer); - } - const existingControls = modalBody.querySelector(".voice-chat-controls"); - if (existingControls) existingControls.remove(); - const controlsDiv = document.createElement("div"); - controlsDiv.className = "voice-chat-controls"; - Object.assign(controlsDiv.style, { display: "flex", gap: "10px", marginTop: "15px" }); - const startBtn = document.createElement("button"); - startBtn.id = "voice-chat-start"; - startBtn.className = "btn btn-primary"; - startBtn.textContent = "Start Listening"; - startBtn.style.width = "100%"; - startBtn.style.padding = "10px"; - startBtn.disabled = window._chatInternals.isListening; - const stopBtn = document.createElement("button"); - stopBtn.id = "voice-chat-stop"; - stopBtn.className = "btn btn-danger"; - stopBtn.textContent = "Stop Listening"; - stopBtn.style.width = "100%"; - stopBtn.style.padding = "10px"; - stopBtn.disabled = !window._chatInternals.isListening; - controlsDiv.appendChild(startBtn); - controlsDiv.appendChild(stopBtn); - modalBody.appendChild(controlsDiv); - startBtn.addEventListener("click", () => { - if (!setupCustomSpeechRecognition()) return showToast("Failed to initialize speech recognition"); - try { - window._chatInternals.recognition.start(); - startVoiceChatSlideshow(); - } catch (error) { - showToast("Could not start speech recognition: " + error.message); - } - }); - stopBtn.addEventListener("click", () => { - if (window._chatInternals.recognition && window._chatInternals.isListening) { - window._chatInternals.recognition.stop(); - stopVoiceChatSlideshow(); - showToast("Voice recognition stopped"); - } - }); - }; - const updateAllVoiceDropdowns = selectedIndex => { - ["voice-select", "voice-select-modal", "voice-settings-modal", "voice-select-voicechat"].forEach(id => { - const dropdown = document.getElementById(id); - if (dropdown) dropdown.value = selectedIndex; - }); - }; - openVoiceChatModalBtn.addEventListener("click", () => { - voiceChatModal.classList.remove("hidden"); - setupVoiceChatControls(); - window._chatInternals.populateAllVoiceDropdowns(); - }); - closeVoiceChatModalBtn.addEventListener("click", () => { - voiceChatModal.classList.add("hidden"); - if (window._chatInternals.recognition && window._chatInternals.isListening) window._chatInternals.recognition.stop(); - stopVoiceChatSlideshow(); - }); - openVoiceSettingsModalBtn.addEventListener("click", () => { - voiceSettingsModal.classList.remove("hidden"); - window._chatInternals.populateAllVoiceDropdowns(); - const voiceSpeedInput = document.getElementById("voice-speed"); - const voicePitchInput = document.getElementById("voice-pitch"); - const voiceSpeedValue = document.getElementById("voice-speed-value"); - const voicePitchValue = document.getElementById("voice-pitch-value"); - voiceSpeedInput.value = localStorage.getItem("voiceSpeed") || 0.9; - voicePitchInput.value = localStorage.getItem("voicePitch") || 1.0; - voiceSpeedValue.textContent = `${voiceSpeedInput.value}x`; - voicePitchValue.textContent = `${voicePitchInput.value}x`; - }); - document.getElementById("voice-settings-modal-close").addEventListener("click", () => voiceSettingsModal.classList.add("hidden")); - document.getElementById("voice-settings-cancel").addEventListener("click", () => voiceSettingsModal.classList.add("hidden")); - document.getElementById("voice-settings-save").addEventListener("click", () => { - const voiceSpeedInput = document.getElementById("voice-speed"); - const voicePitchInput = document.getElementById("voice-pitch"); - const voiceSelectModal = document.getElementById("voice-select-modal"); - const selectedVoiceIndex = voiceSelectModal.value; - const voiceSpeed = voiceSpeedInput.value; - const voicePitch = voicePitchInput.value; - window._chatInternals.selectedVoice = window._chatInternals.voices[selectedVoiceIndex]; - localStorage.setItem("selectedVoiceIndex", selectedVoiceIndex); - localStorage.setItem("voiceSpeed", voiceSpeed); - localStorage.setItem("voicePitch", voicePitch); - window._chatInternals.updateVoiceToggleUI(); - updateAllVoiceDropdowns(selectedVoiceIndex); - voiceSettingsModal.classList.add("hidden"); - showToast("Voice settings saved"); - }); - document.getElementById("voice-speed").addEventListener("input", () => { - document.getElementById("voice-speed-value").textContent = `${document.getElementById("voice-speed").value}x`; - }); - document.getElementById("voice-pitch").addEventListener("input", () => { - document.getElementById("voice-pitch-value").textContent = `${document.getElementById("voice-pitch").value}x`; - }); -}); + checkAndUpdateSessionTitle(); + showToast("Response regenerated successfully"); + }, uniqueUserMessage); + }; + + if (voiceToggleBtn) { + voiceToggleBtn.addEventListener("click", window._chatInternals.toggleAutoSpeak); + window._chatInternals.updateVoiceToggleUI(); + setTimeout(() => { + if (autoSpeakEnabled) { + const testUtterance = new SpeechSynthesisUtterance("Voice check"); + testUtterance.volume = 0.1; + testUtterance.onend = () => {}; + testUtterance.onerror = err => { + window._chatInternals.autoSpeakEnabled = false; + localStorage.setItem("autoSpeakEnabled", "false"); + window._chatInternals.updateVoiceToggleUI(); + showToast("Voice synthesis unavailable. Voice mode disabled."); + }; + synth.speak(testUtterance); + } + }, 5000); + } + if (clearChatBtn) { + clearChatBtn.addEventListener("click", () => { + const currentSession = Storage.getCurrentSession(); + if (confirm("Are you sure you want to clear this chat?")) { + currentSession.messages = []; + Storage.updateSessionMessages(currentSession.id, currentSession.messages); + chatBox.innerHTML = ""; + showToast("Chat cleared"); + } + }); + } + const checkFirstLaunch = () => { + if (localStorage.getItem("firstLaunch") !== "0") return; + const firstLaunchModal = document.getElementById("first-launch-modal"); + if (!firstLaunchModal) return; + firstLaunchModal.classList.remove("hidden"); + const closeModal = () => { + firstLaunchModal.classList.add("hidden"); + localStorage.setItem("firstLaunch", "1"); + }; + document.getElementById("first-launch-close").addEventListener("click", closeModal); + document.getElementById("first-launch-complete").addEventListener("click", closeModal); + document.getElementById("setup-theme").addEventListener("click", () => { + firstLaunchModal.classList.add("hidden"); + document.getElementById("settings-modal").classList.remove("hidden"); + }); + document.getElementById("setup-personalization").addEventListener("click", () => { + firstLaunchModal.classList.add("hidden"); + document.getElementById("personalization-modal").classList.remove("hidden"); + }); + document.getElementById("setup-model").addEventListener("click", () => { + firstLaunchModal.classList.add("hidden"); + document.getElementById("model-select").focus(); + }); + }; + checkFirstLaunch(); + const setupVoiceInputButton = () => { + if (!("webkitSpeechRecognition" in window || "SpeechRecognition" in window)) { + const voiceInputBtn = document.getElementById("voice-input-btn"); + if (voiceInputBtn) { + voiceInputBtn.disabled = true; + voiceInputBtn.title = "Voice input not supported in this browser"; + } + return; + } + const inputButtonsContainer = document.querySelector(".input-buttons-container"); + if (!window._chatInternals.voiceInputBtn && inputButtonsContainer) { + const voiceInputBtn = document.createElement("button"); + voiceInputBtn.id = "voice-input-btn"; + voiceInputBtn.innerHTML = ''; + voiceInputBtn.title = "Voice input"; + inputButtonsContainer.insertBefore(voiceInputBtn, document.getElementById("send-button")); + window._chatInternals.setVoiceInputButton(voiceInputBtn); + voiceInputBtn.addEventListener("click", toggleSpeechRecognition); + } + }; + setupVoiceInputButton(); + if ("webkitSpeechRecognition" in window || "SpeechRecognition" in window) { + try { + toggleSpeechRecognition(); + } catch (err) { + console.error("Automatic speech recognition start failed:", err); + } + } + document.addEventListener("click", e => { + if (e.target.closest(".image-button-container")) { + e.preventDefault(); + e.stopPropagation(); + console.log("Click detected on image-button-container, preventing propagation"); + } + }, true); + + const sendButton = document.getElementById("send-button"); + + const handleSendMessage = () => { + const message = chatInput.value.trim(); + if (!message) return; + + chatInput.value = ""; + chatInput.style.height = "auto"; + window.addNewMessage({ role: "user", content: message }); + // Typed input should always go to the model. Commands are voice-only. + window.sendToPolliLib(() => { + sendButton.disabled = false; + chatInput.disabled = false; + chatInput.focus(); + }); + sendButton.disabled = true; + chatInput.disabled = true; + }; + window._chatInternals.handleSendMessage = handleSendMessage; + chatInput.addEventListener("input", () => { + sendButton.disabled = chatInput.value.trim() === ""; + chatInput.style.height = "auto"; + chatInput.style.height = chatInput.scrollHeight + "px"; + }); + sendButton.addEventListener("click", handleSendMessage); + + // Send on Enter, allow newline with Shift+Enter + chatInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + if (e.shiftKey) return; // allow newline + e.preventDefault(); + // Directly invoke the send handler so the message is processed + // even if the button state would block programmatic clicks. + handleSendMessage(); + } + }); + sendButton.disabled = chatInput.value.trim() === ""; + chatInput.dispatchEvent(new Event("input")); + const initialSession = Storage.getCurrentSession(); + if (initialSession.messages?.length > 0) renderStoredMessages(initialSession.messages); + chatInput.disabled = false; + chatInput.focus(); + const voiceChatModal = document.getElementById("voice-chat-modal"); + const openVoiceChatModalBtn = document.getElementById("open-voice-chat-modal"); + const closeVoiceChatModalBtn = document.getElementById("voice-chat-modal-close"); + const voiceSettingsModal = document.getElementById("voice-settings-modal"); + const openVoiceSettingsModalBtn = document.getElementById("open-voice-settings-modal"); + const voiceChatImage = document.getElementById("voice-chat-image"); + let slideshowInterval = null; + const startVoiceChatSlideshow = () => { + if (slideshowInterval) clearInterval(slideshowInterval); + const currentSession = Storage.getCurrentSession(); + let imagePrompt = getLatestImagePrompt('default scene'); + if (!imagePrompt) imagePrompt = 'default scene'; + imagePrompt = imagePrompt.slice(0, 100) + ', photographic'; + const updateImage = () => { + const seed = randomSeed(); + try { + if (window.polliLib && window.polliClient) { + const url = window.polliLib.mcp.generateImageUrl(window.polliClient, { + prompt: imagePrompt, + width: 512, + height: 512, + seed, + nologo: true + }); + voiceChatImage.src = url; + } else { + voiceChatImage.src = "https://via.placeholder.com/512?text=Image+Unavailable"; + } + } catch (e) { + console.warn('polliLib generateImageUrl failed', e); + voiceChatImage.src = "https://via.placeholder.com/512?text=Image+Unavailable"; + } + }; + updateImage(); + slideshowInterval = setInterval(updateImage, 10000); + }; + const stopVoiceChatSlideshow = () => { + if (slideshowInterval) { + clearInterval(slideshowInterval); + slideshowInterval = null; + } + }; + let voiceBuffer = ""; + let silenceTimeout = null; + const setupCustomSpeechRecognition = () => { + if (!window._chatInternals.recognition) { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!SpeechRecognition) { + showToast("Speech recognition not supported in this browser"); + return false; + } + window._chatInternals.recognition = new SpeechRecognition(); + const recognition = window._chatInternals.recognition; + recognition.continuous = true; + recognition.interimResults = true; + recognition.lang = "en-US"; + recognition.onstart = () => { + window._chatInternals.isListening = true; + showToast("Voice recognition active"); + document.getElementById("voice-chat-start").disabled = true; + document.getElementById("voice-chat-stop").disabled = false; + }; + recognition.onend = () => { + window._chatInternals.isListening = false; + document.getElementById("voice-chat-start").disabled = false; + document.getElementById("voice-chat-stop").disabled = true; + }; + recognition.onerror = event => { + window._chatInternals.isListening = false; + document.getElementById("voice-chat-start").disabled = false; + document.getElementById("voice-chat-stop").disabled = true; + const errors = { + "no-speech": "No speech detected. Please try again.", + "not-allowed": "Microphone access denied. Please allow microphone access in your browser settings.", + "service-not-allowed": "Microphone access denied. Please allow microphone access in your browser settings.", + }; + showToast(errors[event.error] || "Voice recognition error: " + event.error); + }; + recognition.onresult = event => { + let interimTranscript = ""; + let finalTranscript = ""; + for (let i = event.resultIndex; i < event.results.length; i++) { + const transcript = event.results[i][0].transcript; + if (event.results[i].isFinal) { + const processed = transcript.trim(); + if (!handleVoiceCommand(processed)) finalTranscript += processed + " "; + } else { + interimTranscript += transcript; + } + } + voiceBuffer += finalTranscript; + chatInput.value = voiceBuffer + interimTranscript; + if (finalTranscript) { + clearTimeout(silenceTimeout); + silenceTimeout = setTimeout(() => { + if (voiceBuffer.trim()) { + window.addNewMessage({ role: "user", content: voiceBuffer.trim() }); + window.sendToPolliLib(startVoiceChatSlideshow); + voiceBuffer = ""; + chatInput.value = ""; + } + }, 1500); + } + }; + } + return true; + }; + const setupVoiceChatControls = () => { + const modalBody = voiceChatModal.querySelector(".modal-body"); + let voiceSelectChat = modalBody.querySelector("#voice-select-voicechat"); + if (!voiceSelectChat) { + const voiceSelectContainer = document.createElement("div"); + voiceSelectContainer.className = "form-group mb-3"; + const voiceSelectLabel = document.createElement("label"); + voiceSelectLabel.className = "form-label"; + voiceSelectLabel.innerHTML = ' Voice Selection:'; + voiceSelectLabel.htmlFor = "voice-select-voicechat"; + voiceSelectChat = document.createElement("select"); + voiceSelectChat.id = "voice-select-voicechat"; + voiceSelectChat.className = "form-control"; + voiceSelectContainer.appendChild(voiceSelectLabel); + voiceSelectContainer.appendChild(voiceSelectChat); + const insertAfter = modalBody.querySelector("p") || voiceChatImage; + if (insertAfter?.nextSibling) modalBody.insertBefore(voiceSelectContainer, insertAfter.nextSibling); + else modalBody.appendChild(voiceSelectContainer); + } + const existingControls = modalBody.querySelector(".voice-chat-controls"); + if (existingControls) existingControls.remove(); + const controlsDiv = document.createElement("div"); + controlsDiv.className = "voice-chat-controls"; + Object.assign(controlsDiv.style, { display: "flex", gap: "10px", marginTop: "15px" }); + const startBtn = document.createElement("button"); + startBtn.id = "voice-chat-start"; + startBtn.className = "btn btn-primary"; + startBtn.textContent = "Start Listening"; + startBtn.style.width = "100%"; + startBtn.style.padding = "10px"; + startBtn.disabled = window._chatInternals.isListening; + const stopBtn = document.createElement("button"); + stopBtn.id = "voice-chat-stop"; + stopBtn.className = "btn btn-danger"; + stopBtn.textContent = "Stop Listening"; + stopBtn.style.width = "100%"; + stopBtn.style.padding = "10px"; + stopBtn.disabled = !window._chatInternals.isListening; + controlsDiv.appendChild(startBtn); + controlsDiv.appendChild(stopBtn); + modalBody.appendChild(controlsDiv); + startBtn.addEventListener("click", () => { + if (!setupCustomSpeechRecognition()) return showToast("Failed to initialize speech recognition"); + try { + window._chatInternals.recognition.start(); + startVoiceChatSlideshow(); + } catch (error) { + showToast("Could not start speech recognition: " + error.message); + } + }); + stopBtn.addEventListener("click", () => { + if (window._chatInternals.recognition && window._chatInternals.isListening) { + window._chatInternals.recognition.stop(); + stopVoiceChatSlideshow(); + showToast("Voice recognition stopped"); + } + }); + }; + const updateAllVoiceDropdowns = selectedIndex => { + ["voice-select", "voice-select-modal", "voice-settings-modal", "voice-select-voicechat"].forEach(id => { + const dropdown = document.getElementById(id); + if (dropdown) dropdown.value = selectedIndex; + }); + }; + openVoiceChatModalBtn.addEventListener("click", () => { + voiceChatModal.classList.remove("hidden"); + setupVoiceChatControls(); + window._chatInternals.populateAllVoiceDropdowns(); + }); + closeVoiceChatModalBtn.addEventListener("click", () => { + voiceChatModal.classList.add("hidden"); + if (window._chatInternals.recognition && window._chatInternals.isListening) window._chatInternals.recognition.stop(); + stopVoiceChatSlideshow(); + }); + openVoiceSettingsModalBtn.addEventListener("click", () => { + voiceSettingsModal.classList.remove("hidden"); + window._chatInternals.populateAllVoiceDropdowns(); + const voiceSpeedInput = document.getElementById("voice-speed"); + const voicePitchInput = document.getElementById("voice-pitch"); + const voiceSpeedValue = document.getElementById("voice-speed-value"); + const voicePitchValue = document.getElementById("voice-pitch-value"); + voiceSpeedInput.value = localStorage.getItem("voiceSpeed") || 0.9; + voicePitchInput.value = localStorage.getItem("voicePitch") || 1.0; + voiceSpeedValue.textContent = `${voiceSpeedInput.value}x`; + voicePitchValue.textContent = `${voicePitchInput.value}x`; + }); + document.getElementById("voice-settings-modal-close").addEventListener("click", () => voiceSettingsModal.classList.add("hidden")); + document.getElementById("voice-settings-cancel").addEventListener("click", () => voiceSettingsModal.classList.add("hidden")); + document.getElementById("voice-settings-save").addEventListener("click", () => { + const voiceSpeedInput = document.getElementById("voice-speed"); + const voicePitchInput = document.getElementById("voice-pitch"); + const voiceSelectModal = document.getElementById("voice-select-modal"); + const selectedVoiceIndex = voiceSelectModal.value; + const voiceSpeed = voiceSpeedInput.value; + const voicePitch = voicePitchInput.value; + window._chatInternals.selectedVoice = window._chatInternals.voices[selectedVoiceIndex]; + localStorage.setItem("selectedVoiceIndex", selectedVoiceIndex); + localStorage.setItem("voiceSpeed", voiceSpeed); + localStorage.setItem("voicePitch", voicePitch); + window._chatInternals.updateVoiceToggleUI(); + updateAllVoiceDropdowns(selectedVoiceIndex); + voiceSettingsModal.classList.add("hidden"); + showToast("Voice settings saved"); + }); + document.getElementById("voice-speed").addEventListener("input", () => { + document.getElementById("voice-speed-value").textContent = `${document.getElementById("voice-speed").value}x`; + }); + document.getElementById("voice-pitch").addEventListener("input", () => { + document.getElementById("voice-pitch-value").textContent = `${document.getElementById("voice-pitch").value}x`; + }); +}); diff --git a/js/chat/chat-storage.js b/js/chat/chat-storage.js index 1caa7bb..9e57af3 100644 --- a/js/chat/chat-storage.js +++ b/js/chat/chat-storage.js @@ -1,48 +1,65 @@ -document.addEventListener("DOMContentLoaded", () => { - const { chatBox, chatInput, clearChatBtn, voiceToggleBtn, modelSelect, synth, autoSpeakEnabled, speakMessage, stopSpeaking, showToast, toggleSpeechRecognition, initSpeechRecognition, handleVoiceCommand, speakSentences } = window._chatInternals; - const imagePatterns = window.imagePatterns; - - // Thumbnail gallery for main UI removed; retaining screensaver-only implementation - - function generateSessionTitle(messages) { - let title = ""; - for (let i = 0; i < messages.length; i++) { - if (messages[i].role === "ai") { - title = messages[i].content.replace(/[#_*`]/g, "").trim(); - break; - } - } - if (!title) title = "New Chat"; - if (title.length > 50) title = title.substring(0, 50) + "..."; - return title; - } - function checkAndUpdateSessionTitle() { - const currentSession = Storage.getCurrentSession(); - if (!currentSession.name || currentSession.name === "New Chat") { - const newTitle = generateSessionTitle(currentSession.messages); - if (newTitle && newTitle !== currentSession.name) { - Storage.renameSession(currentSession.id, newTitle); - } - } - } +document.addEventListener("DOMContentLoaded", () => { + const { chatBox, chatInput, clearChatBtn, voiceToggleBtn, modelSelect, synth, autoSpeakEnabled, speakMessage, stopSpeaking, showToast, toggleSpeechRecognition, initSpeechRecognition, handleVoiceCommand, speakSentences } = window._chatInternals; + // Thumbnail gallery for main UI removed; retaining screensaver-only implementation + + function generateSessionTitle(messages) { + let title = ""; + for (let i = 0; i < messages.length; i++) { + if (messages[i].role === "ai") { + title = messages[i].content.replace(/[#_*`]/g, "").trim(); + break; + } + } + if (!title) title = "New Chat"; + if (title.length > 50) title = title.substring(0, 50) + "..."; + return title; + } + function checkAndUpdateSessionTitle() { + const currentSession = Storage.getCurrentSession(); + if (!currentSession.name || currentSession.name === "New Chat") { + const newTitle = generateSessionTitle(currentSession.messages); + if (newTitle && newTitle !== currentSession.name) { + Storage.renameSession(currentSession.id, newTitle); + } + } + } + function getLatestImagePrompt(defaultPrompt = 'default scene') { + const currentSession = Storage.getCurrentSession(); + const messages = currentSession?.messages || []; + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]; + const imageMeta = msg?.metadata?.images; + if (Array.isArray(imageMeta)) { + for (const info of imageMeta) { + if (info && typeof info.prompt === 'string' && info.prompt.trim()) { + return info.prompt.trim(); + } + } + } + } + const fallback = messages[messages.length - 1]?.content; + if (typeof fallback === 'string' && fallback.trim()) return fallback.trim(); + return defaultPrompt; + } + function appendMessage({ role, content, index, imageUrls = [], audioUrls = [] }) { - const container = document.createElement("div"); - container.classList.add("message"); - container.dataset.index = index; - container.dataset.role = role; - if (role === "user") { - container.classList.add("user-message"); - container.style.float = "right"; - container.style.clear = "both"; - container.style.maxWidth = "40%"; - container.style.marginRight = "10px"; - } else { - container.classList.add("ai-message"); - container.style.float = "left"; - container.style.clear = "both"; - container.style.maxWidth = "60%"; - container.style.marginLeft = "10px"; - } + const container = document.createElement("div"); + container.classList.add("message"); + container.dataset.index = index; + container.dataset.role = role; + if (role === "user") { + container.classList.add("user-message"); + container.style.float = "right"; + container.style.clear = "both"; + container.style.maxWidth = "40%"; + container.style.marginRight = "10px"; + } else { + container.classList.add("ai-message"); + container.style.float = "left"; + container.style.clear = "both"; + container.style.maxWidth = "60%"; + container.style.marginLeft = "10px"; + } const bubbleContent = document.createElement("div"); bubbleContent.classList.add("message-text"); if (role === "ai") { @@ -65,95 +82,95 @@ document.addEventListener("DOMContentLoaded", () => { } else { bubbleContent.textContent = content; } - container.appendChild(bubbleContent); - if (role === "ai") { - const actionsDiv = document.createElement("div"); - actionsDiv.className = "message-actions"; - const copyBtn = document.createElement("button"); - copyBtn.className = "message-action-btn"; - copyBtn.textContent = "Copy"; - copyBtn.addEventListener("click", () => { - navigator.clipboard.writeText(content).then(() => showToast("AI response copied to clipboard")).catch(() => { - showToast("Failed to copy to clipboard"); - }); - }); - actionsDiv.appendChild(copyBtn); - const speakBtn = document.createElement("button"); - speakBtn.className = "message-action-btn speak-message-btn"; - speakBtn.innerHTML = '🔊 Speak'; - speakBtn.addEventListener("click", () => { - stopSpeaking(); - const sentences = content.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0); - speakSentences(sentences); - }); - actionsDiv.appendChild(speakBtn); - const regenBtn = document.createElement("button"); - regenBtn.className = "message-action-btn"; - regenBtn.textContent = "Re-generate"; - regenBtn.addEventListener("click", () => reGenerateAIResponse(index)); - actionsDiv.appendChild(regenBtn); - const editAIBtn = document.createElement("button"); - editAIBtn.className = "message-action-btn"; - editAIBtn.textContent = "Edit"; - editAIBtn.addEventListener("click", () => editMessage(index)); - actionsDiv.appendChild(editAIBtn); - container.appendChild(actionsDiv); - } else { - const userActionsDiv = document.createElement("div"); - userActionsDiv.className = "message-actions"; - const editUserBtn = document.createElement("button"); - editUserBtn.className = "message-action-btn"; - editUserBtn.textContent = "Edit"; - editUserBtn.addEventListener("click", () => editMessage(index)); - userActionsDiv.appendChild(editUserBtn); - container.appendChild(userActionsDiv); - } - const codeBlocks = bubbleContent.querySelectorAll("pre code"); - codeBlocks.forEach((block) => { - const buttonContainer = document.createElement("div"); - buttonContainer.style.display = "flex"; - buttonContainer.style.gap = "5px"; - buttonContainer.style.marginTop = "5px"; - const codeContent = block.textContent.trim(); - const language = block.className.match(/language-(\w+)/)?.[1] || "text"; - const copyCodeBtn = document.createElement("button"); - copyCodeBtn.className = "message-action-btn"; - copyCodeBtn.textContent = "Copy Code"; - copyCodeBtn.style.fontSize = "12px"; - copyCodeBtn.addEventListener("click", () => { - navigator.clipboard.writeText(codeContent).then(() => { - showToast("Code copied to clipboard"); - }).catch(() => { - showToast("Failed to copy code"); - }); - }); - buttonContainer.appendChild(copyCodeBtn); - const downloadCodeBtn = document.createElement("button"); - downloadCodeBtn.className = "message-action-btn"; - downloadCodeBtn.textContent = "Download"; - downloadCodeBtn.style.fontSize = "12px"; - downloadCodeBtn.addEventListener("click", () => { - downloadCodeAsTxt(codeContent, language); - }); - buttonContainer.appendChild(downloadCodeBtn); - block.parentNode.insertAdjacentElement("afterend", buttonContainer); - }); + container.appendChild(bubbleContent); + if (role === "ai") { + const actionsDiv = document.createElement("div"); + actionsDiv.className = "message-actions"; + const copyBtn = document.createElement("button"); + copyBtn.className = "message-action-btn"; + copyBtn.textContent = "Copy"; + copyBtn.addEventListener("click", () => { + navigator.clipboard.writeText(content).then(() => showToast("AI response copied to clipboard")).catch(() => { + showToast("Failed to copy to clipboard"); + }); + }); + actionsDiv.appendChild(copyBtn); + const speakBtn = document.createElement("button"); + speakBtn.className = "message-action-btn speak-message-btn"; + speakBtn.innerHTML = '🔊 Speak'; + speakBtn.addEventListener("click", () => { + stopSpeaking(); + const sentences = content.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0); + speakSentences(sentences); + }); + actionsDiv.appendChild(speakBtn); + const regenBtn = document.createElement("button"); + regenBtn.className = "message-action-btn"; + regenBtn.textContent = "Re-generate"; + regenBtn.addEventListener("click", () => reGenerateAIResponse(index)); + actionsDiv.appendChild(regenBtn); + const editAIBtn = document.createElement("button"); + editAIBtn.className = "message-action-btn"; + editAIBtn.textContent = "Edit"; + editAIBtn.addEventListener("click", () => editMessage(index)); + actionsDiv.appendChild(editAIBtn); + container.appendChild(actionsDiv); + } else { + const userActionsDiv = document.createElement("div"); + userActionsDiv.className = "message-actions"; + const editUserBtn = document.createElement("button"); + editUserBtn.className = "message-action-btn"; + editUserBtn.textContent = "Edit"; + editUserBtn.addEventListener("click", () => editMessage(index)); + userActionsDiv.appendChild(editUserBtn); + container.appendChild(userActionsDiv); + } + const codeBlocks = bubbleContent.querySelectorAll("pre code"); + codeBlocks.forEach((block) => { + const buttonContainer = document.createElement("div"); + buttonContainer.style.display = "flex"; + buttonContainer.style.gap = "5px"; + buttonContainer.style.marginTop = "5px"; + const codeContent = block.textContent.trim(); + const language = block.className.match(/language-(\w+)/)?.[1] || "text"; + const copyCodeBtn = document.createElement("button"); + copyCodeBtn.className = "message-action-btn"; + copyCodeBtn.textContent = "Copy Code"; + copyCodeBtn.style.fontSize = "12px"; + copyCodeBtn.addEventListener("click", () => { + navigator.clipboard.writeText(codeContent).then(() => { + showToast("Code copied to clipboard"); + }).catch(() => { + showToast("Failed to copy code"); + }); + }); + buttonContainer.appendChild(copyCodeBtn); + const downloadCodeBtn = document.createElement("button"); + downloadCodeBtn.className = "message-action-btn"; + downloadCodeBtn.textContent = "Download"; + downloadCodeBtn.style.fontSize = "12px"; + downloadCodeBtn.addEventListener("click", () => { + downloadCodeAsTxt(codeContent, language); + }); + buttonContainer.appendChild(downloadCodeBtn); + block.parentNode.insertAdjacentElement("afterend", buttonContainer); + }); chatBox.appendChild(container); chatBox.scrollTop = chatBox.scrollHeight; window.highlightUtils?.highlightAllCodeBlocks(chatBox); - } - function downloadCodeAsTxt(codeContent, language) { - const blob = new Blob([codeContent], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `code-${language}-${Date.now()}.txt`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - showToast("Code downloaded as .txt"); - } + } + function downloadCodeAsTxt(codeContent, language) { + const blob = new Blob([codeContent], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `code-${language}-${Date.now()}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + showToast("Code downloaded as .txt"); + } function createImageElement(url) { const imageId = `voice-img-${Date.now()}`; localStorage.setItem(`voiceImageId_${imageId}`, imageId); @@ -234,118 +251,118 @@ document.addEventListener("DOMContentLoaded", () => { return audio; } function attachImageButtons(img, imageId) { - const imgButtonContainer = document.querySelector(`.image-button-container[data-image-id="${imageId}"]`); - if (!imgButtonContainer) { - console.warn(`No image button container found for image ID: ${imageId}`); - return; - } - console.log(`Attaching image button listeners for image ID: ${imageId}`); - imgButtonContainer.innerHTML = ""; - const copyImgBtn = document.createElement("button"); - copyImgBtn.className = "message-action-btn"; - copyImgBtn.textContent = "Copy Image"; - copyImgBtn.style.pointerEvents = "auto"; - copyImgBtn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log(`Copy Image button clicked for image ID: ${imageId}`); - copyImage(img, imageId); - }); - imgButtonContainer.appendChild(copyImgBtn); - const downloadImgBtn = document.createElement("button"); - downloadImgBtn.className = "message-action-btn"; - downloadImgBtn.textContent = "Download Image"; - downloadImgBtn.style.pointerEvents = "auto"; - downloadImgBtn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log(`Download Image button clicked for image ID: ${imageId}`); - downloadImage(img, imageId); - }); - imgButtonContainer.appendChild(downloadImgBtn); - const refreshImgBtn = document.createElement("button"); - refreshImgBtn.className = "message-action-btn"; - refreshImgBtn.textContent = "Refresh Image"; - refreshImgBtn.style.pointerEvents = "auto"; - refreshImgBtn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log(`Refresh Image button clicked for image ID: ${imageId}`); - refreshImage(img, imageId); - }); - imgButtonContainer.appendChild(refreshImgBtn); - const openImgBtn = document.createElement("button"); - openImgBtn.className = "message-action-btn"; - openImgBtn.textContent = "Open in New Tab"; - openImgBtn.style.pointerEvents = "auto"; - openImgBtn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log(`Open in New Tab button clicked for image ID: ${imageId}`); - openImageInNewTab(img, imageId); - }); - imgButtonContainer.appendChild(openImgBtn); - } - function copyImage(img, imageId) { - console.log(`Copying image with ID: ${imageId}`); - if (!img.complete || img.naturalWidth === 0) { - showToast("Image not fully loaded yet. Please try again."); - return; - } - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - canvas.width = img.naturalWidth; - canvas.height = img.naturalHeight; - try { - ctx.drawImage(img, 0, 0); - canvas.toBlob((blob) => { - if (!blob) { - showToast("Failed to copy image: Unable to create blob."); - return; - } - navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]) - .then(() => { - const dataURL = canvas.toDataURL("image/png"); - localStorage.setItem(`lastCopiedImage_${imageId}`, dataURL); - showToast("Image copied to clipboard and saved to local storage"); - }) - .catch((err) => { - console.error("Copy image error:", err); - showToast("Failed to copy image: " + err.message); - }); - }, "image/png"); - } catch (err) { - console.error("Copy image error:", err); - showToast("Failed to copy image due to CORS or other error: " + err.message); - } - } - function downloadImage(img, imageId) { - console.log(`Downloading image with ID: ${imageId}`); - if (!img.src) { - showToast("No image source available to download."); - return; - } - fetch(img.src, { mode: "cors" }) - .then((response) => { - if (!response.ok) throw new Error("Network response was not ok"); - return response.blob(); - }) - .then((blob) => { - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `image-${imageId}-${Date.now()}.png`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - showToast("Image downloaded successfully"); - }) - .catch((err) => { - console.error("Download image error:", err); - showToast("Failed to download image: " + err.message); - }); - } + const imgButtonContainer = document.querySelector(`.image-button-container[data-image-id="${imageId}"]`); + if (!imgButtonContainer) { + console.warn(`No image button container found for image ID: ${imageId}`); + return; + } + console.log(`Attaching image button listeners for image ID: ${imageId}`); + imgButtonContainer.innerHTML = ""; + const copyImgBtn = document.createElement("button"); + copyImgBtn.className = "message-action-btn"; + copyImgBtn.textContent = "Copy Image"; + copyImgBtn.style.pointerEvents = "auto"; + copyImgBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log(`Copy Image button clicked for image ID: ${imageId}`); + copyImage(img, imageId); + }); + imgButtonContainer.appendChild(copyImgBtn); + const downloadImgBtn = document.createElement("button"); + downloadImgBtn.className = "message-action-btn"; + downloadImgBtn.textContent = "Download Image"; + downloadImgBtn.style.pointerEvents = "auto"; + downloadImgBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log(`Download Image button clicked for image ID: ${imageId}`); + downloadImage(img, imageId); + }); + imgButtonContainer.appendChild(downloadImgBtn); + const refreshImgBtn = document.createElement("button"); + refreshImgBtn.className = "message-action-btn"; + refreshImgBtn.textContent = "Refresh Image"; + refreshImgBtn.style.pointerEvents = "auto"; + refreshImgBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log(`Refresh Image button clicked for image ID: ${imageId}`); + refreshImage(img, imageId); + }); + imgButtonContainer.appendChild(refreshImgBtn); + const openImgBtn = document.createElement("button"); + openImgBtn.className = "message-action-btn"; + openImgBtn.textContent = "Open in New Tab"; + openImgBtn.style.pointerEvents = "auto"; + openImgBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log(`Open in New Tab button clicked for image ID: ${imageId}`); + openImageInNewTab(img, imageId); + }); + imgButtonContainer.appendChild(openImgBtn); + } + function copyImage(img, imageId) { + console.log(`Copying image with ID: ${imageId}`); + if (!img.complete || img.naturalWidth === 0) { + showToast("Image not fully loaded yet. Please try again."); + return; + } + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + try { + ctx.drawImage(img, 0, 0); + canvas.toBlob((blob) => { + if (!blob) { + showToast("Failed to copy image: Unable to create blob."); + return; + } + navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]) + .then(() => { + const dataURL = canvas.toDataURL("image/png"); + localStorage.setItem(`lastCopiedImage_${imageId}`, dataURL); + showToast("Image copied to clipboard and saved to local storage"); + }) + .catch((err) => { + console.error("Copy image error:", err); + showToast("Failed to copy image: " + err.message); + }); + }, "image/png"); + } catch (err) { + console.error("Copy image error:", err); + showToast("Failed to copy image due to CORS or other error: " + err.message); + } + } + function downloadImage(img, imageId) { + console.log(`Downloading image with ID: ${imageId}`); + if (!img.src) { + showToast("No image source available to download."); + return; + } + fetch(img.src, { mode: "cors" }) + .then((response) => { + if (!response.ok) throw new Error("Network response was not ok"); + return response.blob(); + }) + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `image-${imageId}-${Date.now()}.png`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + showToast("Image downloaded successfully"); + }) + .catch((err) => { + console.error("Download image error:", err); + showToast("Failed to download image: " + err.message); + }); + } function refreshImage(img, imageId) { console.log(`Refreshing image with ID: ${imageId}`); const { url: finalUrl, error } = window.refreshPolliImage(img?.src, { @@ -379,15 +396,15 @@ document.addEventListener("DOMContentLoaded", () => { }; img.src = finalUrl; } - function openImageInNewTab(img, imageId) { - console.log(`Opening image in new tab with ID: ${imageId}`); - if (!img.src) { - showToast("No image source available to open."); - return; - } - window.open(img.src, "_blank"); - showToast("Image opened in new tab"); - } + function openImageInNewTab(img, imageId) { + console.log(`Opening image in new tab with ID: ${imageId}`); + if (!img.src) { + showToast("No image source available to open."); + return; + } + window.open(img.src, "_blank"); + showToast("Image opened in new tab"); + } function renderStoredMessages(messages) { console.log("Rendering stored messages..."); chatBox.innerHTML = ""; @@ -418,9 +435,9 @@ document.addEventListener("DOMContentLoaded", () => { chatInput.disabled = false; chatInput.focus(); } - window.addNewMessage = function ({ role, content, imageUrls = [], audioUrls = [] }) { + window.addNewMessage = function ({ role, content, imageUrls = [], audioUrls = [], metadata = null }) { const currentSession = Storage.getCurrentSession(); - currentSession.messages.push({ role, content, imageUrls, audioUrls }); + currentSession.messages.push({ role, content, imageUrls, audioUrls, metadata }); Storage.updateSessionMessages(currentSession.id, currentSession.messages); if (!window.polliClient || !window.polliClient.imageBase) { appendMessage({ role, content, index: currentSession.messages.length - 1, imageUrls, audioUrls }); @@ -440,478 +457,471 @@ document.addEventListener("DOMContentLoaded", () => { }); if (role === "ai") checkAndUpdateSessionTitle(); }; - function editMessage(msgIndex) { - const currentSession = Storage.getCurrentSession(); - const oldMessage = currentSession.messages[msgIndex]; - if (!oldMessage) return; - window._chatInternals.stopSpeaking(); - const newContent = prompt("Edit this message:", oldMessage.content); - if (newContent === null || newContent === oldMessage.content) return; - if (oldMessage.role === "user") { - currentSession.messages[msgIndex].content = newContent; - currentSession.messages = currentSession.messages.slice(0, msgIndex + 1); - Storage.updateSessionMessages(currentSession.id, currentSession.messages); - renderStoredMessages(currentSession.messages); - const loadingDiv = document.createElement("div"); - loadingDiv.id = `loading-${Date.now()}`; - loadingDiv.classList.add("message", "ai-message"); - loadingDiv.style.float = "left"; - loadingDiv.style.clear = "both"; - loadingDiv.style.maxWidth = "60%"; - loadingDiv.style.marginLeft = "10px"; - loadingDiv.textContent = "Generating response..."; - chatBox.appendChild(loadingDiv); - chatBox.scrollTop = chatBox.scrollHeight; - window.sendToPolliLib(() => { - loadingDiv.remove(); + function editMessage(msgIndex) { + const currentSession = Storage.getCurrentSession(); + const oldMessage = currentSession.messages[msgIndex]; + if (!oldMessage) return; + window._chatInternals.stopSpeaking(); + const newContent = prompt("Edit this message:", oldMessage.content); + if (newContent === null || newContent === oldMessage.content) return; + if (oldMessage.role === "user") { + currentSession.messages[msgIndex].content = newContent; + currentSession.messages = currentSession.messages.slice(0, msgIndex + 1); + Storage.updateSessionMessages(currentSession.id, currentSession.messages); + renderStoredMessages(currentSession.messages); + const loadingDiv = document.createElement("div"); + loadingDiv.id = `loading-${Date.now()}`; + loadingDiv.classList.add("message", "ai-message"); + loadingDiv.style.float = "left"; + loadingDiv.style.clear = "both"; + loadingDiv.style.maxWidth = "60%"; + loadingDiv.style.marginLeft = "10px"; + loadingDiv.textContent = "Generating response..."; + chatBox.appendChild(loadingDiv); + chatBox.scrollTop = chatBox.scrollHeight; + window.sendToPolliLib(() => { + loadingDiv.remove(); window.highlightUtils?.highlightAllCodeBlocks(chatBox); - }, newContent); - showToast("User message updated and new response generated"); - } else { - currentSession.messages[msgIndex].content = newContent; - Storage.updateSessionMessages(currentSession.id, currentSession.messages); - renderStoredMessages(currentSession.messages); + }, newContent); + showToast("User message updated and new response generated"); + } else { + currentSession.messages[msgIndex].content = newContent; + Storage.updateSessionMessages(currentSession.id, currentSession.messages); + renderStoredMessages(currentSession.messages); window.highlightUtils?.highlightAllCodeBlocks(chatBox); showToast("AI message updated"); } } function reGenerateAIResponse(aiIndex) { - console.log(`Re-generating AI response for index: ${aiIndex}`); - const currentSession = Storage.getCurrentSession(); - if (aiIndex < 0 || aiIndex >= currentSession.messages.length || currentSession.messages[aiIndex].role !== "ai") { - showToast("Invalid AI message index for regeneration."); - return; - } - let userIndex = -1; - for (let i = aiIndex - 1; i >= 0; i--) { - if (currentSession.messages[i].role === "user") { - userIndex = i; - break; - } - } - if (userIndex === -1) { - showToast("No preceding user message found to regenerate from."); - return; - } - window._chatInternals.stopSpeaking(); - const userMessage = currentSession.messages[userIndex].content; - currentSession.messages = currentSession.messages.slice(0, userIndex + 1); - Storage.updateSessionMessages(currentSession.id, currentSession.messages); - renderStoredMessages(currentSession.messages); - const loadingDiv = document.createElement("div"); - loadingDiv.id = `loading-${Date.now()}`; - loadingDiv.classList.add("message", "ai-message"); - loadingDiv.style.float = "left"; - loadingDiv.style.clear = "both"; - loadingDiv.style.maxWidth = "60%"; - loadingDiv.style.marginLeft = "10px"; - loadingDiv.textContent = "Regenerating response..."; - chatBox.appendChild(loadingDiv); - chatBox.scrollTop = chatBox.scrollHeight; - const uniqueUserMessage = `${userMessage} [regen-${Date.now()}-${Math.random().toString(36).substring(2)}]`; - console.log(`Sending re-generate request for user message: ${userMessage} (with unique suffix: ${uniqueUserMessage})`); + console.log(`Re-generating AI response for index: ${aiIndex}`); + const currentSession = Storage.getCurrentSession(); + if (aiIndex < 0 || aiIndex >= currentSession.messages.length || currentSession.messages[aiIndex].role !== "ai") { + showToast("Invalid AI message index for regeneration."); + return; + } + let userIndex = -1; + for (let i = aiIndex - 1; i >= 0; i--) { + if (currentSession.messages[i].role === "user") { + userIndex = i; + break; + } + } + if (userIndex === -1) { + showToast("No preceding user message found to regenerate from."); + return; + } + window._chatInternals.stopSpeaking(); + const userMessage = currentSession.messages[userIndex].content; + currentSession.messages = currentSession.messages.slice(0, userIndex + 1); + Storage.updateSessionMessages(currentSession.id, currentSession.messages); + renderStoredMessages(currentSession.messages); + const loadingDiv = document.createElement("div"); + loadingDiv.id = `loading-${Date.now()}`; + loadingDiv.classList.add("message", "ai-message"); + loadingDiv.style.float = "left"; + loadingDiv.style.clear = "both"; + loadingDiv.style.maxWidth = "60%"; + loadingDiv.style.marginLeft = "10px"; + loadingDiv.textContent = "Regenerating response..."; + chatBox.appendChild(loadingDiv); + chatBox.scrollTop = chatBox.scrollHeight; + const uniqueUserMessage = `${userMessage} [regen-${Date.now()}-${Math.random().toString(36).substring(2)}]`; + console.log(`Sending re-generate request for user message: ${userMessage} (with unique suffix: ${uniqueUserMessage})`); window.sendToPolliLib(() => { loadingDiv.remove(); window.highlightUtils?.highlightAllCodeBlocks(chatBox); showToast("Response regenerated successfully"); }, uniqueUserMessage); } - - if (voiceToggleBtn) { - voiceToggleBtn.addEventListener("click", window._chatInternals.toggleAutoSpeak); - window._chatInternals.updateVoiceToggleUI(); - setTimeout(() => { - if (autoSpeakEnabled) { - const testUtterance = new SpeechSynthesisUtterance("Voice check"); - testUtterance.volume = 0.1; - testUtterance.onend = () => {}; - testUtterance.onerror = (err) => { - window._chatInternals.autoSpeakEnabled = false; - localStorage.setItem("autoSpeakEnabled", "false"); - window._chatInternals.updateVoiceToggleUI(); - showToast("Voice synthesis unavailable. Voice mode disabled."); - }; - synth.speak(testUtterance); - } - }, 5000); - } - if (clearChatBtn) { - clearChatBtn.addEventListener("click", () => { - const currentSession = Storage.getCurrentSession(); - if (confirm("Are you sure you want to clear this chat?")) { - currentSession.messages = []; - Storage.updateSessionMessages(currentSession.id, currentSession.messages); - chatBox.innerHTML = ""; - showToast("Chat cleared"); - chatInput.disabled = false; - chatInput.focus(); - } - }); - } - function checkFirstLaunch() { - const firstLaunch = localStorage.getItem("firstLaunch") === "0"; - if (firstLaunch) { - const firstLaunchModal = document.getElementById("first-launch-modal"); - if (firstLaunchModal) { - firstLaunchModal.classList.remove("hidden"); - document.getElementById("first-launch-close").addEventListener("click", () => { - firstLaunchModal.classList.add("hidden"); - localStorage.setItem("firstLaunch", "1"); - }); - document.getElementById("first-launch-complete").addEventListener("click", () => { - firstLaunchModal.classList.add("hidden"); - localStorage.setItem("firstLaunch", "1"); - }); - document.getElementById("setup-theme").addEventListener("click", () => { - firstLaunchModal.classList.add("hidden"); - document.getElementById("settings-modal").classList.remove("hidden"); - }); - document.getElementById("setup-personalization").addEventListener("click", () => { - firstLaunchModal.classList.add("hidden"); - document.getElementById("personalization-modal").classList.remove("hidden"); - }); - document.getElementById("setup-model").addEventListener("click", () => { - firstLaunchModal.classList.add("hidden"); - document.getElementById("model-select").focus(); - }); - } - } - } - checkFirstLaunch(); - function setupVoiceInputButton() { - if ("webkitSpeechRecognition" in window || "SpeechRecognition" in window) { - const inputButtonsContainer = document.querySelector(".input-buttons-container"); - if (!window._chatInternals.voiceInputBtn && inputButtonsContainer) { - const voiceInputBtn = document.createElement("button"); - voiceInputBtn.id = "voice-input-btn"; - voiceInputBtn.innerHTML = ''; - voiceInputBtn.title = "Voice input"; - inputButtonsContainer.insertBefore(voiceInputBtn, document.getElementById("send-button")); - window._chatInternals.setVoiceInputButton(voiceInputBtn); - let voiceBuffer = ""; - let silenceTimeout = null; - voiceInputBtn.addEventListener("click", () => { - toggleSpeechRecognition(); - }); - } - } else { - const voiceInputBtn = document.getElementById("voice-input-btn"); - if (voiceInputBtn) { - voiceInputBtn.disabled = true; - voiceInputBtn.title = "Voice input not supported in this browser"; - } - } - } - setupVoiceInputButton(); - document.addEventListener('click', function(e) { - if (e.target.closest('.image-button-container')) { - e.preventDefault(); - e.stopPropagation(); - console.log("Click detected on image-button-container, preventing propagation"); - } - }, true); - const sendButton = document.getElementById("send-button"); - function handleSendMessage() { - const message = chatInput.value.trim(); - if (message === "") return; - window.addNewMessage({ role: "user", content: message }); - chatInput.value = ""; - chatInput.style.height = "auto"; - window.sendToPolliLib(() => { - sendButton.disabled = false; - chatInput.disabled = false; - chatInput.focus(); - }); - sendButton.disabled = true; - chatInput.disabled = true; - } - chatInput.addEventListener("input", () => { - sendButton.disabled = chatInput.value.trim() === ""; - chatInput.style.height = "auto"; - chatInput.style.height = chatInput.scrollHeight + "px"; - }); - sendButton.addEventListener("click", () => { - handleSendMessage(); - }); - sendButton.disabled = chatInput.value.trim() === ""; - const initialSession = Storage.getCurrentSession(); - if (initialSession.messages && initialSession.messages.length > 0) { - renderStoredMessages(initialSession.messages); - } else { - chatInput.disabled = false; - chatInput.focus(); - } - const voiceChatModal = document.getElementById("voice-chat-modal"); - const openVoiceChatModalBtn = document.getElementById("open-voice-chat-modal"); - const closeVoiceChatModalBtn = document.getElementById("voice-chat-modal-close"); - const voiceSettingsModal = document.getElementById("voice-settings-modal"); - const openVoiceSettingsModalBtn = document.getElementById("open-voice-settings-modal"); - const voiceChatImage = document.getElementById("voice-chat-image"); - let slideshowInterval = null; - function startVoiceChatSlideshow() { - if (slideshowInterval) clearInterval(slideshowInterval); - const currentSession = Storage.getCurrentSession(); - let lastMessage = currentSession.messages.slice(-1)[0]?.content || "default scene"; - let imagePrompt = ""; - for (const patternObj of imagePatterns) { - const match = lastMessage.match(patternObj.pattern); - if (match) { - imagePrompt = match[patternObj.group].trim(); - break; - } - } - imagePrompt += ", origami"; - if (imagePrompt.length > 100) { - imagePrompt = imagePrompt.substring(0, 100); - } - function updateImage() { - const seed = Math.floor(Math.random() * 1000000); - const imageId = `voice-img-${Date.now()}`; - localStorage.setItem(`voiceImageId_${imageId}`, imageId); - try { - if (window.polliLib && window.polliClient) { - const url = window.polliLib.mcp.generateImageUrl(window.polliClient, { - prompt: imagePrompt, - width: 512, - height: 512, - seed, - nologo: true - }); - voiceChatImage.src = url; - } else { - voiceChatImage.src = "https://via.placeholder.com/512?text=Image+Unavailable"; - } - } catch (e) { - console.warn('polliLib generateImageUrl failed', e); - voiceChatImage.src = "https://via.placeholder.com/512?text=Image+Unavailable"; - } - voiceChatImage.dataset.imageId = imageId; - voiceChatImage.onload = () => { - attachImageButtons(voiceChatImage, imageId); - }; - voiceChatImage.onerror = () => { - showToast("Failed to load slideshow image"); - }; - } - updateImage(); - slideshowInterval = setInterval(updateImage, 10000); - } - function stopVoiceChatSlideshow() { - if (slideshowInterval) { - clearInterval(slideshowInterval); - slideshowInterval = null; - } - } - let voiceBuffer = ""; - let silenceTimeout = null; - function setupCustomSpeechRecognition() { - if (!window._chatInternals.recognition) { - if ('webkitSpeechRecognition' in window) { - window._chatInternals.recognition = new webkitSpeechRecognition(); - } else if ('SpeechRecognition' in window) { - window._chatInternals.recognition = new SpeechRecognition(); - } else { - showToast("Speech recognition not supported in this browser"); - return false; - } - const recognition = window._chatInternals.recognition; - recognition.continuous = true; - recognition.interimResults = true; - recognition.lang = 'en-US'; - recognition.onstart = () => { - window._chatInternals.isListening = true; - showToast("Voice recognition active"); - const startBtn = document.getElementById("voice-chat-start"); - const stopBtn = document.getElementById("voice-chat-stop"); - if (startBtn) startBtn.disabled = true; - if (stopBtn) stopBtn.disabled = false; - }; - recognition.onend = () => { - window._chatInternals.isListening = false; - const startBtn = document.getElementById("voice-chat-start"); - const stopBtn = document.getElementById("voice-chat-stop"); - if (startBtn) startBtn.disabled = false; - if (stopBtn) stopBtn.disabled = true; - }; - recognition.onerror = (event) => { - window._chatInternals.isListening = false; - const startBtn = document.getElementById("voice-chat-start"); - const stopBtn = document.getElementById("voice-chat-stop"); - if (startBtn) startBtn.disabled = false; - if (stopBtn) stopBtn.disabled = true; - if (event.error === "no-speech") { - showToast("No speech detected. Please try again."); - } else if (event.error === "not-allowed" || event.error === "service-not-allowed") { - showToast("Microphone access denied. Please allow microphone access in your browser settings."); - } else { - showToast("Voice recognition error: " + event.error); - } - }; - recognition.onresult = (event) => { - let interimTranscript = ""; - let finalTranscript = ""; - for (let i = event.resultIndex; i < event.results.length; i++) { - const transcript = event.results[i][0].transcript; - if (event.results[i].isFinal) { - const processed = transcript.trim(); - if (!handleVoiceCommand(processed)) { - finalTranscript += processed + " "; - } - } else { - interimTranscript += transcript; - } - } - voiceBuffer += finalTranscript; - chatInput.value = voiceBuffer + interimTranscript; - if (finalTranscript) { - clearTimeout(silenceTimeout); - silenceTimeout = setTimeout(() => { - if (voiceBuffer.trim()) { - window.addNewMessage({ role: "user", content: voiceBuffer.trim() }); - window.sendToPolliLib(() => { - startVoiceChatSlideshow(); - chatInput.focus(); - }); - voiceBuffer = ""; - chatInput.value = ""; - } - }, 1500); - } - }; - } - return true; - } - function setupVoiceChatControls() { - const modalBody = voiceChatModal.querySelector(".modal-body"); - let voiceSelectChat = modalBody.querySelector("#voice-select-voicechat"); - if (!voiceSelectChat) { - const voiceSelectContainer = document.createElement("div"); - voiceSelectContainer.className = "form-group mb-3"; - const voiceSelectLabel = document.createElement("label"); - voiceSelectLabel.className = "form-label"; - voiceSelectLabel.innerHTML = ' Voice Selection:'; - voiceSelectLabel.htmlFor = "voice-select-voicechat"; - voiceSelectChat = document.createElement("select"); - voiceSelectChat.id = "voice-select-voicechat"; - voiceSelectChat.className = "form-control"; - voiceSelectContainer.appendChild(voiceSelectLabel); - voiceSelectContainer.appendChild(voiceSelectChat); - const insertAfterElement = modalBody.querySelector("p") || voiceChatImage; - if (insertAfterElement && insertAfterElement.nextSibling) { - modalBody.insertBefore(voiceSelectContainer, insertAfterElement.nextSibling); - } else { - modalBody.appendChild(voiceSelectContainer); - } - } - const existingControls = modalBody.querySelector(".voice-chat-controls"); - if (existingControls) existingControls.remove(); - const controlsDiv = document.createElement("div"); - controlsDiv.className = "voice-chat-controls"; - controlsDiv.style.display = "flex"; - controlsDiv.style.gap = "10px"; - controlsDiv.style.marginTop = "15px"; - const startBtn = document.createElement("button"); - startBtn.id = "voice-chat-start"; - startBtn.className = "btn btn-primary"; - startBtn.textContent = "Start Listening"; - startBtn.style.width = "100%"; - startBtn.style.padding = "10px"; - startBtn.disabled = window._chatInternals.isListening; - const stopBtn = document.createElement("button"); - stopBtn.id = "voice-chat-stop"; - stopBtn.className = "btn btn-danger"; - stopBtn.textContent = "Stop Listening"; - stopBtn.style.width = "100%"; - stopBtn.style.padding = "10px"; - stopBtn.disabled = !window._chatInternals.isListening; - controlsDiv.appendChild(startBtn); - controlsDiv.appendChild(stopBtn); - modalBody.appendChild(controlsDiv); - startBtn.addEventListener("click", () => { - if (!setupCustomSpeechRecognition()) { - showToast("Failed to initialize speech recognition"); - return; - } - const recognition = window._chatInternals.recognition; - try { - recognition.start(); - startVoiceChatSlideshow(); - } catch (error) { - showToast("Could not start speech recognition: " + error.message); - } - }); - stopBtn.addEventListener("click", () => { - if (window._chatInternals.recognition && window._chatInternals.isListening) { - window._chatInternals.recognition.stop(); - stopVoiceChatSlideshow(); - showToast("Voice recognition stopped"); - } - }); - } - function updateAllVoiceDropdowns(selectedIndex) { - const voiceDropdownIds = [ - "voice-select", - "voice-select-modal", - "voice-select-settings", - "voice-select-voicechat" - ]; - voiceDropdownIds.forEach(id => { - const dropdown = document.getElementById(id); - if (dropdown) { - dropdown.value = selectedIndex; - } - }); - } - openVoiceChatModalBtn.addEventListener("click", () => { - voiceChatModal.classList.remove("hidden"); - setupVoiceChatControls(); - window._chatInternals.populateAllVoiceDropdowns(); - }); - closeVoiceChatModalBtn.addEventListener("click", () => { - voiceChatModal.classList.add("hidden"); - if (window._chatInternals.recognition && window._chatInternals.isListening) { - window._chatInternals.recognition.stop(); - } - stopVoiceChatSlideshow(); - }); - openVoiceSettingsModalBtn.addEventListener("click", () => { - voiceSettingsModal.classList.remove("hidden"); - window._chatInternals.populateAllVoiceDropdowns(); - const voiceSpeedInput = document.getElementById("voice-speed"); - const voicePitchInput = document.getElementById("voice-pitch"); - const voiceSpeedValue = document.getElementById("voice-speed-value"); - const voicePitchValue = document.getElementById("voice-pitch-value"); - voiceSpeedInput.value = localStorage.getItem("voiceSpeed") || 0.9; - voicePitchInput.value = localStorage.getItem("voicePitch") || 1.0; - voiceSpeedValue.textContent = `${voiceSpeedInput.value}x`; - voicePitchValue.textContent = `${voicePitchInput.value}x`; - }); - document.getElementById("voice-settings-modal-close").addEventListener("click", () => { - voiceSettingsModal.classList.add("hidden"); - }); - document.getElementById("voice-settings-cancel").addEventListener("click", () => { - voiceSettingsModal.classList.add("hidden"); - }); - document.getElementById("voice-settings-save").addEventListener("click", () => { - const voiceSpeedInput = document.getElementById("voice-speed"); - const voicePitchInput = document.getElementById("voice-pitch"); - const voiceSelectModal = document.getElementById("voice-select-modal"); - const selectedVoiceIndex = voiceSelectModal.value; - const voiceSpeed = voiceSpeedInput.value; - const voicePitch = voicePitchInput.value; - window._chatInternals.selectedVoice = window._chatInternals.voices[selectedVoiceIndex]; - localStorage.setItem("selectedVoiceIndex", selectedVoiceIndex); - localStorage.setItem("voiceSpeed", voiceSpeed); - localStorage.setItem("voicePitch", voicePitch); - window._chatInternals.updateVoiceToggleUI(); - updateAllVoiceDropdowns(selectedVoiceIndex); - voiceSettingsModal.classList.add("hidden"); - showToast("Voice settings saved"); - }); - document.getElementById("voice-speed").addEventListener("input", () => { - document.getElementById("voice-speed-value").textContent = `${document.getElementById("voice-speed").value}x`; - }); - document.getElementById("voice-pitch").addEventListener("input", () => { - document.getElementById("voice-pitch-value").textContent = `${document.getElementById("voice-pitch").value}x`; - }); -}); + + if (voiceToggleBtn) { + voiceToggleBtn.addEventListener("click", window._chatInternals.toggleAutoSpeak); + window._chatInternals.updateVoiceToggleUI(); + setTimeout(() => { + if (autoSpeakEnabled) { + const testUtterance = new SpeechSynthesisUtterance("Voice check"); + testUtterance.volume = 0.1; + testUtterance.onend = () => {}; + testUtterance.onerror = (err) => { + window._chatInternals.autoSpeakEnabled = false; + localStorage.setItem("autoSpeakEnabled", "false"); + window._chatInternals.updateVoiceToggleUI(); + showToast("Voice synthesis unavailable. Voice mode disabled."); + }; + synth.speak(testUtterance); + } + }, 5000); + } + if (clearChatBtn) { + clearChatBtn.addEventListener("click", () => { + const currentSession = Storage.getCurrentSession(); + if (confirm("Are you sure you want to clear this chat?")) { + currentSession.messages = []; + Storage.updateSessionMessages(currentSession.id, currentSession.messages); + chatBox.innerHTML = ""; + showToast("Chat cleared"); + chatInput.disabled = false; + chatInput.focus(); + } + }); + } + function checkFirstLaunch() { + const firstLaunch = localStorage.getItem("firstLaunch") === "0"; + if (firstLaunch) { + const firstLaunchModal = document.getElementById("first-launch-modal"); + if (firstLaunchModal) { + firstLaunchModal.classList.remove("hidden"); + document.getElementById("first-launch-close").addEventListener("click", () => { + firstLaunchModal.classList.add("hidden"); + localStorage.setItem("firstLaunch", "1"); + }); + document.getElementById("first-launch-complete").addEventListener("click", () => { + firstLaunchModal.classList.add("hidden"); + localStorage.setItem("firstLaunch", "1"); + }); + document.getElementById("setup-theme").addEventListener("click", () => { + firstLaunchModal.classList.add("hidden"); + document.getElementById("settings-modal").classList.remove("hidden"); + }); + document.getElementById("setup-personalization").addEventListener("click", () => { + firstLaunchModal.classList.add("hidden"); + document.getElementById("personalization-modal").classList.remove("hidden"); + }); + document.getElementById("setup-model").addEventListener("click", () => { + firstLaunchModal.classList.add("hidden"); + document.getElementById("model-select").focus(); + }); + } + } + } + checkFirstLaunch(); + function setupVoiceInputButton() { + if ("webkitSpeechRecognition" in window || "SpeechRecognition" in window) { + const inputButtonsContainer = document.querySelector(".input-buttons-container"); + if (!window._chatInternals.voiceInputBtn && inputButtonsContainer) { + const voiceInputBtn = document.createElement("button"); + voiceInputBtn.id = "voice-input-btn"; + voiceInputBtn.innerHTML = ''; + voiceInputBtn.title = "Voice input"; + inputButtonsContainer.insertBefore(voiceInputBtn, document.getElementById("send-button")); + window._chatInternals.setVoiceInputButton(voiceInputBtn); + let voiceBuffer = ""; + let silenceTimeout = null; + voiceInputBtn.addEventListener("click", () => { + toggleSpeechRecognition(); + }); + } + } else { + const voiceInputBtn = document.getElementById("voice-input-btn"); + if (voiceInputBtn) { + voiceInputBtn.disabled = true; + voiceInputBtn.title = "Voice input not supported in this browser"; + } + } + } + setupVoiceInputButton(); + document.addEventListener('click', function(e) { + if (e.target.closest('.image-button-container')) { + e.preventDefault(); + e.stopPropagation(); + console.log("Click detected on image-button-container, preventing propagation"); + } + }, true); + const sendButton = document.getElementById("send-button"); + function handleSendMessage() { + const message = chatInput.value.trim(); + if (message === "") return; + window.addNewMessage({ role: "user", content: message }); + chatInput.value = ""; + chatInput.style.height = "auto"; + window.sendToPolliLib(() => { + sendButton.disabled = false; + chatInput.disabled = false; + chatInput.focus(); + }); + sendButton.disabled = true; + chatInput.disabled = true; + } + chatInput.addEventListener("input", () => { + sendButton.disabled = chatInput.value.trim() === ""; + chatInput.style.height = "auto"; + chatInput.style.height = chatInput.scrollHeight + "px"; + }); + sendButton.addEventListener("click", () => { + handleSendMessage(); + }); + sendButton.disabled = chatInput.value.trim() === ""; + const initialSession = Storage.getCurrentSession(); + if (initialSession.messages && initialSession.messages.length > 0) { + renderStoredMessages(initialSession.messages); + } else { + chatInput.disabled = false; + chatInput.focus(); + } + const voiceChatModal = document.getElementById("voice-chat-modal"); + const openVoiceChatModalBtn = document.getElementById("open-voice-chat-modal"); + const closeVoiceChatModalBtn = document.getElementById("voice-chat-modal-close"); + const voiceSettingsModal = document.getElementById("voice-settings-modal"); + const openVoiceSettingsModalBtn = document.getElementById("open-voice-settings-modal"); + const voiceChatImage = document.getElementById("voice-chat-image"); + let slideshowInterval = null; + function startVoiceChatSlideshow() { + if (slideshowInterval) clearInterval(slideshowInterval); + const currentSession = Storage.getCurrentSession(); + let imagePrompt = getLatestImagePrompt('default scene'); + if (!imagePrompt) imagePrompt = 'default scene'; + imagePrompt = `${imagePrompt}, origami`; + if (imagePrompt.length > 100) { + imagePrompt = imagePrompt.substring(0, 100); + } + function updateImage() { + const seed = Math.floor(Math.random() * 1000000); + const imageId = `voice-img-${Date.now()}`; + localStorage.setItem(`voiceImageId_${imageId}`, imageId); + try { + if (window.polliLib && window.polliClient) { + const url = window.polliLib.mcp.generateImageUrl(window.polliClient, { + prompt: imagePrompt, + width: 512, + height: 512, + seed, + nologo: true + }); + voiceChatImage.src = url; + } else { + voiceChatImage.src = "https://via.placeholder.com/512?text=Image+Unavailable"; + } + } catch (e) { + console.warn('polliLib generateImageUrl failed', e); + voiceChatImage.src = "https://via.placeholder.com/512?text=Image+Unavailable"; + } + voiceChatImage.dataset.imageId = imageId; + voiceChatImage.onload = () => { + attachImageButtons(voiceChatImage, imageId); + }; + voiceChatImage.onerror = () => { + showToast("Failed to load slideshow image"); + }; + } + updateImage(); + slideshowInterval = setInterval(updateImage, 10000); + } + function stopVoiceChatSlideshow() { + if (slideshowInterval) { + clearInterval(slideshowInterval); + slideshowInterval = null; + } + } + let voiceBuffer = ""; + let silenceTimeout = null; + function setupCustomSpeechRecognition() { + if (!window._chatInternals.recognition) { + if ('webkitSpeechRecognition' in window) { + window._chatInternals.recognition = new webkitSpeechRecognition(); + } else if ('SpeechRecognition' in window) { + window._chatInternals.recognition = new SpeechRecognition(); + } else { + showToast("Speech recognition not supported in this browser"); + return false; + } + const recognition = window._chatInternals.recognition; + recognition.continuous = true; + recognition.interimResults = true; + recognition.lang = 'en-US'; + recognition.onstart = () => { + window._chatInternals.isListening = true; + showToast("Voice recognition active"); + const startBtn = document.getElementById("voice-chat-start"); + const stopBtn = document.getElementById("voice-chat-stop"); + if (startBtn) startBtn.disabled = true; + if (stopBtn) stopBtn.disabled = false; + }; + recognition.onend = () => { + window._chatInternals.isListening = false; + const startBtn = document.getElementById("voice-chat-start"); + const stopBtn = document.getElementById("voice-chat-stop"); + if (startBtn) startBtn.disabled = false; + if (stopBtn) stopBtn.disabled = true; + }; + recognition.onerror = (event) => { + window._chatInternals.isListening = false; + const startBtn = document.getElementById("voice-chat-start"); + const stopBtn = document.getElementById("voice-chat-stop"); + if (startBtn) startBtn.disabled = false; + if (stopBtn) stopBtn.disabled = true; + if (event.error === "no-speech") { + showToast("No speech detected. Please try again."); + } else if (event.error === "not-allowed" || event.error === "service-not-allowed") { + showToast("Microphone access denied. Please allow microphone access in your browser settings."); + } else { + showToast("Voice recognition error: " + event.error); + } + }; + recognition.onresult = (event) => { + let interimTranscript = ""; + let finalTranscript = ""; + for (let i = event.resultIndex; i < event.results.length; i++) { + const transcript = event.results[i][0].transcript; + if (event.results[i].isFinal) { + const processed = transcript.trim(); + if (!handleVoiceCommand(processed)) { + finalTranscript += processed + " "; + } + } else { + interimTranscript += transcript; + } + } + voiceBuffer += finalTranscript; + chatInput.value = voiceBuffer + interimTranscript; + if (finalTranscript) { + clearTimeout(silenceTimeout); + silenceTimeout = setTimeout(() => { + if (voiceBuffer.trim()) { + window.addNewMessage({ role: "user", content: voiceBuffer.trim() }); + window.sendToPolliLib(() => { + startVoiceChatSlideshow(); + chatInput.focus(); + }); + voiceBuffer = ""; + chatInput.value = ""; + } + }, 1500); + } + }; + } + return true; + } + function setupVoiceChatControls() { + const modalBody = voiceChatModal.querySelector(".modal-body"); + let voiceSelectChat = modalBody.querySelector("#voice-select-voicechat"); + if (!voiceSelectChat) { + const voiceSelectContainer = document.createElement("div"); + voiceSelectContainer.className = "form-group mb-3"; + const voiceSelectLabel = document.createElement("label"); + voiceSelectLabel.className = "form-label"; + voiceSelectLabel.innerHTML = ' Voice Selection:'; + voiceSelectLabel.htmlFor = "voice-select-voicechat"; + voiceSelectChat = document.createElement("select"); + voiceSelectChat.id = "voice-select-voicechat"; + voiceSelectChat.className = "form-control"; + voiceSelectContainer.appendChild(voiceSelectLabel); + voiceSelectContainer.appendChild(voiceSelectChat); + const insertAfterElement = modalBody.querySelector("p") || voiceChatImage; + if (insertAfterElement && insertAfterElement.nextSibling) { + modalBody.insertBefore(voiceSelectContainer, insertAfterElement.nextSibling); + } else { + modalBody.appendChild(voiceSelectContainer); + } + } + const existingControls = modalBody.querySelector(".voice-chat-controls"); + if (existingControls) existingControls.remove(); + const controlsDiv = document.createElement("div"); + controlsDiv.className = "voice-chat-controls"; + controlsDiv.style.display = "flex"; + controlsDiv.style.gap = "10px"; + controlsDiv.style.marginTop = "15px"; + const startBtn = document.createElement("button"); + startBtn.id = "voice-chat-start"; + startBtn.className = "btn btn-primary"; + startBtn.textContent = "Start Listening"; + startBtn.style.width = "100%"; + startBtn.style.padding = "10px"; + startBtn.disabled = window._chatInternals.isListening; + const stopBtn = document.createElement("button"); + stopBtn.id = "voice-chat-stop"; + stopBtn.className = "btn btn-danger"; + stopBtn.textContent = "Stop Listening"; + stopBtn.style.width = "100%"; + stopBtn.style.padding = "10px"; + stopBtn.disabled = !window._chatInternals.isListening; + controlsDiv.appendChild(startBtn); + controlsDiv.appendChild(stopBtn); + modalBody.appendChild(controlsDiv); + startBtn.addEventListener("click", () => { + if (!setupCustomSpeechRecognition()) { + showToast("Failed to initialize speech recognition"); + return; + } + const recognition = window._chatInternals.recognition; + try { + recognition.start(); + startVoiceChatSlideshow(); + } catch (error) { + showToast("Could not start speech recognition: " + error.message); + } + }); + stopBtn.addEventListener("click", () => { + if (window._chatInternals.recognition && window._chatInternals.isListening) { + window._chatInternals.recognition.stop(); + stopVoiceChatSlideshow(); + showToast("Voice recognition stopped"); + } + }); + } + function updateAllVoiceDropdowns(selectedIndex) { + const voiceDropdownIds = [ + "voice-select", + "voice-select-modal", + "voice-select-settings", + "voice-select-voicechat" + ]; + voiceDropdownIds.forEach(id => { + const dropdown = document.getElementById(id); + if (dropdown) { + dropdown.value = selectedIndex; + } + }); + } + openVoiceChatModalBtn.addEventListener("click", () => { + voiceChatModal.classList.remove("hidden"); + setupVoiceChatControls(); + window._chatInternals.populateAllVoiceDropdowns(); + }); + closeVoiceChatModalBtn.addEventListener("click", () => { + voiceChatModal.classList.add("hidden"); + if (window._chatInternals.recognition && window._chatInternals.isListening) { + window._chatInternals.recognition.stop(); + } + stopVoiceChatSlideshow(); + }); + openVoiceSettingsModalBtn.addEventListener("click", () => { + voiceSettingsModal.classList.remove("hidden"); + window._chatInternals.populateAllVoiceDropdowns(); + const voiceSpeedInput = document.getElementById("voice-speed"); + const voicePitchInput = document.getElementById("voice-pitch"); + const voiceSpeedValue = document.getElementById("voice-speed-value"); + const voicePitchValue = document.getElementById("voice-pitch-value"); + voiceSpeedInput.value = localStorage.getItem("voiceSpeed") || 0.9; + voicePitchInput.value = localStorage.getItem("voicePitch") || 1.0; + voiceSpeedValue.textContent = `${voiceSpeedInput.value}x`; + voicePitchValue.textContent = `${voicePitchInput.value}x`; + }); + document.getElementById("voice-settings-modal-close").addEventListener("click", () => { + voiceSettingsModal.classList.add("hidden"); + }); + document.getElementById("voice-settings-cancel").addEventListener("click", () => { + voiceSettingsModal.classList.add("hidden"); + }); + document.getElementById("voice-settings-save").addEventListener("click", () => { + const voiceSpeedInput = document.getElementById("voice-speed"); + const voicePitchInput = document.getElementById("voice-pitch"); + const voiceSelectModal = document.getElementById("voice-select-modal"); + const selectedVoiceIndex = voiceSelectModal.value; + const voiceSpeed = voiceSpeedInput.value; + const voicePitch = voicePitchInput.value; + window._chatInternals.selectedVoice = window._chatInternals.voices[selectedVoiceIndex]; + localStorage.setItem("selectedVoiceIndex", selectedVoiceIndex); + localStorage.setItem("voiceSpeed", voiceSpeed); + localStorage.setItem("voicePitch", voicePitch); + window._chatInternals.updateVoiceToggleUI(); + updateAllVoiceDropdowns(selectedVoiceIndex); + voiceSettingsModal.classList.add("hidden"); + showToast("Voice settings saved"); + }); + document.getElementById("voice-speed").addEventListener("input", () => { + document.getElementById("voice-speed-value").textContent = `${document.getElementById("voice-speed").value}x`; + }); + document.getElementById("voice-pitch").addEventListener("input", () => { + document.getElementById("voice-pitch-value").textContent = `${document.getElementById("voice-pitch").value}x`; + }); +});