pygpt-net 2.6.37__py3-none-any.whl → 2.6.39__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/handler/anthropic_stream.py +0 -2
  4. pygpt_net/controller/chat/handler/worker.py +6 -2
  5. pygpt_net/controller/debug/debug.py +6 -6
  6. pygpt_net/controller/model/editor.py +20 -42
  7. pygpt_net/controller/model/importer.py +9 -2
  8. pygpt_net/controller/painter/common.py +0 -8
  9. pygpt_net/controller/plugins/plugins.py +11 -3
  10. pygpt_net/controller/presets/presets.py +2 -2
  11. pygpt_net/core/ctx/bag.py +7 -2
  12. pygpt_net/core/ctx/reply.py +17 -2
  13. pygpt_net/core/db/viewer.py +19 -34
  14. pygpt_net/core/render/plain/pid.py +12 -1
  15. pygpt_net/core/render/web/body.py +292 -445
  16. pygpt_net/core/tabs/tab.py +24 -1
  17. pygpt_net/data/config/config.json +3 -3
  18. pygpt_net/data/config/models.json +3 -3
  19. pygpt_net/item/assistant.py +51 -2
  20. pygpt_net/item/attachment.py +21 -20
  21. pygpt_net/item/calendar_note.py +19 -2
  22. pygpt_net/item/ctx.py +115 -2
  23. pygpt_net/item/index.py +9 -2
  24. pygpt_net/item/mode.py +9 -6
  25. pygpt_net/item/model.py +20 -3
  26. pygpt_net/item/notepad.py +14 -2
  27. pygpt_net/item/preset.py +42 -2
  28. pygpt_net/item/prompt.py +8 -2
  29. pygpt_net/plugin/cmd_files/plugin.py +2 -2
  30. pygpt_net/provider/api/anthropic/tools.py +1 -1
  31. pygpt_net/provider/api/google/realtime/client.py +2 -2
  32. pygpt_net/provider/core/attachment/json_file.py +2 -2
  33. pygpt_net/tools/text_editor/tool.py +4 -1
  34. pygpt_net/tools/text_editor/ui/dialogs.py +1 -1
  35. pygpt_net/ui/dialog/db.py +177 -59
  36. pygpt_net/ui/dialog/dictionary.py +57 -59
  37. pygpt_net/ui/dialog/editor.py +3 -2
  38. pygpt_net/ui/dialog/image.py +1 -1
  39. pygpt_net/ui/dialog/logger.py +3 -2
  40. pygpt_net/ui/dialog/models.py +171 -21
  41. pygpt_net/ui/dialog/plugins.py +26 -20
  42. pygpt_net/ui/layout/ctx/ctx_list.py +3 -4
  43. pygpt_net/ui/layout/toolbox/__init__.py +2 -2
  44. pygpt_net/ui/layout/toolbox/assistants.py +8 -9
  45. pygpt_net/ui/layout/toolbox/presets.py +2 -2
  46. pygpt_net/ui/main.py +9 -4
  47. pygpt_net/ui/widget/element/labels.py +2 -2
  48. pygpt_net/ui/widget/textarea/editor.py +0 -4
  49. pygpt_net/utils.py +12 -13
  50. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/METADATA +14 -2
  51. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/RECORD +54 -54
  52. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/LICENSE +0 -0
  53. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/WHEEL +0 -0
  54. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/entry_points.txt +0 -0
@@ -46,30 +46,29 @@ class Body:
46
46
  <script type="text/javascript" src="qrc:///js/highlight.min.js"></script>
47
47
  <script type="text/javascript" src="qrc:///js/katex.min.js"></script>
48
48
  <script>
49
- if (hljs) {
49
+ if (typeof hljs !== 'undefined') {
50
50
  hljs.configure({
51
51
  ignoreUnescapedHTML: true,
52
52
  });
53
- }
54
- let DEBUG_MODE = false; // allow dynamic enabling via debug console
53
+ }
54
+ const streamQ = [];
55
+ let DEBUG_MODE = false;
55
56
  let bridgeConnected = false;
56
57
  let streamHandler;
57
58
  let nodeHandler;
58
59
  let nodeReplaceHandler;
59
- let scrollTimeout = null;
60
60
  let prevScroll = 0;
61
61
  let bridge;
62
- let streamQ = [];
63
62
  let streamRAF = 0;
63
+ let batching = false;
64
+ let needScroll = false;
64
65
  let pid = """
65
66
  _HTML_P2 = """
66
67
  let collapsed_idx = [];
67
- let domOutputStream = document.getElementById('_append_output_');
68
- let domOutput = document.getElementById('_output_');
69
- let domInput = document.getElementById('_input_');
68
+ let domOutputStream = null;
70
69
  let domLastCodeBlock = null;
71
70
  let domLastParagraphBlock = null;
72
- let htmlBuffer = "";
71
+ let domStreamMsg = null;
73
72
  let tips = """
74
73
  _HTML_P3 = """;
75
74
  let tips_hidden = false;
@@ -78,70 +77,77 @@ class Body:
78
77
  let highlightScheduled = false;
79
78
  let pendingHighlightRoot = null;
80
79
  let pendingHighlightMath = false;
81
- let highlightRAF = 0; // RAF id for highlight batcher
80
+ let highlightRAF = 0;
82
81
  let scrollScheduled = false;
83
82
 
84
- // Auto-follow state: when false, live stream auto-scroll is suppressed
85
83
  let autoFollow = true;
86
84
  let lastScrollTop = 0;
87
- // Tracks whether user has performed any scroll-related interaction
88
85
  let userInteracted = false;
89
- const AUTO_FOLLOW_REENABLE_PX = 8; // px from bottom to re-enable auto-follow
86
+ const AUTO_FOLLOW_REENABLE_PX = 8;
90
87
 
91
- // FAB thresholds
92
- const SHOW_DOWN_THRESHOLD_PX = 0; // show "down" only when farther than this from bottom
93
- let currentFabAction = 'none'; // tracks current FAB state to avoid redundant work
88
+ const SHOW_DOWN_THRESHOLD_PX = 0;
89
+ let currentFabAction = 'none';
94
90
 
95
- // timers
96
91
  let tipsTimers = [];
97
92
 
98
- // observers
99
- let roDoc = null;
100
- let roContainer = null;
101
-
102
- // FAB (scroll-to-top/bottom) scheduling
103
93
  let scrollFabUpdateScheduled = false;
104
94
 
105
- // Streaming micro-batching config
106
- const STREAM_MAX_PER_FRAME = 64; // defensive upper bound of operations per frame
107
- const STREAM_EMERGENCY_COALESCE_LEN = 1500; // when queue length is high, coalesce aggressively
95
+ const STREAM_MAX_PER_FRAME = 8;
96
+ const STREAM_EMERGENCY_COALESCE_LEN = 1500;
97
+
98
+ Object.defineProperty(window,'SE',{get(){return document.scrollingElement || document.documentElement;}});
99
+
100
+ let wheelHandler = null;
101
+ let scrollHandler = null;
102
+ let resizeHandler = null;
103
+ let fabClickHandler = null;
104
+ let containerMouseOverHandler = null;
105
+ let containerMouseOutHandler = null;
106
+ let containerClickHandler = null;
107
+ let keydownHandler = null;
108
+ let docClickFocusHandler = null;
109
+
110
+ let fabFreezeUntil = 0;
111
+ const FAB_TOGGLE_DEBOUNCE_MS = 100;
108
112
 
