pygpt-net 2.6.67__py3-none-any.whl → 2.7.1__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 (78) hide show
  1. pygpt_net/CHANGELOG.txt +20 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/assistant/assistant.py +13 -8
  4. pygpt_net/controller/assistant/batch.py +29 -15
  5. pygpt_net/controller/assistant/files.py +19 -14
  6. pygpt_net/controller/assistant/store.py +63 -41
  7. pygpt_net/controller/attachment/attachment.py +45 -35
  8. pygpt_net/controller/chat/attachment.py +50 -39
  9. pygpt_net/controller/config/field/dictionary.py +26 -14
  10. pygpt_net/controller/ctx/common.py +27 -17
  11. pygpt_net/controller/ctx/ctx.py +185 -101
  12. pygpt_net/controller/files/files.py +101 -41
  13. pygpt_net/controller/idx/indexer.py +87 -31
  14. pygpt_net/controller/kernel/kernel.py +13 -2
  15. pygpt_net/controller/mode/mode.py +3 -3
  16. pygpt_net/controller/model/editor.py +70 -15
  17. pygpt_net/controller/model/importer.py +153 -54
  18. pygpt_net/controller/painter/common.py +43 -11
  19. pygpt_net/controller/painter/painter.py +2 -2
  20. pygpt_net/controller/presets/experts.py +68 -15
  21. pygpt_net/controller/presets/presets.py +72 -36
  22. pygpt_net/controller/settings/profile.py +76 -35
  23. pygpt_net/controller/settings/workdir.py +70 -39
  24. pygpt_net/core/assistants/files.py +20 -18
  25. pygpt_net/core/filesystem/actions.py +111 -10
  26. pygpt_net/core/filesystem/filesystem.py +72 -1
  27. pygpt_net/core/filesystem/packer.py +161 -1
  28. pygpt_net/core/idx/idx.py +12 -11
  29. pygpt_net/core/idx/worker.py +13 -1
  30. pygpt_net/core/image/image.py +2 -2
  31. pygpt_net/core/models/models.py +4 -4
  32. pygpt_net/core/profile/profile.py +13 -3
  33. pygpt_net/core/video/video.py +2 -3
  34. pygpt_net/data/config/config.json +3 -3
  35. pygpt_net/data/config/models.json +3 -3
  36. pygpt_net/data/css/style.dark.css +45 -0
  37. pygpt_net/data/css/style.light.css +46 -0
  38. pygpt_net/data/locale/locale.de.ini +5 -1
  39. pygpt_net/data/locale/locale.en.ini +5 -1
  40. pygpt_net/data/locale/locale.es.ini +5 -1
  41. pygpt_net/data/locale/locale.fr.ini +5 -1
  42. pygpt_net/data/locale/locale.it.ini +5 -1
  43. pygpt_net/data/locale/locale.pl.ini +6 -2
  44. pygpt_net/data/locale/locale.uk.ini +5 -1
  45. pygpt_net/data/locale/locale.zh.ini +5 -1
  46. pygpt_net/provider/api/openai/__init__.py +4 -2
  47. pygpt_net/provider/core/config/patch.py +17 -1
  48. pygpt_net/tools/image_viewer/tool.py +17 -0
  49. pygpt_net/tools/text_editor/tool.py +9 -0
  50. pygpt_net/ui/__init__.py +2 -2
  51. pygpt_net/ui/dialog/preset.py +1 -0
  52. pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
  53. pygpt_net/ui/layout/toolbox/image.py +2 -1
  54. pygpt_net/ui/layout/toolbox/indexes.py +2 -0
  55. pygpt_net/ui/layout/toolbox/video.py +5 -1
  56. pygpt_net/ui/main.py +3 -1
  57. pygpt_net/ui/widget/calendar/select.py +3 -3
  58. pygpt_net/ui/widget/draw/painter.py +238 -51
  59. pygpt_net/ui/widget/filesystem/explorer.py +1164 -142
  60. pygpt_net/ui/widget/lists/assistant.py +185 -24
  61. pygpt_net/ui/widget/lists/assistant_store.py +245 -42
  62. pygpt_net/ui/widget/lists/attachment.py +230 -47
  63. pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
  64. pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
  65. pygpt_net/ui/widget/lists/context.py +1253 -70
  66. pygpt_net/ui/widget/lists/experts.py +110 -8
  67. pygpt_net/ui/widget/lists/model_editor.py +217 -14
  68. pygpt_net/ui/widget/lists/model_importer.py +125 -6
  69. pygpt_net/ui/widget/lists/preset.py +460 -71
  70. pygpt_net/ui/widget/lists/profile.py +149 -27
  71. pygpt_net/ui/widget/lists/uploaded.py +230 -38
  72. pygpt_net/ui/widget/option/combo.py +1211 -33
  73. pygpt_net/ui/widget/option/dictionary.py +35 -7
  74. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/METADATA +22 -57
  75. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/RECORD +78 -78
  76. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/LICENSE +0 -0
  77. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/WHEEL +0 -0
  78. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/entry_points.txt +0 -0
