pygpt-net 2.7.5__py3-none-any.whl → 2.7.6__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 (51) hide show
  1. pygpt_net/CHANGELOG.txt +8 -0
  2. pygpt_net/__init__.py +2 -2
  3. pygpt_net/controller/chat/handler/worker.py +9 -31
  4. pygpt_net/controller/chat/handler/xai_stream.py +621 -52
  5. pygpt_net/controller/debug/fixtures.py +3 -2
  6. pygpt_net/controller/files/files.py +65 -4
  7. pygpt_net/core/filesystem/url.py +4 -1
  8. pygpt_net/core/render/web/body.py +3 -2
  9. pygpt_net/core/types/chunk.py +27 -0
  10. pygpt_net/data/config/config.json +2 -2
  11. pygpt_net/data/config/models.json +2 -2
  12. pygpt_net/data/config/settings.json +1 -1
  13. pygpt_net/data/js/app/template.js +1 -1
  14. pygpt_net/data/js/app.min.js +2 -2
  15. pygpt_net/data/locale/locale.de.ini +3 -0
  16. pygpt_net/data/locale/locale.en.ini +3 -0
  17. pygpt_net/data/locale/locale.es.ini +3 -0
  18. pygpt_net/data/locale/locale.fr.ini +3 -0
  19. pygpt_net/data/locale/locale.it.ini +3 -0
  20. pygpt_net/data/locale/locale.pl.ini +3 -0
  21. pygpt_net/data/locale/locale.uk.ini +3 -0
  22. pygpt_net/data/locale/locale.zh.ini +3 -0
  23. pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +2 -2
  24. pygpt_net/item/ctx.py +3 -5
  25. pygpt_net/js_rc.py +2449 -2447
  26. pygpt_net/plugin/cmd_mouse_control/config.py +8 -7
  27. pygpt_net/plugin/cmd_mouse_control/plugin.py +3 -4
  28. pygpt_net/provider/api/anthropic/__init__.py +10 -8
  29. pygpt_net/provider/api/google/__init__.py +6 -5
  30. pygpt_net/provider/api/google/chat.py +1 -2
  31. pygpt_net/provider/api/openai/__init__.py +7 -3
  32. pygpt_net/provider/api/openai/responses.py +0 -0
  33. pygpt_net/provider/api/x_ai/__init__.py +10 -9
  34. pygpt_net/provider/api/x_ai/chat.py +272 -102
  35. pygpt_net/tools/image_viewer/ui/dialogs.py +298 -12
  36. pygpt_net/tools/text_editor/ui/widgets.py +5 -1
  37. pygpt_net/ui/base/context_menu.py +44 -1
  38. pygpt_net/ui/layout/toolbox/indexes.py +22 -19
  39. pygpt_net/ui/layout/toolbox/model.py +28 -5
  40. pygpt_net/ui/widget/image/display.py +25 -8
  41. pygpt_net/ui/widget/tabs/output.py +9 -1
  42. pygpt_net/ui/widget/textarea/editor.py +14 -1
  43. pygpt_net/ui/widget/textarea/input.py +20 -7
  44. pygpt_net/ui/widget/textarea/notepad.py +24 -1
  45. pygpt_net/ui/widget/textarea/output.py +23 -1
  46. pygpt_net/ui/widget/textarea/web.py +16 -1
  47. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.6.dist-info}/METADATA +10 -2
  48. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.6.dist-info}/RECORD +50 -49
  49. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.6.dist-info}/LICENSE +0 -0
  50. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.6.dist-info}/WHEEL +0 -0
  51. {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.6.dist-info}/entry_points.txt +0 -0
@@ -6,17 +6,18 @@
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: 2026.01.03 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from PySide6.QtCore import Qt
12
+ from PySide6.QtCore import Qt, QPoint, QSize, QEvent
13
13
  from PySide6.QtGui import QAction, QIcon
14
- from PySide6.QtWidgets import QMenuBar, QVBoxLayout, QHBoxLayout, QSizePolicy
14
+ from PySide6.QtWidgets import QMenuBar, QVBoxLayout, QHBoxLayout, QSizePolicy, QScrollArea
15
15
 
16
16
  from pygpt_net.ui.widget.dialog.base import BaseDialog
