pygpt-net 2.6.55__py3-none-any.whl → 2.6.57__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 (218) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +26 -22
  4. pygpt_net/config.py +44 -0
  5. pygpt_net/controller/audio/audio.py +0 -0
  6. pygpt_net/controller/calendar/calendar.py +0 -0
  7. pygpt_net/controller/calendar/note.py +0 -0
  8. pygpt_net/controller/chat/chat.py +0 -0
  9. pygpt_net/controller/chat/handler/openai_stream.py +2 -1
  10. pygpt_net/controller/chat/handler/worker.py +0 -0
  11. pygpt_net/controller/chat/remote_tools.py +5 -3
  12. pygpt_net/controller/chat/render.py +0 -0
  13. pygpt_net/controller/chat/text.py +0 -0
  14. pygpt_net/controller/ctx/common.py +0 -0
  15. pygpt_net/controller/debug/debug.py +26 -2
  16. pygpt_net/controller/debug/fixtures.py +1 -1
  17. pygpt_net/controller/dialogs/confirm.py +15 -1
  18. pygpt_net/controller/dialogs/debug.py +2 -0
  19. pygpt_net/controller/lang/mapping.py +0 -0
  20. pygpt_net/controller/launcher/launcher.py +0 -0
  21. pygpt_net/controller/mode/mode.py +0 -0
  22. pygpt_net/controller/presets/presets.py +0 -0
  23. pygpt_net/controller/realtime/realtime.py +0 -0
  24. pygpt_net/controller/theme/theme.py +0 -0
  25. pygpt_net/controller/ui/mode.py +5 -3
  26. pygpt_net/controller/ui/tabs.py +0 -0
  27. pygpt_net/core/agents/agents.py +3 -1
  28. pygpt_net/core/agents/custom.py +150 -0
  29. pygpt_net/core/agents/provider.py +0 -0
  30. pygpt_net/core/builder/__init__.py +12 -0
  31. pygpt_net/core/builder/graph.py +478 -0
  32. pygpt_net/core/calendar/calendar.py +0 -0
  33. pygpt_net/core/ctx/ctx.py +0 -0
  34. pygpt_net/core/ctx/output.py +0 -0
  35. pygpt_net/core/debug/agent.py +0 -0
  36. pygpt_net/core/debug/agent_builder.py +29 -0
  37. pygpt_net/core/debug/console/console.py +0 -0
  38. pygpt_net/core/debug/db.py +0 -0
  39. pygpt_net/core/debug/debug.py +0 -0
  40. pygpt_net/core/debug/events.py +0 -0
  41. pygpt_net/core/debug/indexes.py +0 -0
  42. pygpt_net/core/debug/kernel.py +0 -0
  43. pygpt_net/core/debug/tabs.py +0 -0
  44. pygpt_net/core/filesystem/filesystem.py +0 -0
  45. pygpt_net/core/fixtures/__init__ +0 -0
  46. pygpt_net/core/fixtures/stream/__init__.py +0 -0
  47. pygpt_net/core/fixtures/stream/generator.py +0 -0
  48. pygpt_net/core/models/models.py +2 -1
  49. pygpt_net/core/plugins/plugins.py +60 -0
  50. pygpt_net/core/render/plain/pid.py +0 -0
  51. pygpt_net/core/render/plain/renderer.py +26 -4
  52. pygpt_net/core/render/web/body.py +46 -4
  53. pygpt_net/core/render/web/debug.py +0 -0
  54. pygpt_net/core/render/web/helpers.py +0 -0
  55. pygpt_net/core/render/web/pid.py +0 -0
  56. pygpt_net/core/render/web/renderer.py +15 -20
  57. pygpt_net/core/tabs/tab.py +0 -0
  58. pygpt_net/core/tabs/tabs.py +0 -0
  59. pygpt_net/core/text/utils.py +0 -0
  60. pygpt_net/css.qrc +0 -0
  61. pygpt_net/css_rc.py +0 -0
  62. pygpt_net/data/config/config.json +8 -7
  63. pygpt_net/data/config/models.json +3 -3
  64. pygpt_net/data/config/settings.json +14 -0
  65. pygpt_net/data/css/web-blocks.css +9 -0
  66. pygpt_net/data/css/web-blocks.dark.css +6 -0
  67. pygpt_net/data/css/web-blocks.darkest.css +6 -0
  68. pygpt_net/data/css/web-chatgpt.css +14 -6
  69. pygpt_net/data/css/web-chatgpt.dark.css +6 -0
  70. pygpt_net/data/css/web-chatgpt.darkest.css +6 -0
  71. pygpt_net/data/css/web-chatgpt.light.css +6 -0
  72. pygpt_net/data/css/web-chatgpt_wide.css +14 -6
  73. pygpt_net/data/css/web-chatgpt_wide.dark.css +6 -0
  74. pygpt_net/data/css/web-chatgpt_wide.darkest.css +6 -0
  75. pygpt_net/data/css/web-chatgpt_wide.light.css +6 -0
  76. pygpt_net/data/fixtures/fake_stream.txt +14 -1
  77. pygpt_net/data/icons/case.svg +0 -0
  78. pygpt_net/data/icons/chat1.svg +0 -0
  79. pygpt_net/data/icons/chat2.svg +0 -0
  80. pygpt_net/data/icons/chat3.svg +0 -0
  81. pygpt_net/data/icons/chat4.svg +0 -0
  82. pygpt_net/data/icons/fit.svg +0 -0
  83. pygpt_net/data/icons/note1.svg +0 -0
  84. pygpt_net/data/icons/note2.svg +0 -0
  85. pygpt_net/data/icons/note3.svg +0 -0
  86. pygpt_net/data/icons/stt.svg +0 -0
  87. pygpt_net/data/icons/translate.svg +0 -0
  88. pygpt_net/data/icons/tts.svg +0 -0
  89. pygpt_net/data/icons/url.svg +0 -0
  90. pygpt_net/data/icons/vision.svg +0 -0
  91. pygpt_net/data/icons/web_off.svg +0 -0
  92. pygpt_net/data/icons/web_on.svg +0 -0
  93. pygpt_net/data/js/app/async.js +166 -0
  94. pygpt_net/data/js/app/bridge.js +88 -0
  95. pygpt_net/data/js/app/common.js +212 -0
  96. pygpt_net/data/js/app/config.js +223 -0
  97. pygpt_net/data/js/app/custom.js +961 -0
  98. pygpt_net/data/js/app/data.js +84 -0
  99. pygpt_net/data/js/app/dom.js +322 -0
  100. pygpt_net/data/js/app/events.js +400 -0
  101. pygpt_net/data/js/app/highlight.js +542 -0
  102. pygpt_net/data/js/app/logger.js +305 -0
  103. pygpt_net/data/js/app/markdown.js +1137 -0
  104. pygpt_net/data/js/app/math.js +167 -0
  105. pygpt_net/data/js/app/nodes.js +395 -0
  106. pygpt_net/data/js/app/queue.js +260 -0
  107. pygpt_net/data/js/app/raf.js +250 -0
  108. pygpt_net/data/js/app/runtime.js +582 -0
  109. pygpt_net/data/js/app/scroll.js +433 -0
  110. pygpt_net/data/js/app/stream.js +2708 -0
  111. pygpt_net/data/js/app/template.js +287 -0
  112. pygpt_net/data/js/app/tool.js +87 -0
  113. pygpt_net/data/js/app/ui.js +86 -0
  114. pygpt_net/data/js/app/user.js +380 -0
  115. pygpt_net/data/js/app/utils.js +64 -0
  116. pygpt_net/data/js/app.min.js +880 -0
  117. pygpt_net/data/js/markdown-it/markdown-it-katex.min.js +1 -1
  118. pygpt_net/data/js/markdown-it/markdown-it.min.js +0 -0
  119. pygpt_net/data/locale/locale.de.ini +3 -1
  120. pygpt_net/data/locale/locale.en.ini +8 -0
  121. pygpt_net/data/locale/locale.es.ini +2 -0
  122. pygpt_net/data/locale/locale.fr.ini +2 -0
  123. pygpt_net/data/locale/locale.it.ini +2 -0
  124. pygpt_net/data/locale/locale.pl.ini +3 -1
  125. pygpt_net/data/locale/locale.uk.ini +3 -1
  126. pygpt_net/data/locale/locale.zh.ini +2 -0
  127. pygpt_net/data/locale/plugin.osm.en.ini +24 -24
  128. pygpt_net/data/locale/plugin.wolfram.en.ini +9 -9
  129. pygpt_net/fonts.qrc +0 -0
  130. pygpt_net/fonts_rc.py +0 -0
  131. pygpt_net/icons.qrc +0 -0
  132. pygpt_net/icons_rc.py +0 -0
  133. pygpt_net/item/agent.py +62 -0
  134. pygpt_net/item/builder_layout.py +62 -0
  135. pygpt_net/js.qrc +24 -1
  136. pygpt_net/js_rc.py +51394 -33687
  137. pygpt_net/plugin/base/worker.py +0 -0
  138. pygpt_net/plugin/cmd_web/config.py +17 -17
  139. pygpt_net/plugin/cmd_web/worker.py +325 -171
  140. pygpt_net/plugin/mcp/__init__.py +0 -0
  141. pygpt_net/plugin/mcp/config.py +0 -0
  142. pygpt_net/plugin/mcp/plugin.py +0 -0
  143. pygpt_net/plugin/mcp/worker.py +0 -0
  144. pygpt_net/plugin/osm/__init__.py +0 -0
  145. pygpt_net/plugin/osm/config.py +0 -0
  146. pygpt_net/plugin/osm/plugin.py +0 -0
  147. pygpt_net/plugin/osm/worker.py +0 -0
  148. pygpt_net/plugin/wolfram/__init__.py +0 -0
  149. pygpt_net/plugin/wolfram/config.py +0 -0
  150. pygpt_net/plugin/wolfram/plugin.py +0 -0
  151. pygpt_net/plugin/wolfram/worker.py +0 -0
  152. pygpt_net/provider/api/anthropic/tools.py +0 -0
  153. pygpt_net/provider/api/google/__init__.py +0 -0
  154. pygpt_net/provider/api/google/video.py +0 -0
  155. pygpt_net/provider/api/openai/agents/experts.py +0 -0
  156. pygpt_net/provider/api/openai/agents/remote_tools.py +0 -0
  157. pygpt_net/provider/api/openai/remote_tools.py +0 -0
  158. pygpt_net/provider/api/openai/responses.py +0 -0
  159. pygpt_net/provider/api/x_ai/__init__.py +2 -0
  160. pygpt_net/provider/api/x_ai/remote.py +0 -0
  161. pygpt_net/provider/core/agent/__init__.py +10 -0
  162. pygpt_net/provider/core/agent/base.py +51 -0
  163. pygpt_net/provider/core/agent/json_file.py +200 -0
  164. pygpt_net/provider/core/config/patch.py +33 -0
  165. pygpt_net/provider/core/config/patches/__init__.py +0 -0
  166. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +1 -0
  167. pygpt_net/provider/core/ctx/db_sqlite/storage.py +0 -0
  168. pygpt_net/provider/core/model/patches/__init__.py +0 -0
  169. pygpt_net/provider/core/model/patches/patch_before_2_6_42.py +0 -0
  170. pygpt_net/provider/core/preset/patch.py +0 -0
  171. pygpt_net/provider/core/preset/patches/__init__.py +0 -0
  172. pygpt_net/provider/core/preset/patches/patch_before_2_6_42.py +0 -0
  173. pygpt_net/provider/llms/anthropic.py +4 -0
  174. pygpt_net/provider/llms/base.py +2 -0
  175. pygpt_net/provider/llms/deepseek_api.py +2 -0
  176. pygpt_net/provider/llms/google.py +2 -0
  177. pygpt_net/provider/llms/hugging_face_api.py +4 -0
  178. pygpt_net/provider/llms/hugging_face_embedding.py +0 -0
  179. pygpt_net/provider/llms/hugging_face_router.py +2 -0
  180. pygpt_net/provider/llms/local.py +0 -0
  181. pygpt_net/provider/llms/mistral.py +4 -0
  182. pygpt_net/provider/llms/open_router.py +0 -0
  183. pygpt_net/provider/llms/perplexity.py +2 -0
  184. pygpt_net/provider/llms/utils.py +0 -0
  185. pygpt_net/provider/llms/voyage.py +0 -0
  186. pygpt_net/provider/llms/x_ai.py +2 -0
  187. pygpt_net/tools/agent_builder/__init__.py +12 -0
  188. pygpt_net/tools/agent_builder/tool.py +292 -0
  189. pygpt_net/tools/agent_builder/ui/__init__.py +0 -0
  190. pygpt_net/tools/agent_builder/ui/dialogs.py +152 -0
  191. pygpt_net/tools/agent_builder/ui/list.py +228 -0
  192. pygpt_net/tools/code_interpreter/ui/html.py +0 -0
  193. pygpt_net/tools/code_interpreter/ui/widgets.py +0 -0
  194. pygpt_net/tools/html_canvas/tool.py +23 -6
  195. pygpt_net/tools/html_canvas/ui/widgets.py +224 -2
  196. pygpt_net/ui/layout/chat/chat.py +0 -0
  197. pygpt_net/ui/layout/chat/output.py +5 -5
  198. pygpt_net/ui/main.py +10 -9
  199. pygpt_net/ui/menu/debug.py +39 -1
  200. pygpt_net/ui/widget/builder/__init__.py +12 -0
  201. pygpt_net/ui/widget/builder/editor.py +2001 -0
  202. pygpt_net/ui/widget/dialog/base.py +4 -1
  203. pygpt_net/ui/widget/draw/painter.py +0 -0
  204. pygpt_net/ui/widget/element/labels.py +9 -4
  205. pygpt_net/ui/widget/lists/db.py +0 -0
  206. pygpt_net/ui/widget/lists/debug.py +0 -0
  207. pygpt_net/ui/widget/tabs/body.py +0 -0
  208. pygpt_net/ui/widget/textarea/html.py +1 -0
  209. pygpt_net/ui/widget/textarea/input.py +28 -10
  210. pygpt_net/ui/widget/textarea/output.py +21 -1
  211. pygpt_net/ui/widget/textarea/web.py +31 -3
  212. pygpt_net/utils.py +40 -0
  213. {pygpt_net-2.6.55.dist-info → pygpt_net-2.6.57.dist-info}/METADATA +16 -2
  214. {pygpt_net-2.6.55.dist-info → pygpt_net-2.6.57.dist-info}/RECORD +116 -77
  215. pygpt_net/data/js/app.js +0 -5869
  216. {pygpt_net-2.6.55.dist-info → pygpt_net-2.6.57.dist-info}/LICENSE +0 -0
  217. {pygpt_net-2.6.55.dist-info → pygpt_net-2.6.57.dist-info}/WHEEL +0 -0
  218. {pygpt_net-2.6.55.dist-info → pygpt_net-2.6.57.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,433 @@
1
+ // ==========================================================================
2
+ // Scroll manager
3
+ // ==========================================================================
4
+
5
+ class ScrollManager {
6
+
7
+ // Scroll management
8
+ constructor(cfg, dom, raf) {
9
+ this.cfg = cfg;
10
+ this.dom = dom;
11
+ this.raf = raf;
12
+ this.autoFollow = true;
13
+ this.userInteracted = false;
14
+ this.lastScrollTop = 0;
15
+ this.prevScroll = 0;
16
+ this.currentFabAction = 'none';
17
+ this.fabFreezeUntil = 0;
18
+ this.scrollScheduled = false;
19
+ this.scrollFabUpdateScheduled = false;
20
+ this.scrollRAF = 0;
21
+ this.scrollFabRAF = 0;
22
+ }
23
+
24
+ // Is page near the bottom by given margin?
25
+ isNearBottom(marginPx = 100) {
26
+ const el = Utils.SE;
27
+ const distance = el.scrollHeight - el.clientHeight - el.scrollTop;
28
+ return distance <= marginPx;
29
+ }
30
+
31
+ // Schedule a page scroll to bottom if auto-follow allows it.
32
+ scheduleScroll(live = false) {
33
+ if (live === true && this.autoFollow !== true) return;
34
+ if (this.scrollScheduled) return;
35
+ this.scrollScheduled = true;
36
+ this.raf.schedule('SM:scroll', () => {
37
+ this.scrollScheduled = false;
38
+ this.scrollToBottom(live);
39
+ this.scheduleScrollFabUpdate();
40
+ }, 'ScrollManager', 1);
41
+ }
42
+
43
+ // Cancel any pending page scroll.
44
+ cancelPendingScroll() {
45
+ try {
46
+ this.raf.cancelGroup('ScrollManager');
47
+ } catch (_) {}
48
+ this.scrollScheduled = false;
49
+ this.scrollFabUpdateScheduled = false;
50
+ this.scrollRAF = 0;
51
+ this.scrollFabRAF = 0;
52
+ }
53
+
54
+ // Jump to bottom immediately (no smooth behavior).
55
+ forceScrollToBottomImmediate() {
56
+ const el = Utils.SE;
57
+ el.scrollTop = el.scrollHeight;
58
+ this.prevScroll = el.scrollHeight;
59
+ }
60
+
61
+ // Scroll window to bottom based on auto-follow and margins.
62
+ scrollToBottom(live = false, force = false) {
63
+ const el = Utils.SE;
64
+ const marginPx = this.cfg.UI.SCROLL_NEAR_MARGIN_PX;
65
+ const behavior = 'instant';
66
+ const h = el.scrollHeight;
67
+ if (live === true && this.autoFollow !== true) {
68
+ this.prevScroll = h;
69
+ return;
70
+ }
71
+ if ((live === true && this.userInteracted === false) || this.isNearBottom(marginPx) || live === false || force) {
72
+ try {
73
+ el.scrollTo({
74
+ top: h,
75
+ behavior
76
+ });
77
+ } catch (_) {
78
+ el.scrollTop = h;
79
+ }
80
+ }
81
+ this.prevScroll = el.scrollHeight;
82
+ }
83
+
84
+ // Check if window has vertical scroll bar.
85
+ hasVerticalScroll() {
86
+ const el = Utils.SE;
87
+ return (el.scrollHeight - el.clientHeight) > 1;
88
+ }
89
+
90
+ // Compute the current FAB action (none/up/down).
91
+ computeFabAction() {
92
+ const el = Utils.SE;
93
+ const h = el.scrollHeight;
94
+ const c = el.clientHeight;
95
+ const hasScroll = (h - c) > 1;
96
+ if (!hasScroll) return 'none';
97
+ const dist = h - c - el.scrollTop;
98
+ if (dist <= 2) return 'up';
99
+ if (dist >= this.cfg.FAB.SHOW_DOWN_THRESHOLD_PX) return 'down';
100
+ return 'none';
101
+ }
102
+
103
+ // Update FAB to show correct direction and label.
104
+ updateScrollFab(force = false, actionOverride = null, bypassFreeze = false) {
105
+ const btn = this.dom.get('scrollFab');
106
+ const icon = this.dom.get('scrollFabIcon');
107
+ if (!btn || !icon) return;
108
+ const now = Utils.now();
109
+ const action = actionOverride || this.computeFabAction();
110
+ if (!force && !bypassFreeze && now < this.fabFreezeUntil && action !== this.currentFabAction) return;
111
+ if (action === 'none') {
112
+ if (this.currentFabAction !== 'none' || force) {
113
+ btn.classList.remove('visible');
114
+ this.currentFabAction = 'none';
115
+ }
116
+ return;
117
+ }
118
+ if (action !== this.currentFabAction || force) {
119
+ if (action === 'up') {
120
+ if (icon.dataset.dir !== 'up') {
121
+ icon.src = this.cfg.ICONS.COLLAPSE;
122
+ icon.dataset.dir = 'up';
123
+ }
124
+ btn.title = "Go to top";
125
+ } else {
126
+ if (icon.dataset.dir !== 'down') {
127
+ icon.src = this.cfg.ICONS.EXPAND;
128
+ icon.dataset.dir = 'down';
129
+ }
130
+ btn.title = "Go to bottom";
131
+ }
132
+ btn.setAttribute('aria-label', btn.title);
133
+ this.currentFabAction = action;
134
+ btn.classList.add('visible');
135
+ } else if (!btn.classList.contains('visible')) btn.classList.add('visible');
136
+ }
137
+
138
+ // Schedule a FAB state refresh.
139
+ scheduleScrollFabUpdate() {
140
+ if (this.scrollFabUpdateScheduled) return;
141
+ this.scrollFabUpdateScheduled = true;
142
+ this.raf.schedule('SM:fab', () => {
143
+ this.scrollFabUpdateScheduled = false;
144
+ const action = this.computeFabAction();
145
+ if (action !== this.currentFabAction) this.updateScrollFab(false, action);
146
+ }, 'ScrollManager', 2);
147
+ }
148
+
149
+ // If user is near bottom, enable auto-follow again.
150
+ maybeEnableAutoFollowByProximity() {
151
+ const el = Utils.SE;
152
+ if (!this.autoFollow) {
153
+ const dist = el.scrollHeight - el.clientHeight - el.scrollTop;
154
+ if (dist <= this.cfg.UI.AUTO_FOLLOW_REENABLE_PX) this.autoFollow = true;
155
+ }
156
+ }
157
+
158
+ // User-triggered scroll to top; disables auto-follow.
159
+ scrollToTopUser() {
160
+ this.userInteracted = true;
161
+ this.autoFollow = false;
162
+ try {
163
+ const el = Utils.SE;
164
+ el.scrollTo({
165
+ top: 0,
166
+ behavior: 'instant'
167
+ });
168
+ this.lastScrollTop = el.scrollTop;
169
+ } catch (_) {
170
+ const el = Utils.SE;
171
+ el.scrollTop = 0;
172
+ this.lastScrollTop = 0;
173
+ }
174
+ }
175
+
176
+ // User-triggered scroll to bottom; may re-enable auto-follow if near bottom.
177
+ scrollToBottomUser() {
178
+ this.userInteracted = true;
179
+ this.autoFollow = false;
180
+ try {
181
+ const el = Utils.SE;
182
+ el.scrollTo({
183
+ top: el.scrollHeight,
184
+ behavior: 'instant'
185
+ });
186
+ this.lastScrollTop = el.scrollTop;
187
+ } catch (_) {
188
+ const el = Utils.SE;
189
+ el.scrollTop = el.scrollHeight;
190
+ this.lastScrollTop = el.scrollTop;
191
+ }
192
+ this.maybeEnableAutoFollowByProximity();
193
+ }
194
+ }
195
+
196
+ // ==========================================================================
197
+ // Code scroll state manager
198
+ // ==========================================================================
199
+
200
+ class CodeScrollState {
201
+
202
+ // Code scroll state manager for tracking scroll positions and interactions.
203
+ constructor(cfg, raf) {
204
+ this.cfg = cfg;
205
+ this.raf = raf;
206
+ this.map = new WeakMap();
207
+ this.rafMap = new WeakMap();
208
+ this.rafIds = new Set(); // legacy
209
+ this.rafKeyMap = new WeakMap();
210
+ }
211
+
212
+ // Get or create per-code element state.
213
+ state(el) {
214
+ let s = this.map.get(el);
215
+ if (!s) {
216
+ s = {
217
+ autoFollow: false,
218
+ lastScrollTop: 0,
219
+ userInteracted: false,
220
+ freezeUntil: 0,
221
+ listeners: null, // { onScroll, onWheel, onTouchStart }
222
+ };
223
+ this.map.set(el, s);
224
+ }
225
+ return s;
226
+ }
227
+
228
+ // Check if code block is already finalized (not streaming).
229
+ isFinalizedCode(el) {
230
+ if (!el || el.tagName !== 'CODE') return false;
231
+ if (el.dataset && el.dataset._active_stream === '1') return false;
232
+ const highlighted = (el.getAttribute('data-highlighted') === 'yes') || el.classList.contains('hljs');
233
+ return highlighted;
234
+ }
235
+
236
+ // Is element scrolled close to the bottom by a margin?
237
+ isNearBottomEl(el, margin = 100) {
238
+ if (!el) return true;
239
+ const distance = el.scrollHeight - el.clientHeight - el.scrollTop;
240
+ return distance <= margin;
241
+ }
242
+
243
+ // Scroll code element to the bottom respecting interaction state.
244
+ scrollToBottom(el, live = false, force = false) {
245
+ if (!el || !el.isConnected) return;
246
+ if (!force && this.isFinalizedCode(el)) return;
247
+
248
+ const st = this.state(el);
249
+ const now = Utils.now();
250
+ if (!force && st.freezeUntil && now < st.freezeUntil) return;
251
+
252
+ const distNow = el.scrollHeight - el.clientHeight - el.scrollTop;
253
+ if (!force && distNow <= 1) {
254
+ st.lastScrollTop = el.scrollTop;
255
+ return;
256
+ }
257
+
258
+ const marginPx = live ? 96 : this.cfg.CODE_SCROLL.NEAR_MARGIN_PX;
259
+ const behavior = 'instant';
260
+
261
+ if (!force) {
262
+ if (live && st.autoFollow !== true) return;
263
+ if (!live && !(st.autoFollow === true || this.isNearBottomEl(el, marginPx) || !st.userInteracted)) return;
264
+ }
265
+
266
+ try {
267
+ el.scrollTo({
268
+ top: el.scrollHeight,
269
+ behavior
270
+ });
271
+ } catch (_) {
272
+ el.scrollTop = el.scrollHeight;
273
+ }
274
+ st.lastScrollTop = el.scrollTop;
275
+ }
276
+
277
+ // Schedule bottom scroll in rAF (coalesces multiple calls).
278
+ scheduleScroll(el, live = false, force = false) {
279
+ if (!el || !el.isConnected) return;
280
+ if (!force && this.isFinalizedCode(el)) return;
281
+ if (this.rafMap.get(el)) return;
282
+ this.rafMap.set(el, true);
283
+
284
+ let key = this.rafKeyMap.get(el);
285
+ if (!key) {
286
+ key = Symbol('codeScroll');
287
+ this.rafKeyMap.set(el, key);
288
+ }
289
+
290
+ this.raf.schedule(key, () => {
291
+ this.rafMap.delete(el);
292
+ this.scrollToBottom(el, live, force);
293
+ }, 'CodeScroll', 0);
294
+ }
295
+
296
+ // Attach scroll/wheel/touch handlers to manage auto-follow state.
297
+ attachHandlers(codeEl) {
298
+ if (!codeEl || codeEl.dataset.csListeners === '1') return;
299
+ if (codeEl.dataset._active_stream !== '1') return;
300
+ codeEl.dataset.csListeners = '1';
301
+ const st = this.state(codeEl);
302
+
303
+ const onScroll = (ev) => {
304
+ const top = codeEl.scrollTop;
305
+ const isUser = !!(ev && ev.isTrusted === true);
306
+ const now = Utils.now();
307
+
308
+ if (this.isFinalizedCode(codeEl)) {
309
+ if (isUser) st.userInteracted = true;
310
+ st.autoFollow = false;
311
+ st.lastScrollTop = top;
312
+ return;
313
+ }
314
+
315
+ if (isUser) {
316
+ if (top + 1 < st.lastScrollTop) {
317
+ st.autoFollow = false;
318
+ st.userInteracted = true;
319
+ st.freezeUntil = now + 1000;
320
+ } else if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) {
321
+ st.autoFollow = true;
322
+ }
323
+ } else {
324
+ if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) st.autoFollow = true;
325
+ }
326
+ st.lastScrollTop = top;
327
+ };
328
+
329
+ const onWheel = (ev) => {
330
+ st.userInteracted = true;
331
+ const now = Utils.now();
332
+
333
+ if (this.isFinalizedCode(codeEl)) {
334
+ st.autoFollow = false;
335
+ return;
336
+ }
337
+
338
+ if (ev.deltaY < 0) {
339
+ st.autoFollow = false;
340
+ st.freezeUntil = now + 1000;
341
+ } else if (this.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.AUTO_FOLLOW_REENABLE_PX)) {
342
+ st.autoFollow = true;
343
+ }
344
+ };
345
+
346
+ const onTouchStart = () => {
347
+ st.userInteracted = true;
348
+ };
349
+
350
+ codeEl.addEventListener('scroll', onScroll, {
351
+ passive: true
352
+ });
353
+ codeEl.addEventListener('wheel', onWheel, {
354
+ passive: true
355
+ });
356
+ codeEl.addEventListener('touchstart', onTouchStart, {
357
+ passive: true
358
+ });
359
+ st.listeners = {
360
+ onScroll,
361
+ onWheel,
362
+ onTouchStart
363
+ };
364
+ }
365
+
366
+ // Detach event handlers from code element.
367
+ detachHandlers(codeEl) {
368
+ if (!codeEl) return;
369
+ const st = this.map.get(codeEl);
370
+ const h = st && st.listeners;
371
+ if (!h) {
372
+ codeEl.dataset.csListeners = '0';
373
+ return;
374
+ }
375
+ try {
376
+ codeEl.removeEventListener('scroll', h.onScroll);
377
+ } catch (_) {}
378
+ try {
379
+ codeEl.removeEventListener('wheel', h.onWheel);
380
+ } catch (_) {}
381
+ try {
382
+ codeEl.removeEventListener('touchstart', h.onTouchStart);
383
+ } catch (_) {}
384
+ st.listeners = null;
385
+ codeEl.dataset.csListeners = '0';
386
+ }
387
+
388
+ // Attach handlers to all bot code blocks under root (or document).
389
+ // IMPORTANT: We intentionally do NOT auto-scroll finalized/static code blocks to the bottom.
390
+ // Only actively streaming code blocks (data-_active_stream="1") are auto-followed live.
391
+ initScrollableBlocks(root) {
392
+ const scope = root || document;
393
+ let nodes = [];
394
+ if (scope.nodeType === 1 && scope.closest && scope.closest('.msg-box.msg-bot')) {
395
+ nodes = scope.querySelectorAll('pre code');
396
+ } else {
397
+ nodes = document.querySelectorAll('.msg-box.msg-bot pre code');
398
+ }
399
+ if (!nodes.length) return;
400
+
401
+ nodes.forEach((code) => {
402
+ if (code.dataset._active_stream === '1') {
403
+ this.attachHandlers(code); // only attach to streaming code blocks
404
+ const st = this.state(code);
405
+ st.autoFollow = true;
406
+ this.scheduleScroll(code, true, false);
407
+ } else {
408
+ this.detachHandlers(code);
409
+ }
410
+ });
411
+ }
412
+
413
+ // Transfer stored scroll state between elements (after replace).
414
+ transfer(oldEl, newEl) {
415
+ if (!oldEl || !newEl || oldEl === newEl) return;
416
+ const oldState = this.map.get(oldEl);
417
+ if (oldState) this.map.set(newEl, {
418
+ ...oldState
419
+ });
420
+ this.detachHandlers(oldEl);
421
+ this.attachHandlers(newEl);
422
+ }
423
+
424
+ // Cancel any scheduled scroll tasks for code blocks.
425
+ cancelAllScrolls() {
426
+ try {
427
+ this.raf.cancelGroup('CodeScroll');
428
+ } catch (_) {}
429
+ this.rafMap = new WeakMap();
430
+ this.rafIds.clear();
431
+ this.rafKeyMap = new WeakMap();
432
+ }
433
+ }