@@ -6,12 +6,12 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 23:00:00 #
9
+ # Updated Date: 2025.12.27 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import QItemSelectionModel, QPoint
13
13
  from PySide6.QtGui import QAction, QIcon, Qt
14
- from PySide6.QtWidgets import QMenu
14
+ from PySide6.QtWidgets import QMenu, QAbstractItemView
15
15
 
16
16
  from pygpt_net.ui.widget.lists.base import BaseList
17
17
  from pygpt_net.utils import trans
@@ -20,7 +20,7 @@ from pygpt_net.utils import trans
20
20
  class AssistantList(BaseList):
21
21
  def __init__(self, window=None, id=None):
22
22
  """
23
- Presets select menu
23
+ Assistants select menu
24
24
 
25
25
  :param window: main window
26
26
  :param id: input id
@@ -34,7 +34,61 @@ class AssistantList(BaseList):
34
34
  self.restore_after_ctx_menu = True
35
35
  self._backup_selection = None
36
36
 
37
+ # Enable row-based multi-select with Ctrl/Shift gestures
38
+ self.setSelectionBehavior(QAbstractItemView.SelectRows)
39
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection)
40
+
41
+ # Virtual multi-select helpers to suppress business click after Ctrl/Shift
42
+ self._suppress_item_click = False
43
+ self._ctrl_multi_active = False
44
+ self._ctrl_multi_index = None
45
+ self._was_shift_click = False
46
+
47
+ # ----------------------------
48
+ # Helpers
49
+ # ----------------------------
50
+
51
+ def _selected_indexes(self):
52
+ """Return list of selected row indexes (column 0)."""
53
+ try:
54
+ return list(self.selectionModel().selectedRows())
55
+ except Exception:
56
+ return []
57
+
58
+ def _selected_rows(self) -> list[int]:
59
+ """Return list of selected row numbers."""
60
+ try:
61
+ return [ix.row() for ix in self.selectionModel().selectedRows()]
62
+ except Exception:
63
+ return []
64
+
65
+ def _has_multi_selection(self) -> bool:
66
+ """Check whether more than one row is selected."""
67
+ try:
68
+ return len(self.selectionModel().selectedRows()) > 1
69
+ except Exception:
70
+ return False
71
+
72
+ # ----------------------------
73
+ # Clicks
74
+ # ----------------------------
75
+
37
76
  def click(self, val):
77
+ """
78
+ Row click handler.
79
+
80
+ Suppresses business click when triggered by virtual Ctrl/Shift multi-select
81
+ and when multiple rows are selected.
82
+ """
83
+ # Skip business click right after Ctrl/Shift selection
84
+ if self._suppress_item_click:
85
+ self._suppress_item_click = False
86
+ return
87
+
88
+ # Ignore business click if multiple are selected
89
+ if self._has_multi_selection():
90
+ return
91
+
38
92
  self.window.controller.assistant.select(val.row())
39
93
 
40
94
  def dblclick(self, val):
@@ -43,7 +97,13 @@ class AssistantList(BaseList):
43
97
 
44
98
  :param val: double click event
45
99
  """