17
17
  from pygpt_net.ui.widget.image.display import ImageLabel
18
18
  from pygpt_net.utils import trans
19
19
 
20
+
20
21
  class DialogSpawner:
21
22
  def __init__(self, window=None):
22
23
  """
@@ -44,8 +45,14 @@ class DialogSpawner:
44
45
  pixmap = ImageLabel(dialog, self.path)
45
46
  pixmap.setSizePolicy(QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored))
46
47
 
48
+ # scrollable container for pixmap
49
+ scroll = QScrollArea(dialog)
50
+ scroll.setWidgetResizable(True)
51
+ scroll.setAlignment(Qt.AlignCenter)
52
+ scroll.setWidget(pixmap)
53
+
47
54
  row = QHBoxLayout()
48
- row.addWidget(pixmap)
55
+ row.addWidget(scroll)
49
56
 
50
57
  layout = QVBoxLayout()
51
58
  layout.addLayout(row)
@@ -53,6 +60,10 @@ class DialogSpawner:
53
60
  dialog.append_layout(layout)
54
61
  dialog.source = source
55
62
  dialog.pixmap = pixmap
63
+ dialog.scroll_area = scroll
64
+
65
+ # install event filter for wheel zoom and panning and cursor changes
66
+ dialog.scroll_area.viewport().installEventFilter(dialog)
56
67
 
57
68
  return dialog
58
69
 
@@ -74,13 +85,32 @@ class ImageViewerDialog(BaseDialog):
74
85
  self.actions = {}
75
86
  self.source = None
76
87
  self.pixmap = None
88
+ self.scroll_area = None
89
+
90
+ # cache for image change / resize handling
77
91
  self._last_src_key = 0
78
92
  self._last_target_size = None
93
+
94
+ # icons
79
95
  self._icon_add = QIcon(":/icons/add.svg")
80
96
  self._icon_folder = QIcon(":/icons/folder.svg")
81
97
  self._icon_save = QIcon(":/icons/save.svg")
82
98
  self._icon_logout = QIcon(":/icons/logout.svg")
83
99
 
100
+ # zoom / pan state
101
+ self._zoom_mode = 'fit' # 'fit' or 'manual'
102
+ self._zoom_factor = 1.0 # current manual factor (image space)
103
+ self._fit_factor = 1.0 # computed on-the-fly for fit mode
104
+ self._min_zoom = 0.05
105
+ self._max_zoom = 16.0
106
+ self._zoom_step = 1.25
107
+ self._drag_active = False
108
+ self._drag_last_pos = QPoint()
109
+
110
+ # performance guards to prevent extremely huge widget sizes
111
+ self._max_widget_dim = 32768 # max single dimension (px) for the view widget
112
+ self._max_total_pixels = 80_000_000 # max total pixels of the view widget (about 80MP)
113
+
84
114
  def append_layout(self, layout):
85
115
  """
86
116
  Update layout
