PageSourceSearch

https://odysseus-lxcq.onrender.com/static/js/chatStream.js?v=20260722emailfastindex1

js odysseus-lxcq.onrender.com collected 2026-10-03 11:42:46 UTC 13,479 bytes, 298 lines download raw bytes

1// static/js/chatStream.js
2// SSE event handlers extracted from chat.js handleChatSubmit
3// Handles: ui_control events, background stream management
4
5import uiModule from './ui.js';
6import Storage from './storage.js';
7import themeModule from './theme.js';
8import markdownModule from './markdown.js';
9import sessionModule from './sessions.js';
10import documentModule from './document.js?v=20260722emailfastindex1';
11
12/**
13 * Handle a ui_control SSE event — AI-driven UI manipulation.
14 * Extracted from the duplicated ui_control + tool_output.ui_event handlers.
15 */
16export function handleUIControl(uiData) {
17  var uiEvent = uiData.ui_event || uiData;
18  var esc = uiModule.esc;
19
20  try {
21    if (uiEvent === 'toggle' || uiData.ui_event === 'toggle') {
22      var toggleMap = {
23        web: 'web-toggle', bash: 'bash-toggle', rag: 'rag-toggle',
24        research: 'research-toggle', incognito: 'incognito-toggle',
25      };
26      var btnMap = {
27        web: 'web-toggle-btn', bash: 'bash-toggle-btn', rag: 'rag-indicator-btn',
28      };
29      var chkId = toggleMap[uiData.toggle_name];
30      var btnId = btnMap[uiData.toggle_name];
31      if (uiData.toggle_name === 'rag' && window._syncRagIndicator) {
32        window._syncRagIndicator(!!uiData.state);
33      } else {
34        if (chkId) {
35          var chk = document.getElementById(chkId);
36          if (chk) chk.checked = !!uiData.state;
37        }
38        if (btnId) {
39          var btn = document.getElementById(btnId);
40          if (btn) btn.classList.toggle('active', !!uiData.state);
41        }
42      }
43      var ts = Storage.getJSON(Storage.KEYS.TOGGLES, {});
44      ts[uiData.toggle_name] = !!uiData.state;
45      Storage.setJSON(Storage.KEYS.TOGGLES, ts);
46
47    } else if (uiEvent === 'set_mode' || uiData.ui_event === 'set_mode') {
48      var modeVal = uiData.mode;
49      var agentBtn = document.getElementById('mode-agent-btn');
50      var chatBtn = document.getElementById('mode-chat-btn');
51      if (agentBtn && chatBtn) {
52        agentBtn.classList.toggle('active', modeVal === 'agent');
53        chatBtn.classList.toggle('active', modeVal !== 'agent');
54      }
55      var ts2 = Storage.getJSON(Storage.KEYS.TOGGLES, {});
56      ts2.mode = modeVal;
57      Storage.setJSON(Storage.KEYS.TOGGLES, ts2);
58      document.querySelectorAll('[data-mode-tool]').forEach(function(b) {
59        b.style.display = modeVal === 'agent' ? '' : 'none';
60      });
61
62    } else if (uiEvent === 'switch_model' || uiData.ui_event === 'switch_model') {
63      var modelDisplay = document.querySelector('.current-model-name, #current-model');
64      if (modelDisplay) modelDisplay.textContent = uiData.model;
65
66    } else if (uiEvent === 'set_theme' || uiData.ui_event === 'set_theme') {
67      var tm = themeModule;
68      if (tm && tm.THEMES && tm.applyColors && tm.save) {
69        var themeName = uiData.theme_name;
70        if (themeName === 'chatgpt') themeName = 'gpt';  // renamed preset
71        var customThemes = tm.getCustomThemes ? tm.getCustomThemes() : {};
72        var colors = tm.THEMES[themeName] || customThemes[themeName] || uiData.colors;
73        if (colors) {
74          tm.applyColors(colors);
75          tm.save(themeName, colors);
76          var grid = document.getElementById('themeGrid');
77          if (grid) {
78            grid.querySelectorAll('.theme-swatch').forEach(function(s) { s.classList.remove('active'); });
79            var sw = grid.querySelector('[data-theme="' + themeName + '"]');
80            if (sw) sw.classList.add('active');
81          }
82        }
83      }
84
85    } else if (uiEvent === 'create_theme' || uiData.ui_event === 'create_theme') {
86      var tm2 = themeModule;
87      if (tm2 && tm2.applyColors && tm2.save) {
88        var colors2 = uiData.colors;
89        var name = uiData.theme_name || 'custom';
90        if (colors2) {
91          tm2.applyColors(colors2);
92          tm2.save(name, colors2);
93          // Background effects (animated pattern / frosted glass) the model
94          // optionally set — apply them live and persist with the theme so
95          // they survive re-applying it later.
96          var bg = uiData.bg || null;
97          var opts = {};
98          if (bg) {
99            if (bg.pattern && tm2.applyBgPattern) { tm2.applyBgPattern(bg.pattern); opts.bgPattern = bg.pattern; }
100            if (bg.effectColor && tm2.applyBgEffectColor) { tm2.applyBgEffectColor(bg.effectColor); opts.bgEffectColor = bg.effectColor; }
101            if (bg.effectIntensity != null && tm2.applyBgEffectIntensity) { tm2.applyBgEffectIntensity(bg.effectIntensity); opts.bgEffectIntensity = bg.effectIntensity; }
102            if (bg.effectSize != null && tm2.applyBgEffectSize) { tm2.applyBgEffectSize(bg.effectSize); opts.bgEffectSize = bg.effectSize; }
103            if (bg.frosted != null && tm2.applyFrostedGlass) { tm2.applyFrostedGlass(bg.frosted); opts.frosted = bg.frosted; }
104          }
105          if (tm2.saveCustomTheme) tm2.saveCustomTheme(name, colors2, Object.keys(opts).length ? opts : undefined);
106        }
107      }
108
109    } else if (uiEvent === 'highlight' || uiData.ui_event === 'highlight') {
110      document.querySelectorAll('.odysseus-highlight').forEach(function(e) { e.classList.remove('odysseus-highlight'); });
111      document.querySelectorAll('.odysseus-hl-label').forEach(function(e) { e.remove(); });
112      var target = document.querySelector(uiData.selector);
113      if (target) {
114        target.classList.add('odysseus-highlight');
115        target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
116        if (uiData.label) {
117          var lbl = document.createElement('div');
118          lbl.className = 'odysseus-hl-label';
119          lbl.textContent = uiData.label;
120          if (!target.style.position) target.style.position = 'relative';
121          target.appendChild(lbl);
122        }
123      }
124
125    } else if (uiEvent === 'clear_highlight' || uiData.ui_event === 'clear_highlight') {
126      document.querySelectorAll('.odysseus-highlight').forEach(function(e) { e.classList.remove('odysseus-highlight'); });
127      document.querySelectorAll('.odysseus-hl-label').forEach(function(e) { e.remove(); });
128
129    } else if (uiEvent === 'research_started' || uiData.ui_event === 'research_started') {
130      // Agent kicked off deep research — adopt the session into the
131      // sidebar immediately so the user sees it without waiting for
132      // the 12s active-poll.
133      var rsid = uiData.research_session_id || uiData.session_id;
134      if (rsid) {
135        import('./research/jobs.js').then(function(mod) {
136          var fn = mod.adoptSession || (mod.default && mod.default.adoptSession);
137          if (fn) fn(rsid);
138        }).catch(function(){});
139        // The clickable "Open in Deep Research" link is now emitted by the
140        // agent loop as a `#research-<id>` markdown anchor in the assistant's
141        // response text — it renders as a regular clickable chat link AND
142        // persists across refresh (saved with the message). No ephemeral
143        // chip injection needed here anymore.
144      }
145
146    } else if (uiEvent === 'open_panel' || uiData.ui_event === 'open_panel') {
147      var panel = uiData.panel;
148      if (panel === 'documents') {
149        import('./documentLibrary.js').then(function(mod) {
150          var fn = mod.openLibrary || (mod.default && mod.default.openLibrary);
151          if (fn) fn();
152        }).catch(function(){});
153      } else if (panel === 'gallery') {
154        import('./gallery.js').then(function(mod) {
155          var fn = mod.openGallery || (mod.default && mod.default.openGallery);
156          if (fn) fn();
157        }).catch(function(){});
158      } else if (panel === 'email') {
159        import('./emailLibrary.js?v=20260722emailfastindex1').then(function(mod) {
160          var fn = mod.openEmailLibrary || (mod.default && mod.default.openEmailLibrary);
161          if (fn) fn();
162        }).catch(function(){});
163      } else if (panel === 'sessions') {
164        import('./sessions.js').then(function(mod) {
165          var fn = mod.openLibrary || (mod.default && mod.default.openLibrary);
166          if (fn) fn();
167        }).catch(function(){});
168      } else if (panel === 'cookbook') {
169        import('./cookbook.js').then(function(mod) {
170          var fn = mod.open || (mod.default && mod.default.open);
171          if (fn) fn();
172        }).catch(function(){});
173      } else if (panel === 'notes') {
174        import('./notes.js').then(function(mod) {
175          var fn = mod.openPanel || mod.openNotes || (mod.default && (mod.default.openPanel || mod.default.openNotes));
176          if (fn) fn();
177        }).catch(function(){});
178      } else if (panel === 'memories' || panel === 'skills' || panel === 'settings') {
179        // These live in the sidebar / settings drawer — most just need
180        // an existing button click.
181        var ids = { memories: 'tool-memory-btn', skills: 'skills-btn', settings: 'open-settings-btn' };
182        var btn = document.getElementById(ids[panel]);
183        if (btn) btn.click();
184      }
185
186    } else if (uiEvent === 'open_email_reply' || uiData.ui_event === 'open_email_reply') {
187      try {
188        var activeCtx = documentModule && documentModule.getActiveEmailComposerContext
189          ? documentModule.getActiveEmailComposerContext()
190          : null;
191        var sameActiveDraft = activeCtx
192          && String(activeCtx.sourceUid || '') === String(uiData.uid || '')
193          && String(activeCtx.sourceFolder || 'INBOX') === String(uiData.folder || 'INBOX');
194        var existingDocId = sameActiveDraft && activeCtx.docId
195          ? activeCtx.docId
196          : (documentModule && documentModule.findEmailDocId
197            ? documentModule.findEmailDocId(uiData.uid, uiData.folder || 'INBOX')
198            : null);
199        if (existingDocId && documentModule.replaceEmailReplyBody) {
200          if (documentModule.loadDocument) documentModule.loadDocument(existingDocId);
201          documentModule.replaceEmailReplyBody(existingDocId, uiData.body || '', { force: true });
202          if (uiModule && uiModule.showToast) uiModule.showToast('Wrote reply into the open email');
203          return;
204        }
205      } catch (e) {
206        console.warn('open_email_reply existing draft update failed:', e);
207      }
208      import('./emailInbox.js?v=20260722emailfastindex1').then(function(mod) {
209        var fn = mod.openReplyDraft || (mod.default && mod.default.openReplyDraft);
210        if (fn) fn(uiData.uid, uiData.folder || 'INBOX', uiData.mode || 'reply', uiData.body || '');
211      }).catch(function(e) {
212        console.warn('open_email_reply failed:', e);
213      });
214    }
215  } catch(e) {
216    console.warn('ui_control handler error:', e);
217  }
218}
219
220/**
221 * Notify user when a background stream completes.
222 */
223export function notifyStreamComplete(sessionId, query) {
224  var isHidden = document.hidden;
225  var isOtherSession = sessionModule && sessionModule.getCurrentSessionId() !== sessionId;
226  if (!isHidden && !isOtherSession) return;
227  if (!('Notification' in window) || Notification.permission !== 'granted') return;
228  var body = query ? 'Response to "' + query.substring(0, 60) + '" is ready' : 'Your chat response has completed';
229  var notification = new Notification('Response Complete', {
230    body: body,
231    tag: 'stream-' + sessionId,
232  });
233  notification.onclick = function() {
234    window.focus();
235    if (isOtherSession && sessionModule) {
236      sessionModule.selectSession(sessionId);
237    }
238    notification.close();
239  };
240  setTimeout(function() { notification.close(); }, 10000);
241}
242
243/**
244 * Insert a clickable in-chat toast when a background stream finishes.
245 */
246export function insertStreamDoneToast(sessionId, query) {
247  var box = document.getElementById('chat-history');
248  if (!box) return;
249  var sessions = sessionModule ? sessionModule.getSessions() : [];
250  var sess = sessions.find(function(s) { return s.id === sessionId; });
251  var name = sess ? sess.name : 'another session';
252  var preview = query ? '"' + query.substring(0, 50) + (query.length > 50 ? '...' : '') + '"' : '';
253  var div = document.createElement('div');
254  div.className = 'msg msg-system stream-done-toast';
255  div.innerHTML = '<div class="body">'
256    + '<span class="stream-done-indicator">●</span>'
257    + '<span>Response ready in <strong>' + (name || 'session').replace(/</g, '&lt;') + '</strong>'
258    + (preview ? ' &mdash; ' + preview.replace(/</g, '&lt;') : '')
259    + '</span>'
260    + '</div>';
261  div.addEventListener('click', function() {
262    if (sessionModule) sessionModule.selectSession(sessionId);
263  });
264  box.appendChild(div);
265  uiModule.scrollHistory();
266}
267
268/**
269 * Notify when research completes (browser notification).
270 */
271export function notifyResearchComplete(sessionId, query) {
272  var isHidden = document.hidden;
273  var isOtherSession = sessionModule && sessionModule.getCurrentSessionId() !== sessionId;
274  if (!isHidden && !isOtherSession) return;
275  if (!('Notification' in window) || Notification.permission !== 'granted') return;
276  var body = query ? 'Research on "' + query.substring(0, 60) + '" is ready' : 'Your deep research has completed';
277  var notification = new Notification('Research Complete', {
278    body: body,
279    tag: 'research-' + sessionId,
280  });
281  notification.onclick = function() {
282    window.focus();
283    if (isOtherSession && sessionModule) {
284      sessionModule.selectSession(sessionId);
285    }
286    notification.close();
287  };
288  setTimeout(function() { notification.close(); }, 10000);
289}
290
291const chatStream = {
292  handleUIControl,
293  notifyStreamComplete,
294  insertStreamDoneToast,
295  notifyResearchComplete,
296};
297
298export default chatStream;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.