46
- self.window.controller.assistant.editor.edit(val.row())
100
+ row = val.row()
101
+ if row >= 0:
102
+ self.window.controller.assistant.editor.edit(row)
103
+
104
+ # ----------------------------
105
+ # Context menu
106
+ # ----------------------------
47
107
 
48
108
  def show_context_menu(self, pos: QPoint):
49
109
  """
@@ -52,24 +112,41 @@ class AssistantList(BaseList):
52
112
  :param pos: QPoint
53
113
  """
54
114
  global_pos = self.viewport().mapToGlobal(pos)
55
- item = self.indexAt(pos)
115
+ index = self.indexAt(pos)
116
+ idx = index.row() if index.isValid() else -1
117
+
118
+ selected_rows = self._selected_rows()
119
+ multi = len(selected_rows) > 1
120
+
121
+ # Allow menu on empty area only when multi-selection is active
122
+ if not index.isValid() and not multi:
123
+ return
56
124
 
57
125
  actions = {}
58
126
  actions['edit'] = QAction(QIcon(":/icons/edit.svg"), trans('assistant.action.edit'), self)
127
+ actions['delete'] = QAction(QIcon(":/icons/delete.svg"), trans('assistant.action.delete'), self)
128
+
129
+ # Edit only for single selection
130
+ actions['edit'].setEnabled(idx >= 0 and not multi)
59
131
  actions['edit'].triggered.connect(
60
- lambda checked=False, item=item: self.action_edit(item))
132
+ lambda checked=False, item=index: self.action_edit(item)
133
+ )
61
134
 
62
- actions['delete'] = QAction(QIcon(":/icons/delete.svg"), trans('assistant.action.delete'), self)
63
- actions['delete'].triggered.connect(
64
- lambda checked=False, item=item: self.action_delete(item))
135
+ # Delete for single or multi; pass list when multi
136
+ if multi:
137
+ actions['delete'].triggered.connect(
138
+ lambda checked=False, rows=list(selected_rows): self.action_delete(rows)
139
+ )
140
+ else:
141
+ actions['delete'].triggered.connect(
142
+ lambda checked=False, item=index: self.action_delete(item)
143
+ )
65
144
 
66
145
  menu = QMenu(self)
67
146
  menu.addAction(actions['edit'])
68
147
  menu.addAction(actions['delete'])
69
148
 
70
- idx = item.row()
71
- if idx >= 0:
72
- #self.window.controller.assistant.select(item.row())
149
+ if idx >= 0 or multi:
73
150
  menu.exec_(global_pos)
74
151
 
75
152
  # store previous scroll position
@@ -79,9 +156,9 @@ class AssistantList(BaseList):
79
156
  if self.restore_after_ctx_menu:
80
157
  if self._backup_selection is not None:
81
158
  self.selectionModel().clearSelection()
82
- for idx in self._backup_selection:
159
+ for i in self._backup_selection:
83
160
  self.selectionModel().select(
84
- idx, QItemSelectionModel.Select | QItemSelectionModel.Rows
161
+ i, QItemSelectionModel.Select | QItemSelectionModel.Rows
85
162
  )
86
163
  self._backup_selection = None
87
164
 
@@ -89,11 +166,15 @@ class AssistantList(BaseList):
89
166
  self.restore_after_ctx_menu = True
90
167
  self.restore_scroll_position()
91
168
 
169
+ # ----------------------------
170
+ # Context actions
171
+ # ----------------------------
172
+
92
173
  def action_edit(self, item):