109
- // clear previous references
110
113
  function resetEphemeralDomRefs() {
111
114
  domLastCodeBlock = null;
112
115
  domLastParagraphBlock = null;
116
+ domStreamMsg = null;
113
117
  }
114
118
  function dropIfDetached() {
115
119
  if (domLastCodeBlock && !domLastCodeBlock.isConnected) domLastCodeBlock = null;
116
120
  if (domLastParagraphBlock && !domLastParagraphBlock.isConnected) domLastParagraphBlock = null;
121
+ if (domStreamMsg && !domStreamMsg.isConnected) domStreamMsg = null;
117
122
  }
118
123
  function stopTipsTimers() {
119
124
  tipsTimers.forEach(clearTimeout);
120
125
  tipsTimers = [];
121
126
  }
122
127
 
123
- function teardown() {
124
- // Cancel timers/RAF/observers to prevent background CPU usage
128
+ function cleanup() {
129
+ try { if (highlightRAF) { cancelAnimationFrame(highlightRAF); highlightRAF = 0; } } catch (e) {}
130
+ try { if (streamRAF) { cancelAnimationFrame(streamRAF); streamRAF = 0; } } catch (e) {}
125
131
  stopTipsTimers();
126
- try {
127
- if (streamRAF) {
128
- cancelAnimationFrame(streamRAF);
129
- streamRAF = 0;
130
- }
131
- if (highlightRAF) {
132
- cancelAnimationFrame(highlightRAF);
133
- highlightRAF = 0;
134
- }
135
- } catch (e) { /* ignore */ }
136
- // Clear streaming queue to release memory immediately
132
+ try { bridgeDisconnect(); } catch (e) {}
133
+ if (wheelHandler) document.removeEventListener('wheel', wheelHandler, { passive: true });
134
+ if (scrollHandler) window.removeEventListener('scroll', scrollHandler, { passive: true });
135
+ if (resizeHandler) window.removeEventListener('resize', resizeHandler, { passive: true });
136
+ if (fabClickHandler && els.scrollFab) els.scrollFab.removeEventListener('click', fabClickHandler, { passive: false });
137
+ if (containerMouseOverHandler && els.container) els.container.removeEventListener('mouseover', containerMouseOverHandler, { passive: true });
138
+ if (containerMouseOutHandler && els.container) els.container.removeEventListener('mouseout', containerMouseOutHandler, { passive: true });
139
+ if (containerClickHandler && els.container) els.container.removeEventListener('click', containerClickHandler, { passive: false });
140
+ if (keydownHandler) document.removeEventListener('keydown', keydownHandler, { passive: false });
141
+ if (docClickFocusHandler) document.removeEventListener('click', docClickFocusHandler, { passive: true });
137
142
  streamQ.length = 0;
138
- scrollFabUpdateScheduled = false;
139
- scrollScheduled = false;
140
- highlightScheduled = false;
143
+ collapsed_idx.length = 0;
144
+ resetEphemeralDomRefs();
145
+ els = {};
146
+ try { history.scrollRestoration = "auto"; } catch (e) {}
141
147
  }
142
148
 
143
149
  history.scrollRestoration = "manual";
144
- document.addEventListener('keydown', function(event) {
150
+ keydownHandler = function(event) {
145
151
  if (event.ctrlKey && event.key === 'f') {
146
152
  window.location.href = 'bridge://open_find:' + pid;
147
153
  event.preventDefault();
@@ -150,12 +156,17 @@ class Body:
150
156
  window.location.href = 'bridge://escape';
151
157
  event.preventDefault();
152
158
  }
153
- });
154
- document.addEventListener('click', function(event) {
159
+ };
160
+ document.addEventListener('keydown', keydownHandler, { passive: false });
161
+
162
+ docClickFocusHandler = function(event) {
163
+ if (event.target.closest('#scrollFab')) return;
155
164
  if (event.target.tagName !== 'A' && !event.target.closest('a')) {
156
165
  window.location.href = 'bridge://focus';
157
166
  }
158
- });
167
+ };
168
+ document.addEventListener('click', docClickFocusHandler, { passive: true });
169
+
159
170
  function log(text) {
160
171
  if (bridge) {
161
172
  bridge.log(text);
@@ -171,16 +182,13 @@ class Body:
171
182
  els.footer = document.getElementById('_footer_');
172
183
  els.loader = document.getElementById('_loader_');
173
184
  els.tips = document.getElementById('tips');
174
- // FAB refs
175
185
  els.scrollFab = document.getElementById('scrollFab');
176
186
  els.scrollFabIcon = document.getElementById('scrollFabIcon');
177
187
  }
188
+
178
189
  function bridgeConnect() {
179
- // Idempotent connect
180
- if (!bridge || !bridge.chunk || typeof bridge.chunk.connect !== 'function') return false;
190
+ if (!bridge) return false;
181
191
  if (bridgeConnected) return true;
182
-
183
- // Ensure handler exists and is stable (same identity for disconnect/connect)
184
192
  if (!streamHandler) {
185
193
  streamHandler = (name, html, chunk, replace, isCode) => {
186
194
  appendStream(name, html, chunk, replace, isCode);
@@ -193,9 +201,9 @@ class Body:
193
201
  };
194
202
  }
195
203
  try {
196
- bridge.chunk.connect(streamHandler);
197
- bridge.node.connect(nodeHandler);
198
- bridge.nodeReplace.connect(nodeReplaceHandler);
204
+ if (bridge.chunk && typeof bridge.chunk.connect === 'function') bridge.chunk.connect(streamHandler);
205
+ if (bridge.node && typeof bridge.node.connect === 'function') bridge.node.connect(nodeHandler);
206
+ if (bridge.nodeReplace && typeof bridge.nodeReplace.connect === 'function') bridge.nodeReplace.connect(nodeReplaceHandler);
199
207
  bridgeConnected = true;
200
208
  return true;
201
209
  } catch (e) {
@@ -203,29 +211,24 @@ class Body:
203
211
  return false;
204
212
  }
205
213
  }
206
-
207
214
  function bridgeDisconnect() {
208
- // Idempotent disconnect
209
- if (!bridge || !bridge.chunk || typeof bridge.chunk.disconnect !== 'function') return false;
215
+ if (!bridge) return false;
210
216
  if (!bridgeConnected) return true;
211
-
212
217
  try {
213
- bridge.chunk.disconnect(streamHandler);
214
- bridge.node.disconnect(nodeHandler);
215
- bridge.nodeReplace.disconnect(nodeReplaceHandler);
216
- } catch (e) { /* ignore */ }
218
+ if (bridge.chunk && typeof bridge.chunk.disconnect === 'function') bridge.chunk.disconnect(streamHandler);
219
+ if (bridge.node && typeof bridge.node.disconnect === 'function') bridge.node.disconnect(nodeHandler);
220
+ if (bridge.nodeReplace && typeof bridge.nodeReplace.disconnect === 'function') bridge.nodeReplace.disconnect(nodeReplaceHandler);
221
+ } catch (e) { }
217
222
  bridgeConnected = false;
218
-
219
- // Stop scheduled work and release pending chunks immediately
220
- try { if (streamRAF) { cancelAnimationFrame(streamRAF); streamRAF = 0; } } catch (e) { /* ignore */ }
223
+ try { if (streamRAF) { cancelAnimationFrame(streamRAF); streamRAF = 0; } } catch (e) { }
221
224
  streamQ.length = 0;
222
225
  return true;
223
226
  }
224
-
225
227
  function bridgeReconnect() {
226
228
  bridgeDisconnect();
227
229
  return bridgeConnect();
228
230
  }
231
+
229
232
  function scheduleHighlight(root, withMath = true) {
230
233
  const scope = root && root.nodeType === 1 ? root : document;
231
234
  if (!pendingHighlightRoot || pendingHighlightRoot === document) {
@@ -237,7 +240,6 @@ class Body:
237
240
  if (highlightScheduled) return;
238
241
  highlightScheduled = true;
239
242
  if (highlightRAF) {
240
- // Ensure we do not queue multiple highlight frames
241
243
  cancelAnimationFrame(highlightRAF);
242
244
  highlightRAF = 0;
243
245
  }
@@ -258,14 +260,10 @@ class Body:
258
260
  });
259
261
  if (withMath) {
260
262
  renderMath(root);
261
- if (DEBUG_MODE) log("math");
262
263
  }
263
- if (DEBUG_MODE) log("execute highlight");
264
264
  }
