pygpt-net 2.6.57__py3-none-any.whl → 2.6.59__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 (47) hide show
  1. pygpt_net/CHANGELOG.txt +10 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +30 -25
  4. pygpt_net/controller/debug/debug.py +3 -3
  5. pygpt_net/controller/dialogs/info.py +6 -2
  6. pygpt_net/controller/ui/tabs.py +17 -0
  7. pygpt_net/core/agents/runners/llama_workflow.py +0 -0
  8. pygpt_net/core/filesystem/parser.py +37 -24
  9. pygpt_net/core/filesystem/url.py +5 -2
  10. pygpt_net/data/config/config.json +4 -3
  11. pygpt_net/data/config/models.json +3 -3
  12. pygpt_net/data/config/settings.json +41 -2
  13. pygpt_net/data/js/app/ui.js +1 -1
  14. pygpt_net/data/js/app.min.js +2 -2
  15. pygpt_net/data/locale/locale.de.ini +5 -1
  16. pygpt_net/data/locale/locale.en.ini +5 -1
  17. pygpt_net/data/locale/locale.es.ini +5 -1
  18. pygpt_net/data/locale/locale.fr.ini +5 -1
  19. pygpt_net/data/locale/locale.it.ini +5 -1
  20. pygpt_net/data/locale/locale.pl.ini +5 -1
  21. pygpt_net/data/locale/locale.uk.ini +5 -1
  22. pygpt_net/data/locale/locale.zh.ini +5 -1
  23. pygpt_net/data/locale/plugin.cmd_system.en.ini +68 -0
  24. pygpt_net/js_rc.py +5 -5
  25. pygpt_net/plugin/base/plugin.py +3 -5
  26. pygpt_net/plugin/cmd_system/config.py +377 -1
  27. pygpt_net/plugin/cmd_system/plugin.py +52 -8
  28. pygpt_net/plugin/cmd_system/runner.py +508 -32
  29. pygpt_net/plugin/cmd_system/winapi.py +481 -0
  30. pygpt_net/plugin/cmd_system/worker.py +88 -15
  31. pygpt_net/provider/agents/llama_index/workflow/supervisor.py +0 -0
  32. pygpt_net/provider/core/config/patch.py +8 -1
  33. pygpt_net/provider/llms/openai.py +6 -4
  34. pygpt_net/tools/code_interpreter/ui/html.py +2 -1
  35. pygpt_net/tools/html_canvas/ui/widgets.py +19 -18
  36. pygpt_net/tools/web_browser/__init__.py +12 -0
  37. pygpt_net/tools/web_browser/tool.py +232 -0
  38. pygpt_net/tools/web_browser/ui/__init__.py +0 -0
  39. pygpt_net/tools/web_browser/ui/dialogs.py +123 -0
  40. pygpt_net/tools/web_browser/ui/widgets.py +351 -0
  41. pygpt_net/ui/widget/textarea/html.py +172 -24
  42. pygpt_net/ui/widget/textarea/web.py +1 -1
  43. {pygpt_net-2.6.57.dist-info → pygpt_net-2.6.59.dist-info}/METADATA +81 -61
  44. {pygpt_net-2.6.57.dist-info → pygpt_net-2.6.59.dist-info}/RECORD +45 -39
  45. {pygpt_net-2.6.57.dist-info → pygpt_net-2.6.59.dist-info}/LICENSE +0 -0
  46. {pygpt_net-2.6.57.dist-info → pygpt_net-2.6.59.dist-info}/WHEEL +0 -0
  47. {pygpt_net-2.6.57.dist-info → pygpt_net-2.6.59.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.09.22 19:00:00 #
10
+ # ================================================== #
11
+
12
+ from PySide6.QtCore import Qt, Slot, QUrl, QObject, Signal, QSize
13
+ from PySide6.QtGui import QIcon
14
+ from PySide6.QtWidgets import (
15
+ QVBoxLayout,
16
+ QHBoxLayout,
17
+ QLineEdit,
18
+ QPushButton,
19
+ QWidget,
20
+ QSizePolicy,
21
+ )
22
+
23
+ from pygpt_net.ui.widget.element.labels import HelpLabel
24
+ from pygpt_net.ui.widget.textarea.html import HtmlOutput
25
+ from pygpt_net.utils import trans
26
+
27
+
28
+ class ToolWidget:
29
+ def __init__(self, window=None, tool=None):
30
+ """
31
+ HTML/JS canvas widget
32
+
33
+ :param window: Window instance
34
+ :param tool: Tool instance
35
+ """
36
+ self.window = window
37
+ self.tool = tool # tool instance
38
+ self.output = None # output
39
+
40
+ # --- Navigation bar state ---
41
+ self.nav_bar = None
42
+ self.nav_layout = None
43
+ self.address_bar = None
44
+ self.btn_back = None
45
+ self.btn_next = None
46
+ self.btn_reload = None
47
+ self.btn_go = None
48
+
49
+ def on_open(self):
50
+ """On open"""
51
+ pass
52
+
53
+ def on_delete(self):
54
+ """On delete"""
55
+ if self.tool:
56
+ self.tool.signals.url.disconnect(self.open_url) # keep connections clean
57
+ try:
58
+ if hasattr(self.output, 'titleChanged'):
59
+ self.output.titleChanged.disconnect(self.on_update_title)
60
+ elif hasattr(self.output, 'page') and self.output.page() and hasattr(self.output.page(), 'titleChanged'):
61
+ self.output.page().titleChanged.disconnect(self.on_update_title)
62
+ except Exception:
63
+ pass
64
+ if self.output:
65
+ self.output.on_delete()
66
+
67
+ def set_tab(self, tab):
68
+ """
69
+ Set tab
70
+
71
+ :param tab: Tab
72
+ """
73
+ self.output.set_tab(tab)
74
+
75
+ def setup(self, all: bool = True) -> QVBoxLayout:
76
+ """
77
+ Setup widget body
78
+
79
+ :param all: If True, setup all widgets
80
+ :return: QVBoxLayout
81
+ """
82
+ self.output = BrowserOutput(self.window)
83
+ if hasattr(self.output, 'titleChanged'):
84
+ try:
85
+ self.output.titleChanged.connect(self.on_update_title)
86
+ except TypeError:
87
+ pass
88
+ elif hasattr(self.output, 'page') and self.output.page() and hasattr(self.output.page(), 'titleChanged'):
89
+ try:
90
+ self.output.page().titleChanged.connect(self.on_update_title)
91
+ except TypeError:
92
+ pass
93
+ else:
94
+ def _emit_title_from_load(ok: bool):
95
+ if not ok:
96
+ return
97
+ title = ""
98
+ try:
99
+ if hasattr(self.output, 'title'):
100
+ title = self.output.title()
101
+ elif hasattr(self.output, 'page') and self.output.page():
102
+ title = self.output.page().title()
103
+ except Exception:
104
+ title = ""
105
+ if title and title.strip() and title != "about:blank":
106
+ self.on_update_title(title)
107
+
108
+ # ---- Navigation bar ----
109
+ self.nav_bar = QWidget()
110
+ self.nav_layout = QHBoxLayout(self.nav_bar)
111
+ self.nav_layout.setContentsMargins(6, 4, 6, 4) # small margins for compact look
112
+ self.nav_layout.setSpacing(6)
113
+ self.nav_bar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
114
+
115
+ # Icon size and fixed navbar height (to prevent taking too much space)
116
+ icon_size_px = 20 # base icon size for QPushButtons
117
+ nav_height = max(32, min(44, icon_size_px + 16)) # compact, never half-screen
118
+ self.nav_bar.setFixedHeight(nav_height)
119
+
120
+ # Buttons
121
+ self.btn_back = QPushButton()
122
+ self.btn_back.setToolTip("Back")
123
+ self.btn_back.setIcon(QIcon(":/icons/back.svg"))
124
+ self.btn_back.setIconSize(QSize(icon_size_px, icon_size_px))
125
+ self.btn_back.setFixedHeight(nav_height - 8)
126
+ self.btn_back.setEnabled(False)
127
+ self.btn_back.setAutoDefault(False) # prevent Enter from triggering this
128
+ try:
129
+ self.btn_back.setDefault(False)
130
+ except Exception:
131
+ pass
132
+
133
+ self.btn_next = QPushButton()
134
+ self.btn_next.setToolTip("Next")
135
+ self.btn_next.setIcon(QIcon(":/icons/forward.svg"))
136
+ self.btn_next.setIconSize(QSize(icon_size_px, icon_size_px))
137
+ self.btn_next.setFixedHeight(nav_height - 8)
138
+ self.btn_next.setEnabled(False)
139
+ self.btn_next.setAutoDefault(False)
140
+ try:
141
+ self.btn_next.setDefault(False)
142
+ except Exception:
143
+ pass
144
+
145
+ self.btn_reload = QPushButton()
146
+ self.btn_reload.setToolTip("Reload")
147
+ self.btn_reload.setIcon(QIcon(":/icons/reload.svg"))
148
+ self.btn_reload.setIconSize(QSize(icon_size_px, icon_size_px))
149
+ self.btn_reload.setFixedHeight(nav_height - 8)
150
+ self.btn_reload.setAutoDefault(False)
151
+ try:
152
+ self.btn_reload.setDefault(False)
153
+ except Exception:
154
+ pass
155
+
156
+ # "Go" button
157
+ self.btn_go = QPushButton()
158
+ self.btn_go.setToolTip("Open URL")
159
+ self.btn_go.setIcon(QIcon(":/icons/redo.svg"))
160
+ self.btn_go.setIconSize(QSize(icon_size_px, icon_size_px))
161
+ self.btn_go.setFixedHeight(nav_height - 8)
162
+ self.btn_go.setAutoDefault(False) # avoid stealing Enter
163
+ try:
164
+ self.btn_go.setDefault(False)
165
+ except Exception:
166
+ pass
167
+
168
+ # Address bar (custom line edit to capture Enter and avoid default button reload)
169
+ self.address_bar = AddressLineEdit(on_return_callback=self._on_address_enter)
170
+ self.address_bar.setPlaceholderText("Enter URL and press Enter")
171
+ self.address_bar.setFixedHeight(nav_height - 8)
172
+ # Still keep the Qt signal for completeness (won't fire because we intercept the key)
173
+ self.address_bar.returnPressed.connect(self._on_address_enter)
174
+
175
+ # Hook up button actions
176
+ self.btn_back.clicked.connect(lambda: self._navigate('back'))
177
+ self.btn_next.clicked.connect(lambda: self._navigate('forward'))
178
+ self.btn_reload.clicked.connect(lambda: self._navigate('reload'))
179
+ self.btn_go.clicked.connect(self._on_address_enter) # "Go" triggers address open
180
+
181
+ # Build nav layout
182
+ self.nav_layout.addWidget(self.btn_back)
183
+ self.nav_layout.addWidget(self.btn_next)
184
+ self.nav_layout.addWidget(self.btn_reload)
185
+ self.nav_layout.addWidget(self.address_bar, 1)
186
+ self.nav_layout.addWidget(self.btn_go)
187
+
188
+ sec_label = HelpLabel(trans("tool.web_browser.security.footer"))
189
+ sec_label.setAlignment(Qt.AlignCenter)
190
+
191
+ bottom_layout = QHBoxLayout()
192
+ bottom_layout.addWidget(sec_label)
193
+
194
+ output_layout = QVBoxLayout()
195
+ # put navigation bar above the web output
196
+ output_layout.addWidget(self.nav_bar)
197
+ output_layout.addWidget(self.output, 1)
198
+ output_layout.setContentsMargins(0, 0, 0, 0)
199
+
200
+ # connect signals
201
+ self.output.signals.save_as.connect(
202
+ self.tool.handle_save_as)
203
+ self.output.signals.audio_read.connect(
204
+ self.window.controller.chat.render.handle_audio_read)
205
+
206
+ # Keep nav bar in sync with the underlying view if signals are available
207
+ # (urlChanged/loadStarted/loadFinished are part of QWebEngineView)
208
+ if hasattr(self.output, 'urlChanged'):
209
+ self.output.urlChanged.connect(self._on_url_changed)
210
+ if hasattr(self.output, 'loadStarted'):
211
+ try:
212
+ self.output.loadStarted.connect(lambda: self._update_nav_controls())
213
+ except TypeError:
214
+ pass
215
+ if hasattr(self.output, 'loadFinished'):
216
+ try:
217
+ self.output.loadFinished.connect(lambda ok: self._update_nav_controls())
218
+ except TypeError:
219
+ pass
220
+
221
+ self.tool.signals.url.connect(self.open_url)
222
+ self.tool.signals.closed.connect(self.on_delete)
223
+
224
+ layout = QVBoxLayout()
225
+ layout.addLayout(output_layout, 1)
226
+ layout.addLayout(bottom_layout, 0)
227
+ return layout
228
+
229
+ @Slot(str)
230
+ def open_url(self, url: str):
231
+ """
232
+ Open URL in output
233
+
234
+ :param url: URL
235
+ """
236
+ print("Opening URL:", url)
237
+ if self.address_bar:
238
+ self.address_bar.setText(url)
239
+ self.output.setUrl(QUrl(url))
240
+ self._update_nav_controls()
241
+
242
+ # ------------------
243
+ # Navigation helpers
244
+ # ------------------
245
+ def _show_navbar(self, show: bool):
246
+ """Show/hide the navigation bar."""
247
+ if self.nav_bar:
248
+ self.nav_bar.setVisible(show)
249
+ # Keep controls state up-to-date
250
+ self._update_nav_controls()
251
+
252
+ def _navigate(self, action: str):
253
+ """Perform navigation action on the output view."""
254
+ if not self.output:
255
+ return
256
+ try:
257
+ if action == 'back' and hasattr(self.output, 'back'):
258
+ self.output.back()
259
+ elif action == 'forward' and hasattr(self.output, 'forward'):
260
+ self.output.forward()
261
+ elif action == 'reload' and hasattr(self.output, 'reload'):
262
+ self.output.reload()
263
+ finally:
264
+ self._update_nav_controls()
265
+
266
+ def _on_address_enter(self):
267
+ """Handle Enter pressed in the address bar or clicking GO."""
268
+ if not self.output or not self.address_bar:
269
+ return
270
+ text = self.address_bar.text().strip()
271
+ if not text:
272
+ return
273
+ url = QUrl.fromUserInput(text)
274
+ if url.isValid():
275
+ self._show_navbar(True)
276
+ self.output.setUrl(url)
277
+ self._update_nav_controls()
278
+
279
+ def _on_url_changed(self, url: QUrl):
280
+ """Keep address bar in sync with the view URL."""
281
+ if self.address_bar:
282
+ self.address_bar.setText(url.toString())
283
+ self._update_nav_controls()
284
+
285
+ def _update_nav_controls(self):
286
+ """Enable/disable back/forward buttons based on history."""
287
+ if not self.output:
288
+ return
289
+ # Reload is generally available when navbar is visible
290
+ if self.btn_reload:
291
+ self.btn_reload.setEnabled(True)
292
+ # Back/forward depend on history if available
293
+ if self.btn_back and self.btn_next:
294
+ try:
295
+ hist = self.output.history()
296
+ self.btn_back.setEnabled(bool(hist and hist.canGoBack()))
297
+ self.btn_next.setEnabled(bool(hist and hist.canGoForward()))
298
+ except Exception:
299
+ # If history is not available, keep safe defaults
300
+ self.btn_back.setEnabled(False)
301
+ self.btn_next.setEnabled(False)
302
+
303
+ @Slot(str)
304
+ def on_update_title(self, title: str):
305
+ """Update tab title when the page title changes."""
306
+ if not self.output:
307
+ return
308
+ tab = self.output.tab
309
+ if tab is None:
310
+ return
311
+ if self.window and title and title.strip() and title != "about:blank":
312
+ try:
313
+ self.window.controller.ui.tabs.update_title_by_tab(tab, title)
314
+ except Exception:
315
+ pass
316
+
317
+
318
+ class BrowserOutput(HtmlOutput):
319
+ def __init__(self, window=None):
320
+ """
321
+ HTML canvas output
322
+
323
+ :param window: main window
324
+ """
325
+ super(BrowserOutput, self).__init__(window)
326
+ self.window = window
327
+
328
+
329
+ # --- Address bar input widget ---
330
+ class AddressLineEdit(QLineEdit):
331
+ """
332
+ Custom QLineEdit that ensures Enter triggers opening the typed URL
333
+ and prevents default buttons (e.g., Reload) from consuming the key.
334
+ """
335
+ def __init__(self, on_return_callback=None, *args, **kwargs):
336
+ super().__init__(*args, **kwargs)
337
+ self._on_return_callback = on_return_callback
338
+
339
+ def keyPressEvent(self, event):
340
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
341
+ # Call the assigned handler and stop propagation so default buttons won't trigger.
342
+ if callable(self._on_return_callback):
343
+ self._on_return_callback()
344
+ event.accept()
345
+ return
346
+ super().keyPressEvent(event)
347
+
348
+
349
+ class ToolSignals(QObject):
350
+ url = Signal(str) # url
351
+ closed = Signal() # dialog closed
@@ -6,12 +6,12 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 23:00:00 #
9
+ # Updated Date: 2025.09.22 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import re
13
13
 
14
- from PySide6.QtCore import Qt, QObject, Signal, Slot, QEvent
14
+ from PySide6.QtCore import Qt, QObject, Signal, Slot, QEvent, QCoreApplication, QEventLoop, QUrl
15
15
  from PySide6.QtWebChannel import QWebChannel
16
16
  from PySide6.QtWebEngineCore import QWebEngineSettings, QWebEnginePage
17
17
  from PySide6.QtWebEngineWidgets import QWebEngineView
@@ -42,34 +42,131 @@ class HtmlOutput(QWebEngineView):
42
42
  self.meta = None
43
43
  self.tab = None
44
44
  self.setMouseTracking(True)
45
+
46
+ # OpenGL widgets
47
+ self._glwidget = None
48
+ self._glwidget_filter_installed = False
49
+ self._unloaded = False
50
+ self._destroyed = False
51
+
45
52
  self.installEventFilter(self)
46
53
 
54
+ def _detach_gl_event_filter(self):
55
+ """Detach OpenGL widget event filter if installed"""
56
+ if self._glwidget and self._glwidget_filter_installed:
57
+ try:
58
+ self._glwidget.removeEventFilter(self)
59
+ except Exception as e:
60
+ self._on_delete_failed(e)
61
+ self._glwidget = None
62
+ self._glwidget_filter_installed = False
63
+
64
+ def _on_delete_failed(self, e):
65
+ """
66
+ Handle delete failure
67
+
68
+ :param e: Exception instance
69
+ """
70
+ pass
71
+ self.window.core.debug.log(e)
72
+
73
+ def unload(self):
74
+ """Unload the current page and free resources"""
75
+ try:
76
+ self.hide()
77
+ p = self.page()
78
+ p.triggerAction(QWebEnginePage.Stop)
79
+ p.setUrl(QUrl("about:blank"))
80
+ p.history().clear()
81
+ p.setLifecycleState(QWebEnginePage.LifecycleState.Discarded)
82
+ except Exception as e:
83
+ self._on_delete_failed(e)
84
+ finally:
85
+ self._unloaded = True
86
+
47
87
  def on_delete(self):
48
88
  """Clean up on delete"""
89
+ if self._destroyed:
90
+ return
91
+ if not self._unloaded:
92
+ self.unload()
93
+
94
+ self.hide()
95
+ self._detach_gl_event_filter()
96
+
49
97
  if self.finder:
50
- self.finder.disconnect() # disconnect finder
51
- self.finder = None # delete finder
98
+ try:
99
+ self.finder.disconnect()
100
+ except Exception as e:
101
+ self._on_delete_failed(e)
102
+ finally:
103
+ self.finder = None
52
104
 
53
- self.tab = None # clear tab reference
105
+ self.tab = None
106
+
107
+ # disconnect signals
108
+ try:
109
+ self.loadFinished.disconnect(self.on_page_loaded)
110
+ self.customContextMenuRequested.disconnect(self.on_context_menu)
111
+ self.signals.save_as.disconnect()
112
+ self.signals.audio_read.disconnect()
113
+ except Exception as e:
114
+ self._on_delete_failed(e)
54
115
 
55
- # delete page
56
116
  page = self.page()
57
- if page:
58
- if hasattr(page, 'bridge'):
59
- page.bridge.deleteLater()
60
- if hasattr(page, 'channel'):
61
- page.channel.deleteLater()
62
- if hasattr(page, 'signals') and page.signals:
63
- page.signals.deleteLater()
64
- page.deleteLater() # delete page
65
117
 
66
- # disconnect signals
67
- self.loadFinished.disconnect(self.on_page_loaded)
68
- self.customContextMenuRequested.disconnect(self.on_context_menu)
69
- self.signals.save_as.disconnect()
70
- self.signals.audio_read.disconnect()
118
+ try:
119
+ page.triggerAction(QWebEnginePage.Stop)
120
+ except Exception as e:
121
+ self._on_delete_failed(e)
122
+
123
+ try:
124
+ page.setUrl(QUrl("about:blank"))
125
+ except Exception as e:
126
+ self._on_delete_failed(e)
71
127
 
72
- self.deleteLater() # delete widget
128
+ try:
129
+ page.history().clear()
130
+ except Exception as e:
131
+ self._on_delete_failed(e)
132
+
133
+ try:
134
+ page.setLifecycleState(QWebEnginePage.LifecycleState.Discarded)
135
+ except Exception as e:
136
+ self._on_delete_failed(e)
137
+
138
+ try:
139
+ if hasattr(page, "setWebChannel"):
140
+ page.setWebChannel(None)
141
+ except Exception as e:
142
+ self._on_delete_failed(e)
143
+
144
+ prof = None
145
+ try:
146
+ prof = page.profile()
147
+ except Exception as e:
148
+ self._on_delete_failed(e)
149
+
150
+ """
151
+ if prof is not None:
152
+ try:
153
+ prof.deleteLater()
154
+ except Exception as e:
155
+ self._on_delete_failed(e)
156
+ """
157
+
158
+ try:
159
+ self.deleteLater()
160
+ except Exception as e:
161
+ self._on_delete_failed(e)
162
+
163
+ try:
164
+ QCoreApplication.sendPostedEvents(None, QEvent.DeferredDelete)
165
+ QCoreApplication.processEvents(QEventLoop.AllEvents, 50)
166
+ except Exception as e:
167
+ self._on_delete_failed(e)
168
+
169
+ self._destroyed = True
73
170
 
74
171
  def set_tab(self, tab):
75
172
  """
@@ -251,11 +348,15 @@ class HtmlOutput(QWebEngineView):
251
348
  :param source: source
252
349
  :param event: event
253
350
  """
254
- if (event.type() == QEvent.ChildAdded and
255
- source is self and
256
- event.child().isWidgetType()):
351
+ if event.type() == QEvent.ChildAdded and source is self and event.child().isWidgetType():
352
+ self._detach_gl_event_filter()
257
353
  self._glwidget = event.child()
258
- self._glwidget.installEventFilter(self)
354
+ try:
355
+ self._glwidget.installEventFilter(self)
356
+ self._glwidget_filter_installed = True
357
+ except Exception:
358
+ self._glwidget = None
359
+ self._glwidget_filter_installed = False
259
360
  elif (event.type() == event.Type.MouseButtonPress):
260
361
  if self.tab:
261
362
  col_idx = self.tab.column_idx
@@ -288,6 +389,8 @@ class CustomWebEnginePage(QWebEnginePage):
288
389
  if self.window.core.config.has("zoom"):
289
390
  self.setZoomFactor(self.window.core.config.get("zoom"))
290
391
 
392
+ self.loaded = False
393
+
291
394
  # bridge Python <> JavaScript
292
395
  self.bridge = Bridge(self.window)
293
396
  self.channel = QWebChannel(self)
@@ -306,6 +409,9 @@ class CustomWebEnginePage(QWebEnginePage):
306
409
  self.parent.finder.matches = num
307
410
  self.parent.finder.on_find_finished()
308
411
 
412
+ def set_loaded(self, loaded: bool = True):
413
+ self.loaded = loaded
414
+
309
415
  def on_view_changed(self):
310
416
  """On view changed"""
311
417
  zoom = self.zoomFactor()
@@ -346,6 +452,37 @@ class CustomWebEnginePage(QWebEnginePage):
346
452
  """
347
453
  self.signals.js_message.emit(line_number, message, source_id) # handled in debug controller
348
454
 
455
+ def cleanup(self):
456
+ """Cleanup method to release resources"""
457
+ self.loaded = False
458
+ try:
459
+ self.findTextFinished.disconnect()
460
+ self.zoomFactorChanged.disconnect()
461
+ self.selectionChanged.disconnect()
462
+ except Exception:
463
+ pass
464
+
465
+ if self.bridge:
466
+ try:
467
+ self.bridge.cleanup()
468
+ except Exception:
469
+ pass
470
+ self.bridge = None
471
+
472
+ if self.channel:
473
+ try:
474
+ self.channel.unregisterObject("bridge")
475
+ except Exception:
476
+ pass
477
+ self.channel = None
478
+
479
+ if self.signals:
480
+ try:
481
+ self.signals.deleteLater()
482
+ except Exception:
483
+ pass
484
+ self.signals = None
485
+
349
486
 
350
487
  class Bridge(QObject):
351
488
  """Bridge between Python and JavaScript"""
@@ -380,6 +517,17 @@ class Bridge(QObject):
380
517
  """
381
518
  self.window.controller.chat.render.scroll = pos
382
519
 
520
+ def cleanup(self):
521
+ """Cleanup method to release resources"""
522
+ if self.window:
523
+ try:
524
+ self.window = None
525
+ except Exception:
526
+ pass
527
+
528
+ # delete the bridge object
529
+ self.deleteLater()
530
+
383
531
 
384
532
  class WebEngineSignals(QObject):
385
533
  save_as = Signal(str, str)
@@ -531,7 +531,7 @@ class CustomWebEnginePage(QWebEnginePage):
531
531
  return super().acceptNavigationRequest(url, _type, isMainFrame)
532
532
 
533
533
  def javaScriptConsoleMessage(self, level, message, line_number, source_id):
534
- print("[JS CONSOLE] Line", line_number, ":", message)
534
+ # print("[JS CONSOLE] Line", line_number, ":", message)
535
535
  self.signals.js_message.emit(line_number, message, source_id) # handled in debug controller
536
536
 
537
537
  def cleanup(self):