93
174
  """
94
175
  Edit action handler
95
176
 
96
- :param item: list item
177
+ :param item: QModelIndex
97
178
  """
98
179
  idx = item.row()
99
180
  if idx >= 0:
@@ -104,35 +185,115 @@ class AssistantList(BaseList):
104
185
  """
105
186
  Delete action handler
106
187
 
107
- :param item: list item
188
+ :param item: QModelIndex for single, or list[int] for multi
108
189
  """
190
+ if isinstance(item, (list, tuple)):
191
+ self.restore_after_ctx_menu = False # do not restore selection after context menu
192
+ self.window.controller.assistant.delete(list(item))
193
+ return
194
+
109
195
  idx = item.row()
110
196
  if idx >= 0:
111
197
  self.restore_after_ctx_menu = False # do not restore selection after context menu
112
198
  self.window.controller.assistant.delete(idx)
113
199
 
200
+ # ----------------------------
201
+ # Mouse events (virtual multi-select)
202
+ # ----------------------------
203
+
114
204
  def mousePressEvent(self, event):
205
+ # Ctrl+Left: virtual toggle without business click
206
+ if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ControlModifier):
207
+ idx = self.indexAt(event.pos())
208
+ self._suppress_item_click = True
209
+ if idx.isValid():
210
+ self._ctrl_multi_active = True
211
+ self._ctrl_multi_index = idx
212
+ event.accept()
213
+ return
214
+ # Ctrl on empty area -> just suppress business click
215
+ event.accept()
216
+ return
217
+
218
+ # Shift+Left: let Qt perform range selection, suppress business click
219
+ if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ShiftModifier):
220
+ idx = self.indexAt(event.pos())
221
+ self._suppress_item_click = True
222
+ self._was_shift_click = True
223
+ if idx.isValid():
224
+ super().mousePressEvent(event) # default range selection behavior
225
+ else:
226
+ event.accept()
227
+ return
228
+
115
229
  if event.button() == Qt.LeftButton:
116
230
  index = self.indexAt(event.pos())
117
- if not index.isValid():
231
+
232
+ # When multiple are selected, a single click clears the multi-selection
233
+ if self._has_multi_selection():
234
+ self.selectionModel().clearSelection()
235
+ if not index.isValid():
236
+ event.accept()
237
+ return
238
+ # continue with default single selection for clicked row
239
+
240
+ # Proceed with default handling
241
+ if index.isValid():
242
+ super().mousePressEvent(event)
118
243
  return
119
- super().mousePressEvent(event)
244
+ else:
245
+ # Click on empty area -> clear any single selection
246
+ self.selectionModel().clearSelection()
247
+ event.accept()
248
+ return
249
+
120
250
  elif event.button() == Qt.RightButton:
121
251
  index = self.indexAt(event.pos())
252
+ sel_model = self.selectionModel()
253
+ selected_rows = [ix.row() for ix in sel_model.selectedRows()]
254
+ multi = len(selected_rows) > 1
255
+
122
256
  if index.isValid():
123
- self._backup_selection = list(self.selectionModel().selectedIndexes())
124
- self.selectionModel().clearSelection()
125
- self.selectionModel().select(
126
- index, QItemSelectionModel.Select | QItemSelectionModel.Rows
127
- )
257
+ if multi and index.row() in selected_rows:
258
+ # Keep existing multi-selection; do not alter selection on right click
259
+ self._backup_selection = None
260
+ else:
261
+ # Select the clicked row temporarily for single or when clicking outside current multi
262
+ self._backup_selection = list(sel_model.selectedIndexes())
263
+ sel_model.clearSelection()
264
+ sel_model.select(index, QItemSelectionModel.Select | QItemSelectionModel.Rows)
128
265
  event.accept()
266
+ return
267
+
129
268
  else:
130
269
  super().mousePressEvent(event)
131
270
 
