pygpt-net 2.6.47__py3-none-any.whl → 2.6.49__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.
- pygpt_net/CHANGELOG.txt +8 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app_core.py +39 -39
- pygpt_net/controller/__init__.py +72 -62
- pygpt_net/controller/ctx/common.py +0 -7
- pygpt_net/controller/ctx/ctx.py +176 -8
- pygpt_net/controller/ctx/extra.py +3 -3
- pygpt_net/controller/settings/editor.py +3 -1
- pygpt_net/controller/theme/common.py +8 -2
- pygpt_net/controller/ui/tabs.py +10 -43
- pygpt_net/core/ctx/ctx.py +79 -26
- pygpt_net/core/render/web/renderer.py +4 -10
- pygpt_net/core/tabs/tabs.py +50 -11
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/css/web-blocks.css +256 -270
- pygpt_net/data/css/web-chatgpt.css +276 -301
- pygpt_net/data/css/web-chatgpt_wide.css +286 -294
- pygpt_net/provider/core/config/patch.py +9 -0
- pygpt_net/provider/core/ctx/db_sqlite/storage.py +19 -5
- pygpt_net/tools/code_interpreter/ui/html.py +176 -31
- pygpt_net/tools/code_interpreter/ui/widgets.py +1 -4
- pygpt_net/tools/html_canvas/ui/widgets.py +2 -5
- pygpt_net/ui/__init__.py +9 -14
- pygpt_net/ui/layout/chat/chat.py +2 -2
- pygpt_net/ui/layout/ctx/ctx_list.py +71 -1
- pygpt_net/ui/widget/lists/base.py +32 -1
- pygpt_net/ui/widget/lists/context.py +45 -2
- pygpt_net/ui/widget/tabs/body.py +23 -1
- pygpt_net/ui/widget/textarea/web.py +85 -45
- {pygpt_net-2.6.47.dist-info → pygpt_net-2.6.49.dist-info}/METADATA +10 -2
- {pygpt_net-2.6.47.dist-info → pygpt_net-2.6.49.dist-info}/RECORD +35 -35
- {pygpt_net-2.6.47.dist-info → pygpt_net-2.6.49.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.47.dist-info → pygpt_net-2.6.49.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.47.dist-info → pygpt_net-2.6.49.dist-info}/entry_points.txt +0 -0
|
@@ -88,6 +88,15 @@ class Patch:
|
|
|
88
88
|
patch_css('web-chatgpt_wide.light.css', True)
|
|
89
89
|
updated = True
|
|
90
90
|
|
|
91
|
+
# < 2.6.48
|
|
92
|
+
if old < parse_version("2.6.48"):
|
|
93
|
+
print("Migrating config from < 2.6.48...")
|
|
94
|
+
# reformat
|
|
95
|
+
patch_css('web-chatgpt.css', True)
|
|
96
|
+
patch_css('web-chatgpt_wide.css', True)
|
|
97
|
+
patch_css('web-blocks.css', True)
|
|
98
|
+
updated = True
|
|
99
|
+
|
|
91
100
|
# update file
|
|
92
101
|
migrated = False
|
|
93
102
|
if updated:
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2025.09.15 22:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from datetime import datetime
|
|
@@ -109,6 +109,12 @@ class Storage:
|
|
|
109
109
|
continue
|
|
110
110
|
mode = filter.get('mode', '=')
|
|
111
111
|
value = filter.get('value', '')
|
|
112
|
+
|
|
113
|
+
# handle special case for "ungrouped" (group_id IS NULL OR = 0)
|
|
114
|
+
if key == 'group_id' and str(mode).upper() == 'NULL_OR_ZERO':
|
|
115
|
+
where_clauses.append("(m.group_id IS NULL OR m.group_id = 0)")
|
|
116
|
+
continue
|
|
117
|
+
|
|
112
118
|
key_name = 'm.' + key
|
|
113
119
|
if isinstance(value, int):
|
|
114
120
|
where_clauses.append(f"{key_name} {mode} :{key}")
|
|
@@ -116,7 +122,7 @@ class Storage:
|
|
|
116
122
|
elif isinstance(value, str):
|
|
117
123
|
where_clauses.append(f"{key_name} {mode} :{key}")
|
|
118
124
|
bind_params[key] = f"%{value}%"
|
|
119
|
-
elif isinstance(value, list):
|
|
125
|
+
elif isinstance(value, list) and len(value) > 0:
|
|
120
126
|
values = "(" + ",".join([str(x) for x in value]) + ")"
|
|
121
127
|
where_clauses.append(f"{key_name} {mode} {values}")
|
|
122
128
|
|
|
@@ -148,15 +154,21 @@ class Storage:
|
|
|
148
154
|
:return: dict of CtxMeta
|
|
149
155
|
"""
|
|
150
156
|
limit_suffix = ""
|
|
151
|
-
if limit is not None and limit > 0:
|
|
152
|
-
limit_suffix = " LIMIT {}".format(limit)
|
|
153
|
-
|
|
154
157
|
where_statement, join_statement, bind_params = self.prepare_query(
|
|
155
158
|
search_string=search_string,
|
|
156
159
|
filters=filters,
|
|
157
160
|
search_content=search_content,
|
|
158
161
|
append_date_ranges=True,
|
|
159
162
|
)
|
|
163
|
+
|
|
164
|
+
# Build LIMIT/OFFSET only when limit > 0; LIMIT 0 would mean "no rows"
|
|
165
|
+
if limit is not None and int(limit) > 0:
|
|
166
|
+
limit_suffix = " LIMIT :limit"
|
|
167
|
+
bind_params['limit'] = int(limit)
|
|
168
|
+
if offset is not None and int(offset) > 0:
|
|
169
|
+
limit_suffix += " OFFSET :offset"
|
|
170
|
+
bind_params['offset'] = int(offset)
|
|
171
|
+
|
|
160
172
|
stmt_text = f"""
|
|
161
173
|
SELECT
|
|
162
174
|
m.*,
|
|
@@ -168,6 +180,8 @@ class Storage:
|
|
|
168
180
|
{join_statement}
|
|
169
181
|
WHERE
|
|
170
182
|
{where_statement}
|
|
183
|
+
GROUP BY
|
|
184
|
+
m.id
|
|
171
185
|
ORDER BY
|
|
172
186
|
m.updated_ts DESC {limit_suffix}
|
|
173
187
|
"""
|
|
@@ -6,16 +6,16 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.09.16 02:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
13
|
import re
|
|
14
14
|
from typing import Optional
|
|
15
15
|
|
|
16
|
-
from PySide6.QtCore import Qt, QObject, Signal, Slot, QEvent, QTimer
|
|
16
|
+
from PySide6.QtCore import Qt, QObject, Signal, Slot, QEvent, QTimer, QUrl, QCoreApplication, QEventLoop
|
|
17
17
|
from PySide6.QtWebChannel import QWebChannel
|
|
18
|
-
from PySide6.QtWebEngineCore import QWebEngineSettings, QWebEnginePage
|
|
18
|
+
from PySide6.QtWebEngineCore import QWebEngineSettings, QWebEnginePage, QWebEngineProfile
|
|
19
19
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
|
20
20
|
from PySide6.QtGui import QAction, QIcon, QKeySequence
|
|
21
21
|
from PySide6.QtWidgets import QMenu
|
|
@@ -25,6 +25,7 @@ from pygpt_net.core.text.web_finder import WebFinder
|
|
|
25
25
|
from pygpt_net.tools.code_interpreter.body import Body
|
|
26
26
|
from pygpt_net.utils import trans
|
|
27
27
|
|
|
28
|
+
|
|
28
29
|
class CodeBlock:
|
|
29
30
|
def __init__(
|
|
30
31
|
self,
|
|
@@ -109,32 +110,119 @@ class HtmlOutput(QWebEngineView):
|
|
|
109
110
|
self.is_dialog = False
|
|
110
111
|
self.nodes = [] # code blocks
|
|
111
112
|
|
|
113
|
+
# OpenGL widgets
|
|
114
|
+
self._glwidget = None
|
|
115
|
+
self._glwidget_filter_installed = False
|
|
116
|
+
self._unloaded = False # flag to check if unloaded
|
|
117
|
+
self._destroyed = False
|
|
118
|
+
|
|
119
|
+
# self._profile = self._make_profile(self)
|
|
120
|
+
self.setPage(CustomWebEnginePage(self.window, self, profile=None))
|
|
121
|
+
|
|
122
|
+
def _make_profile(self, parent=None) -> QWebEngineProfile:
|
|
123
|
+
"""Make profile"""
|
|
124
|
+
profile = QWebEngineProfile()
|
|
125
|
+
profile.setPersistentCookiesPolicy(QWebEngineProfile.NoPersistentCookies)
|
|
126
|
+
profile.setSpellCheckEnabled(False)
|
|
127
|
+
return profile
|
|
128
|
+
|
|
129
|
+
def _detach_gl_event_filter(self):
|
|
130
|
+
"""Detach OpenGL widget event filter if installed"""
|
|
131
|
+
if self._glwidget and self._glwidget_filter_installed:
|
|
132
|
+
try:
|
|
133
|
+
self._glwidget.removeEventFilter(self)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
self._on_delete_failed(e)
|
|
136
|
+
self._glwidget = None
|
|
137
|
+
self._glwidget_filter_installed = False
|
|
138
|
+
|
|
139
|
+
def _on_delete_failed(self, e):
|
|
140
|
+
"""
|
|
141
|
+
Handle delete failure
|
|
142
|
+
|
|
143
|
+
:param e: Exception instance
|
|
144
|
+
"""
|
|
145
|
+
pass
|
|
146
|
+
# self.window.core.debug.log(e)
|
|
147
|
+
|
|
148
|
+
def unload(self):
|
|
149
|
+
"""Unload the current page and free resources"""
|
|
150
|
+
try:
|
|
151
|
+
self.hide()
|
|
152
|
+
p = self.page()
|
|
153
|
+
p.triggerAction(QWebEnginePage.Stop)
|
|
154
|
+
p.setUrl(QUrl("about:blank"))
|
|
155
|
+
p.history().clear()
|
|
156
|
+
p.setLifecycleState(QWebEnginePage.LifecycleState.Discarded)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
self._on_delete_failed(e)
|
|
159
|
+
finally:
|
|
160
|
+
self._unloaded = True
|
|
161
|
+
|
|
112
162
|
def on_delete(self):
|
|
113
163
|
"""Clean up on delete"""
|
|
164
|
+
if self._destroyed:
|
|
165
|
+
return
|
|
166
|
+
if not self._unloaded:
|
|
167
|
+
self.unload()
|
|
168
|
+
|
|
169
|
+
self.hide()
|
|
170
|
+
self._detach_gl_event_filter()
|
|
171
|
+
|
|
114
172
|
if self.finder:
|
|
115
|
-
|
|
116
|
-
|
|
173
|
+
try:
|
|
174
|
+
self.finder.disconnect()
|
|
175
|
+
except Exception as e:
|
|
176
|
+
self._on_delete_failed(e)
|
|
177
|
+
finally:
|
|
178
|
+
self.finder = None
|
|
117
179
|
|
|
118
|
-
self.tab = None
|
|
180
|
+
self.tab = None
|
|
181
|
+
self.meta = None
|
|
119
182
|
|
|
120
|
-
# delete page
|
|
121
183
|
page = self.page()
|
|
122
|
-
|
|
123
|
-
if hasattr(page, 'bridge'):
|
|
124
|
-
page.bridge.deleteLater()
|
|
125
|
-
if hasattr(page, 'channel'):
|
|
126
|
-
page.channel.deleteLater()
|
|
127
|
-
if hasattr(page, 'signals') and page.signals:
|
|
128
|
-
page.signals.deleteLater()
|
|
129
|
-
page.deleteLater() # delete page
|
|
184
|
+
prof = page.profile()
|
|
130
185
|
|
|
131
186
|
# disconnect signals
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
187
|
+
try:
|
|
188
|
+
self.loadFinished.disconnect()
|
|
189
|
+
self.customContextMenuRequested.disconnect()
|
|
190
|
+
self.signals.save_as.disconnect()
|
|
191
|
+
self.signals.audio_read.disconnect()
|
|
192
|
+
except Exception as e:
|
|
193
|
+
self._on_delete_failed(e)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
if hasattr(page, "setWebChannel"):
|
|
197
|
+
page.setWebChannel(None)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
self._on_delete_failed(e)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
page.cleanup()
|
|
203
|
+
except Exception as e:
|
|
204
|
+
self._on_delete_failed(e)
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
if prof is not None:
|
|
208
|
+
try:
|
|
209
|
+
prof.deleteLater()
|
|
210
|
+
except Exception as e:
|
|
211
|
+
self._on_delete_failed(e)
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
self.deleteLater()
|
|
216
|
+
except Exception as e:
|
|
217
|
+
self._on_delete_failed(e)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
QCoreApplication.sendPostedEvents(None, QEvent.DeferredDelete)
|
|
221
|
+
QCoreApplication.processEvents(QEventLoop.AllEvents, 50)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
self._on_delete_failed(e)
|
|
136
224
|
|
|
137
|
-
self.
|
|
225
|
+
self._destroyed = True
|
|
138
226
|
|
|
139
227
|
def init(self, force: bool = False):
|
|
140
228
|
"""
|
|
@@ -164,7 +252,7 @@ class HtmlOutput(QWebEngineView):
|
|
|
164
252
|
|
|
165
253
|
def reload(self):
|
|
166
254
|
"""Reload HTML output"""
|
|
167
|
-
#return
|
|
255
|
+
# return
|
|
168
256
|
self.reload_css()
|
|
169
257
|
|
|
170
258
|
def get_nodes(self) -> list:
|
|
@@ -203,7 +291,7 @@ class HtmlOutput(QWebEngineView):
|
|
|
203
291
|
|
|
204
292
|
:param text: text
|
|
205
293
|
"""
|
|
206
|
-
self.plain+= str(text)
|
|
294
|
+
self.plain += str(text)
|
|
207
295
|
|
|
208
296
|
def set_plaintext(self, text: str):
|
|
209
297
|
"""
|
|
@@ -267,7 +355,7 @@ class HtmlOutput(QWebEngineView):
|
|
|
267
355
|
if node.content != "":
|
|
268
356
|
self.nodes.append(node)
|
|
269
357
|
if self.loaded:
|
|
270
|
-
|
|
358
|
+
self.insert_output(node)
|
|
271
359
|
self.update_current_content()
|
|
272
360
|
QTimer.singleShot(100, self.scroll_to_bottom) # wait for rendering to complete
|
|
273
361
|
|
|
@@ -508,7 +596,7 @@ class HtmlOutput(QWebEngineView):
|
|
|
508
596
|
menu.addSeparator()
|
|
509
597
|
action = QAction(QIcon(":/icons/search.svg"), trans('text.context_menu.find'), self)
|
|
510
598
|
action.triggered.connect(self.find_open)
|
|
511
|
-
#action.setShortcut(QKeySequence("Ctrl+F"))
|
|
599
|
+
# action.setShortcut(QKeySequence("Ctrl+F"))
|
|
512
600
|
menu.addAction(action)
|
|
513
601
|
|
|
514
602
|
menu.exec_(self.mapToGlobal(position))
|
|
@@ -615,14 +703,20 @@ class HtmlOutput(QWebEngineView):
|
|
|
615
703
|
if (event.type() == QEvent.ChildAdded and
|
|
616
704
|
source is self and
|
|
617
705
|
event.child().isWidgetType()):
|
|
706
|
+
self._detach_gl_event_filter()
|
|
618
707
|
self._glwidget = event.child()
|
|
619
|
-
|
|
708
|
+
try:
|
|
709
|
+
self._glwidget.installEventFilter(self)
|
|
710
|
+
self._glwidget_filter_installed = True
|
|
711
|
+
except Exception:
|
|
712
|
+
self._glwidget = None
|
|
713
|
+
self._glwidget_filter_installed = False
|
|
620
714
|
elif event.type() == event.Type.MouseButtonPress:
|
|
621
715
|
if self.tab:
|
|
622
716
|
col_idx = self.tab.column_idx
|
|
623
717
|
self.window.controller.ui.tabs.on_column_focus(col_idx)
|
|
624
718
|
elif event.type() == event.Type.FocusIn:
|
|
625
|
-
if self.tab
|
|
719
|
+
if self.tab:
|
|
626
720
|
col_idx = self.tab.column_idx
|
|
627
721
|
self.window.controller.ui.tabs.on_column_focus(col_idx)
|
|
628
722
|
elif event.type() == QEvent.KeyPress:
|
|
@@ -633,10 +727,17 @@ class HtmlOutput(QWebEngineView):
|
|
|
633
727
|
|
|
634
728
|
class CustomWebEnginePage(QWebEnginePage):
|
|
635
729
|
"""Custom WebEnginePage to handle web events"""
|
|
636
|
-
|
|
637
|
-
|
|
730
|
+
|
|
731
|
+
def __init__(self, window, view, profile: QWebEngineProfile = None):
|
|
732
|
+
|
|
733
|
+
# use the profile if provided, otherwise the default
|
|
734
|
+
if profile is not None:
|
|
735
|
+
super(CustomWebEnginePage, self).__init__(profile, view)
|
|
736
|
+
else:
|
|
737
|
+
super(CustomWebEnginePage, self).__init__(view)
|
|
738
|
+
|
|
638
739
|
self.window = window
|
|
639
|
-
self.parent =
|
|
740
|
+
self.parent = view
|
|
640
741
|
self.signals = WebEnginePageSignals()
|
|
641
742
|
self.findTextFinished.connect(self.on_find_finished)
|
|
642
743
|
self.zoomFactorChanged.connect(self.on_view_changed)
|
|
@@ -687,7 +788,7 @@ class CustomWebEnginePage(QWebEnginePage):
|
|
|
687
788
|
"""On selection changed"""
|
|
688
789
|
pass
|
|
689
790
|
|
|
690
|
-
def acceptNavigationRequest(self, url,
|
|
791
|
+
def acceptNavigationRequest(self, url, _type, isMainFrame):
|
|
691
792
|
"""
|
|
692
793
|
On navigation (link click) event
|
|
693
794
|
|
|
@@ -698,7 +799,7 @@ class CustomWebEnginePage(QWebEnginePage):
|
|
|
698
799
|
if _type == QWebEnginePage.NavigationTypeLinkClicked:
|
|
699
800
|
self.window.core.filesystem.url.handle(url)
|
|
700
801
|
return False
|
|
701
|
-
return super().acceptNavigationRequest(url,
|
|
802
|
+
return super().acceptNavigationRequest(url, _type, isMainFrame)
|
|
702
803
|
|
|
703
804
|
def javaScriptConsoleMessage(self, level, message, line_number, source_id):
|
|
704
805
|
"""
|
|
@@ -711,9 +812,40 @@ class CustomWebEnginePage(QWebEnginePage):
|
|
|
711
812
|
"""
|
|
712
813
|
self.signals.js_message.emit(line_number, message, source_id) # handled in debug controller
|
|
713
814
|
|
|
815
|
+
def cleanup(self):
|
|
816
|
+
"""Cleanup method to release resources"""
|
|
817
|
+
try:
|
|
818
|
+
self.findTextFinished.disconnect()
|
|
819
|
+
self.zoomFactorChanged.disconnect()
|
|
820
|
+
self.selectionChanged.disconnect()
|
|
821
|
+
except Exception:
|
|
822
|
+
pass
|
|
823
|
+
|
|
824
|
+
if self.bridge:
|
|
825
|
+
try:
|
|
826
|
+
self.bridge.cleanup()
|
|
827
|
+
except Exception:
|
|
828
|
+
pass
|
|
829
|
+
self.bridge = None
|
|
830
|
+
|
|
831
|
+
if self.channel:
|
|
832
|
+
try:
|
|
833
|
+
self.channel.unregisterObject("bridge")
|
|
834
|
+
except Exception:
|
|
835
|
+
pass
|
|
836
|
+
self.channel = None
|
|
837
|
+
|
|
838
|
+
if self.signals:
|
|
839
|
+
try:
|
|
840
|
+
self.signals.deleteLater()
|
|
841
|
+
except Exception:
|
|
842
|
+
pass
|
|
843
|
+
self.signals = None
|
|
844
|
+
|
|
714
845
|
|
|
715
846
|
class Bridge(QObject):
|
|
716
847
|
"""Bridge between Python and JavaScript"""
|
|
848
|
+
|
|
717
849
|
def __init__(self, window):
|
|
718
850
|
super(Bridge, self).__init__(window)
|
|
719
851
|
self.window = window
|
|
@@ -754,10 +886,23 @@ class Bridge(QObject):
|
|
|
754
886
|
"""
|
|
755
887
|
self.window.controller.chat.render.scroll = pos
|
|
756
888
|
|
|
889
|
+
def cleanup(self):
|
|
890
|
+
"""Cleanup method to release resources"""
|
|
891
|
+
if self.window:
|
|
892
|
+
try:
|
|
893
|
+
self.window = None
|
|
894
|
+
except Exception:
|
|
895
|
+
pass
|
|
896
|
+
|
|
897
|
+
# delete the bridge object
|
|
898
|
+
self.deleteLater()
|
|
899
|
+
|
|
900
|
+
|
|
757
901
|
class WebEngineSignals(QObject):
|
|
758
902
|
save_as = Signal(str, str)
|
|
759
903
|
audio_read = Signal(str)
|
|
760
904
|
|
|
905
|
+
|
|
761
906
|
class WebEnginePageSignals(QObject):
|
|
762
907
|
js_message = Signal(int, str, str) # on Javascript message
|
|
763
908
|
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.09.16 02:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6 import QtCore
|
|
@@ -100,9 +100,6 @@ class ToolWidget:
|
|
|
100
100
|
:return: QVBoxLayout
|
|
101
101
|
"""
|
|
102
102
|
self.output = HtmlOutput(self.window, self.tool)
|
|
103
|
-
self.output.setPage(
|
|
104
|
-
CustomWebEnginePage(self.window, self.output)
|
|
105
|
-
)
|
|
106
103
|
self.output.signals.save_as.connect(self.window.controller.chat.render.handle_save_as)
|
|
107
104
|
self.output.signals.audio_read.connect(self.window.controller.chat.render.handle_audio_read)
|
|
108
105
|
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.09.16 02:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt, Slot, QUrl, QObject, Signal
|
|
@@ -14,7 +14,7 @@ from PySide6.QtWidgets import QVBoxLayout, QCheckBox, QHBoxLayout
|
|
|
14
14
|
|
|
15
15
|
from pygpt_net.ui.widget.element.labels import HelpLabel
|
|
16
16
|
from pygpt_net.ui.widget.textarea.editor import BaseCodeEditor
|
|
17
|
-
from pygpt_net.ui.widget.textarea.html import HtmlOutput
|
|
17
|
+
from pygpt_net.ui.widget.textarea.html import HtmlOutput
|
|
18
18
|
from pygpt_net.utils import trans
|
|
19
19
|
|
|
20
20
|
|
|
@@ -61,9 +61,6 @@ class ToolWidget:
|
|
|
61
61
|
:return: QVBoxLayout
|
|
62
62
|
"""
|
|
63
63
|
self.output = CanvasOutput(self.window)
|
|
64
|
-
self.output.setPage(
|
|
65
|
-
CustomWebEnginePage(self.window, self.output)
|
|
66
|
-
)
|
|
67
64
|
self.edit = CanvasEdit(self.window)
|
|
68
65
|
self.edit.setVisible(False)
|
|
69
66
|
self.edit.textChanged.connect(
|
pygpt_net/ui/__init__.py
CHANGED
|
@@ -16,13 +16,13 @@ from PySide6.QtCore import Qt, QTimer
|
|
|
16
16
|
from PySide6.QtGui import QFontDatabase, QIcon
|
|
17
17
|
from PySide6.QtWidgets import QSplitter, QMessageBox
|
|
18
18
|
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
22
|
-
from
|
|
23
|
-
from
|
|
24
|
-
from
|
|
25
|
-
from
|
|
19
|
+
from .base.context_menu import ContextMenu
|
|
20
|
+
from .dialogs import Dialogs
|
|
21
|
+
from .layout.chat import ChatMain
|
|
22
|
+
from .layout.ctx import CtxMain
|
|
23
|
+
from .layout.toolbox import ToolboxMain
|
|
24
|
+
from .menu import Menu
|
|
25
|
+
from .tray import Tray
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class UI:
|
|
@@ -139,12 +139,7 @@ class UI:
|
|
|
139
139
|
suffix = self.window.core.platforms.get_env_suffix()
|
|
140
140
|
profile_name = self.window.core.config.profile.get_current_name()
|
|
141
141
|
self.window.setWindowTitle(
|
|
142
|
-
|
|
143
|
-
self.window.meta['version'],
|
|
144
|
-
self.window.meta['build'].replace('.', '-'),
|
|
145
|
-
suffix,
|
|
146
|
-
profile_name,
|
|
147
|
-
)
|
|
142
|
+
f"PyGPT - Desktop AI Assistant {self.window.meta['version']} | build {self.window.meta['build'].replace('.', '-')}{suffix} ({profile_name})"
|
|
148
143
|
)
|
|
149
144
|
|
|
150
145
|
def post_setup(self):
|
|
@@ -164,7 +159,7 @@ class UI:
|
|
|
164
159
|
return
|
|
165
160
|
msg = str(text)
|
|
166
161
|
msg = msg.replace("\n", " ")
|
|
167
|
-
status = msg[:self.STATUS_MAX_CHARS]
|
|
162
|
+
status = f"{msg[:self.STATUS_MAX_CHARS]}..." if len(msg) > self.STATUS_MAX_CHARS else msg # truncate
|
|
168
163
|
self.nodes['status'].setText(status)
|
|
169
164
|
|
|
170
165
|
def get_status(self):
|
pygpt_net/ui/layout/chat/chat.py
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
from PySide6.QtCore import Qt, Slot
|
|
13
13
|
from PySide6.QtWidgets import QSplitter
|
|
14
14
|
|
|
15
|
-
from
|
|
16
|
-
from
|
|
15
|
+
from .input import Input
|
|
16
|
+
from .output import Output
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class ChatMain:
|
|
@@ -6,7 +6,7 @@
|
|
|
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.09.
|
|
9
|
+
# Updated Date: 2025.09.15 22:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6 import QtCore
|
|
@@ -113,9 +113,79 @@ class CtxList:
|
|
|
113
113
|
self.update_items_pinned(id, data)
|
|
114
114
|
self.update_items(id, data)
|
|
115
115
|
self.update_groups(id, data, expand=expand)
|
|
116
|
+
|
|
117
|
+
# APPLY PENDING SCROLL BEFORE RE-ENABLING UPDATES (prevents top flicker)
|
|
118
|
+
try:
|
|
119
|
+
node.apply_pending_scroll()
|
|
120
|
+
node.clear_pending_scroll()
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
116
123
|
finally:
|
|
117
124
|
node.setUpdatesEnabled(True)
|
|
118
125
|
|
|
126
|
+
def _find_first_group_row(self, model) -> int:
|
|
127
|
+
"""Find the row index of the first GroupItem; return -1 if none."""
|
|
128
|
+
for r in range(model.rowCount()):
|
|
129
|
+
it = model.item(r)
|
|
130
|
+
if isinstance(it, GroupItem):
|
|
131
|
+
return r
|
|
132
|
+
return -1
|
|
133
|
+
|
|
134
|
+
def append_unpaginated(self, id: str, data: dict, add_ids: list[int]):
|
|
135
|
+
"""
|
|
136
|
+
Append more ungrouped and not pinned items without rebuilding the model.
|
|
137
|
+
Keeps scroll position perfectly stable.
|
|
138
|
+
"""
|
|
139
|
+
if not add_ids:
|
|
140
|
+
return
|
|
141
|
+
node = self.window.ui.nodes[id]
|
|
142
|
+
model = self.window.ui.models[id]
|
|
143
|
+
|
|
144
|
+
folders_top = bool(self.window.core.config.get("ctx.records.folders.top"))
|
|
145
|
+
# decide insertion point: at the end, or just before the first group row
|
|
146
|
+
insert_pos = model.rowCount()
|
|
147
|
+
if not folders_top:
|
|
148
|
+
grp_idx = self._find_first_group_row(model)
|
|
149
|
+
insert_pos = grp_idx if grp_idx >= 0 else model.rowCount()
|
|
150
|
+
|
|
151
|
+
# find last dt of existing ungrouped area before insertion point (for date sections)
|
|
152
|
+
last_dt_str = None
|
|
153
|
+
for r in range(insert_pos - 1, -1, -1):
|
|
154
|
+
it = model.item(r)
|
|
155
|
+
if isinstance(it, Item):
|
|
156
|
+
data_role = it.data(QtCore.Qt.ItemDataRole.UserRole) or {}
|
|
157
|
+
if not data_role.get("in_group", False) and not data_role.get("is_important", False):
|
|
158
|
+
last_dt_str = getattr(it, "dt", None)
|
|
159
|
+
break
|
|
160
|
+
elif isinstance(it, GroupItem):
|
|
161
|
+
break # hit groups boundary going upwards
|
|
162
|
+
else:
|
|
163
|
+
# SectionItem or others – skip
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
node.setUpdatesEnabled(False)
|
|
167
|
+
try:
|
|
168
|
+
# append strictly in the order provided by add_ids (older first)
|
|
169
|
+
for mid in add_ids:
|
|
170
|
+
meta = data.get(mid)
|
|
171
|
+
if meta is None:
|
|
172
|
+
continue
|
|
173
|
+
item = self.build_item(mid, meta, is_group=False)
|
|
174
|
+
|
|
175
|
+
# Optional date sections (same logic as in update_items)
|
|
176
|
+
if self._group_separators and (not item.isPinned or self._pinned_separators):
|
|
177
|
+
if last_dt_str is None or last_dt_str != item.dt:
|
|
178
|
+
section = self.build_date_section(item.dt, group=False)
|
|
179
|
+
if section:
|
|
180
|
+
model.insertRow(insert_pos, section)
|
|
181
|
+
insert_pos += 1
|
|
182
|
+
last_dt_str = item.dt
|
|
183
|
+
|
|
184
|
+
model.insertRow(insert_pos, item)
|
|
185
|
+
insert_pos += 1
|
|
186
|
+
finally:
|
|
187
|
+
node.setUpdatesEnabled(True)
|
|
188
|
+
|
|
119
189
|
def update_items(self, id, data):
|
|
120
190
|
"""
|
|
121
191
|
Update items
|
|
@@ -6,7 +6,7 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.09.15 22:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import QItemSelectionModel
|
|
@@ -36,6 +36,10 @@ class BaseList(QTreeView):
|
|
|
36
36
|
self.v_scroll_value = 0
|
|
37
37
|
self.h_scroll_value = 0
|
|
38
38
|
|
|
39
|
+
# pending scroll values applied while updates are disabled (to avoid top flicker)
|
|
40
|
+
self._pending_v_scroll_value = None
|
|
41
|
+
self._pending_h_scroll_value = None
|
|
42
|
+
|
|
39
43
|
def click(self, val):
|
|
40
44
|
self.window.controller.mode.select(self.id)
|
|
41
45
|
self.selection = self.selectionModel().selection()
|
|
@@ -103,3 +107,30 @@ class BaseList(QTreeView):
|
|
|
103
107
|
"""Restore scroll position"""
|
|
104
108
|
self.verticalScrollBar().setValue(self.v_scroll_value)
|
|
105
109
|
self.horizontalScrollBar().setValue(self.h_scroll_value)
|
|
110
|
+
|
|
111
|
+
def set_pending_v_scroll(self, value: int):
|
|
112
|
+
"""
|
|
113
|
+
Set vertical scroll value to apply while updates are disabled.
|
|
114
|
+
This prevents a visible jump to the top during model rebuild.
|
|
115
|
+
"""
|
|
116
|
+
self._pending_v_scroll_value = int(value)
|
|
117
|
+
|
|
118
|
+
def set_pending_h_scroll(self, value: int):
|
|
119
|
+
"""Optional: set horizontal pending value."""
|
|
120
|
+
self._pending_h_scroll_value = int(value)
|
|
121
|
+
|
|
122
|
+
def clear_pending_scroll(self):
|
|
123
|
+
"""Clear pending scroll values."""
|
|
124
|
+
self._pending_v_scroll_value = None
|
|
125
|
+
self._pending_h_scroll_value = None
|
|
126
|
+
|
|
127
|
+
def apply_pending_scroll(self):
|
|
128
|
+
"""
|
|
129
|
+
Apply pending scroll values immediately.
|
|
130
|
+
IMPORTANT: Call this before re-enabling updates to avoid repaint at top.
|
|
131
|
+
"""
|
|
132
|
+
if self._pending_v_scroll_value is not None:
|
|
133
|
+
self.verticalScrollBar().setValue(self._pending_v_scroll_value)
|
|
134
|
+
if self._pending_h_scroll_value is not None:
|
|
135
|
+
self.horizontalScrollBar().setValue(self._pending_h_scroll_value)
|
|
136
|
+
# do not clear here; let caller decide when to clear
|