265
265
  function highlightCode(withMath = true, root = null) {
266
- if (DEBUG_MODE) log("queue highlight, withMath: " + withMath);
267
- highlightCodeInternal(root || document, withMath); // prevent blink on fast updates
268
- // scheduleHighlight(root || document, withMath); // disabled
266
+ highlightCodeInternal(root || document, withMath);
269
267
  }
270
268
  function hideTips() {
271
269
  if (tips_hidden) return;
@@ -304,7 +302,6 @@ class Body:
304
302
  showNextTip();
305
303
  }
306
304
  function renderMath(root) {
307
- if (DEBUG_MODE) log("execute math");
308
305
  const scope = root || document;
309
306
  const scripts = scope.querySelectorAll('script[type^="math/tex"]');
310
307
  scripts.forEach(function(script) {
@@ -323,53 +320,46 @@ class Body:
323
320
  if (parent) parent.replaceChild(element, script);
324
321
  });
325
322
  }
323
+
326
324
  function isNearBottom(marginPx = 100) {
327
- const el = document.scrollingElement || document.documentElement;
325
+ const el = SE;
328
326
  const distanceToBottom = el.scrollHeight - el.clientHeight - el.scrollTop;
329
327
  return distanceToBottom <= marginPx;
330
328
  }
331
329
  function scheduleScroll(live = false) {
332
- // Skip scheduling live auto-scroll when user disabled follow
333
330
  if (live === true && autoFollow !== true) return;
334
331
  if (scrollScheduled) return;
335
332
  scrollScheduled = true;
336
333
  requestAnimationFrame(function() {
337
334
  scrollScheduled = false;
338
335
  scrollToBottom(live);
339
- // keep FAB state in sync after any programmatic scroll
340
336
  scheduleScrollFabUpdate();
341
337
  });
342
338
  }
343
- // Force immediate scroll to bottom (pre-interaction bootstrap)
344
339
  function forceScrollToBottomImmediate() {
345
- const el = document.scrollingElement || document.documentElement;
346
- el.scrollTop = el.scrollHeight; // no behavior, no RAF, deterministic
340
+ const el = SE;
341
+ el.scrollTop = el.scrollHeight;
347
342
  prevScroll = el.scrollHeight;
348
343
  }
349
344
  function scrollToBottom(live = false, force = false) {
350
- const el = document.scrollingElement || document.documentElement;
345
+ const el = SE;
351
346
  const marginPx = 450;
352
347
  const behavior = (live === true) ? 'instant' : 'smooth';
353
-
354
- // Respect user-follow state during live updates
355
348
  if (live === true && autoFollow !== true) {
356
- // Keep prevScroll consistent for potential consumers
357
349
  prevScroll = el.scrollHeight;
358
350
  return;
359
351
  }
360
-
361
- // Allow initial auto-follow before any user interaction
362
352
  if ((live === true && userInteracted === false) || isNearBottom(marginPx) || live == false || force) {
363
353
  el.scrollTo({ top: el.scrollHeight, behavior });
364
354
  }
365
355
  prevScroll = el.scrollHeight;
366
356
  }
357
+
367
358
  function appendToInput(content) {
368
359
  userInteracted = false;
369
360
  const element = els.appendInput || document.getElementById('_append_input_');
370
361
  if (element) {
371
362
  element.insertAdjacentHTML('beforeend', content);
372
- highlightCode(true, element);
373
363
  scheduleScroll();
374
364
  }
375
365
  }
@@ -383,28 +373,49 @@ class Body:
383
373
  }
384
374
  return element;
385
375
  }
376
+ function getStreamMsg(create = true, name_header = '') {
377
+ const container = getStreamContainer();
378
+ if (!container) return null;
379
+ if (domStreamMsg && domStreamMsg.isConnected) return domStreamMsg;
380
+ let box = container.querySelector('.msg-box');
381
+ let msg = null;
382
+ if (!box && create) {
383
+ box = document.createElement('div');
384
+ box.classList.add('msg-box');
385
+ box.classList.add('msg-bot');
386
+ if (name_header != '') {
387
+ const name = document.createElement('div');
388
+ name.classList.add('name-header');
389
+ name.classList.add('name-bot');
390
+ name.innerHTML = name_header;
391
+ box.appendChild(name);
392
+ }
393
+ msg = document.createElement('div');
394
+ msg.classList.add('msg');
395
+ box.appendChild(msg);
396
+ container.appendChild(box);
397
+ } else if (box) {
398
+ msg = box.querySelector('.msg');
399
+ }
400
+ if (msg) domStreamMsg = msg;
401
+ return msg;
402
+ }
386
403
  function appendNode(content) {
387
404
  userInteracted = false;
388
- if (DEBUG_MODE) {
389
- log("APPEND NODE: {" + content + "}");
390
- }
391
405
  clearStreamBefore();
392
406
  prevScroll = 0;
393
407
  const element = els.nodes || document.getElementById('_nodes_');
394
408
  if (element) {
395
409
  element.classList.remove('empty_list');
396
410
  element.insertAdjacentHTML('beforeend', content);
397
- highlightCode(true, element);
398
- scrollToBottom(false); // without schedule
411
+ scheduleHighlight(element, true);
412
+ scrollToBottom(false);
399
413
  scheduleScrollFabUpdate();
400
414
  }
401
415
  clearHighlightCache();
402
416
  }
403
417
  function replaceNodes(content) {
404
418
  userInteracted = false;
405
- if (DEBUG_MODE) {
406
- log("REPLACE NODES: {" + content + "}");
407
- }
408
419
  clearStreamBefore();
409
420
  prevScroll = 0;
410
421
  const element = els.nodes || document.getElementById('_nodes_');
@@ -412,16 +423,13 @@ class Body:
412
423
  element.classList.remove('empty_list');
413
424
  element.replaceChildren();
414
425
  element.insertAdjacentHTML('beforeend', content);
415
- highlightCode(true, element);
416
- scrollToBottom(false, true); // without schedule
426
+ scheduleHighlight(element, true);
427
+ scrollToBottom(false, true);
417
428
  scheduleScrollFabUpdate();
418
429
  }