271
+ def mouseReleaseEvent(self, event):
272
+ # Finish Shift-range selection: skip business click path
273
+ if event.button() == Qt.LeftButton and self._was_shift_click:
274
+ self._was_shift_click = False
275
+ super().mouseReleaseEvent(event)
276
+ return
277
+
278
+ # Finish "virtual" Ctrl toggle on same row
279
+ if event.button() == Qt.LeftButton and self._ctrl_multi_active:
280
+ try:
281
+ idx = self.indexAt(event.pos())
282
+ if idx.isValid() and self._ctrl_multi_index and idx == self._ctrl_multi_index:
283
+ sel_model = self.selectionModel()
284
+ sel_model.select(idx, QItemSelectionModel.Toggle | QItemSelectionModel.Rows)
285
+ finally:
286
+ self._ctrl_multi_active = False
287
+ self._ctrl_multi_index = None
288
+ event.accept()
289
+ return
290
+
291
+ super().mouseReleaseEvent(event)
292
+
132
293
  def selectionCommand(self, index, event=None):
133
294
  """
134
295
  Selection command
135
296
  :param index: Index
136
297
  :param event: Event
137
298
  """
138
- return super().selectionCommand(index, event)
299
+ return super().selectionCommand(index, event)
@@ -6,11 +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: 2024.04.29 16:00:00 #
9
+ # Updated Date: 2025.12.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtGui import QAction, QIcon
13
- from PySide6.QtWidgets import QMenu
13
+ from PySide6.QtCore import Qt, QItemSelectionModel
14
+ from PySide6.QtWidgets import QMenu, QAbstractItemView
14
15
 
15
16
  from pygpt_net.ui.widget.lists.base import BaseList
16
17
  from pygpt_net.utils import trans
@@ -29,9 +30,153 @@ class AssistantVectorStoreEditorList(BaseList):
29
30
  self.window = window
30
31
  self.id = id
31
32
 
