From e3cf60b175873c47082bab7f832582e5446357a6 Mon Sep 17 00:00:00 2001
From: Hackall <36754621+hackall360@users.noreply.github.com>
Date: Mon, 15 Sep 2025 13:54:40 -0700
Subject: [PATCH] Improve structured Pollinations response handling
---
js/chat/chat-core.js | 1559 ++++++++++++++++++++++++---------------
js/chat/chat-init.js | 1430 +++++++++++++++++------------------
js/chat/chat-storage.js | 1442 ++++++++++++++++++------------------
3 files changed, 2404 insertions(+), 2027 deletions(-)
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`;
+ });
+});