419
430
  clearHighlightCache();
420
431
  }
421
432
  function clean() {
422
- if (DEBUG_MODE) {
423
- log("-- CLEAN DOM --");
424
- }
425
433
  userInteracted = false;
426
434
  const el = els.nodes || document.getElementById('_nodes_');
427
435
  if (el) {
@@ -429,15 +437,6 @@ class Body:
429
437
  }
430
438
  resetEphemeralDomRefs();
431
439
  els = {};
432
- /*
433
- try {
434
- if (window.gc) {
435
- window.gc();
436
- }
437
- } catch (e) {
438
- // gc not available
439
- }
440
- */
441
440
  }
442
441
  function clearHighlightCache() {
443
442
  //
@@ -450,7 +449,7 @@ class Body:
450
449
  const extra = element.querySelector('.msg-extra');
451
450
  if (extra) {
452
451
  extra.insertAdjacentHTML('beforeend', content);
453
- highlightCode(true, extra);
452
+ scheduleHighlight(extra, true);
454
453
  scheduleScroll();
455
454
  }
456
455
  }
@@ -484,58 +483,33 @@ class Body:
484
483
  }
485
484
  });
486
485
  resetEphemeralDomRefs();
487
- highlightCode(true, container);
486
+ scheduleHighlight(container, true);
488
487
  scheduleScroll();
489
488
  }
490
489
  }
491
490
  function clearStream() {
492
491
  hideTips();
493
- if (DEBUG_MODE) {
494
- log("STREAM CLEAR");
495
- }
496
492
  domLastParagraphBlock = null;
497
493
  domLastCodeBlock = null;
494
+ domStreamMsg = null;
498
495
  domOutputStream = null;
499
- const element = getStreamContainer();
500
- if (element) {
501
- let box = element.querySelector('.msg-box');
502
- let msg;
503
- if (!box) {
504
- box = document.createElement('div');
505
- box.classList.add('msg-box');
506
- box.classList.add('msg-bot');
507
- msg = document.createElement('div');
508
- msg.classList.add('msg');
509
- box.appendChild(msg);
510
- element.appendChild(box);
511
- } else {
512
- msg = box.querySelector('.msg');
513
- }
514
- if (msg) {
515
- msg.replaceChildren();
516
- }
517
- }
496
+ el = getStreamContainer();
497
+ if (!el) return;
498
+ el.replaceChildren();
518
499
  }
519
500
  function beginStream() {
520
501
  hideTips();
521
- if (DEBUG_MODE) {
522
- log("STREAM BEGIN");
523
- }
524
502
  userInteracted = false;
525
503
  clearOutput();
526
- // Ensure initial auto-follow baseline before any chunks overflow
527
504
  forceScrollToBottomImmediate();
528
505
  scheduleScroll();
529
506
  }
530
507
  function endStream() {
531
- if (DEBUG_MODE) {
532
- log("STREAM END");
533
- }
534
508
  clearOutput();
535
509
  bridgeReconnect();
536
510
  }