33
+ # Virtual multi-select helpers
34
+ self._suppress_item_click = False # suppress business click after Ctrl/Shift selection
35
+ self._ctrl_multi_active = False # Ctrl gesture in progress
36
+ self._ctrl_multi_index = None
37
+ self._was_shift_click = False # Shift range gesture
38
+
39
+ # Context menu selection backup (temporary right-click selection)
40
+ self._backup_selection = None
41
+ self.restore_after_ctx_menu = True
42
+
43
+ # Row-based multi-selection behavior
44
+ self.setSelectionBehavior(QAbstractItemView.SelectRows)
45
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection)
46
+
47
+ # Disable default BaseList click handler; business action is handled manually
48
+ self.clicked.disconnect(self.click)
49
+
50
+ # ----------------------------
51
+ # Selection helpers
52
+ # ----------------------------
53
+
54
+ def _selected_rows(self) -> list[int]:
55
+ """Return list of selected row numbers."""
56
+ try:
57
+ return sorted([ix.row() for ix in self.selectionModel().selectedRows()])
58
+ except Exception:
59
+ return []
60
+
61
+ def _has_multi_selection(self) -> bool:
62
+ """Check whether more than one row is selected."""
63
+ try:
64
+ return len(self.selectionModel().selectedRows()) > 1
65
+ except Exception:
66
+ return False
67
+
68
+ # ----------------------------
69
+ # Mouse events (virtual multi-select)
70
+ # ----------------------------
71
+
72
+ def mousePressEvent(self, event):
73
+ """
74
+ Mouse press event
75
+
76
+ :param event: mouse event
77
+ """
78
+ # Ctrl+Left: virtual toggle without business click
79
+ if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ControlModifier):
80
+ idx = self.indexAt(event.pos())
81
+ if idx.isValid():
82
+ self._ctrl_multi_active = True
83
+ self._ctrl_multi_index = idx
84
+ self._suppress_item_click = True
85
+ event.accept()
86
+ return
87
+ self._suppress_item_click = True
88
+ event.accept()
89
+ return
90
+
91
+ # Shift+Left: let Qt perform range selection (anchor->clicked), suppress business click
92
+ if event.button() == Qt.LeftButton and (event.modifiers() & Qt.ShiftModifier):
93
+ idx = self.indexAt(event.pos())
94
+ self._suppress_item_click = True
95
+ self._was_shift_click = True
96
+ if idx.isValid():
97
+ super(AssistantVectorStoreEditorList, self).mousePressEvent(event)
98
+ else:
99
+ event.accept()
100
+ return
101
+
102
+ # Plain left click
103
+ if event.button() == Qt.LeftButton:
104
+ idx = self.indexAt(event.pos())
105
+
106
+ # When multiple are selected, a single plain click clears the multi-selection.
107
+ if self._has_multi_selection():
108
+ sel_model = self.selectionModel()
109
+ sel_model.clearSelection()
110
+ if not idx.isValid():
111
+ event.accept()
112
+ return
113
+ # continue with default single selection for clicked row
114
+
115
+ super(AssistantVectorStoreEditorList, self).mousePressEvent(event)
116
+ return
117
+
118
+ # Right click: prepare selection for context menu
119
+ if event.button() == Qt.RightButton:
120
+ idx = self.indexAt(event.pos())
121
+ sel_model = self.selectionModel()
122
+ selected_rows = [ix.row() for ix in sel_model.selectedRows()]
123
+ multi = len(selected_rows) > 1
124
+
125
+ if idx.isValid():
126
+ if multi and idx.row() in selected_rows:
127
+ # Keep existing multi-selection; do not alter selection on right click
128
+ self._backup_selection = None
129
+ else:
130
+ # Temporarily select the clicked row; backup previous selection to restore later
131
+ self._backup_selection = list(sel_model.selectedIndexes())
132
+ sel_model.clearSelection()
133
+ sel_model.select(idx, QItemSelectionModel.Select | QItemSelectionModel.Rows)
134
+ event.accept()
135
+ return
136
+
137
+ super(AssistantVectorStoreEditorList, self).mousePressEvent(event)
138
+
139
+ def mouseReleaseEvent(self, event):
140
+ # If the click was a Shift-based range selection, bypass business click
141
+ if event.button() == Qt.LeftButton and self._was_shift_click:
142
+ self._was_shift_click = False
143
+ self._suppress_item_click = False
144
+ super(AssistantVectorStoreEditorList, self).mouseReleaseEvent(event)
145
+ return
146
+
147
+ # Finish "virtual" Ctrl toggle on same row (no business click)
148
+ if event.button() == Qt.LeftButton and self._ctrl_multi_active:
149
+ try:
150
+ idx = self.indexAt(event.pos())
151
+ if idx.isValid() and self._ctrl_multi_index and idx == self._ctrl_multi_index:
152
+ sel_model = self.selectionModel()
153
+ sel_model.select(idx, QItemSelectionModel.Toggle | QItemSelectionModel.Rows)
154
+ finally:
155
+ self._ctrl_multi_active = False
156
+ self._ctrl_multi_index = None
157
+ self._suppress_item_click = False
158
+ event.accept()
159
+ return
160
+
161
+ # Plain left: perform business selection only for single selection
162
+ if event.button() == Qt.LeftButton:
163
+ idx = self.indexAt(event.pos())
164
+ if not self._has_multi_selection():
165
+ if idx.isValid() and not self._suppress_item_click:
166
+ self.window.controller.assistant.store.select(idx.row())
167
+ self._suppress_item_click = False
168
+ super(AssistantVectorStoreEditorList, self).mouseReleaseEvent(event)
169
+ return
170
+
171
+ super(AssistantVectorStoreEditorList, self).mouseReleaseEvent(event)
172
+
32
173
  def click(self, val):