@@ -98,18 +128,30 @@ class ImageViewerDialog(BaseDialog):
98
128
  """
99
129
  src = self.source.pixmap() if self.source is not None else None
100
130
  if src and not src.isNull() and self.pixmap is not None:
101
- target_size = self.pixmap.size()
102
131
  key = src.cacheKey()
103
- if key != self._last_src_key or target_size != self._last_target_size:
104
- self.pixmap.setPixmap(
105
- src.scaled(
132
+ # reset to fit mode on new image
133
+ if key != self._last_src_key:
134
+ self._zoom_mode = 'fit'
135
+ self._zoom_factor = 1.0
136
+ if self.scroll_area is not None:
137
+ self.scroll_area.setWidgetResizable(True)
138
+ # ensure no distortion in fit mode
139
+ if self.pixmap is not None:
140
+ self.pixmap.setScaledContents(False)
141
+
142
+ if self._zoom_mode == 'fit':
143
+ target_size = self._viewport_size()
144
+ if key != self._last_src_key or target_size != self._last_target_size:
145
+ # scale to viewport while keeping aspect ratio, smooth transform
146
+ scaled = src.scaled(
106
147
  target_size,
107
148
  Qt.KeepAspectRatio,
108
149
  Qt.SmoothTransformation
109
150
  )
110
- )
111
- self._last_src_key = key
112
- self._last_target_size = target_size
151
+ self.pixmap.setPixmap(scaled)
152
+ self._fit_factor = self._compute_fit_factor(src.size(), target_size)
153
+ self._last_src_key = key
154
+ self._last_target_size = target_size
113
155
  super(ImageViewerDialog, self).resizeEvent(event)
114
156
 
115
157
  def setup_menu(self) -> QMenuBar:
@@ -152,4 +194,248 @@ class ImageViewerDialog(BaseDialog):
152
194
  self.file_menu.addAction(self.actions["save_as"])
153
195
  self.file_menu.addAction(self.actions["exit"])
154
196
 
155
- return self.menu_bar
197
+ return self.menu_bar
198
+
199
+ # =========================
200
+ # Zoom / pan implementation
201
+ # =========================
202
+
203
+ def eventFilter(self, obj, event):
204
+ """
205
+ Handle wheel zoom, panning and cursor changes on the scroll area viewport.
206
+ """
207
+ if self.scroll_area is None or obj is not self.scroll_area.viewport():
208
+ return super(ImageViewerDialog, self).eventFilter(obj, event)
209
+
210
+ et = event.type()
211
+
212
+ # Always show grab cursor when mouse is over the image area
213
+ if et == QEvent.Enter:
214
+ if self._has_image():
215
+ self.scroll_area.viewport().setCursor(Qt.OpenHandCursor)
216
+ return False
217
+
218
+ if et == QEvent.Leave:
219
+ self.scroll_area.viewport().unsetCursor()
220
+ return False
221
+
222
+ if et == QEvent.MouseButtonPress:
223
+ if event.button() == Qt.LeftButton and self._can_drag():
224
+ self._drag_active = True
225
+ self._drag_last_pos = self._event_pos(event)
226
+ self.scroll_area.viewport().setCursor(Qt.ClosedHandCursor)
227
+ event.accept()
228
+ return True
229
+ return False
230
+
231
+ if et == QEvent.MouseMove:
232
+ if self._drag_active:
233
+ pos = self._event_pos(event)
234
+ delta = pos - self._drag_last_pos
235
+ self._drag_last_pos = pos
236
+ # move scrollbars opposite to mouse movement
237
+ hbar = self.scroll_area.horizontalScrollBar()
238
+ vbar = self.scroll_area.verticalScrollBar()
239
+ hbar.setValue(hbar.value() - delta.x())
240
+ vbar.setValue(vbar.value() - delta.y())
241
+ event.accept()
242
+ return True
243
+ return False
244
+
245
+ if et == QEvent.MouseButtonRelease:
246
+ if event.button() == Qt.LeftButton and self._drag_active:
247
+ self._drag_active = False
248
+ # back to grab cursor
249
+ if self._has_image():
250
+ self.scroll_area.viewport().setCursor(Qt.OpenHandCursor)
251
+ event.accept()
252
+ return True
253
+ return False
254
+
255
+ if et == QEvent.Wheel:
256
+ # zoom in/out with mouse wheel
257
+ if not self._has_image():
258
+ return False
259
+
260
+ # start manual zoom from current fit factor if needed
261
+ if self._zoom_mode == 'fit':
262
+ self._fit_factor = self._compute_fit_factor(
263
+ self.source.pixmap().size(),
264
+ self._viewport_size()
265
+ )
266
+ self._zoom_factor = self._fit_factor
267
+ self._zoom_mode = 'manual'
268
+ # switch to manual rendering path: original pixmap + scaled contents
269
+ self.scroll_area.setWidgetResizable(False)
270
+ self.pixmap.setScaledContents(True)
271
+ # ensure we display original image for better performance (no giant intermediate pixmaps)
272
+ self.pixmap.setPixmap(self.source.pixmap())
273
+
274
+ old_w = max(1, self.pixmap.width())
275
+ old_h = max(1, self.pixmap.height())
276
+
277
+ angle = 0
278
+ try:
279
+ angle = event.angleDelta().y()
280
+ if angle == 0:
281
+ angle = event.angleDelta().x()
282
+ except Exception:
283
+ pass
284
+
285
+ if angle == 0:
286
+ return False
287
+
288
+ step = self._zoom_step if angle > 0 else (1.0 / self._zoom_step)
289
+ # compute tentative new factor and clamp by hard min/max and size guards
290
+ tentative = self._zoom_factor * step
291
+ tentative = max(self._min_zoom, min(self._max_zoom, tentative))
292
+ # apply size-based guards to avoid extremely huge widget sizes
293
+ tentative = self._clamp_factor_by_size(tentative)
294
+
295
+ if abs(tentative - self._zoom_factor) < 1e-9:
296
+ event.accept()
297
+ return True
298
+
299
+ vp_pos = self._event_pos(event)
300
+ hbar = self.scroll_area.horizontalScrollBar()
301
+ vbar = self.scroll_area.verticalScrollBar()
302
+
303
+ # position in content coords before zoom (keep point under cursor stable)
304
+ content_x = hbar.value() + vp_pos.x()
305
+ content_y = vbar.value() + vp_pos.y()
306
+ rx = content_x / float(old_w)
307
+ ry = content_y / float(old_h)
308
+
309
+ self._zoom_factor = tentative
310
+ self._set_scaled_pixmap_by_factor(self._zoom_factor)
311
+
312
+ new_w = max(1, self.pixmap.width())
313
+ new_h = max(1, self.pixmap.height())
314
+
315
+ hbar.setValue(int(rx * new_w - vp_pos.x()))
316
+ vbar.setValue(int(ry * new_h - vp_pos.y()))
317
+
318
+ event.accept()
319
+ return True
320
+
321
+ return super(ImageViewerDialog, self).eventFilter(obj, event)
322
+
323
+ def _has_image(self) -> bool:
324
+ """Check if source image is available."""
325
+ return (
326
+ self.source is not None
327
+ and self.source.pixmap() is not None
328
+ and not self.source.pixmap().isNull()
329
+ and self.pixmap is not None
330
+ )
331
+
332
+ def _can_drag(self) -> bool:
333
+ """Allow dragging only when image does not fit into the viewport."""
334
+ if not self._has_image():
335
+ return False
336
+ if self._zoom_mode != 'manual':
337
+ return False
338
+ vp = self._viewport_size()
339
+ return self.pixmap.width() > vp.width() or self.pixmap.height() > vp.height()
340
+
341
+ def _viewport_size(self) -> QSize:
342
+ """Get current viewport size."""
343
+ if self.scroll_area is not None:
344
+ return self.scroll_area.viewport().size()
345
+ return self.size()
346
+
347
+ def _compute_fit_factor(self, img_size: QSize, target: QSize) -> float:
348
+ """Compute factor for fitting image into target size."""
349
+ iw = max(1, img_size.width())
350
+ ih = max(1, img_size.height())
351
+ tw = max(1, target.width())
352
+ th = max(1, target.height())
353
+ return min(tw / float(iw), th / float(ih))
354
+
355
+ def _clamp_factor_by_size(self, factor: float) -> float:
356
+ """
357
+ Clamp zoom factor to avoid creating extremely large widget sizes.
358
+ This keeps interactivity smooth by limiting max resulting dimensions and total pixels.
359
+ """
360
+ src = self.source.pixmap()
361
+ if not src or src.isNull():
362
+ return factor
363
+
364
+ iw = max(1, src.width())
365
+ ih = max(1, src.height())
366
+
367
+ # only guard when zooming in; zooming out should not be limited by these caps
368
+ if factor <= 1.0:
369
+ return factor
370
+
371
+ # desired size
372
+ dw = iw * factor
373
+ dh = ih * factor
374
+
375
+ scale = 1.0
376
+
377
+ # total pixel cap
378
+ total = dw * dh
379
+ if total > self._max_total_pixels:
380
+ from math import sqrt
381
+ scale = min(scale, sqrt(self._max_total_pixels / float(total)))
382
+
383
+ # dimension caps
384
+ if dw * scale > self._max_widget_dim:
385
+ scale = min(scale, self._max_widget_dim / float(dw))
386
+ if dh * scale > self._max_widget_dim:
387
+ scale = min(scale, self._max_widget_dim / float(dh))
388
+
389
+ if scale < 1.0:
390
+ return max(self._min_zoom, factor * scale)
391
+ return factor
392
+
393
+ def _set_scaled_pixmap_by_factor(self, factor: float):
394
+ """
395
+ Scale and display the image using provided factor relative to the original image.
396
+ In manual mode this avoids allocating giant intermediate QPixmaps by:
397
+ - drawing the original pixmap;
398
+ - enabling scaled contents;
399
+ - resizing the label to the required size.
400
+ """
401
+ if not self._has_image():
402
+ return
403
+
404
+ src = self.source.pixmap()
405
+ iw = max(1, src.width())
406
+ ih = max(1, src.height())
407
+
408
+ # target size based on factor (KeepAspectRatio preserved by proportional math)
409
+ new_w = max(1, int(round(iw * factor)))
410
+ new_h = max(1, int(round(ih * factor)))
411
+
412
+ # enforce guards once more to be safe
413
+ guarded_factor = self._clamp_factor_by_size(factor)
414
+ if abs(guarded_factor - factor) > 1e-9:
415
+ new_w = max(1, int(round(iw * guarded_factor)))
416
+ new_h = max(1, int(round(ih * guarded_factor)))
417
+ self._zoom_factor = guarded_factor # keep internal factor in sync
418
+
419
+ if self._zoom_mode == 'manual':
420
+ # ensure manual path uses original pixmap and scaled contents
421
+ if self.pixmap.pixmap() is None or self.pixmap.pixmap().cacheKey() != src.cacheKey():
422
+ self.pixmap.setPixmap(src)
423
+ self.pixmap.setScaledContents(True)
424
+ self.pixmap.resize(new_w, new_h)
425
+ else:
426
+ # fallback (not expected here): keep classic high-quality scaling
427
+ scaled = src.scaled(new_w, new_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
428
+ self.pixmap.setScaledContents(False)
429
+ self.pixmap.setPixmap(scaled)
430
+ if self.scroll_area is not None:
431
+ self.scroll_area.setWidgetResizable(True)
432
+
433
+ def _event_pos(self, event) -> QPoint:
434
+ """
435
+ Extract integer QPoint from mouse/touchpad event position (supports QPointF in 6.9+).
436
+ """
437
+ if hasattr(event, "position"):
438
+ return event.position().toPoint()
439
+ if hasattr(event, "pos"):
440
+ return event.pos()
441
+ return QPoint(0, 0)
@@ -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.08.24 23:00:00 #
9
+ # Updated Date: 2026.01.03 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtGui import QAction, QIcon, QKeySequence
@@ -67,6 +67,10 @@ class TextFileEditor(BaseCodeEditor):
67
67
  )
68
68
  menu.addAction(action)
69
69
 
70
+ # Add zoom submenu
71
+ zoom_menu = self.window.ui.context_menu.get_zoom_menu(self, "editor", self.value, self.on_zoom_changed)
72
+ menu.addMenu(zoom_menu)
73
+
70
74
  action = QAction(self._icon_search, trans('text.context_menu.find'), menu)
71
75
  action.triggered.connect(self.find_open)
72
76
  action.setShortcut(QKeySequence.StandardKey.Find)
@@ -6,8 +6,9 @@
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: 2026.01.03 00:00:00 #
10
10
  # ================================================== #
11
+ from typing import Union
11
12
 
12
13
  from PySide6.QtWidgets import QMenu
13
14
  from PySide6.QtGui import QAction, QIcon
@@ -24,6 +25,8 @@ class ContextMenu:
24
25
  _ICON_CODE = QIcon(":/icons/code.svg")
25
26
  _ICON_TEXT = QIcon(":/icons/text.svg")
26
27
  _ICON_TRANSLATOR = QIcon(":/icons/translate.svg")
28
+ _ICON_ZOOM_IN = QIcon(":/icons/zoom_in.svg")
29
+ _ICON_ZOOM_OUT = QIcon(":/icons/zoom_out.svg")
27
30
 
28
31
  def __init__(self, window=None):
29
32
  """
