diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-18 18:00:54 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-18 18:00:54 +0200 |
| commit | 0eca39021874a673933ebb82b973002b682e7a4b (patch) | |
| tree | dbfbb685e40d10e6255e68629b72194b23709854 /modules/core/module-chatstudio.js | |
| parent | fba6df22dae297c89fa0df065628dddded66c69c (diff) | |
auto: 2026-06-18_18:00 changes
Diffstat (limited to 'modules/core/module-chatstudio.js')
| -rw-r--r-- | modules/core/module-chatstudio.js | 131 |
1 files changed, 102 insertions, 29 deletions
diff --git a/modules/core/module-chatstudio.js b/modules/core/module-chatstudio.js index d558445..8e9d208 100644 --- a/modules/core/module-chatstudio.js +++ b/modules/core/module-chatstudio.js @@ -35,14 +35,17 @@ class ChatStudioModule { _loadOverlayAssets() { return new Promise((resolve, reject) => { - // Load CSS + // Load CSS — gate resolve on onload to prevent FOUC + var cssReady = Promise.resolve(); if (!this._cssLoaded) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = 'chat/chat-ui.css?v=5'; - link.onload = () => { this._cssLoaded = true; }; - link.onerror = () => reject(new Error('Failed to load chat/chat-ui.css')); - document.head.appendChild(link); + cssReady = new Promise((res, rej) => { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'chat/chat-ui.css?v=' + (window.NEWOS4_VERSION || '5'); + link.onload = () => { this._cssLoaded = true; res(); }; + link.onerror = () => rej(new Error('Failed to load chat/chat-ui.css')); + document.head.appendChild(link); + }); } // Expose ALL Lite globals BEFORE scripts load so chat-modules.js & chat-commands.js can use them @@ -56,7 +59,7 @@ class ChatStudioModule { window.logs = []; window.messages = []; window.isStreaming = false; - window.CHATSTUDIO_VERSION = '1.0.0-lite-inline'; + window.CHATSTUDIO_VERSION = window.NEWOS4_VERSION || '1.0.0-lite-inline'; window.modules = []; window.currentChatModule = 'default'; @@ -94,13 +97,14 @@ class ChatStudioModule { window._csGlobalsInited = true; } - // Fetch HTML template (cached after first load) + // Fetch HTML template (cached after first load) — wait for CSS first if (this._overlayHtml) { - // Overlay div already exists (just hidden). No-op. - resolve(); + // Overlay div already exists (just hidden). Wait for CSS then resolve. + cssReady.then(() => resolve()); return; } - fetch('chat/chat-ui.html?v=5') + cssReady + .then(() => fetch('chat/chat-ui.html?v=' + (window.NEWOS4_VERSION || '5'))) .then(r => r.text()) .then(html => { this._overlayHtml = html; // cache for re-open @@ -114,7 +118,7 @@ class ChatStudioModule { const loadScript = (src) => { return new Promise((res, rej) => { const s = document.createElement('script'); - s.src = src + '?v=5'; + s.src = src + '?v=' + (window.NEWOS4_VERSION || '5'); s.onload = () => res(); s.onerror = () => rej(new Error('Failed to load ' + src)); document.body.appendChild(s); @@ -228,11 +232,13 @@ class ChatStudioModule { window.closeChatOverlay = () => this._closeOverlay(); // ── Constants ── - const CHATSTUDIO_VERSION = '1.0.0-lite-inline'; + const CHATSTUDIO_VERSION = window.NEWOS4_VERSION || '1.0.0-lite-inline'; const API_KEY = 'webx11_chat_api'; const MESSAGES_KEY = 'cslite_messages'; const SETTINGS_KEY = 'webx11_chat_settings'; const LOGS_KEY = 'webx11_chat_logs'; + // P2#11: API key is now kept in-memory only (not persisted to localStorage) + // It's loaded into OLLAMA_API_KEY during session, cleared on page reload // ── State ── let OLLAMA_URL = '/ollama'; @@ -273,6 +279,17 @@ class ChatStudioModule { return Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 9); } + // Fetch with timeout — AbortController-based, 30s default + var _streamAbort = null; // AbortController for streaming, null when not active + function fetchWithTimeout(url, opts, timeout) { + timeout = timeout || 30000; + opts = opts || {}; + var ctrl = new AbortController(); + opts.signal = ctrl.signal; + var timer = setTimeout(function() { ctrl.abort(); }, timeout); + return fetch(url, opts).finally(function() { clearTimeout(timer); }); + } + function showToast(msg) { var t = document.getElementById('toast'); if (!t) return; @@ -320,13 +337,15 @@ class ChatStudioModule { } // ── Log System ── + var MAX_LOGS = 200; // P2#17: Configurable log limit (was hardcoded 50) function addLog(action, detail) { var time = new Date().toLocaleTimeString(); logs.unshift({ time: time, action: action, detail: detail }); - if (logs.length > 50) logs.pop(); + if (logs.length > MAX_LOGS) logs.length = MAX_LOGS; renderLogs(); try { - localStorage.setItem(LOGS_KEY, utf8_to_b64(JSON.stringify(logs))); + // P2#12: Use plain JSON instead of base64 (consistent with messages storage) + localStorage.setItem(LOGS_KEY, JSON.stringify(logs)); } catch (e) { /* ignore */ } } @@ -364,7 +383,14 @@ class ChatStudioModule { function loadLogs() { try { var saved = localStorage.getItem(LOGS_KEY); - if (saved) logs = JSON.parse(b64_to_utf8(saved)); + if (saved) { + // P2#12: Handle both old base64 and new plain JSON + try { + logs = JSON.parse(saved); + } catch (e1) { + logs = JSON.parse(b64_to_utf8(saved)); + } + } } catch (e) { logs = []; } @@ -376,7 +402,10 @@ class ChatStudioModule { try { var saved = localStorage.getItem(SETTINGS_KEY); if (saved) { - var settings = JSON.parse(b64_to_utf8(saved)); + // P2#12: Handle both old base64 and new plain JSON + var settings; + try { settings = JSON.parse(saved); } + catch (e1) { settings = JSON.parse(b64_to_utf8(saved)); } OLLAMA_URL = settings.ollamaUrl || '/ollama'; OLLAMA_API_KEY = settings.ollamaApiKey || ''; DEFAULT_MODEL = settings.defaultModel || 'llama3.2'; @@ -412,12 +441,13 @@ class ChatStudioModule { try { var settings = { ollamaUrl: OLLAMA_URL, - ollamaApiKey: OLLAMA_API_KEY, + // P2#11: Don't persist API key — keep in memory only defaultModel: DEFAULT_MODEL, useStreaming: USE_STREAMING, apiBase: API_BASE }; - localStorage.setItem(SETTINGS_KEY, utf8_to_b64(JSON.stringify(settings))); + // P2#12: Use plain JSON instead of base64 (consistent storage) + localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch (e) { console.warn('Failed to save settings:', e); } @@ -444,7 +474,7 @@ class ChatStudioModule { try { var headers = { 'Content-Type': 'application/json' }; if (OLLAMA_API_KEY) headers['Authorization'] = 'Bearer ' + OLLAMA_API_KEY; - var response = await fetch(OLLAMA_URL + '/api/tags', { headers: headers }); + var response = await fetchWithTimeout(OLLAMA_URL + '/api/tags', { headers: headers }); if (response.ok) { var data = await response.json(); var modelCount = data.models ? data.models.length : 0; @@ -463,7 +493,7 @@ class ChatStudioModule { // ── Models ── async function loadModels() { try { - var response = await fetch(OLLAMA_URL + '/api/tags'); + var response = await fetchWithTimeout(OLLAMA_URL + '/api/tags'); if (!response.ok) throw new Error('Failed to fetch models'); var data = await response.json(); var select = document.getElementById('modelSelect'); @@ -500,8 +530,15 @@ class ChatStudioModule { } // ── Message History ── + var MAX_MESSAGES = 100; // P2#13: Cap chat history to prevent localStorage overflow function saveMessages(msgs) { msgs = msgs || messages; + // P2#13: Trim to last MAX_MESSAGES before saving + if (msgs.length > MAX_MESSAGES) { + msgs = msgs.slice(-MAX_MESSAGES); + messages.length = 0; + messages.push.apply(messages, msgs); + } try { localStorage.setItem(MESSAGES_KEY, JSON.stringify(msgs)); } catch (e) { @@ -546,7 +583,8 @@ class ChatStudioModule { ? '<button class="msg-copy" onclick="showHtmlPreview()" title="Preview HTML">👁️</button>' : ''; if (role === 'ai') latestAiContent = content; - var renderedContent = role === 'user' ? escapeHtml(content) : formatContent(content); + // P2#16: Apply markdown formatting to user messages too (was plain escapeHtml only) + var renderedContent = role === 'user' ? formatContent(escapeHtml(content)) : formatContent(content); div.innerHTML = '<div class="role">' + '<span>' + (role === 'user' ? 'You' : 'AI') + '</span>' + '<div style="display:flex;gap:4px;">' + @@ -727,6 +765,12 @@ class ChatStudioModule { var model = modelSelect ? modelSelect.value : DEFAULT_MODEL; var mod = typeof getCurrentModule === 'function' ? getCurrentModule() : null; var systemPrompt = mod && mod.prompt ? mod.prompt : ''; + // Inject current date into system prompt + var now = new Date(); + var dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); + var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; + var timeStr = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0'); + systemPrompt = 'Current date: ' + dateStr + ' (' + days[now.getDay()] + ') ' + timeStr + '\n\n' + systemPrompt; var chatHistory = buildChatContext(); try { @@ -762,6 +806,7 @@ class ChatStudioModule { div.innerHTML = '<div class="role"><span>AI</span><div style="display:flex;gap:4px;">' + '<button class="msg-copy" onclick="copyMessage(this, \'ai\')" title="Copy">📋</button>' + '<button class="msg-copy" onclick="showHtmlPreview()" title="Preview HTML">👁️</button>' + + '<button id="streamStopBtn" class="msg-copy" onclick="stopStreaming()" title="Stop" style="color:var(--danger);">⏹</button>' + '</div></div><div class="msg-content"><span class="streaming-cursor"></span></div>'; div.dataset.content = ''; container.appendChild(div); @@ -771,6 +816,7 @@ class ChatStudioModule { var fullContent = ''; try { + _streamAbort = new AbortController(); var response = await fetch(OLLAMA_URL + '/api/generate', { method: 'POST', headers: headers, @@ -778,7 +824,8 @@ class ChatStudioModule { model: model, prompt: prompt, stream: true - }) + }), + signal: _streamAbort.signal }); if (!response.ok) { @@ -849,16 +896,32 @@ class ChatStudioModule { addLog('AI', fullContent.substring(0, 50) + '...'); } catch (e) { - if (contentEl) contentEl.textContent = 'Error: ' + e.message; - console.error('Stream error:', e); + if (e.name === 'AbortError') { + if (contentEl && !fullContent) contentEl.textContent = '⏹ Stopped by user.'; + } else { + if (contentEl) contentEl.textContent = 'Error: ' + e.message; + console.error('Stream error:', e); + } } isStreaming = false; + _streamAbort = null; + // Remove stop button if present + var stopBtn = document.getElementById('streamStopBtn'); + if (stopBtn) stopBtn.remove(); + } + + function stopStreaming() { + if (_streamAbort) { + _streamAbort.abort(); + _streamAbort = null; + } + isStreaming = false; } async function sendNonStreamingRequest(headers, model, prompt) { try { - var response = await fetch(OLLAMA_URL + '/api/generate', { + var response = await fetchWithTimeout(OLLAMA_URL + '/api/generate', { method: 'POST', headers: headers, body: JSON.stringify({ @@ -886,7 +949,16 @@ class ChatStudioModule { for (var i = 0; i < messages.length; i++) { var m = messages[i]; var role = m.role === 'user' ? 'User' : 'AI'; - var content = m.content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); + // P2#15: Use DOMParser instead of naive regex HTML strip + var content = m.content; + try { + var doc = new DOMParser().parseFromString(content, 'text/html'); + content = doc.body.textContent || doc.body.innerText || ''; + } catch (e) { + // Fallback to regex if DOMParser fails + content = content.replace(/<[^>]+>/g, ' '); + } + content = content.replace(/\s+/g, ' ').trim(); context += role + ': ' + content + '\n'; } return context; @@ -1051,8 +1123,9 @@ class ChatStudioModule { renderMessagesFromData(); addLog('Init', 'Chat overlay initialized'); } catch (err) { - // Reset initialization flag so user can retry + // Reset initialization flags so user can retry this._initialized = false; + window._csGlobalsInited = false; // Print error to terminal if (this.terminal && this.terminal.print) { |