33
- idx = val.row()
34
- self.window.controller.assistant.store.select(idx)
174
+ # Not used; single-selection business click is handled in mouseReleaseEvent
175
+ pass
176
+
177
+ # ----------------------------
178
+ # Context menu
179
+ # ----------------------------
35
180
 
36
181
  def contextMenuEvent(self, event):
37
182
  """
@@ -40,24 +185,22 @@ class AssistantVectorStoreEditorList(BaseList):
40
185
  :param event: context menu event
41
186
  """
42
187
  actions = {}
43
- actions['refresh'] = QAction(QIcon(":/icons/reload.svg"),
44
- trans('dialog.assistant.store.menu.current.refresh_store'),
45
- self)
46
- actions['refresh'].triggered.connect(
47
- lambda: self.action_refresh(event))
48
-
188
+ actions['refresh'] = QAction(
189
+ QIcon(":/icons/reload.svg"),
190
+ trans('dialog.assistant.store.menu.current.refresh_store'),
191
+ self
192
+ )
49
193
  actions['delete'] = QAction(QIcon(":/icons/delete.svg"), trans('action.delete'), self)
50
- actions['delete'].triggered.connect(
51
- lambda: self.action_delete(event))
52
-
53
- actions['clear'] = QAction(QIcon(":/icons/close.svg"), trans('dialog.assistant.store.menu.current.clear_files'), self)
54
- actions['clear'].triggered.connect(
55
- lambda: self.action_clear(event))
56
-
57
- actions['truncate'] = QAction(QIcon(":/icons/delete.svg"), trans('dialog.assistant.store.menu.current.truncate_files'),
58
- self)
59
- actions['truncate'].triggered.connect(
60
- lambda: self.action_truncate(event))
194
+ actions['clear'] = QAction(
195
+ QIcon(":/icons/close.svg"),
196
+ trans('dialog.assistant.store.menu.current.clear_files'),
197
+ self
198
+ )
199
+ actions['truncate'] = QAction(
200
+ QIcon(":/icons/delete.svg"),
201
+ trans('dialog.assistant.store.menu.current.truncate_files'),
202
+ self
203
+ )
61
204
 
62
205
  menu = QMenu(self)
63
206
  menu.addAction(actions['refresh'])
@@ -65,50 +208,110 @@ class AssistantVectorStoreEditorList(BaseList):
65
208
  menu.addAction(actions['clear'])
66
209
  menu.addAction(actions['truncate'])
67
210
 
68
- item = self.indexAt(event.pos())
69
- idx = item.row()
70
- if idx >= 0:
71
- menu.exec_(event.globalPos())
211
+ index = self.indexAt(event.pos())
212
+ idx = index.row() if index.isValid() else -1
213
+
214
+ # Selection state for multi / single
215
+ selected_rows = self._selected_rows()
216
+ multi = len(selected_rows) > 1
72
217
 