@@ -33,6 +36,46 @@ class ContextMenu:
33
36
  """
34
37
  self.window = window
35
38
 
39
+ def get_zoom_menu(self, parent, parent_type: str, current_zoom: Union[int, float], callback = None) -> QMenu:
40
+ """
41
+ Get zoom menu (Zoom In/Out)
42
+
43
+ :param parent: Parent menu
44
+ :param parent_type: Type of textarea ('chat', 'notepad', etc.)
45
+ :param current_zoom: Current zoom level
46
+ :param callback: Callback function on zoom change
47
+ :return: Menu
48
+ """
49
+ menu = QMenu(trans('context_menu.zoom'), parent)
50
+ if parent_type in ("font_size.input", "font_size", "editor"):
51
+ action_zoom_in = QAction(self._ICON_ZOOM_IN, trans('context_menu.zoom.in'), menu)
52
+ new_zoom = current_zoom + 2
53
+ action_zoom_in.triggered.connect(
54
+ lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
55
+ )
56
+ menu.addAction(action_zoom_in)
57
+ action_zoom_out = QAction(self._ICON_ZOOM_OUT, trans('context_menu.zoom.out'), menu)
58
+ new_zoom = current_zoom - 2
59
+ action_zoom_out.triggered.connect(
60
+ lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
61
+ )
62
+ menu.addAction(action_zoom_out)
63
+ elif parent_type in ("zoom"):
64
+ action_zoom_in = QAction(self._ICON_ZOOM_IN, trans('context_menu.zoom.in'), menu)
65
+ new_zoom = current_zoom + 0.1
66
+ action_zoom_in.triggered.connect(
67
+ lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
68
+ )
69
+ menu.addAction(action_zoom_in)
70
+ action_zoom_out = QAction(self._ICON_ZOOM_OUT, trans('context_menu.zoom.out'), menu)
71
+ new_zoom = current_zoom - 0.1
72
+ action_zoom_out.triggered.connect(
73
+ lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
74
+ )
75
+ menu.addAction(action_zoom_out)
76
+
77
+ return menu
78
+
36
79
  def get_copy_to_menu(
37
80
  self,
38
81
  parent,
@@ -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.08.24 23:00:00 #
9
+ # Updated Date: 2026.01.03 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from PySide6.QtGui import QStandardItemModel, Qt, QIcon
12
+ from PySide6.QtCore import Qt, QSize
13
+ from PySide6.QtGui import QStandardItemModel, QIcon
13
14
  from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QCheckBox, QSizePolicy
14
15
 
15
16
  from pygpt_net.ui.widget.element.labels import HelpLabel, TitleLabel
@@ -54,6 +55,14 @@ class Indexes:
54
55
  """
