aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-chatstudio.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-18 18:00:54 +0200
committerAgando <roduccomm@gmail.com>2026-06-18 18:00:54 +0200
commit0eca39021874a673933ebb82b973002b682e7a4b (patch)
treedbfbb685e40d10e6255e68629b72194b23709854 /modules/core/module-chatstudio.js
parentfba6df22dae297c89fa0df065628dddded66c69c (diff)
auto: 2026-06-18_18:00 changes
Diffstat (limited to 'modules/core/module-chatstudio.js')
-rw-r--r--modules/core/module-chatstudio.js131
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) {