511
+
537
512
  function enqueueStream(name_header, content, chunk, replace = false, is_code_block = false) {
538
- // Push incoming chunk; scheduling is done with RAF to batch DOM ops
539
513
  streamQ.push({name_header, content, chunk, replace, is_code_block});
540
514
  if (!streamRAF) {
541
515
  streamRAF = requestAnimationFrame(drainStream);
@@ -544,139 +518,112 @@ class Body:
544
518
  function drainStream() {
545
519
  streamRAF = 0;
546
520
  let processed = 0;
547
-
548
- // Emergency coalescing if queue grows too large
549
521
  const shouldAggressiveCoalesce = streamQ.length >= STREAM_EMERGENCY_COALESCE_LEN;
550
-
522
+
523
+ batching = true; // start partii
551
524
  while (streamQ.length && processed < STREAM_MAX_PER_FRAME) {
552
525
  let {name_header, content, chunk, replace, is_code_block} = streamQ.shift();
553
-
554
- // Coalesce contiguous simple appends to reduce DOM churn
555
526
  if (!replace && !content && (chunk && chunk.length > 0)) {
556
- // Collect chunks into an array to avoid O(n^2) string concatenation
557
527
  const chunks = [chunk];
558
528
  while (streamQ.length) {
559
529
  const next = streamQ[0];
560
530
  if (!next.replace && !next.content && next.is_code_block === is_code_block && next.name_header === name_header) {
561
531
  chunks.push(next.chunk);
562
532
  streamQ.shift();
563
- if (!shouldAggressiveCoalesce) {
564
- // Light coalescing per frame is enough under normal conditions
565
- break;
566
- }
567
- } else {
568
- break;
569
- }
533
+ if (!shouldAggressiveCoalesce) break;
534
+ } else break;
570
535
  }
571
536
  chunk = chunks.join('');
572
537
  }
573
-
574
538
  applyStream(name_header, content, chunk, replace, is_code_block);
575
539
  processed++;
576
540
  }
577
-
578
- // If there are remaining items re-schedule next frame
541
+ batching = false; // koniec partii
542
+
543
+ // jedno przewinięcie na partię
544
+ if (needScroll) {
545
+ if (userInteracted === false) forceScrollToBottomImmediate();
546
+ else scheduleScroll(true);
547
+ needScroll = false;
548
+ }
549
+
579
550
  if (streamQ.length) {
580
551
  streamRAF = requestAnimationFrame(drainStream);
581
552
  }
582
553
  }
583
- // Public API: enqueue and process in the next animation frame
584
554
  function appendStream(name_header, content, chunk, replace = false, is_code_block = false) {
585
555
  enqueueStream(name_header, content, chunk, replace, is_code_block);
586
556
  }
587
- // Internal: performs actual DOM updates for a single merged chunk
588
557
  function applyStream(name_header, content, chunk, replace = false, is_code_block = false) {
589
- dropIfDetached(); // clear references to detached elements
558
+ dropIfDetached();
590
559
  hideTips();
591
- if (DEBUG_MODE) {
592
- log("APPLY CHUNK: {" + chunk + "}, CONTENT: {"+content+"}, replace: " + replace + ", is_code_block: " + is_code_block);
593
- }
594
- const element = getStreamContainer();
595
- let msg;
596
- if (element) {
597
- let box = element.querySelector('.msg-box');
598
- if (!box) {
599
- box = document.createElement('div');
600
- box.classList.add('msg-box');
601
- box.classList.add('msg-bot');
602
- if (name_header != '') {
603
- const name = document.createElement('div');
604
- name.classList.add('name-header');
605
- name.classList.add('name-bot');
606
- name.innerHTML = name_header;
607
- box.appendChild(name);
560
+ const msg = getStreamMsg(true, name_header);
561
+ if (msg) {
562
+ if (replace) {
563
+ domLastCodeBlock = null;
564
+ domLastParagraphBlock = null;
565
+ msg.replaceChildren();
566
+ if (content) {
567
+ msg.insertAdjacentHTML('afterbegin', content);
568
+ }
569
+ let doMath = true;
570
+ if (is_code_block) {
571
+ doMath = false;
608
572
  }
609
- msg = document.createElement('div');
610
- msg.classList.add('msg');
611
- box.appendChild(msg);
612
- element.appendChild(box);
573
+ highlightCode(doMath, msg);
613
574
  } else {
614
- msg = box.querySelector('.msg');
615
- }
616
- if (msg) {
617
- if (replace) {
618
- domLastCodeBlock = null;
619
- domLastParagraphBlock = null;
620
- msg.replaceChildren();
621
- if (content) {
622
- msg.insertAdjacentHTML('afterbegin', content);
623
- }
624
- let doMath = true;
625
- if (is_code_block) {
626
- doMath = false;
627
- }
628
- highlightCode(doMath, msg);
629
- } else {
630
- if (is_code_block) {
631
- // Try to reuse cached last code block; fallback to cheap lastElementChild check
632
- let lastCodeBlock = domLastCodeBlock;
633
- if (!lastCodeBlock || !msg.contains(lastCodeBlock)) {
634
- const last = msg.lastElementChild;
635
- if (last && last.tagName === 'PRE') {
636
- const codeEl = last.querySelector('code');
637
- if (codeEl) {
638
- lastCodeBlock = codeEl;
639
- }
640
- } else {
641
- // Fallback scan only when necessary
642
- const codes = msg.querySelectorAll('pre code');
643
- if (codes.length > 0) {
644
- lastCodeBlock = codes[codes.length - 1];
645
- }
575
+ if (is_code_block) {
576
+ let lastCodeBlock = domLastCodeBlock;
577
+ if (!lastCodeBlock || !msg.contains(lastCodeBlock)) {
578
+ const last = msg.lastElementChild;
579
+ if (last && last.tagName === 'PRE') {
580
+ const codeEl = last.querySelector('code');
581
+ if (codeEl) {
582
+ lastCodeBlock = codeEl;
646
583
  }
647
- }
648
- if (lastCodeBlock) {
649
- lastCodeBlock.insertAdjacentHTML('beforeend', chunk);
650
- domLastCodeBlock = lastCodeBlock;
651
584
  } else {
652
- msg.insertAdjacentHTML('beforeend', chunk);
653
- domLastCodeBlock = null;
585
+ const codes = msg.querySelectorAll('pre code');
586
+ if (codes.length > 0) {
587
+ lastCodeBlock = codes[codes.length - 1];
588
+ }
654
589
  }
590
+ }
591
+ if (lastCodeBlock) {
592
+ lastCodeBlock.insertAdjacentHTML('beforeend', chunk);
593
+ domLastCodeBlock = lastCodeBlock;
655
594
  } else {
595
+ msg.insertAdjacentHTML('beforeend', chunk);
656
596
  domLastCodeBlock = null;
657
- let p = (domLastParagraphBlock && msg.contains(domLastParagraphBlock))
658
- ? domLastParagraphBlock
659
- : (msg.lastElementChild && msg.lastElementChild.tagName === 'P'
660
- ? msg.lastElementChild
661
- : null);
662
- if (p) {
663
- p.insertAdjacentHTML('beforeend', chunk);
664
- domLastParagraphBlock = p;
665
- } else {
666
- msg.insertAdjacentHTML('beforeend', chunk);
667
- const last = msg.lastElementChild;
668
- domLastParagraphBlock = (last && last.tagName === 'P') ? last : null;
669
- }
597
+ }
598
+ } else {
599
+ domLastCodeBlock = null;
600
+ let p = (domLastParagraphBlock && msg.contains(domLastParagraphBlock))
601
+ ? domLastParagraphBlock
602
+ : (msg.lastElementChild && msg.lastElementChild.tagName === 'P'
603
+ ? msg.lastElementChild
604
+ : null);
605
+ if (p) {
606
+ p.insertAdjacentHTML('beforeend', chunk);
607
+ domLastParagraphBlock = p;
608
+ } else {
609
+ msg.insertAdjacentHTML('beforeend', chunk);
610
+ const last = msg.lastElementChild;
611
+ domLastParagraphBlock = (last && last.tagName === 'P') ? last : null;
670
612
  }
671
613
  }
672
614
  }
673
615
  }
674
- // Initial auto-follow until first user interaction
675
- if (userInteracted === false) {
676
- forceScrollToBottomImmediate();
677
- } else {
678
- scheduleScroll(true);
679
- }
616
+ if (batching) {
617
+ needScroll = true;
618
+ } else {
619
+ if (userInteracted === false) {
620
+ forceScrollToBottomImmediate();
621
+ } else {
622
+ scheduleScroll(true);
623
+ }
624
+ }
625
+ fabFreezeUntil = performance.now() + FAB_TOGGLE_DEBOUNCE_MS;
626
+ scheduleScrollFabUpdate();
680
627
  }
681
628
  function nextStream() {
682
629
  hideTips();
@@ -690,6 +637,7 @@ class Body:
690
637
  elementBefore.appendChild(frag);
691
638
  domLastCodeBlock = null;
692
639
  domLastParagraphBlock = null;
640
+ domStreamMsg = null;
693
641
  scheduleScroll();
694
642
  }
695
643
  }
@@ -798,7 +746,7 @@ class Body:
798
746
  element.classList.add('visible');
799
747
  }
800
748
  element.innerHTML = content;
801
- highlightCode(true, element);
749
+ scheduleHighlight(element, true);
802
750
  scheduleScroll();
803
751
  }
804
752
  }
@@ -828,7 +776,8 @@ class Body:
828
776
  clearStreamBefore();
829
777
  domLastCodeBlock = null;
830
778
  domLastParagraphBlock = null;
831
- domOutputStream = null; // release handle to allow GC on old container subtree
779
+ domStreamMsg = null;
780
+ domOutputStream = null;
832
781
  const element = els.appendOutput || document.getElementById('_append_output_');
833
782
  if (element) {
834
783
  element.replaceChildren();
@@ -906,7 +855,7 @@ class Body:
906
855
  const source = wrapper.querySelector('code');
907
856
  if (source && collapsed_idx.includes(index)) {
908
857
  source.style.display = 'none';
909
- const collapseBtn = wrapper.querySelector('code-header-collapse');
858
+ const collapseBtn = wrapper.querySelector('.code-header-collapse');
910
859
  if (collapseBtn) {
911
860
  const collapseSpan = collapseBtn.querySelector('span');
912
861
  if (collapseSpan) {
@@ -974,116 +923,112 @@ class Body:
974
923
  }
975
924
  }
976
925
 
977
- // ---------- scroll bottom ----------
978
926
  function hasVerticalScroll() {
979
- const el = document.scrollingElement || document.documentElement;
927
+ const el = SE;
980
928
  return (el.scrollHeight - el.clientHeight) > 1;
981
929
  }
982
930
  function distanceToBottomPx() {
983
- const el = document.scrollingElement || document.documentElement;
931
+ const el = SE;
984
932
  return el.scrollHeight - el.clientHeight - el.scrollTop;
985
933
  }
986
934
  function isAtBottom(thresholdPx = 2) {
987
935
  return distanceToBottomPx() <= thresholdPx;
988
936
  }
989
- function isLoaderHidden() {
990
- const el = els.loader || document.getElementById('_loader_');
991
- // If loader element is missing treat as hidden to avoid blocking FAB unnecessarily
992
- return !el || el.classList.contains('hidden');
937
+
938
+ function computeFabAction() {
939
+ const el = SE;
940
+ const hasScroll = (el.scrollHeight - el.clientHeight) > 1;
941
+ if (!hasScroll) return 'none';
942
+ const dist = el.scrollHeight - el.clientHeight - el.scrollTop;
943
+ if (dist <= 2) return 'up';
944
+ if (dist >= SHOW_DOWN_THRESHOLD_PX) return 'down';
945
+ return 'none';
993
946
  }
994
- function updateScrollFab() {
947
+ function updateScrollFab(force = false, actionOverride = null, bypassFreeze = false) {
995
948
  const btn = els.scrollFab || document.getElementById('scrollFab');
996
949
  const icon = els.scrollFabIcon || document.getElementById('scrollFabIcon');
997
950
  if (!btn || !icon) return;
998
951
 
999
- const hasScroll = hasVerticalScroll();
1000
- if (!hasScroll) {
1001
- btn.classList.remove('visible');
1002
- currentFabAction = 'none';
952
+ const action = actionOverride || computeFabAction();
953
+ if (!force && !bypassFreeze && performance.now() < fabFreezeUntil && action !== currentFabAction) {
1003
954
  return;
1004
955
  }
1005
-
1006
- const atBottom = isAtBottom();
1007
- const dist = distanceToBottomPx();
1008
- const loaderHidden = isLoaderHidden();
1009
-
1010
- // Determine desired action and visibility based on requirements:
1011
- // - Show "down" only when at least SHOW_DOWN_THRESHOLD_PX away from bottom.
1012
- // - Show "up" only when loader-global has the 'hidden' class.
1013
- // - Otherwise hide the FAB to prevent overlap and noise.
1014
- let action = 'none'; // 'up' | 'down' | 'none'
1015
- if (atBottom) {
1016
- if (loaderHidden) {
1017
- action = 'up';
1018
- } else {
1019
- action = 'none';
1020
- }
1021
- } else {
1022
- if (dist >= SHOW_DOWN_THRESHOLD_PX) {
1023
- action = 'down';
1024
- } else {
1025
- action = 'none';
1026
- }
1027
- }
1028
-
1029
956
  if (action === 'none') {
1030
- btn.classList.remove('visible');
1031
- currentFabAction = 'none';
957
+ if (currentFabAction !== 'none' || force) {
958
+ btn.classList.remove('visible');
959
+ currentFabAction = 'none';
960
+ }
1032
961
  return;
1033
962
  }
1034
-
1035
- // Update icon and semantics only if changed to avoid redundant 'load' events
1036
- if (action !== currentFabAction) {
963
+ if (action !== currentFabAction || force) {
1037
964
  if (action === 'up') {
1038
- if (icon.src !== ICON_COLLAPSE) icon.src = ICON_COLLAPSE;
965
+ if (icon.dataset.dir !== 'up') {
966
+ icon.src = ICON_COLLAPSE;
967
+ icon.dataset.dir = 'up';
968
+ }
1039
969
  btn.title = "Go to top";
1040
970
  } else {
1041
- if (icon.src !== ICON_EXPAND) icon.src = ICON_EXPAND;
971
+ if (icon.dataset.dir !== 'down') {
972
+ icon.src = ICON_EXPAND;
973
+ icon.dataset.dir = 'down';
974
+ }
1042
975
  btn.title = "Go to bottom";
1043
976
  }
1044
977
  btn.setAttribute('aria-label', btn.title);
1045
978
  currentFabAction = action;
979
+ btn.classList.add('visible');
980
+ } else if (!btn.classList.contains('visible')) {
981
+ btn.classList.add('visible');
1046
982
  }
1047
-
1048
- // Finally show
1049
- btn.classList.add('visible');
1050
983
  }
1051
984
  function scheduleScrollFabUpdate() {
1052
985
  if (scrollFabUpdateScheduled) return;
1053
986
  scrollFabUpdateScheduled = true;
1054
987
  requestAnimationFrame(function() {
1055
988
  scrollFabUpdateScheduled = false;
1056
- updateScrollFab();
989
+ const action = computeFabAction();
990
+ if (action !== currentFabAction) {
991
+ updateScrollFab(false, action);
992
+ }
1057
993
  });
1058
994
  }
995
+
996
+ function maybeEnableAutoFollowByProximity() {
997
+ const el = SE;
998
+ if (!autoFollow) {
999
+ const distanceToBottom = el.scrollHeight - el.clientHeight - el.scrollTop;
1000
+ if (distanceToBottom <= AUTO_FOLLOW_REENABLE_PX) {
1001
+ autoFollow = true;
1002
+ }
1003
+ }
1004
+ }
1059
1005
  function scrollToTopUser() {
1060
- // Explicit user-driven scroll disables auto-follow
1061
1006
  userInteracted = true;
1062
1007
  autoFollow = false;
1063
1008
  try {
1064
- const el = document.scrollingElement || document.documentElement;
1009
+ const el = SE;
1065
1010
  el.scrollTo({ top: 0, behavior: 'smooth' });
1011
+ lastScrollTop = el.scrollTop;
1066
1012
  } catch (e) {
1067
- // Fallback in environments without smooth scrolling support
1068
- const el = document.scrollingElement || document.documentElement;
1013
+ const el = SE;
1069
1014
  el.scrollTop = 0;
1015
+ lastScrollTop = 0;
1070
1016
  }
1071
- scheduleScrollFabUpdate();
1072
1017
  }
1073
1018
  function scrollToBottomUser() {
1074
- // User action to go to bottom re-enables auto-follow
1075
1019
  userInteracted = true;
1076
- autoFollow = true;
1020
+ autoFollow = false;
1077
1021
  try {
1078
- const el = document.scrollingElement || document.documentElement;
1022
+ const el = SE;
1079
1023
  el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
1024
+ lastScrollTop = el.scrollTop;
1080
1025
  } catch (e) {
1081
- const el = document.scrollingElement || document.documentElement;
1026
+ const el = SE;
1082
1027
  el.scrollTop = el.scrollHeight;
1028
+ lastScrollTop = el.scrollTop;
1083
1029
  }
1084
- scheduleScrollFabUpdate();
1030
+ maybeEnableAutoFollowByProximity();
1085
1031
  }
1086
- // ---------- end of scroll bottom ----------
1087
1032
 
1088
1033
  document.addEventListener('DOMContentLoaded', function() {
1089
1034
  new QWebChannel(qt.webChannelTransport, function (channel) {
@@ -1093,6 +1038,7 @@ class Body:
1093
1038
  });
1094
1039
  initDomRefs();
1095
1040
  const container = els.container;
1041
+
1096
1042
  function addClassToMsg(id, className) {
1097
1043
  const msgElement = document.getElementById('msg-bot-' + id);
1098
1044
  if (msgElement) {
@@ -1105,61 +1051,71 @@ class Body:
1105
1051
  msgElement.classList.remove(className);
1106
1052
  }
1107
1053
  }
1108
- container.addEventListener('mouseover', function(event) {
1054
+ containerMouseOverHandler = function(event) {
1109
1055
  if (event.target.classList.contains('action-img')) {
1110
1056
  const id = event.target.getAttribute('data-id');
1111
1057
  addClassToMsg(id, 'msg-highlight');
1112
1058
  }
1113
- });
1114
- container.addEventListener('mouseout', function(event) {
1059
+ };
1060
+ containerMouseOutHandler = function(event) {
1115
1061
  if (event.target.classList.contains('action-img')) {
1116
1062
  const id = event.target.getAttribute('data-id');
1117
1063
  removeClassFromMsg(id, 'msg-highlight');
1118
1064
  }
1119
- });
1120
- // Wheel up disables auto-follow immediately (works even at absolute bottom)
1121
- document.addEventListener('wheel', function(ev) {
1065
+ };
1066
+ container.addEventListener('mouseover', containerMouseOverHandler, { passive: true });
1067
+ container.addEventListener('mouseout', containerMouseOutHandler, { passive: true });
1068
+
1069
+ wheelHandler = function(ev) {
1122
1070
  userInteracted = true;
1123
1071
  if (ev.deltaY < 0) {
1124
1072
  autoFollow = false;
1073
+ } else {
1074
+ maybeEnableAutoFollowByProximity();
1125
1075
  }
1126
- }, { passive: true });
1076
+ };
1077
+ document.addEventListener('wheel', wheelHandler, { passive: true });
1127
1078
 
1128
- // Track scroll direction and restore auto-follow when user returns to bottom
1129
- window.addEventListener('scroll', function() {
1130
- const el = document.scrollingElement || document.documentElement;
1079
+ scrollHandler = function() {
1080
+ const el = SE;
1131
1081
  const top = el.scrollTop;
1132
-
1133
- // User scrolled up (ignore tiny jitter)
1134
1082
  if (top + 1 < lastScrollTop) {
1135
1083
  autoFollow = false;
1136
- } else if (!autoFollow) {
1137
- const distanceToBottom = el.scrollHeight - el.clientHeight - top;
1138
- if (distanceToBottom <= AUTO_FOLLOW_REENABLE_PX) {
1139
- autoFollow = true;
1140
- }
1141
1084
  }
1085
+ maybeEnableAutoFollowByProximity();
1142
1086
  lastScrollTop = top;
1143
- }, { passive: true });
1087
+ const action = computeFabAction();
1088
+ if (action !== currentFabAction) {
1089
+ updateScrollFab(false, action, true);
1090
+ }
1091
+ };
1092
+ window.addEventListener('scroll', scrollHandler, { passive: true });
1144
1093
 
1145
- // Scroll-to-top/bottom FAB wiring
1146
1094
  if (els.scrollFab) {
1147
- els.scrollFab.addEventListener('click', function(ev) {
1095
+ fabClickHandler = function(ev) {
1148
1096
  ev.preventDefault();
1149
- if (isAtBottom()) {
1097
+ ev.stopPropagation();
1098
+ const action = computeFabAction();
1099
+ if (action === 'up') {
1150
1100
  scrollToTopUser();
1151
- } else {
1101
+ } else if (action === 'down') {
1152
1102
  scrollToBottomUser();
1153
1103
  }
1154
- }, { passive: false });
1104
+ fabFreezeUntil = performance.now() + FAB_TOGGLE_DEBOUNCE_MS;
1105
+ updateScrollFab(true);
1106
+ };
1107
+ els.scrollFab.addEventListener('click', fabClickHandler, { passive: false });
1155
1108
  }
1156
- window.addEventListener('scroll', scheduleScrollFabUpdate, { passive: true });
1157
- window.addEventListener('resize', scheduleScrollFabUpdate, { passive: true });
1158
1109
 
1159
- // Initial state
1160
- scheduleScrollFabUpdate();
1110
+ resizeHandler = function() {
1111
+ maybeEnableAutoFollowByProximity();
1112
+ scheduleScrollFabUpdate();
1113
+ };
1114
+ window.addEventListener('resize', resizeHandler, { passive: true });
1115
+
1116
+ updateScrollFab(true);
1161
1117
 
1162
- container.addEventListener('click', function(event) {
1118
+ containerClickHandler = function(event) {
1163
1119
  const copyButton = event.target.closest('.code-header-copy');
1164
1120
  if (copyButton) {
1165
1121
  event.preventDefault();
@@ -1232,13 +1188,13 @@ class Body:
1232
1188
  }
1233
1189
  }
1234
1190
  }
1235
- });
1191
+ };
1192
+ container.addEventListener('click', containerClickHandler, { passive: false });
1236
1193
 
1237
- // Cleanup on page lifecycle changes
1238
- window.addEventListener('pagehide', teardown, { passive: true });
1239
- window.addEventListener('beforeunload', teardown, { passive: true });
1194
+ // window.addEventListener('pagehide', cleanup, { once: true });
1195
+ // window.addEventListener('beforeunload', cleanup, { once: true });
1240
1196
  });
1241
- setTimeout(cycleTips, 10000); // after 10 seconds
1197
+ setTimeout(cycleTips, 10000);
1242
1198
  </script>
1243
1199
  </head>
1244
1200
  <body """
@@ -1308,7 +1264,6 @@ class Body:
1308
1264
  }
1309
1265
  """
1310
1266
 
1311
- # CSS for the scroll-to-top/bottom
1312
1267
  _SCROLL_FAB_CSS = """
1313
1268
  #scrollFab.scroll-fab {
1314
1269
  position: fixed;
@@ -1375,11 +1330,6 @@ class Body:
1375
1330
  """
1376
1331
 
1377
1332
  def __init__(self, window=None):
1378
- """
1379
- HTML Body
1380
-
1381
- :param window: Window instance
1382
- """
1383
1333
  self.window = window
1384
1334
  self.highlight = SyntaxHighlight(window)
1385
1335
  self._tip_keys = tuple(f"output.tips.{i}" for i in range(1, self.NUM_TIPS + 1))
@@ -1403,21 +1353,10 @@ class Body:
1403
1353
  "zenburn",
1404
1354
  )
1405
1355
 
1406
-
1407
1356
  def is_timestamp_enabled(self) -> bool:
1408
- """
1409
- Check if timestamp is enabled
1410
-
1411
- :return: True if timestamp is enabled
1412
- """
1413
1357
  return self.window.core.config.get('output_timestamp')
1414
1358
 
1415
1359
  def prepare_styles(self) -> str:
1416
- """
1417
- Prepare CSS styles
1418
-
1419
- :return: CSS styles
1420
- """
1421
1360
  cfg = self.window.core.config
1422
1361
  fonts_path = os.path.join(cfg.get_app_path(), "data", "fonts").replace("\\", "/")
1423
1362
  syntax_style = self.window.core.config.get("render.code_syntax") or "default"
@@ -1428,30 +1367,17 @@ class Body:
1428
1367
  theme_css,
1429
1368
  "pre { color: #fff; }" if syntax_style in self._syntax_dark else "pre { color: #000; }",
1430
1369
  self.highlight.get_style_defs(),
1431
- self._PERFORMANCE_CSS # performance improvements
1370
+ self._PERFORMANCE_CSS
1432
1371
  ]
1433
1372
  return "\n".join(parts)
1434
1373
 
1435
1374
  def prepare_action_icons(self, ctx: CtxItem) -> str:
1436
- """
1437
- Append action icons
1438
-
1439
- :param ctx: context item
1440
- :return: HTML code
1441
- """
1442
1375
  icons_html = "".join(self.get_action_icons(ctx, all=True))
1443
1376
  if icons_html:
1444
1377
  return f'<div class="action-icons" data-id="{ctx.id}">{icons_html}</div>'
1445
1378
  return ""
1446
1379
 
1447
1380
  def get_action_icons(self, ctx: CtxItem, all: bool = False) -> List[str]:
1448
- """
1449
- Get action icons for context item
1450
-
1451
- :param ctx: context item
1452
- :param all: True to show all icons
1453
- :return: list of icons
1454
- """
1455
1381
  icons: List[str] = []
1456
1382
  if ctx.output:
1457
1383
  cid = ctx.id
@@ -1471,46 +1397,17 @@ class Body:
1471
1397
  f'<a href="extra-join:{cid}" class="action-icon edit-icon" data-id="{cid}" role="button"><span class="cmd">{self.get_icon("playlist_add", t("ctx.extra.join"), ctx)}</span></a>')
1472
1398
  return icons
1473
1399
 
1474
- def get_icon(
1475
- self,
1476
- icon: str,
1477
- title: Optional[str] = None,
1478
- item: Optional[CtxItem] = None
1479
- ) -> str:
1480
- """
1481
- Get icon
1482
-
1483
- :param icon: icon name
1484
- :param title: icon title
1485
- :param item: context item
1486
- :return: icon HTML
1487
- """
1400
+ def get_icon(self, icon: str, title: Optional[str] = None, item: Optional[CtxItem] = None) -> str:
1488
1401
  app_path = self.window.core.config.get_app_path()
1489
1402
  icon_path = os.path.join(app_path, "data", "icons", f"{icon}.svg")
1490
1403
  return f'<img src="file://{icon_path}" class="action-img" title="{title}" alt="{title}" data-id="{item.id}">'
1491
1404
 
1492
- def get_image_html(
1493
- self,
1494
- url: str,
1495
- num: Optional[int] = None,
1496
- num_all: Optional[int] = None
1497
- ) -> str:
1498
- """
1499
- Get media image/video/audio HTML
1500
-
1501
- :param url: URL to image
1502
- :param num: number of image
1503
- :param num_all: number of all images
1504
- :return: HTML code
1505
- """
1405
+ def get_image_html(self, url: str, num: Optional[int] = None, num_all: Optional[int] = None) -> str:
1506
1406
  url, path = self.window.core.filesystem.extract_local_url(url)
1507
1407
  basename = os.path.basename(path)
1508
-
1509
- # if video file then embed video player
1510
1408
  ext = os.path.splitext(basename)[1].lower()
1511
1409
  video_exts = (".mp4", ".webm", ".ogg", ".mov", ".avi", ".mkv")
1512
1410
  if ext in video_exts:
1513
- # check if .webm file exists for better compatibility
1514
1411
  if ext != ".webm":
1515
1412
  webm_path = os.path.splitext(path)[0] + ".webm"
1516
1413
  if os.path.exists(webm_path):
@@ -1526,20 +1423,7 @@ class Body:
1526
1423
  '''
1527
1424
  return f'<div class="extra-src-img-box" title="{url}"><div class="img-outer"><div class="img-wrapper"><a href="{url}"><img src="{path}" class="image"></a></div><a href="{url}" class="title">{elide_filename(basename)}</a></div></div><br/>'
1528
1425
 
1529
- def get_url_html(
1530
- self,
1531
- url: str,
1532
- num: Optional[int] = None,
1533
- num_all: Optional[int] = None
1534
- ) -> str:
1535
- """
1536
- Get URL HTML
1537
-
1538
- :param url: external URL
1539
- :param num: number of URL
1540
- :param num_all: number of all URLs
1541
- :return: HTML code
1542
- """
1426
+ def get_url_html(self, url: str, num: Optional[int] = None, num_all: Optional[int] = None) -> str:
1543
1427
  app_path = self.window.core.config.get_app_path()
1544
1428
  icon_path = os.path.join(app_path, "data", "icons", "language.svg").replace("\\", "/")
1545
1429
  icon = f'<img src="file://{icon_path}" class="extra-src-icon">'
@@ -1547,12 +1431,6 @@ class Body:
1547
1431
  return f'{icon}<a href="{url}" title="{url}">{url}</a> <small>{num_str}</small>'
1548
1432
 
1549
1433
  def get_docs_html(self, docs: List[Dict]) -> str:
1550
- """
1551
- Get Llama-index doc metadata HTML
1552
-
1553
- :param docs: list of document metadata
1554
- :return: HTML code
1555
- """
1556
1434
  html_parts: List[str] = []
1557
1435
  src_parts: List[str] = []
1558
1436
  num = 1
@@ -1582,20 +1460,7 @@ class Body:
1582
1460
 
1583
1461
  return "".join(html_parts)
1584
1462
 
1585
- def get_file_html(
1586
- self,
1587
- url: str,
1588
- num: Optional[int] = None,
1589
- num_all: Optional[int] = None
1590
- ) -> str:
1591
- """
1592
- Get file HTML
1593
-
1594
- :param url: URL to file
1595
- :param num: number of file
1596
- :param num_all: number of all files
1597
- :return: HTML code
1598
- """
1463
+ def get_file_html(self, url: str, num: Optional[int] = None, num_all: Optional[int] = None) -> str:
1599
1464
  app_path = self.window.core.config.get_app_path()
1600
1465
  icon_path = os.path.join(app_path, "data", "icons", "attachments.svg").replace("\\", "/")
1601
1466
  icon = f'<img src="file://{icon_path}" class="extra-src-icon">'
@@ -1604,12 +1469,6 @@ class Body:
1604
1469
  return f'{icon} <b>{num_str}</b> <a href="{url}">{path}</a>'
1605
1470
 
1606
1471
  def prepare_tool_extra(self, ctx: CtxItem) -> str:
1607
- """
1608
- Prepare footer extra
1609
-
1610
- :param ctx: context item
1611
- :return: HTML code
1612
- """
1613
1472
  extra = ctx.extra
1614
1473
  if not extra:
1615
1474
  return ""
@@ -1647,11 +1506,6 @@ class Body:
1647
1506
  return "".join(parts)
1648
1507
 
1649
1508
  def get_all_tips(self) -> str:
1650
- """
1651
- Get all tips for the output view
1652
-
1653
- :return: JSON string of tips
1654
- """
1655
1509
  if not self.window.core.config.get("layout.tooltips", False):
1656
1510
  return "[]"
1657
1511
 
@@ -1665,12 +1519,6 @@ class Body:
1665
1519
  return _json_dumps(tips)
1666
1520
 
1667
1521
  def get_html(self, pid: int) -> str:
1668
- """
1669
- Build webview HTML code (fast path, minimal allocations)
1670
-
1671
- :param pid: process ID
1672
- :return: HTML code
1673
- """
1674
1522
  cfg_get = self.window.core.config.get
1675
1523
  style = cfg_get("theme.style", "blocks")
1676
1524
  classes = ["theme-" + style]
@@ -1684,7 +1532,6 @@ class Body:
1684
1532
  styles_css = self.prepare_styles()
1685
1533
  tips_json = self.get_all_tips()
1686
1534
 
1687
- # Build file:// paths for FAB icons
1688
1535
  app_path = self.window.core.config.get_app_path().replace("\\", "/")
1689
1536
  expand_path = os.path.join(app_path, "data", "icons", "expand.svg").replace("\\", "/")
1690
1537
  collapse_path = os.path.join(app_path, "data", "icons", "collapse.svg").replace("\\", "/")