55
56
  nodes = self.window.ui.nodes
56
57
  nodes['indexes.new'] = QPushButton(self._settings_icon, "")
58
+ # Configure compact, borderless settings button aligned to the right
59
+ icon_size = 20
60
+ nodes['indexes.new'].setFlat(True)
61
+ nodes['indexes.new'].setStyleSheet("QPushButton { border: none; background: transparent; padding: 0; }")
62
+ nodes['indexes.new'].setIconSize(QSize(icon_size, icon_size))
63
+ nodes['indexes.new'].setFixedSize(icon_size, icon_size)
64
+ nodes['indexes.new'].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
65
+ nodes['indexes.new'].setFocusPolicy(Qt.NoFocus)
57
66
  nodes['indexes.new'].clicked.connect(self._open_llama_index_settings)
58
67
 
59
68
  nodes['indexes.label'] = TitleLabel(trans("toolbox.indexes.label"))
@@ -119,6 +128,14 @@ class Indexes:
119
128
  nodes['llama_index.mode.select'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
120
129
 
121
130
  nodes['indexes.new'] = QPushButton(self._settings_icon, "")
131
+ # Configure compact, borderless settings button for options row
132
+ icon_size = 20
133
+ nodes['indexes.new'].setFlat(True)
134
+ nodes['indexes.new'].setStyleSheet("QPushButton { border: none; background: transparent; padding: 0; }")
135
+ nodes['indexes.new'].setIconSize(QSize(icon_size, icon_size))
136
+ nodes['indexes.new'].setFixedSize(icon_size, icon_size)
137
+ nodes['indexes.new'].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
138
+ nodes['indexes.new'].setFocusPolicy(Qt.NoFocus)
122
139
  nodes['indexes.new'].clicked.connect(self._open_llama_index_settings)
123
140
 
124
141
  nodes['indexes.label'] = TitleLabel(trans("toolbox.indexes.label"))
@@ -129,6 +146,8 @@ class Indexes:
129
146
  idx_layout.addWidget(nodes['indexes.select'])
130
147
  idx_layout.addWidget(nodes['indexes.new'], alignment=Qt.AlignRight)
131
148
  idx_layout.setContentsMargins(0, 0, 0, 10)
149
+ # Ensure the combo takes maximum horizontal space
150
+ idx_layout.setStretch(1, 1)
132
151
  idx_widget = QWidget()
133
152
  idx_widget.setLayout(idx_layout)
134
153
  idx_widget.setMinimumHeight(55)
@@ -138,6 +157,7 @@ class Indexes:
138
157
  mode_layout.addWidget(nodes['llama_index.mode.label'])
139
158
  mode_layout.addWidget(nodes['llama_index.mode.select'])
140
159
  mode_layout.setContentsMargins(0, 0, 0, 10)
160
+ mode_layout.setStretch(1, 1)
141
161
  mode_widget = QWidget()
142
162
  mode_widget.setLayout(mode_layout)
143
163
  mode_widget.setMinimumHeight(55)
@@ -173,23 +193,6 @@ class Indexes:
173
193
  if self._last_combo_signature != signature:
174
194
  self.window.ui.nodes['indexes.select'].set_keys(combo_keys)
175
195
  self._last_combo_signature = signature
176
- """
177
- # store previous selection
178
- self.window.ui.nodes[self.id].backup_selection()
179
-
180
- self.window.ui.models[self.id].removeRows(0, self.window.ui.models[self.id].rowCount())
181
- i = 0
182
- for item in data:
183
- self.window.ui.models[self.id].insertRow(i)
184
- name = item['name']
185
- index = self.window.ui.models[self.id].index(i, 0)
186
- self.window.ui.models[self.id].setData(index, "ID: " + item['id'], QtCore.Qt.ToolTipRole)
187
- self.window.ui.models[self.id].setData(self.window.ui.models[self.id].index(i, 0), name)
188
- i += 1
189
-
190
- # restore previous selection
191
- self.window.ui.nodes[self.id].restore_selection()
192
- """
193
196
 
194
197
  def _open_llama_index_settings(self):
195
198
  self.window.controller.settings.open_section('llama-index')
@@ -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.08.24 23:00:00 #
9
+ # Updated Date: 2026.01.03 17:00:00 #
10
10
  # ================================================== #
11
-
12
- from PySide6.QtWidgets import QVBoxLayout, QWidget
11
+ from PySide6.QtCore import Qt, QSize
12
+ from PySide6.QtGui import QIcon
13
+ from PySide6.QtWidgets import QVBoxLayout, QWidget, QPushButton, QHBoxLayout, QSizePolicy
13
14
 
14
15
  from pygpt_net.ui.widget.element.labels import TitleLabel
15
16
  from pygpt_net.ui.widget.lists.model_combo import ModelCombo
@@ -27,6 +28,7 @@ class Model:
27
28
  self.window = window
28
29
  self.id = 'prompt.model'
29
30
  self.label_key = f'{self.id}.label'
31
+ self._settings_icon = QIcon(":/icons/settings.svg")
30
32
 
31
33
  def setup(self) -> QWidget:
32
34
  """
@@ -51,12 +53,33 @@ class Model:
51
53
  nodes[self.label_key] = label
52
54
 
53
55
  combo = ModelCombo(self.window, self.id)
56
+ # Ensure combo takes maximum horizontal space
57
+ combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
54
58
  nodes[self.id] = combo
55
59
 
60
+ nodes['prompt.model.settings'] = QPushButton(self._settings_icon, "")
61
+ # Configure compact, borderless settings button aligned to the right
62
+ icon_size = 20
63
+ nodes['prompt.model.settings'].setFlat(True)
64
+ nodes['prompt.model.settings'].setStyleSheet("QPushButton { border: none; padding: 0; }")
65
+ nodes['prompt.model.settings'].setIconSize(QSize(icon_size, icon_size))
66
+ nodes['prompt.model.settings'].setFixedSize(icon_size, icon_size)
67
+ nodes['prompt.model.settings'].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
68
+ nodes['prompt.model.settings'].setFocusPolicy(Qt.NoFocus)
69
+ nodes['prompt.model.settings'].clicked.connect(self._open_settings)
70
+
71
+ model_cols = QHBoxLayout()
72
+ model_cols.addWidget(combo, 1) # stretch to take remaining space
73
+ model_cols.addWidget(nodes['prompt.model.settings'], alignment=Qt.AlignRight)
74
+ model_cols.setContentsMargins(0, 0, 0, 0)
75
+
56
76
  layout = QVBoxLayout()
57
77
  layout.addWidget(label)
58
- layout.addWidget(combo)
78
+ layout.addLayout(model_cols)
59
79
  layout.addStretch()
60
80
  layout.setContentsMargins(2, 5, 5, 5)
61
81
 
62
- return layout
82
+ return layout
83
+
84
+ def _open_settings(self):
85
+ self.window.controller.model.editor.open()
@@ -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.08.24 23:00:00 #
9
+ # Updated Date: 2026.01.03 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtGui import QAction, QIcon
@@ -27,6 +27,17 @@ class ImageLabel(QLabel):
27
27
  self.window = window
28
28
  self.path = path
29
29
 
30
+ def _get_window(self):
31
+ """
32
+ Get main window instance
33
+
34
+ :return: Window instance
35
+ """
36
+ if self.window is not None:
37
+ if hasattr(self.window, 'window'):
38
+ return self.window.window
39
+ return self.window
40
+
30
41
  def contextMenuEvent(self, event):
31
42
  """
32
43
  Context menu event
@@ -36,6 +47,8 @@ class ImageLabel(QLabel):
36
47
  if not self.path:
37
48
  return
38
49
 
50
+ win = self._get_window()
51
+
39
52
  actions = {}
40
53
  use_actions = []
41
54
  actions['open'] = QAction(QIcon(":/icons/fullscreen.svg"), trans('img.action.open'), self)
@@ -64,13 +77,13 @@ class ImageLabel(QLabel):
64
77
  self,
65
78
  )
