pygpt-net 2.6.22__py3-none-any.whl → 2.6.24__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 +16 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/agent/llama.py +3 -0
- pygpt_net/controller/chat/response.py +6 -1
- pygpt_net/controller/files/files.py +24 -55
- pygpt_net/controller/theme/theme.py +3 -3
- pygpt_net/core/agents/observer/evaluation.py +2 -2
- pygpt_net/core/agents/runners/loop.py +1 -0
- pygpt_net/core/attachments/context.py +4 -4
- pygpt_net/core/bridge/bridge.py +2 -0
- pygpt_net/core/filesystem/opener.py +261 -0
- pygpt_net/core/filesystem/url.py +13 -10
- pygpt_net/core/idx/chat.py +1 -1
- pygpt_net/core/idx/indexing.py +3 -3
- pygpt_net/core/idx/llm.py +61 -2
- pygpt_net/core/platforms/platforms.py +5 -4
- pygpt_net/data/config/config.json +21 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +18 -0
- pygpt_net/data/css/web-blocks.dark.css +7 -1
- pygpt_net/data/css/web-blocks.light.css +5 -2
- pygpt_net/data/css/web-chatgpt.dark.css +7 -1
- pygpt_net/data/css/web-chatgpt.light.css +3 -0
- pygpt_net/data/css/web-chatgpt_wide.dark.css +7 -1
- pygpt_net/data/css/web-chatgpt_wide.light.css +3 -0
- pygpt_net/data/locale/locale.de.ini +47 -0
- pygpt_net/data/locale/locale.en.ini +50 -1
- pygpt_net/data/locale/locale.es.ini +47 -0
- pygpt_net/data/locale/locale.fr.ini +47 -0
- pygpt_net/data/locale/locale.it.ini +47 -0
- pygpt_net/data/locale/locale.pl.ini +47 -0
- pygpt_net/data/locale/locale.uk.ini +47 -0
- pygpt_net/data/locale/locale.zh.ini +47 -0
- pygpt_net/provider/agents/llama_index/codeact_workflow.py +8 -7
- pygpt_net/provider/agents/llama_index/planner_workflow.py +11 -10
- pygpt_net/provider/agents/llama_index/supervisor_workflow.py +9 -8
- pygpt_net/provider/agents/openai/agent_b2b.py +30 -17
- pygpt_net/provider/agents/openai/agent_planner.py +29 -29
- pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +21 -23
- pygpt_net/provider/agents/openai/agent_with_feedback.py +21 -23
- pygpt_net/provider/agents/openai/bot_researcher.py +25 -30
- pygpt_net/provider/agents/openai/evolve.py +37 -39
- pygpt_net/provider/agents/openai/supervisor.py +16 -18
- pygpt_net/provider/core/config/patch.py +20 -1
- pygpt_net/provider/llms/anthropic.py +5 -4
- pygpt_net/provider/llms/google.py +2 -2
- pygpt_net/ui/layout/toolbox/agent_llama.py +2 -3
- pygpt_net/ui/widget/tabs/layout.py +6 -4
- pygpt_net/ui/widget/tabs/output.py +348 -13
- pygpt_net/ui/widget/textarea/input.py +74 -8
- {pygpt_net-2.6.22.dist-info → pygpt_net-2.6.24.dist-info}/METADATA +34 -25
- {pygpt_net-2.6.22.dist-info → pygpt_net-2.6.24.dist-info}/RECORD +55 -54
- {pygpt_net-2.6.22.dist-info → pygpt_net-2.6.24.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.22.dist-info → pygpt_net-2.6.24.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.22.dist-info → pygpt_net-2.6.24.dist-info}/entry_points.txt +0 -0
|
@@ -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.08.
|
|
9
|
+
# Updated Date: 2025.08.25 18:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.QtWidgets import QTabWidget, QMenu, QPushButton
|
|
13
|
-
from PySide6.QtCore import Qt, Slot
|
|
12
|
+
from PySide6.QtWidgets import QTabWidget, QMenu, QPushButton, QToolButton, QTabBar
|
|
13
|
+
from PySide6.QtCore import Qt, Slot, QTimer, QEvent
|
|
14
14
|
from PySide6.QtGui import QAction, QIcon, QGuiApplication
|
|
15
15
|
|
|
16
16
|
from pygpt_net.core.tabs.tab import Tab
|
|
@@ -18,6 +18,7 @@ from pygpt_net.utils import trans
|
|
|
18
18
|
|
|
19
19
|
_ICON_CACHE = {}
|
|
20
20
|
|
|
21
|
+
|
|
21
22
|
def icon(path: str) -> QIcon:
|
|
22
23
|
if QGuiApplication.instance() is None:
|
|
23
24
|
return QIcon()
|
|
@@ -27,6 +28,7 @@ def icon(path: str) -> QIcon:
|
|
|
27
28
|
_ICON_CACHE[path] = cached
|
|
28
29
|
return cached
|
|
29
30
|
|
|
31
|
+
|
|
30
32
|
ICON_PATH_ADD = ':/icons/add.svg'
|
|
31
33
|
ICON_PATH_EDIT = ':/icons/edit.svg'
|
|
32
34
|
ICON_PATH_CLOSE = ':/icons/close.svg'
|
|
@@ -35,6 +37,267 @@ ICON_PATH_FORWARD = ':/icons/forward'
|
|
|
35
37
|
ICON_PATH_BACK = ':/icons/back'
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
class OutputTabBar(QTabBar):
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
window=None,
|
|
44
|
+
column=None,
|
|
45
|
+
tabs=None,
|
|
46
|
+
corner_button=None,
|
|
47
|
+
parent=None
|
|
48
|
+
):
|
|
49
|
+
super().__init__(parent)
|
|
50
|
+
self.window = window
|
|
51
|
+
self.column = column
|
|
52
|
+
self.tabs = tabs
|
|
53
|
+
self.corner_button = corner_button
|
|
54
|
+
|
|
55
|
+
# inline [+] just after the last tab (only when there is real free space)
|
|
56
|
+
self.add_btn_inline = AddButton(window, column, tabs)
|
|
57
|
+
self.add_btn_inline.setParent(self)
|
|
58
|
+
self.add_btn_inline.setVisible(False)
|
|
59
|
+
self.add_btn_inline.raise_()
|
|
60
|
+
|
|
61
|
+
# visual gap between last tab and [+]
|
|
62
|
+
self._spacing = 3
|
|
63
|
+
|
|
64
|
+
# add button vertical offset (to align with text)
|
|
65
|
+
self._inline_y_offset = 2 # px up
|
|
66
|
+
|
|
67
|
+
# keep tabs natural width (do not stretch)
|
|
68
|
+
self.setExpanding(False)
|
|
69
|
+
|
|
70
|
+
# allow scroll buttons when needed
|
|
71
|
+
self.setUsesScrollButtons(True)
|
|
72
|
+
|
|
73
|
+
# ensure the tab bar stays visible even with 0 tabs
|
|
74
|
+
self._min_bar_height = max(self.add_btn_inline.sizeHint().height() + 6, 28)
|
|
75
|
+
self.setMinimumHeight(self._min_bar_height)
|
|
76
|
+
|
|
77
|
+
if hasattr(self.tabs, "setTabBarAutoHide"):
|
|
78
|
+
self.tabs.setTabBarAutoHide(False)
|
|
79
|
+
|
|
80
|
+
# state
|
|
81
|
+
self._inline_mode = False
|
|
82
|
+
self._corner_current = None # track where the corner [+] currently is
|
|
83
|
+
self._last_inline_pos = None # track last inline position to avoid useless moves
|
|
84
|
+
|
|
85
|
+
# coalesced updates (debounce to 1 per event-loop turn)
|
|
86
|
+
self._update_timer = QTimer(self)
|
|
87
|
+
self._update_timer.setSingleShot(True)
|
|
88
|
+
self._update_timer.timeout.connect(self.updateAddButtonPlacement)
|
|
89
|
+
|
|
90
|
+
# re-layout updates
|
|
91
|
+
self.currentChanged.connect(lambda _: self._queue_update())
|
|
92
|
+
self.tabMoved.connect(lambda _from, _to: self._queue_update())
|
|
93
|
+
|
|
94
|
+
# initial placement
|
|
95
|
+
QTimer.singleShot(0, self.updateAddButtonPlacement)
|
|
96
|
+
|
|
97
|
+
def sizeHint(self):
|
|
98
|
+
"""
|
|
99
|
+
Override sizeHint to
|
|
100
|
+
|
|
101
|
+
:return: QSize
|
|
102
|
+
"""
|
|
103
|
+
# keep a non-zero height even with 0 tabs
|
|
104
|
+
sh = super().sizeHint()
|
|
105
|
+
sh.setHeight(max(sh.height(), self._min_bar_height))
|
|
106
|
+
return sh
|
|
107
|
+
|
|
108
|
+
def minimumSizeHint(self):
|
|
109
|
+
"""
|
|
110
|
+
Override minimumSizeHint
|
|
111
|
+
|
|
112
|
+
:return: QSize
|
|
113
|
+
"""
|
|
114
|
+
m = super().minimumSizeHint()
|
|
115
|
+
m.setHeight(max(m.height(), self._min_bar_height))
|
|
116
|
+
return m
|
|
117
|
+
|
|
118
|
+
def _queue_update(self):
|
|
119
|
+
"""Queue an update to recompute [+] placement (debounced)."""
|
|
120
|
+
# Coalesce many triggers into a single call at the end of the event loop.
|
|
121
|
+
self._update_timer.start(0)
|
|
122
|
+
|
|
123
|
+
def _visible_scroll_buttons(self):
|
|
124
|
+
"""
|
|
125
|
+
Find the left and right scroll buttons if they are visible.
|
|
126
|
+
|
|
127
|
+
:return: (left_button, right_button) or (None, None) if not found
|
|
128
|
+
"""
|
|
129
|
+
# find the scroll arrow buttons created by QTabBar
|
|
130
|
+
left = right = None
|
|
131
|
+
for btn in self.findChildren(QToolButton):
|
|
132
|
+
if not btn.isVisible():
|
|
133
|
+
continue
|
|
134
|
+
if not btn.autoRepeat():
|
|
135
|
+
continue
|
|
136
|
+
if left is None or btn.x() < left.x():
|
|
137
|
+
left = btn
|
|
138
|
+
if right is None or btn.x() > right.x():
|
|
139
|
+
right = btn
|
|
140
|
+
return left, right
|
|
141
|
+
|
|
142
|
+
def _set_corner_target(self, corner: Qt.Corner | None) -> bool:
|
|
143
|
+
"""
|
|
144
|
+
Move the corner_button to a given corner (or detach it) only when it changes.
|
|
145
|
+
|
|
146
|
+
:param corner: target corner or None to detach
|
|
147
|
+
:return: True if changed, False if no change was needed
|
|
148
|
+
"""
|
|
149
|
+
if self.corner_button is None:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
if self._corner_current == corner:
|
|
153
|
+
return False # nothing to do
|
|
154
|
+
|
|
155
|
+
# detach only from the previously used corner
|
|
156
|
+
if self._corner_current is not None:
|
|
157
|
+
self.tabs.setCornerWidget(None, self._corner_current)
|
|
158
|
+
|
|
159
|
+
if corner is not None:
|
|
160
|
+
self.tabs.setCornerWidget(self.corner_button, corner)
|
|
161
|
+
|
|
162
|
+
self._corner_current = corner
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def _column_index(self) -> int:
|
|
166
|
+
"""
|
|
167
|
+
Get the column index this tab bar belongs to (0 or 1).
|
|
168
|
+
|
|
169
|
+
:return: Column index
|
|
170
|
+
"""
|
|
171
|
+
idx = 0
|
|
172
|
+
if self.column is None:
|
|
173
|
+
return idx
|
|
174
|
+
return int(self.column.get_idx())
|
|
175
|
+
|
|
176
|
+
def updateAddButtonPlacement(self):
|
|
177
|
+
"""
|
|
178
|
+
Recompute where the [+] button should be placed (inline or corner).
|
|
179
|
+
|
|
180
|
+
This method is called automatically on relevant events.
|
|
181
|
+
1. If there are no tabs, show [+] in the left or right corner based on column index.
|
|
182
|
+
2. If there are tabs but scroll buttons are visible (overflow), show [+] in the top-right corner.
|
|
183
|
+
3. If there are tabs and no scroll buttons, show [+] inline after the last tab if there's room.
|
|
184
|
+
4. Otherwise, show [+] in the top-right corner.
|
|
185
|
+
"""
|
|
186
|
+
# CASE 1: no tabs -> show [+] in left or right corner based on column index
|
|
187
|
+
if self.count() == 0:
|
|
188
|
+
idx = self._column_index()
|
|
189
|
+
corner = Qt.TopLeftCorner if idx == 0 else Qt.TopRightCorner
|
|
190
|
+
|
|
191
|
+
changed = False
|
|
192
|
+
changed |= self._set_corner_target(corner)
|
|
193
|
+
|
|
194
|
+
if self._inline_mode:
|
|
195
|
+
self._inline_mode = False
|
|
196
|
+
changed = True
|
|
197
|
+
|
|
198
|
+
if self.add_btn_inline.isVisible():
|
|
199
|
+
self.add_btn_inline.setVisible(False)
|
|
200
|
+
changed = True
|
|
201
|
+
|
|
202
|
+
if self.corner_button is not None and not self.corner_button.isVisible():
|
|
203
|
+
self.corner_button.setVisible(True)
|
|
204
|
+
changed = True
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
# CASE 2: tabs exist
|
|
208
|
+
# if scroll buttons are visible we are in overflow -> use top-right corner [+]
|
|
209
|
+
left_sb, right_sb = self._visible_scroll_buttons()
|
|
210
|
+
if left_sb or right_sb:
|
|
211
|
+
changed = False
|
|
212
|
+
if self._inline_mode:
|
|
213
|
+
self._inline_mode = False
|
|
214
|
+
changed = True
|
|
215
|
+
if self.add_btn_inline.isVisible():
|
|
216
|
+
self.add_btn_inline.setVisible(False)
|
|
217
|
+
changed = True
|
|
218
|
+
changed |= self._set_corner_target(Qt.TopRightCorner)
|
|
219
|
+
if self.corner_button is not None and not self.corner_button.isVisible():
|
|
220
|
+
self.corner_button.setVisible(True)
|
|
221
|
+
changed = True
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# otherwise, put [+] inline (right after the last tab) if there's real room
|
|
225
|
+
last_rect = self.tabRect(self.count() - 1)
|
|
226
|
+
x = last_rect.right() + 1 + self._spacing
|
|
227
|
+
want_inline = (x + self.add_btn_inline.width()) <= (self.width() - 1)
|
|
228
|
+
|
|
229
|
+
if want_inline:
|
|
230
|
+
changed = False
|
|
231
|
+
|
|
232
|
+
if not self._inline_mode:
|
|
233
|
+
self._inline_mode = True
|
|
234
|
+
changed = True
|
|
235
|
+
|
|
236
|
+
# hide any corner [+]
|
|
237
|
+
if self.corner_button is not None and self.corner_button.isVisible():
|
|
238
|
+
self.corner_button.setVisible(False)
|
|
239
|
+
changed = True
|
|
240
|
+
changed |= self._set_corner_target(None)
|
|
241
|
+
|
|
242
|
+
# compute position
|
|
243
|
+
y = (self.height() - self.add_btn_inline.height()) // 2
|
|
244
|
+
# NOTE: lift inline [+] slightly to align with tabs
|
|
245
|
+
y = max(0, y - self._inline_y_offset) # clamp to avoid negative
|
|
246
|
+
x = min(x, self.width() - self.add_btn_inline.width() - 1) # clamp inside the bar
|
|
247
|
+
new_pos = (x, y)
|
|
248
|
+
if self._last_inline_pos != new_pos:
|
|
249
|
+
self.add_btn_inline.move(x, y)
|
|
250
|
+
self._last_inline_pos = new_pos
|
|
251
|
+
changed = True
|
|
252
|
+
|
|
253
|
+
if not self.add_btn_inline.isVisible():
|
|
254
|
+
self.add_btn_inline.setVisible(True)
|
|
255
|
+
changed = True
|
|
256
|
+
|
|
257
|
+
self.add_btn_inline.raise_()
|
|
258
|
+
else:
|
|
259
|
+
# not enough room -> top-right corner
|
|
260
|
+
changed = False
|
|
261
|
+
if self._inline_mode:
|
|
262
|
+
self._inline_mode = False
|
|
263
|
+
changed = True
|
|
264
|
+
if self.add_btn_inline.isVisible():
|
|
265
|
+
self.add_btn_inline.setVisible(False)
|
|
266
|
+
changed = True
|
|
267
|
+
changed |= self._set_corner_target(Qt.TopRightCorner)
|
|
268
|
+
if self.corner_button is not None and not self.corner_button.isVisible():
|
|
269
|
+
self.corner_button.setVisible(True)
|
|
270
|
+
changed = True
|
|
271
|
+
|
|
272
|
+
def resizeEvent(self, event):
|
|
273
|
+
"""Resize event handler to recompute [+] placement."""
|
|
274
|
+
super().resizeEvent(event)
|
|
275
|
+
self._queue_update()
|
|
276
|
+
|
|
277
|
+
def showEvent(self, event):
|
|
278
|
+
"""Show event handler to recompute [+] placement."""
|
|
279
|
+
super().showEvent(event)
|
|
280
|
+
self._queue_update()
|
|
281
|
+
|
|
282
|
+
def tabInserted(self, index):
|
|
283
|
+
"""Tab inserted event handler to recompute [+] placement."""
|
|
284
|
+
super().tabInserted(index)
|
|
285
|
+
self._queue_update()
|
|
286
|
+
|
|
287
|
+
def tabRemoved(self, index):
|
|
288
|
+
"""Tab removed event handler to recompute [+] placement."""
|
|
289
|
+
super().tabRemoved(index)
|
|
290
|
+
self._queue_update()
|
|
291
|
+
|
|
292
|
+
def event(self, e):
|
|
293
|
+
"""Event handler to catch layout/style changes that may affect [+] placement."""
|
|
294
|
+
res = super().event(e)
|
|
295
|
+
# only queue updates
|
|
296
|
+
if e.type() in (QEvent.LayoutRequest, QEvent.StyleChange, QEvent.PolishRequest, QEvent.FontChange):
|
|
297
|
+
self._queue_update()
|
|
298
|
+
return res
|
|
299
|
+
|
|
300
|
+
|
|
38
301
|
class AddButton(QPushButton):
|
|
39
302
|
def __init__(self, window=None, column=None, tabs=None):
|
|
40
303
|
super(AddButton, self).__init__(icon(ICON_PATH_ADD), "", window)
|
|
@@ -100,6 +363,7 @@ class AddButton(QPushButton):
|
|
|
100
363
|
|
|
101
364
|
return menu
|
|
102
365
|
|
|
366
|
+
|
|
103
367
|
class OutputTabs(QTabWidget):
|
|
104
368
|
def __init__(self, window=None, column=None):
|
|
105
369
|
super(OutputTabs, self).__init__(window)
|
|
@@ -111,14 +375,6 @@ class OutputTabs(QTabWidget):
|
|
|
111
375
|
self.setMovable(True)
|
|
112
376
|
self.init()
|
|
113
377
|
|
|
114
|
-
def set_active(self, active: bool):
|
|
115
|
-
"""Set the active state of the tab bar."""
|
|
116
|
-
self.active = active
|
|
117
|
-
if self.active:
|
|
118
|
-
self.setStyleSheet("QTabBar::tab { border-bottom-width: 2px; }")
|
|
119
|
-
else:
|
|
120
|
-
self.setStyleSheet("QTabBar::tab { border-bottom-width: 0px; }")
|
|
121
|
-
|
|
122
378
|
def init(self):
|
|
123
379
|
"""Initialize"""
|
|
124
380
|
# create the [+] button
|
|
@@ -127,6 +383,38 @@ class OutputTabs(QTabWidget):
|
|
|
127
383
|
# add the button to the top right corner of the tab bar
|
|
128
384
|
self.setCornerWidget(add_button, corner=Qt.TopRightCorner)
|
|
129
385
|
|
|
386
|
+
self.setDocumentMode(True)
|
|
387
|
+
|
|
388
|
+
# use a custom tab bar that shows an inline [+] right after the tabs
|
|
389
|
+
tab_bar = OutputTabBar(
|
|
390
|
+
window=self.window,
|
|
391
|
+
column=self.column,
|
|
392
|
+
tabs=self,
|
|
393
|
+
corner_button=add_button,
|
|
394
|
+
parent=self,
|
|
395
|
+
)
|
|
396
|
+
self.setTabBar(tab_bar)
|
|
397
|
+
self.setMovable(True)
|
|
398
|
+
self.tabBar().setMovable(True)
|
|
399
|
+
|
|
400
|
+
self.setDocumentMode(True) # QT Material fix
|
|
401
|
+
self.tabBar().setDrawBase(False) # QT Material fix
|
|
402
|
+
|
|
403
|
+
# the custom tab bar decides when to show inline or corner [+]
|
|
404
|
+
add_button.setVisible(False)
|
|
405
|
+
|
|
406
|
+
# ensure initial recompute happens after the first layout pass
|
|
407
|
+
QTimer.singleShot(0, self._refresh_plus_button)
|
|
408
|
+
|
|
409
|
+
# tab bar visible even when empty
|
|
410
|
+
if hasattr(self, "setTabBarAutoHide"):
|
|
411
|
+
self.setTabBarAutoHide(False)
|
|
412
|
+
|
|
413
|
+
# IMPORTANT: reserve vertical space for the bar even with 0 tabs
|
|
414
|
+
# (prevents the whole widget from collapsing)
|
|
415
|
+
mh = max(self.tabBar().minimumSizeHint().height() + 2, 30) # +2
|
|
416
|
+
self.setMinimumHeight(mh)
|
|
417
|
+
|
|
130
418
|
# connect signals
|
|
131
419
|
self.currentChanged.connect(self._on_current_changed)
|
|
132
420
|
self.tabBarClicked.connect(self._on_tabbar_clicked)
|
|
@@ -134,6 +422,47 @@ class OutputTabs(QTabWidget):
|
|
|
134
422
|
self.tabCloseRequested.connect(self._on_tab_close_requested)
|
|
135
423
|
self.tabBar().tabMoved.connect(self._on_tab_moved)
|
|
136
424
|
|
|
425
|
+
def set_active(self, active: bool):
|
|
426
|
+
"""
|
|
427
|
+
Set the active state of the tab bar.
|
|
428
|
+
|
|
429
|
+
:param active: True to activate, False to deactivate
|
|
430
|
+
"""
|
|
431
|
+
self.active = active
|
|
432
|
+
|
|
433
|
+
def _refresh_plus_button(self):
|
|
434
|
+
"""Force the tab bar to recompute [+] placement after tab changes."""
|
|
435
|
+
tb = self.tabBar()
|
|
436
|
+
if hasattr(tb, "updateAddButtonPlacement"):
|
|
437
|
+
tb.updateAddButtonPlacement()
|
|
438
|
+
|
|
439
|
+
def addTab(self, *args, **kwargs):
|
|
440
|
+
"""Add a new tab and refresh [+] placement."""
|
|
441
|
+
idx = super().addTab(*args, **kwargs)
|
|
442
|
+
QTimer.singleShot(0, self._refresh_plus_button) # defer until layout is done
|
|
443
|
+
return idx
|
|
444
|
+
|
|
445
|
+
def insertTab(self, *args, **kwargs):
|
|
446
|
+
"""Insert a new tab at a specific index and refresh [+] placement."""
|
|
447
|
+
idx = super().insertTab(*args, **kwargs)
|
|
448
|
+
QTimer.singleShot(0, self._refresh_plus_button)
|
|
449
|
+
return idx
|
|
450
|
+
|
|
451
|
+
def removeTab(self, index):
|
|
452
|
+
"""Remove a tab and refresh [+] placement."""
|
|
453
|
+
super().removeTab(index)
|
|
454
|
+
QTimer.singleShot(0, self._refresh_plus_button)
|
|
455
|
+
|
|
456
|
+
def setTabText(self, index: int, text: str):
|
|
457
|
+
"""Set tab text and refresh [+] placement."""
|
|
458
|
+
super().setTabText(index, text)
|
|
459
|
+
QTimer.singleShot(0, self._refresh_plus_button)
|
|
460
|
+
|
|
461
|
+
def clear(self):
|
|
462
|
+
"""Clear all tabs and refresh [+] placement."""
|
|
463
|
+
super().clear()
|
|
464
|
+
QTimer.singleShot(0, self._refresh_plus_button)
|
|
465
|
+
|
|
137
466
|
def get_column(self):
|
|
138
467
|
"""
|
|
139
468
|
Get column
|
|
@@ -170,7 +499,7 @@ class OutputTabs(QTabWidget):
|
|
|
170
499
|
elif tab.type == Tab.TAB_TOOL:
|
|
171
500
|
self.show_tool_menu(idx, column_idx, event.globalPos()) # tool
|
|
172
501
|
else:
|
|
173
|
-
self.show_default_menu(idx, column_idx, event.globalPos())
|
|
502
|
+
self.show_default_menu(idx, column_idx, event.globalPos()) # default
|
|
174
503
|
super(OutputTabs, self).mousePressEvent(event)
|
|
175
504
|
|
|
176
505
|
def prepare_menu(self, index: int, column_idx: int) -> QMenu:
|
|
@@ -313,22 +642,28 @@ class OutputTabs(QTabWidget):
|
|
|
313
642
|
|
|
314
643
|
@Slot(int)
|
|
315
644
|
def _on_current_changed(self, _idx: int):
|
|
645
|
+
"""On current tab changed"""
|
|
316
646
|
self.window.controller.ui.tabs.on_tab_changed(self.currentIndex(), self.column.get_idx())
|
|
317
647
|
|
|
318
648
|
@Slot(int)
|
|
319
649
|
def _on_tabbar_clicked(self, _idx: int):
|
|
650
|
+
"""On tab bar clicked"""
|
|
320
651
|
self.window.controller.ui.tabs.on_tab_clicked(self.currentIndex(), self.column.get_idx())
|
|
321
652
|
|
|
322
653
|
@Slot(int)
|
|
323
654
|
def _on_tabbar_dbl_clicked(self, _idx: int):
|
|
655
|
+
"""On tab bar double clicked"""
|
|
324
656
|
self.window.controller.ui.tabs.on_tab_dbl_clicked(self.currentIndex(), self.column.get_idx())
|
|
325
657
|
|
|
326
658
|
@Slot(int)
|
|
327
659
|
def _on_tab_close_requested(self, _idx: int):
|
|
660
|
+
"""On tab close requested"""
|
|
328
661
|
self.window.controller.ui.tabs.on_tab_closed(self.currentIndex(), self.column.get_idx())
|
|
662
|
+
QTimer.singleShot(0, self._refresh_plus_button) # defer until layout is done
|
|
329
663
|
|
|
330
664
|
@Slot(int, int)
|
|
331
665
|
def _on_tab_moved(self, _from: int, _to: int):
|
|
666
|
+
"""On tab moved"""
|
|
332
667
|
self.window.controller.ui.tabs.on_tab_moved(self.currentIndex(), self.column.get_idx())
|
|
333
668
|
|
|
334
669
|
@Slot()
|
|
@@ -375,7 +710,7 @@ class OutputTabs(QTabWidget):
|
|
|
375
710
|
index = self.window.core.tabs.get_max_idx_by_column(column_idx)
|
|
376
711
|
if index == -1:
|
|
377
712
|
index = 0
|
|
378
|
-
|
|
713
|
+
|
|
379
714
|
self.window.controller.ui.tabs.append(
|
|
380
715
|
type=type,
|
|
381
716
|
tool_id=tool_id,
|
|
@@ -6,13 +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.
|
|
9
|
+
# Updated Date: 2025.08.25 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6 import
|
|
13
|
-
from PySide6.QtCore import Qt
|
|
12
|
+
from PySide6.QtCore import Qt, QSize
|
|
14
13
|
from PySide6.QtGui import QAction, QIcon, QImage
|
|
15
|
-
from PySide6.QtWidgets import QTextEdit, QApplication
|
|
14
|
+
from PySide6.QtWidgets import QTextEdit, QApplication, QPushButton
|
|
16
15
|
|
|
17
16
|
from pygpt_net.utils import trans
|
|
18
17
|
|
|
@@ -21,6 +20,7 @@ class ChatInput(QTextEdit):
|
|
|
21
20
|
ICON_PASTE = QIcon(":/icons/paste.svg")
|
|
22
21
|
ICON_VOLUME = QIcon(":/icons/volume.svg")
|
|
23
22
|
ICON_SAVE = QIcon(":/icons/save.svg")
|
|
23
|
+
ICON_ATTACHMENT = QIcon(":/icons/add.svg")
|
|
24
24
|
|
|
25
25
|
def __init__(self, window=None):
|
|
26
26
|
"""
|
|
@@ -35,9 +35,24 @@ class ChatInput(QTextEdit):
|
|
|
35
35
|
self.value = self.window.core.config.data['font_size.input']
|
|
36
36
|
self.max_font_size = 42
|
|
37
37
|
self.min_font_size = 8
|
|
38
|
+
self._text_top_padding = 12
|
|
38
39
|
self.textChanged.connect(self.window.controller.ui.update_tokens)
|
|
39
40
|
self.setProperty('class', 'layout-input')
|
|
40
41
|
|
|
42
|
+
# Add a "+" button in the top-left corner to add attachments
|
|
43
|
+
self._init_attachment_button()
|
|
44
|
+
self._apply_text_top_padding()
|
|
45
|
+
|
|
46
|
+
def _apply_text_top_padding(self):
|
|
47
|
+
"""Apply extra top padding inside the text area by using viewport margins."""
|
|
48
|
+
m = self.viewportMargins()
|
|
49
|
+
self.setViewportMargins(m.left(), self._text_top_padding, m.right(), m.bottom())
|
|
50
|
+
|
|
51
|
+
def set_text_top_padding(self, px: int):
|
|
52
|
+
"""Public helper to adjust top padding at runtime."""
|
|
53
|
+
self._text_top_padding = max(0, int(px))
|
|
54
|
+
self._apply_text_top_padding()
|
|
55
|
+
|
|
41
56
|
def insertFromMimeData(self, source):
|
|
42
57
|
"""
|
|
43
58
|
Insert from mime data
|
|
@@ -136,16 +151,16 @@ class ChatInput(QTextEdit):
|
|
|
136
151
|
"""
|
|
137
152
|
handled = False
|
|
138
153
|
key = event.key()
|
|
139
|
-
if key in (
|
|
154
|
+
if key in (Qt.Key_Return, Qt.Key_Enter):
|
|
140
155
|
mode = self.window.core.config.get('send_mode')
|
|
141
156
|
if mode > 0:
|
|
142
157
|
modifiers = event.modifiers()
|
|
143
158
|
if mode == 2:
|
|
144
|
-
if modifiers ==
|
|
159
|
+
if modifiers == Qt.ShiftModifier or modifiers == Qt.ControlModifier:
|
|
145
160
|
self.window.controller.chat.input.send_input()
|
|
146
161
|
handled = True
|
|
147
162
|
else:
|
|
148
|
-
if modifiers !=
|
|
163
|
+
if modifiers != Qt.ShiftModifier and modifiers != Qt.ControlModifier:
|
|
149
164
|
self.window.controller.chat.input.send_input()
|
|
150
165
|
handled = True
|
|
151
166
|
self.setFocus()
|
|
@@ -178,4 +193,55 @@ class ChatInput(QTextEdit):
|
|
|
178
193
|
self.window.controller.ui.update_font_size()
|
|
179
194
|
event.accept()
|
|
180
195
|
return
|
|
181
|
-
super().wheelEvent(event)
|
|
196
|
+
super().wheelEvent(event)
|
|
197
|
+
|
|
198
|
+
# -------------------- Attachment button (top-left) --------------------
|
|
199
|
+
|
|
200
|
+
def _init_attachment_button(self):
|
|
201
|
+
"""Create and place the '+' attachment button pinned in the top-left corner."""
|
|
202
|
+
self._attach_margin = 6 # inner padding around the button
|
|
203
|
+
self._attach_offset_y = -4 # shift the button 2px up
|
|
204
|
+
|
|
205
|
+
self._attach_btn = QPushButton(self)
|
|
206
|
+
self._attach_btn.setObjectName("chatInputAttachBtn")
|
|
207
|
+
self._attach_btn.setIconSize(QSize(18, 18)) # icon size (slightly larger for visibility)
|
|
208
|
+
self._attach_btn.setFixedSize(24, 24) # full button size
|
|
209
|
+
self._attach_btn.setCursor(Qt.PointingHandCursor)
|
|
210
|
+
self._attach_btn.setToolTip(trans("attachments.btn.input.add"))
|
|
211
|
+
self._attach_btn.setFocusPolicy(Qt.NoFocus)
|
|
212
|
+
self._attach_btn.setFlat(True) # flat button style
|
|
213
|
+
|
|
214
|
+
self._attach_btn.setIcon(self.ICON_ATTACHMENT)
|
|
215
|
+
self._attach_btn.clicked.connect(self.action_add_attachment)
|
|
216
|
+
self._update_viewport_margins_for_attachment()
|
|
217
|
+
self._reposition_attachment_button()
|
|
218
|
+
|
|
219
|
+
def _update_viewport_margins_for_attachment(self):
|
|
220
|
+
"""Reserve space for the attachment button on the left and apply top text padding."""
|
|
221
|
+
top = self._text_top_padding
|
|
222
|
+
left = self._attach_btn.width() + self._attach_margin * 2 if hasattr(self, "_attach_btn") else self.viewportMargins().left()
|
|
223
|
+
self.setViewportMargins(left, top, 0, 0)
|
|
224
|
+
|
|
225
|
+
def _reposition_attachment_button(self):
|
|
226
|
+
"""Keep the attachment button pinned to the top-left corner."""
|
|
227
|
+
if hasattr(self, "_attach_btn"):
|
|
228
|
+
fw = self.frameWidth()
|
|
229
|
+
x = fw + self._attach_margin
|
|
230
|
+
y = fw + self._attach_margin + self._attach_offset_y # shift up by ~2px
|
|
231
|
+
if y < 0:
|
|
232
|
+
y = 0
|
|
233
|
+
self._attach_btn.move(x, y)
|
|
234
|
+
self._attach_btn.raise_()
|
|
235
|
+
|
|
236
|
+
def resizeEvent(self, event):
|
|
237
|
+
"""Resize event keeps the attachment button in place."""
|
|
238
|
+
super().resizeEvent(event)
|
|
239
|
+
# Keep the attachment button pinned when resizing
|
|
240
|
+
try:
|
|
241
|
+
self._reposition_attachment_button()
|
|
242
|
+
except Exception:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
def action_add_attachment(self):
|
|
246
|
+
"""Add attachment (button click)."""
|
|
247
|
+
self.window.controller.attachment.open_add()
|