73
- def action_delete(self, event):
218
+ # Allow menu on empty area only when multi-selection is active
219
+ if not index.isValid() and not multi:
220
+ if self._backup_selection is not None and self.restore_after_ctx_menu:
221
+ sel_model = self.selectionModel()
222
+ sel_model.clearSelection()
223
+ for i in self._backup_selection:
224
+ sel_model.select(i, QItemSelectionModel.Select | QItemSelectionModel.Rows)
225
+ self._backup_selection = None
226
+ return
227
+
228
+ # Route actions: pass list on multi, int on single
229
+ if multi:
230
+ actions['refresh'].triggered.connect(lambda: self.action_refresh(list(selected_rows)))
231
+ actions['delete'].triggered.connect(lambda: self.action_delete(list(selected_rows)))
232
+ actions['clear'].triggered.connect(lambda: self.action_clear(list(selected_rows)))
233
+ actions['truncate'].triggered.connect(lambda: self.action_truncate(list(selected_rows)))
234
+ else:
235
+ actions['refresh'].triggered.connect(lambda: self.action_refresh(idx))
236
+ actions['delete'].triggered.connect(lambda: self.action_delete(idx))
237
+ actions['clear'].triggered.connect(lambda: self.action_clear(idx))
238
+ actions['truncate'].triggered.connect(lambda: self.action_truncate(idx))
239
+
240
+ menu.exec_(event.globalPos())
241
+
242
+ # Restore selection after context menu if it was temporarily changed
243
+ if self.restore_after_ctx_menu and self._backup_selection is not None:
244
+ sel_model = self.selectionModel()
245
+ sel_model.clearSelection()
246
+ for i in self._backup_selection:
247
+ sel_model.select(i, QItemSelectionModel.Select | QItemSelectionModel.Rows)
248
+ self._backup_selection = None
249
+ self.restore_after_ctx_menu = True
250
+
251
+ # ----------------------------
252
+ # Context actions (single or multi)
253
+ # If 'item' is a list/tuple -> pass list of row ints to external code.
254
+ # If 'item' is an int -> pass single row int to external code.
255
+ # ----------------------------
256
+
257
+ def action_delete(self, item):
74
258
  """
75
259
  Delete action handler
76
260
 
77
- :param event: mouse event
261
+ :param item: int row or list of rows
78
262
  """
79
- item = self.indexAt(event.pos())
80
- idx = item.row()
263
+ if isinstance(item, (list, tuple)):
264
+ if item:
265
+ self.restore_after_ctx_menu = False
266
+ self.window.controller.assistant.store.delete_by_idx(list(item))
267
+ return
268
+ idx = int(item)
81
269
  if idx >= 0:
270
+ self.restore_after_ctx_menu = False
82
271
  self.window.controller.assistant.store.delete_by_idx(idx)
83
272
 
84
- def action_clear(self, event):
273
+ def action_clear(self, item):
85
274
  """
86
275
  Clear action handler
87
276
 
88
- :param event: mouse event
277
+ :param item: int row or list of rows
89
278
  """
90
- item = self.indexAt(event.pos())
91
- idx = item.row()
279
+ if isinstance(item, (list, tuple)):
280
+ if item:
281
+ self.restore_after_ctx_menu = False
282
+ self.window.controller.assistant.batch.clear_store_files_by_idx(list(item))
283
+ return
284
+ idx = int(item)
92
285
  if idx >= 0:
286
+ self.restore_after_ctx_menu = False
93
287
  self.window.controller.assistant.batch.clear_store_files_by_idx(idx)
94
288
 
95
- def action_truncate(self, event):
289
+ def action_truncate(self, item):
96
290
  """
97
291
  Truncate action handler
98
292
 
99
- :param event: mouse event
293
+ :param item: int row or list of rows
100
294
  """
101
- item = self.indexAt(event.pos())
102
- idx = item.row()
295
+ if isinstance(item, (list, tuple)):
296
+ if item:
297
+ self.restore_after_ctx_menu = False
298
+ self.window.controller.assistant.batch.truncate_store_files_by_idx(list(item))
299
+ return
300
+ idx = int(item)
103
301
  if idx >= 0:
302
+ self.restore_after_ctx_menu = False
104
303
  self.window.controller.assistant.batch.truncate_store_files_by_idx(idx)
105
304
 
106
- def action_refresh(self, event):
305
+ def action_refresh(self, item):
107
306
  """
108
307
  Refresh action handler
109
308
  """
110
- item = self.indexAt(event.pos())
111
- idx = item.row()
309
+ if isinstance(item, (list, tuple)):
310
+ if item:
311
+ self.restore_after_ctx_menu = False
312
+ self.window.controller.assistant.store.refresh_by_idx(list(item))
313
+ return
314
+ idx = int(item)
112
315
  if idx >= 0:
113
- self.window.controller.assistant.store.refresh_by_idx(idx)
114
-
316
+ self.restore_after_ctx_menu = False
317
+ self.window.controller.assistant.store.refresh_by_idx(idx)