pygpt-net 2.6.59__py3-none-any.whl → 2.6.61__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 +11 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +9 -5
- pygpt_net/controller/__init__.py +1 -0
- pygpt_net/controller/chat/common.py +115 -6
- pygpt_net/controller/chat/input.py +4 -1
- pygpt_net/controller/presets/editor.py +442 -39
- pygpt_net/controller/presets/presets.py +121 -6
- pygpt_net/controller/settings/editor.py +0 -15
- pygpt_net/controller/theme/markdown.py +2 -5
- pygpt_net/controller/ui/ui.py +4 -7
- pygpt_net/core/agents/custom/__init__.py +281 -0
- pygpt_net/core/agents/custom/debug.py +64 -0
- pygpt_net/core/agents/custom/factory.py +109 -0
- pygpt_net/core/agents/custom/graph.py +71 -0
- pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
- pygpt_net/core/agents/custom/llama_index/factory.py +100 -0
- pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
- pygpt_net/core/agents/custom/llama_index/runner.py +562 -0
- pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
- pygpt_net/core/agents/custom/llama_index/utils.py +253 -0
- pygpt_net/core/agents/custom/logging.py +50 -0
- pygpt_net/core/agents/custom/memory.py +51 -0
- pygpt_net/core/agents/custom/router.py +155 -0
- pygpt_net/core/agents/custom/router_streamer.py +187 -0
- pygpt_net/core/agents/custom/runner.py +455 -0
- pygpt_net/core/agents/custom/schema.py +127 -0
- pygpt_net/core/agents/custom/utils.py +193 -0
- pygpt_net/core/agents/provider.py +72 -7
- pygpt_net/core/agents/runner.py +7 -4
- pygpt_net/core/agents/runners/helpers.py +1 -1
- pygpt_net/core/agents/runners/llama_workflow.py +3 -0
- pygpt_net/core/agents/runners/openai_workflow.py +8 -1
- pygpt_net/core/db/viewer.py +11 -5
- pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
- pygpt_net/core/{builder → node_editor}/graph.py +28 -226
- pygpt_net/core/node_editor/models.py +118 -0
- pygpt_net/core/node_editor/types.py +78 -0
- pygpt_net/core/node_editor/utils.py +17 -0
- pygpt_net/core/presets/presets.py +216 -29
- pygpt_net/core/render/markdown/parser.py +0 -2
- pygpt_net/core/render/web/renderer.py +10 -8
- pygpt_net/data/config/config.json +5 -6
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +2 -38
- pygpt_net/data/locale/locale.de.ini +64 -1
- pygpt_net/data/locale/locale.en.ini +63 -4
- pygpt_net/data/locale/locale.es.ini +64 -1
- pygpt_net/data/locale/locale.fr.ini +64 -1
- pygpt_net/data/locale/locale.it.ini +64 -1
- pygpt_net/data/locale/locale.pl.ini +65 -2
- pygpt_net/data/locale/locale.uk.ini +64 -1
- pygpt_net/data/locale/locale.zh.ini +64 -1
- pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
- pygpt_net/item/agent.py +5 -1
- pygpt_net/item/preset.py +19 -1
- pygpt_net/provider/agents/base.py +33 -2
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
- pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
- pygpt_net/provider/core/agent/json_file.py +11 -5
- pygpt_net/provider/core/config/patch.py +10 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
- pygpt_net/tools/agent_builder/tool.py +233 -52
- pygpt_net/tools/agent_builder/ui/dialogs.py +172 -28
- pygpt_net/tools/agent_builder/ui/list.py +37 -10
- pygpt_net/ui/__init__.py +2 -4
- pygpt_net/ui/dialog/about.py +58 -38
- pygpt_net/ui/dialog/db.py +142 -3
- pygpt_net/ui/dialog/preset.py +62 -8
- pygpt_net/ui/layout/toolbox/presets.py +52 -16
- pygpt_net/ui/main.py +1 -1
- pygpt_net/ui/widget/dialog/db.py +0 -0
- pygpt_net/ui/widget/lists/preset.py +644 -60
- pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
- pygpt_net/ui/widget/node_editor/command.py +373 -0
- pygpt_net/ui/widget/node_editor/config.py +157 -0
- pygpt_net/ui/widget/node_editor/editor.py +2070 -0
- pygpt_net/ui/widget/node_editor/item.py +493 -0
- pygpt_net/ui/widget/node_editor/node.py +1460 -0
- pygpt_net/ui/widget/node_editor/utils.py +17 -0
- pygpt_net/ui/widget/node_editor/view.py +364 -0
- pygpt_net/ui/widget/tabs/output.py +1 -1
- pygpt_net/ui/widget/textarea/input.py +2 -2
- pygpt_net/utils.py +114 -2
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +80 -93
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +88 -61
- pygpt_net/core/agents/custom.py +0 -150
- pygpt_net/ui/widget/builder/editor.py +0 -2001
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/entry_points.txt +0 -0
pygpt_net/ui/dialog/db.py
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
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.26 03:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.QtCore import Qt, QTimer, QSignalBlocker
|
|
12
|
+
from PySide6.QtCore import Qt, QTimer, QSignalBlocker, QObject, QEvent
|
|
13
13
|
from PySide6.QtGui import QAction, QIcon, QIntValidator
|
|
14
14
|
from PySide6.QtWidgets import QGridLayout, QScrollArea, QSplitter, QComboBox, QLineEdit, QPushButton, \
|
|
15
|
-
QHBoxLayout, QVBoxLayout, QLabel, QWidget, QSizePolicy, QCheckBox, QMenuBar, QAbstractItemView, QHeaderView, QStyledItemDelegate
|
|
15
|
+
QHBoxLayout, QVBoxLayout, QLabel, QWidget, QSizePolicy, QCheckBox, QMenuBar, QAbstractItemView, QHeaderView, QStyledItemDelegate, QMenu, QApplication
|
|
16
16
|
|
|
17
17
|
from pygpt_net.ui.widget.dialog.db import DatabaseDialog
|
|
18
18
|
from pygpt_net.ui.widget.lists.db import DatabaseList, DatabaseTableModel
|
|
@@ -32,6 +32,98 @@ class _FastTextDelegate(QStyledItemDelegate):
|
|
|
32
32
|
return s
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
class _ContextMenuStyler(QObject):
|
|
36
|
+
"""
|
|
37
|
+
Ensures that any QMenu spawned from the observed widgets adopts the same
|
|
38
|
+
application stylesheet, palette and font (e.g. Qt Material), even if
|
|
39
|
+
the menu is created without a parent or uses platform defaults.
|
|
40
|
+
The filter is installed only on specific widgets (no app-wide filters).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, style_source: QWidget):
|
|
44
|
+
super().__init__(style_source)
|
|
45
|
+
self._source = style_source
|
|
46
|
+
self._scan_timer = QTimer(self)
|
|
47
|
+
self._scan_timer.setInterval(50) # fast enough to catch freshly spawned menus
|
|
48
|
+
self._scan_timer.timeout.connect(self._scan_and_style)
|
|
49
|
+
self._scan_ticks = 0
|
|
50
|
+
self._scan_ticks_max = 8 # up to ~400ms window
|
|
51
|
+
|
|
52
|
+
def eventFilter(self, obj, event):
|
|
53
|
+
# Trigger styling round right before/around menu show moments
|
|
54
|
+
if event.type() in (QEvent.ContextMenu, QEvent.MouseButtonPress, QEvent.KeyPress):
|
|
55
|
+
if event.type() == QEvent.ContextMenu:
|
|
56
|
+
self._start_scan()
|
|
57
|
+
elif event.type() == QEvent.MouseButtonPress and hasattr(event, "button") and event.button() == Qt.RightButton:
|
|
58
|
+
self._start_scan()
|
|
59
|
+
elif event.type() == QEvent.KeyPress:
|
|
60
|
+
key = getattr(event, "key", lambda: None)()
|
|
61
|
+
mods = getattr(event, "modifiers", lambda: Qt.NoModifier)()
|
|
62
|
+
if key == Qt.Key_Menu or (key == Qt.Key_F10 and (mods & Qt.ShiftModifier)):
|
|
63
|
+
self._start_scan()
|
|
64
|
+
return False # do not consume events
|
|
65
|
+
|
|
66
|
+
def _start_scan(self):
|
|
67
|
+
# Start a short, repeated scan to reliably catch the menu top-level window
|
|
68
|
+
self._scan_ticks = 0
|
|
69
|
+
if not self._scan_timer.isActive():
|
|
70
|
+
# immediate pass + scheduled ones
|
|
71
|
+
QTimer.singleShot(0, self._scan_and_style)
|
|
72
|
+
self._scan_timer.start()
|
|
73
|
+
|
|
74
|
+
def _scan_and_style(self):
|
|
75
|
+
self._scan_ticks += 1
|
|
76
|
+
app = QApplication.instance()
|
|
77
|
+
if app is None:
|
|
78
|
+
self._scan_timer.stop()
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
found_any = False
|
|
82
|
+
for w in app.topLevelWidgets():
|
|
83
|
+
if isinstance(w, QMenu):
|
|
84
|
+
found_any = True
|
|
85
|
+
self._ensure_menu_style(w)
|
|
86
|
+
|
|
87
|
+
# Stop once we scanned enough frames, or if no menus appear after a short while
|
|
88
|
+
if self._scan_ticks >= self._scan_ticks_max or (self._scan_ticks >= 2 and not found_any):
|
|
89
|
+
self._scan_timer.stop()
|
|
90
|
+
|
|
91
|
+
def _ensure_menu_style(self, menu: QMenu):
|
|
92
|
+
# Avoid re-styling the same widget repeatedly
|
|
93
|
+
if bool(menu.property("_pygpt_menu_styled")):
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
app = QApplication.instance()
|
|
97
|
+
try:
|
|
98
|
+
# Apply application-level stylesheet (covers Qt Material CSS selectors for QMenu)
|
|
99
|
+
if app is not None:
|
|
100
|
+
ss = app.styleSheet()
|
|
101
|
+
if ss:
|
|
102
|
+
# Assign directly to the menu; do not append, to avoid duplication on repeated shows
|
|
103
|
+
menu.setStyleSheet(ss)
|
|
104
|
+
|
|
105
|
+
# Copy palette, font and style from the source window to guarantee readable contrast
|
|
106
|
+
src = self._source.window() if self._source is not None else None
|
|
107
|
+
if src is None and app is not None:
|
|
108
|
+
src = app.activeWindow()
|
|
109
|
+
|
|
110
|
+
if src is not None:
|
|
111
|
+
menu.setPalette(src.palette())
|
|
112
|
+
menu.setFont(src.font())
|
|
113
|
+
# Also align QStyle to prevent platform defaults from clashing with stylesheet
|
|
114
|
+
menu.setStyle(src.style())
|
|
115
|
+
|
|
116
|
+
# Mark as processed
|
|
117
|
+
menu.setProperty("_pygpt_menu_styled", True)
|
|
118
|
+
|
|
119
|
+
# Update to reflect changes immediately if the menu is already visible
|
|
120
|
+
if menu.isVisible():
|
|
121
|
+
menu.update()
|
|
122
|
+
except Exception:
|
|
123
|
+
# Fail-safe: never break the context menu on styling issues
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
35
127
|
class Database:
|
|
36
128
|
def __init__(self, window=None):
|
|
37
129
|
"""
|
|
@@ -94,6 +186,9 @@ class Database:
|
|
|
94
186
|
self.batch_actions_menu.addAction(self.delete_all_action)
|
|
95
187
|
self.batch_actions_menu.addAction(self.truncate_action)
|
|
96
188
|
|
|
189
|
+
# Ensure QMenu used by menu bar follows the application/theme styling
|
|
190
|
+
self._apply_menu_style(self.batch_actions_menu)
|
|
191
|
+
|
|
97
192
|
layout = QGridLayout()
|
|
98
193
|
layout.addWidget(splitter, 1, 0)
|
|
99
194
|
layout.setMenuBar(self.menu_bar)
|
|
@@ -102,6 +197,22 @@ class Database:
|
|
|
102
197
|
self.window.ui.dialog['debug.db'].setLayout(layout)
|
|
103
198
|
self.window.ui.dialog['debug.db'].setWindowTitle("Debug: Database (SQLite)")
|
|
104
199
|
|
|
200
|
+
def _apply_menu_style(self, menu: QMenu):
|
|
201
|
+
"""Ensure QMenu follows application/theme styling consistently."""
|
|
202
|
+
try:
|
|
203
|
+
app = QApplication.instance()
|
|
204
|
+
if app is not None:
|
|
205
|
+
ss = app.styleSheet()
|
|
206
|
+
if ss:
|
|
207
|
+
menu.setStyleSheet(ss)
|
|
208
|
+
if self.window is not None:
|
|
209
|
+
menu.setStyle(self.window.style())
|
|
210
|
+
menu.setPalette(self.window.palette())
|
|
211
|
+
menu.setFont(self.window.font())
|
|
212
|
+
except Exception:
|
|
213
|
+
# Never break UI if styling fails
|
|
214
|
+
pass
|
|
215
|
+
|
|
105
216
|
def _on_splitter_moved(self, pos, index):
|
|
106
217
|
if self._text_viewer is not None:
|
|
107
218
|
self._text_viewer.setUpdatesEnabled(False)
|
|
@@ -267,6 +378,34 @@ class DataBrowser(QWidget):
|
|
|
267
378
|
self.setLayout(main_layout)
|
|
268
379
|
|
|
269
380
|
view = self.get_list_widget()
|
|
381
|
+
|
|
382
|
+
# Robust, local-only context menu styling (no app-wide filters)
|
|
383
|
+
self._menu_styler = _ContextMenuStyler(self.window if self.window is not None else self)
|
|
384
|
+
if hasattr(view, "installEventFilter"):
|
|
385
|
+
view.installEventFilter(self._menu_styler)
|
|
386
|
+
# Context menu usually originates from the viewport in item views
|
|
387
|
+
if hasattr(view, "viewport") and callable(view.viewport):
|
|
388
|
+
vp = view.viewport()
|
|
389
|
+
if vp is not None and hasattr(vp, "installEventFilter"):
|
|
390
|
+
vp.installEventFilter(self._menu_styler)
|
|
391
|
+
# Also watch headers (they may expose their own context menus)
|
|
392
|
+
if hasattr(view, "horizontalHeader"):
|
|
393
|
+
hh = view.horizontalHeader()
|
|
394
|
+
if hh is not None:
|
|
395
|
+
hh.installEventFilter(self._menu_styler)
|
|
396
|
+
if hasattr(hh, "viewport"):
|
|
397
|
+
hvp = hh.viewport()
|
|
398
|
+
if hvp is not None:
|
|
399
|
+
hvp.installEventFilter(self._menu_styler)
|
|
400
|
+
if hasattr(view, "verticalHeader"):
|
|
401
|
+
vh = view.verticalHeader()
|
|
402
|
+
if vh is not None:
|
|
403
|
+
vh.installEventFilter(self._menu_styler)
|
|
404
|
+
if hasattr(vh, "viewport"):
|
|
405
|
+
vvp = vh.viewport()
|
|
406
|
+
if vvp is not None:
|
|
407
|
+
vvp.installEventFilter(self._menu_styler)
|
|
408
|
+
|
|
270
409
|
if hasattr(view, "setUniformRowHeights"):
|
|
271
410
|
view.setUniformRowHeights(True)
|
|
272
411
|
if hasattr(view, "setWordWrap"):
|
pygpt_net/ui/dialog/preset.py
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
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.25 13:04:51 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt
|
|
13
|
+
from PySide6.QtGui import QIcon
|
|
13
14
|
from PySide6.QtWidgets import QPushButton, QHBoxLayout, QLabel, QVBoxLayout, QSplitter, QWidget, QSizePolicy, \
|
|
14
15
|
QTabWidget, QFileDialog
|
|
15
16
|
|
|
@@ -155,6 +156,8 @@ class Preset(BaseConfigDialog):
|
|
|
155
156
|
modes = QWidget()
|
|
156
157
|
modes.setLayout(rows_mode)
|
|
157
158
|
modes.setContentsMargins(0, 0, 0, 0)
|
|
159
|
+
# Ensure modes column never grows taller than its content; avoids creating vertical gap above the splitter.
|
|
160
|
+
modes.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
|
158
161
|
self.window.ui.nodes['preset.editor.modes'] = modes
|
|
159
162
|
|
|
160
163
|
# experts
|
|
@@ -202,9 +205,23 @@ class Preset(BaseConfigDialog):
|
|
|
202
205
|
"ai_personalize",
|
|
203
206
|
]
|
|
204
207
|
for key in left_keys:
|
|
208
|
+
option_layout = options[key]
|
|
209
|
+
if key in ["agent_provider", "agent_provider_openai"]:
|
|
210
|
+
# special case with settings icon on right
|
|
211
|
+
node_layout = QHBoxLayout()
|
|
212
|
+
builder_btn = QPushButton(QIcon(":/icons/robot.svg"), "")
|
|
213
|
+
builder_btn.setToolTip(trans("agent.builder.tooltip"))
|
|
214
|
+
builder_btn.clicked.connect(lambda: self.window.tools.get("agent_builder").toggle())
|
|
215
|
+
node_layout.setContentsMargins(0, 0, 0, 0)
|
|
216
|
+
node_layout.addLayout(options[key])
|
|
217
|
+
node_layout.addWidget(builder_btn)
|
|
218
|
+
# align top right
|
|
219
|
+
node_layout.setAlignment(builder_btn, Qt.AlignTop | Qt.AlignRight)
|
|
220
|
+
option_layout = node_layout
|
|
221
|
+
|
|
205
222
|
node_key = f"preset.editor.{key}"
|
|
206
223
|
node = QWidget()
|
|
207
|
-
node.setLayout(
|
|
224
|
+
node.setLayout(option_layout)
|
|
208
225
|
node.setContentsMargins(0, 0, 0, 0)
|
|
209
226
|
rows.addWidget(node)
|
|
210
227
|
self.window.ui.nodes[node_key] = node
|
|
@@ -250,6 +267,8 @@ class Preset(BaseConfigDialog):
|
|
|
250
267
|
widget_base = QWidget()
|
|
251
268
|
widget_base.setLayout(rows)
|
|
252
269
|
widget_base.setMinimumWidth(300)
|
|
270
|
+
# Keep base column at content height; do not stretch vertically.
|
|
271
|
+
widget_base.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
|
253
272
|
|
|
254
273
|
self.window.ui.nodes['preset.editor.experts'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
255
274
|
|
|
@@ -259,12 +278,16 @@ class Preset(BaseConfigDialog):
|
|
|
259
278
|
|
|
260
279
|
widget_main = QWidget()
|
|
261
280
|
widget_main.setLayout(main)
|
|
281
|
+
# Critical: ensure the whole top pane (base + modes) stays at content height.
|
|
282
|
+
widget_main.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
|
|
262
283
|
|
|
263
284
|
splitter = QSplitter(Qt.Vertical)
|
|
264
285
|
splitter.addWidget(widget_main)
|
|
265
286
|
splitter.addWidget(widget_prompt)
|
|
266
|
-
splitter.
|
|
267
|
-
|
|
287
|
+
splitter.setChildrenCollapsible(False) # avoid accidental collapsing of any pane
|
|
288
|
+
# All extra vertical space goes to bottom (extra agent options); top stays at its sizeHint.
|
|
289
|
+
splitter.setStretchFactor(0, 0)
|
|
290
|
+
splitter.setStretchFactor(1, 1)
|
|
268
291
|
self.window.ui.splitters['editor.presets'] = splitter
|
|
269
292
|
|
|
270
293
|
widget_personalize = QWidget()
|
|
@@ -383,13 +406,17 @@ class AvatarWidget(QWidget):
|
|
|
383
406
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
384
407
|
|
|
385
408
|
current_avatar_label = QLabel(trans("preset.personalize.avatar.current"))
|
|
409
|
+
# Center the section title for better visual balance.
|
|
410
|
+
current_avatar_label.setAlignment(Qt.AlignHCenter)
|
|
386
411
|
main_layout.addWidget(current_avatar_label)
|
|
387
412
|
|
|
388
413
|
self.avatar_preview = QLabel(self)
|
|
389
414
|
self.avatar_preview.setFixedSize(200, 200)
|
|
390
|
-
|
|
415
|
+
# Keep a visible border, but round it to visually indicate the circular avatar.
|
|
416
|
+
self.avatar_preview.setStyleSheet("border: 1px solid gray; border-radius: 100px; background: transparent;")
|
|
391
417
|
self.avatar_preview.setAlignment(Qt.AlignCenter)
|
|
392
|
-
|
|
418
|
+
# Center the avatar widget within the column.
|
|
419
|
+
main_layout.addWidget(self.avatar_preview, 0, Qt.AlignHCenter)
|
|
393
420
|
|
|
394
421
|
buttons_layout = QHBoxLayout()
|
|
395
422
|
|
|
@@ -402,6 +429,8 @@ class AvatarWidget(QWidget):
|
|
|
402
429
|
self.remove_button.setEnabled(False)
|
|
403
430
|
buttons_layout.addWidget(self.remove_button)
|
|
404
431
|
buttons_layout.setContentsMargins(0, 10, 0, 0)
|
|
432
|
+
# Center the action buttons below the avatar.
|
|
433
|
+
buttons_layout.setAlignment(Qt.AlignCenter)
|
|
405
434
|
|
|
406
435
|
main_layout.addLayout(buttons_layout)
|
|
407
436
|
main_layout.setContentsMargins(0, 10, 0, 0)
|
|
@@ -425,7 +454,9 @@ class AvatarWidget(QWidget):
|
|
|
425
454
|
pixmap = QPixmap(file_path)
|
|
426
455
|
if not pixmap.isNull():
|
|
427
456
|
cover_pix = self.get_cover_pixmap(pixmap, self.avatar_preview.width(), self.avatar_preview.height())
|
|
428
|
-
|
|
457
|
+
# Render the avatar as a circular pixmap.
|
|
458
|
+
circle_pix = self.get_circular_pixmap(cover_pix, self.avatar_preview.width(), self.avatar_preview.height())
|
|
459
|
+
self.avatar_preview.setPixmap(circle_pix)
|
|
429
460
|
self.remove_button.setEnabled(True)
|
|
430
461
|
|
|
431
462
|
def enable_remove_button(self, enabled: bool = True):
|
|
@@ -457,7 +488,30 @@ class AvatarWidget(QWidget):
|
|
|
457
488
|
y = (scaled_pix.height() - target_height) // 2
|
|
458
489
|
return scaled_pix.copy(x, y, target_width, target_height)
|
|
459
490
|
|
|
491
|
+
def get_circular_pixmap(self, pixmap, target_width: int, target_height: int):
|
|
492
|
+
"""
|
|
493
|
+
Create a circular version of the given pixmap using an antialiased clip path.
|
|
494
|
+
|
|
495
|
+
:param pixmap: Source pixmap already scaled/cropped to target size
|
|
496
|
+
:param target_width: Target width
|
|
497
|
+
:param target_height: Target height
|
|
498
|
+
:return: Circular masked pixmap with transparent corners
|
|
499
|
+
"""
|
|
500
|
+
from PySide6.QtGui import QPainter, QPainterPath, QPixmap # local import to avoid altering global imports
|
|
501
|
+
result = QPixmap(target_width, target_height)
|
|
502
|
+
result.fill(Qt.transparent)
|
|
503
|
+
|
|
504
|
+
painter = QPainter(result)
|
|
505
|
+
painter.setRenderHint(QPainter.Antialiasing, True)
|
|
506
|
+
path = QPainterPath()
|
|
507
|
+
path.addEllipse(0, 0, target_width, target_height)
|
|
508
|
+
painter.setClipPath(path)
|
|
509
|
+
painter.drawPixmap(0, 0, pixmap)
|
|
510
|
+
painter.end()
|
|
511
|
+
|
|
512
|
+
return result
|
|
513
|
+
|
|
460
514
|
def remove_avatar(self):
|
|
461
515
|
"""Remove the current avatar image."""
|
|
462
516
|
self.avatar_preview.clear()
|
|
463
|
-
self.remove_button.setEnabled(False)
|
|
517
|
+
self.remove_button.setEnabled(False)
|
|
@@ -6,11 +6,11 @@
|
|
|
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.26 03:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6 import QtCore
|
|
13
|
-
from PySide6.QtGui import QStandardItemModel, Qt, QIcon
|
|
13
|
+
from PySide6.QtGui import QStandardItemModel, QStandardItem, Qt, QIcon
|
|
14
14
|
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QSizePolicy
|
|
15
15
|
|
|
16
16
|
from pygpt_net.core.types import (
|
|
@@ -113,35 +113,42 @@ class Presets:
|
|
|
113
113
|
nodes = self.window.ui.nodes
|
|
114
114
|
models = self.window.ui.models
|
|
115
115
|
|
|
116
|
-
view = nodes[self.id]
|
|
116
|
+
view: PresetList = nodes[self.id]
|
|
117
117
|
model = models.get(self.id)
|
|
118
118
|
|
|
119
|
-
view
|
|
119
|
+
# If view requested selection override, do NOT override it by backup
|
|
120
|
+
selection_override_ids = getattr(view, "_selection_override_ids", None)
|
|
121
|
+
if not selection_override_ids:
|
|
122
|
+
view.backup_selection()
|
|
120
123
|
|
|
121
124
|
if model is None:
|
|
122
125
|
model = self.create_model(self.window)
|
|
123
126
|
models[self.id] = model
|
|
124
127
|
view.setModel(model)
|
|
125
128
|
|
|
129
|
+
# Block user input during model rebuild to avoid crashes on quick clicks
|
|
130
|
+
view.begin_model_update()
|
|
131
|
+
|
|
132
|
+
# Turn off updates to avoid flicker and transient artifacts
|
|
126
133
|
view.setUpdatesEnabled(False)
|
|
127
134
|
try:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
count = len(data)
|
|
132
|
-
model.setRowCount(count)
|
|
135
|
+
# Rebuild model cleanly to avoid any stale items causing visual glitches
|
|
136
|
+
model.clear()
|
|
137
|
+
model.setColumnCount(1)
|
|
133
138
|
|
|
139
|
+
if data:
|
|
134
140
|
is_expert_mode = (mode == MODE_EXPERT)
|
|
135
141
|
is_agent_mode = (mode == MODE_AGENT)
|
|
136
142
|
count_experts = self.window.core.experts.count_experts if is_agent_mode else None
|
|
137
143
|
startswith_current = "current."
|
|
138
144
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
tooltip_role = QtCore.Qt.ToolTipRole
|
|
145
|
+
role_uuid = QtCore.Qt.UserRole + 1
|
|
146
|
+
role_id = QtCore.Qt.UserRole + 2
|
|
147
|
+
role_is_special = QtCore.Qt.UserRole + 3
|
|
143
148
|
|
|
144
149
|
for i, (key, item) in enumerate(data.items()):
|
|
150
|
+
qitem = QStandardItem()
|
|
151
|
+
|
|
145
152
|
name = item.name
|
|
146
153
|
if is_expert_mode and item.enabled and not key.startswith(startswith_current):
|
|
147
154
|
name = f"[x] {name}"
|
|
@@ -153,9 +160,38 @@ class Presets:
|
|
|
153
160
|
prompt = str(item.prompt)
|
|
154
161
|
tooltip = prompt if len(prompt) <= 80 else f"{prompt[:80]}..."
|
|
155
162
|
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
qitem.setText(name)
|
|
164
|
+
qitem.setToolTip(tooltip)
|
|
165
|
+
qitem.setData(item.uuid, role_uuid)
|
|
166
|
+
qitem.setData(key, role_id)
|
|
167
|
+
qitem.setData(key.startswith(startswith_current), role_is_special)
|
|
168
|
+
|
|
169
|
+
# Pin row 0 (no drag, no drop)
|
|
170
|
+
# Other rows: drag enabled only; drop is handled by view between rows
|
|
171
|
+
if i != 0 and not key.startswith(startswith_current):
|
|
172
|
+
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
|
|
173
|
+
else:
|
|
174
|
+
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
|
175
|
+
qitem.setFlags(flags)
|
|
176
|
+
|
|
177
|
+
model.appendRow(qitem)
|
|
158
178
|
finally:
|
|
179
|
+
# Apply pending scroll (if any) before re-enabling updates
|
|
180
|
+
view.apply_pending_scroll()
|
|
159
181
|
view.setUpdatesEnabled(True)
|
|
160
182
|
|
|
161
|
-
|
|
183
|
+
dnd_enabled = bool(self.window.core.config.get('presets.drag_and_drop.enabled'))
|
|
184
|
+
view.set_dnd_enabled(dnd_enabled)
|
|
185
|
+
|
|
186
|
+
# If override requested, force saved selection IDs to those override IDs
|
|
187
|
+
if selection_override_ids:
|
|
188
|
+
view._saved_selection_ids = list(selection_override_ids)
|
|
189
|
+
view._selection_override_ids = None # consume one-shot override
|
|
190
|
+
|
|
191
|
+
# Restore selection by ID (so it follows the same item even if rows moved)
|
|
192
|
+
view.restore_selection()
|
|
193
|
+
# Force repaint in case Qt defers layout until next input
|
|
194
|
+
view.viewport().update()
|
|
195
|
+
|
|
196
|
+
# Re-enable user interaction after the rebuild is fully done
|
|
197
|
+
view.end_model_update()
|
pygpt_net/ui/main.py
CHANGED
|
@@ -356,7 +356,7 @@ class MainWindow(QMainWindow, QtStyleTools):
|
|
|
356
356
|
self.core.presets.save_all()
|
|
357
357
|
print("Exiting...")
|
|
358
358
|
print("")
|
|
359
|
-
print("
|
|
359
|
+
print("Do you like PyGPT? Support the development of the project: https://pygpt.net/#donate")
|
|
360
360
|
|
|
361
361
|
def changeEvent(self, event):
|
|
362
362
|
"""
|
pygpt_net/ui/widget/dialog/db.py
CHANGED
|
File without changes
|