66
79
  actions['use_attachment'].triggered.connect(
67
- lambda: self.window.controller.files.use_attachment(self.path),
80
+ lambda: win.controller.files.use_attachment(self.path),
68
81
  )
69
82
  use_actions.append(actions['use_attachment'])
70
83
 
71
84
  # use by filetype
72
- if self.window.core.filesystem.actions.has_use(self.path):
73
- extra_use_actions = self.window.core.filesystem.actions.get_use(self, self.path)
85
+ if win.core.filesystem.actions.has_use(self.path):
86
+ extra_use_actions = win.core.filesystem.actions.get_use(self, self.path)
74
87
  for action in extra_use_actions:
75
88
  use_actions.append(action)
76
89
 
@@ -97,7 +110,8 @@ class ImageLabel(QLabel):
97
110
 
98
111
  :param event: mouse event
99
112
  """
100
- self.window.tools.get("viewer").open(self.path)
113
+ win = self._get_window()
114
+ win.tools.get("viewer").open(self.path)
101
115
 
102
116
  def action_open_dir(self, event):
103
117
  """
@@ -105,7 +119,8 @@ class ImageLabel(QLabel):
105
119
 
106
120
  :param event: mouse event
107
121
  """
108
- self.window.tools.get("viewer").open_dir(self.path)
122
+ win = self._get_window()
123
+ win.tools.get("viewer").open_dir(self.path)
109
124
 
110
125
  def action_save(self, event):
111
126
  """
@@ -113,7 +128,8 @@ class ImageLabel(QLabel):
113
128
 
114
129
  :param event: mouse event
115
130
  """
116
- self.window.tools.get("viewer").save(self.path)
131
+ win = self._get_window()
132
+ win.tools.get("viewer").save(self.path)
117
133
 
118
134
  def action_delete(self, event):
119
135
  """
@@ -121,4 +137,5 @@ class ImageLabel(QLabel):
121
137
 
122
138
  :param event: mouse event
123
139
  """
124
- self.window.tools.get("viewer").delete(self.path)
140
+ win = self._get_window()
141
+ win.tools.get("viewer").delete(self.path)