pygpt-net 2.6.67__py3-none-any.whl → 2.7.0__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 +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/assistant/assistant.py +13 -8
- pygpt_net/controller/assistant/batch.py +29 -15
- pygpt_net/controller/assistant/files.py +19 -14
- pygpt_net/controller/assistant/store.py +63 -41
- pygpt_net/controller/attachment/attachment.py +45 -35
- pygpt_net/controller/chat/attachment.py +50 -39
- pygpt_net/controller/config/field/dictionary.py +26 -14
- pygpt_net/controller/ctx/common.py +27 -17
- pygpt_net/controller/ctx/ctx.py +182 -101
- pygpt_net/controller/files/files.py +101 -41
- pygpt_net/controller/idx/indexer.py +87 -31
- pygpt_net/controller/kernel/kernel.py +13 -2
- pygpt_net/controller/mode/mode.py +3 -3
- pygpt_net/controller/model/editor.py +70 -15
- pygpt_net/controller/model/importer.py +153 -54
- pygpt_net/controller/painter/painter.py +2 -2
- pygpt_net/controller/presets/experts.py +68 -15
- pygpt_net/controller/presets/presets.py +72 -36
- pygpt_net/controller/settings/profile.py +76 -35
- pygpt_net/controller/settings/workdir.py +70 -39
- pygpt_net/core/assistants/files.py +20 -18
- pygpt_net/core/filesystem/actions.py +111 -10
- pygpt_net/core/filesystem/filesystem.py +2 -1
- pygpt_net/core/idx/idx.py +12 -11
- pygpt_net/core/idx/worker.py +13 -1
- pygpt_net/core/models/models.py +4 -4
- pygpt_net/core/profile/profile.py +13 -3
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/css/style.dark.css +39 -1
- pygpt_net/data/css/style.light.css +39 -1
- pygpt_net/data/locale/locale.de.ini +3 -1
- pygpt_net/data/locale/locale.en.ini +3 -1
- pygpt_net/data/locale/locale.es.ini +3 -1
- pygpt_net/data/locale/locale.fr.ini +3 -1
- pygpt_net/data/locale/locale.it.ini +3 -1
- pygpt_net/data/locale/locale.pl.ini +4 -2
- pygpt_net/data/locale/locale.uk.ini +3 -1
- pygpt_net/data/locale/locale.zh.ini +3 -1
- pygpt_net/provider/api/openai/__init__.py +4 -2
- pygpt_net/provider/core/config/patch.py +9 -1
- pygpt_net/tools/image_viewer/tool.py +17 -0
- pygpt_net/tools/text_editor/tool.py +9 -0
- pygpt_net/ui/__init__.py +2 -2
- pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
- pygpt_net/ui/main.py +3 -1
- pygpt_net/ui/widget/calendar/select.py +3 -3
- pygpt_net/ui/widget/filesystem/explorer.py +1082 -142
- pygpt_net/ui/widget/lists/assistant.py +185 -24
- pygpt_net/ui/widget/lists/assistant_store.py +245 -42
- pygpt_net/ui/widget/lists/attachment.py +230 -47
- pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
- pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
- pygpt_net/ui/widget/lists/context.py +1253 -70
- pygpt_net/ui/widget/lists/experts.py +110 -8
- pygpt_net/ui/widget/lists/model_editor.py +217 -14
- pygpt_net/ui/widget/lists/model_importer.py +125 -6
- pygpt_net/ui/widget/lists/preset.py +460 -71
- pygpt_net/ui/widget/lists/profile.py +149 -27
- pygpt_net/ui/widget/lists/uploaded.py +230 -38
- pygpt_net/ui/widget/option/combo.py +1046 -32
- pygpt_net/ui/widget/option/dictionary.py +35 -7
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/METADATA +14 -57
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/RECORD +69 -69
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -6,13 +6,14 @@
|
|
|
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.12.27 21:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import copy
|
|
13
13
|
import os
|
|
14
14
|
from typing import List, Dict, Optional, Any
|
|
15
15
|
|
|
16
|
+
from PySide6 import QtCore
|
|
16
17
|
from PySide6.QtWidgets import QApplication
|
|
17
18
|
|
|
18
19
|
from pygpt_net.core.events import Event
|
|
@@ -45,8 +46,10 @@ class Importer:
|
|
|
45
46
|
self.items_current = {} # current models in use
|
|
46
47
|
self.pending = {} # waiting to be imported models
|
|
47
48
|
self.removed = {} # waiting to be removed models
|
|
48
|
-
self.selected_available = None # selected available model
|
|
49
|
-
self.selected_current = None # selected current model
|
|
49
|
+
self.selected_available = None # selected available model (single)
|
|
50
|
+
self.selected_current = None # selected current model (single)
|
|
51
|
+
self.selected_available_ids: List[str] = [] # multi-selected available models
|
|
52
|
+
self.selected_current_ids: List[str] = [] # multi-selected current models
|
|
50
53
|
self.all = False # show all models, not only available for import
|
|
51
54
|
self.provider = "_" # default provider
|
|
52
55
|
|
|
@@ -69,43 +72,71 @@ class Importer:
|
|
|
69
72
|
|
|
70
73
|
def change_available(self):
|
|
71
74
|
"""On change available model selection"""
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if
|
|
75
|
+
view = self.window.ui.nodes["models.importer.available"]
|
|
76
|
+
sel_model = view.selectionModel()
|
|
77
|
+
rows = sel_model.selectedRows() if sel_model else []
|
|
78
|
+
# collect selected model keys by reading stored tooltip role (stable ID)
|
|
79
|
+
selected_ids = []
|
|
80
|
+
for ix in rows:
|
|
81
|
+
key = ix.data(QtCore.Qt.ToolTipRole)
|
|
82
|
+
if not key:
|
|
83
|
+
key = self.get_by_idx(ix.row(), self.items_available)
|
|
84
|
+
if key and key not in selected_ids:
|
|
85
|
+
selected_ids.append(key)
|
|
86
|
+
|
|
87
|
+
self.selected_available_ids = selected_ids
|
|
88
|
+
if not selected_ids:
|
|
75
89
|
self.selected_available = None
|
|
76
90
|
self.window.ui.nodes["models.importer.add"].setEnabled(False)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# keep a single reference for backward compatibility (last selected)
|
|
94
|
+
self.selected_available = selected_ids[-1]
|
|
95
|
+
|
|
96
|
+
# enable add if any of selected can be added (not already in current)
|
|
97
|
+
can_add = False
|
|
98
|
+
for key in selected_ids:
|
|
99
|
+
if key is None:
|
|
100
|
+
continue
|
|
101
|
+
if not self.in_current(key) and self.items_available.get(key) is not None:
|
|
102
|
+
can_add = True
|
|
103
|
+
break
|
|
104
|
+
self.window.ui.nodes["models.importer.add"].setEnabled(can_add)
|
|
88
105
|
|
|
89
106
|
def change_current(self):
|
|
90
107
|
"""On change current model selection"""
|
|
91
108
|
if self.provider not in self.items_current:
|
|
92
109
|
self.items_current[self.provider] = {}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if
|
|
110
|
+
view = self.window.ui.nodes["models.importer.current"]
|
|
111
|
+
sel_model = view.selectionModel()
|
|
112
|
+
rows = sel_model.selectedRows() if sel_model else []
|
|
113
|
+
# collect selected model keys by reading stored tooltip role (stable ID)
|
|
114
|
+
selected_ids = []
|
|
115
|
+
for ix in rows:
|
|
116
|
+
key = ix.data(QtCore.Qt.ToolTipRole)
|
|
117
|
+
if not key:
|
|
118
|
+
key = self.get_by_idx(ix.row(), self.items_current[self.provider])
|
|
119
|
+
if key and key not in selected_ids:
|
|
120
|
+
selected_ids.append(key)
|
|
121
|
+
|
|
122
|
+
self.selected_current_ids = selected_ids
|
|
123
|
+
if not selected_ids:
|
|
96
124
|
self.selected_current = None
|
|
97
125
|
self.window.ui.nodes["models.importer.remove"].setEnabled(False)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# keep a single reference for backward compatibility (last selected)
|
|
129
|
+
self.selected_current = selected_ids[-1]
|
|
130
|
+
|
|
131
|
+
# enable remove if any of selected can be removed
|
|
132
|
+
can_remove = False
|
|
133
|
+
for key in selected_ids:
|
|
134
|
+
if key is None:
|
|
135
|
+
continue
|
|
136
|
+
if (key in self.items_current[self.provider]) and self.in_available(key):
|
|
137
|
+
can_remove = True
|
|
138
|
+
break
|
|
139
|
+
self.window.ui.nodes["models.importer.remove"].setEnabled(can_remove)
|
|
109
140
|
|
|
110
141
|
def in_available(self, model: str) -> bool:
|
|
111
142
|
"""
|
|
@@ -122,43 +153,111 @@ class Importer:
|
|
|
122
153
|
return True
|
|
123
154
|
return False
|
|
124
155
|
|
|
156
|
+
def _selected_keys_from_view(self, node_id: str, items: Dict) -> List[str]:
|
|
157
|
+
"""
|
|
158
|
+
Resolve selected model keys from a given list view using stored tooltip role,
|
|
159
|
+
falling back to index->dict mapping when needed.
|
|
160
|
+
|
|
161
|
+
:param node_id: ui node id
|
|
162
|
+
:param items: dict used to build the view's model
|
|
163
|
+
:return: list of selected keys
|
|
164
|
+
"""
|
|
165
|
+
keys: List[str] = []
|
|
166
|
+
try:
|
|
167
|
+
view = self.window.ui.nodes[node_id]
|
|
168
|
+
sel_model = view.selectionModel()
|
|
169
|
+
rows = sel_model.selectedRows() if sel_model else []
|
|
170
|
+
for ix in rows:
|
|
171
|
+
key = ix.data(QtCore.Qt.ToolTipRole)
|
|
172
|
+
if not key:
|
|
173
|
+
key = self.get_by_idx(ix.row(), items)
|
|
174
|
+
if key and key not in keys:
|
|
175
|
+
keys.append(key)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
return keys
|
|
179
|
+
|
|
125
180
|
def add(self):
|
|
126
|
-
"""Add model to current list"""
|
|
181
|
+
"""Add model(s) to current list"""
|
|
127
182
|
if self.provider not in self.items_current:
|
|
128
183
|
self.items_current[self.provider] = {}
|
|
129
|
-
|
|
184
|
+
|
|
185
|
+
# collect multi-selection; fallback to single selection
|
|
186
|
+
keys = self._selected_keys_from_view(
|
|
187
|
+
'models.importer.available',
|
|
188
|
+
self.items_available,
|
|
189
|
+
)
|
|
190
|
+
if not keys and self.selected_available is not None:
|
|
191
|
+
keys = [self.selected_available]
|
|
192
|
+
|
|
193
|
+
if not keys:
|
|
130
194
|
self.set_status(trans('models.importer.error.add.no_model'))
|
|
131
195
|
return
|
|
132
|
-
|
|
196
|
+
|
|
197
|
+
any_added = False
|
|
198
|
+
for key in list(keys):
|
|
199
|
+
if key is None:
|
|
200
|
+
continue
|
|
201
|
+
if self.in_current(key):
|
|
202
|
+
continue
|
|
203
|
+
model = self.items_available.get(key)
|
|
204
|
+
if model is None:
|
|
205
|
+
continue
|
|
206
|
+
self.items_current[self.provider][key] = model
|
|
207
|
+
if key not in self.pending:
|
|
208
|
+
self.pending[key] = model
|
|
209
|
+
if key in self.removed:
|
|
210
|
+
del self.removed[key]
|
|
211
|
+
if not self.all and key in self.items_available:
|
|
212
|
+
del self.items_available[key]
|
|
213
|
+
any_added = True
|
|
214
|
+
|
|
215
|
+
if not any_added:
|
|
133
216
|
self.set_status(trans('models.importer.error.add.not_exists'))
|
|
134
217
|
return
|
|
135
|
-
|
|
136
|
-
self.items_current[self.provider][self.selected_available] = model
|
|
137
|
-
if self.selected_available not in self.pending:
|
|
138
|
-
self.pending[self.selected_available] = model
|
|
139
|
-
if self.selected_available in self.removed:
|
|
140
|
-
del self.removed[self.selected_available]
|
|
141
|
-
if not self.all:
|
|
142
|
-
del self.items_available[self.selected_available]
|
|
218
|
+
|
|
143
219
|
self.refresh()
|
|
144
220
|
|
|
145
221
|
def remove(self):
|
|
146
|
-
"""Remove model from current list"""
|
|
222
|
+
"""Remove model(s) from current list"""
|
|
147
223
|
if self.provider not in self.items_current:
|
|
148
224
|
self.items_current[self.provider] = {}
|
|
149
|
-
|
|
225
|
+
|
|
226
|
+
# collect multi-selection; fallback to single selection
|
|
227
|
+
keys = self._selected_keys_from_view(
|
|
228
|
+
'models.importer.current',
|
|
229
|
+
self.items_current[self.provider],
|
|
230
|
+
)
|
|
231
|
+
if not keys and self.selected_current is not None:
|
|
232
|
+
keys = [self.selected_current]
|
|
233
|
+
|
|
234
|
+
if not keys:
|
|
150
235
|
self.set_status(trans('models.importer.error.remove.no_model'))
|
|
151
236
|
return
|
|
152
|
-
|
|
237
|
+
|
|
238
|
+
any_removed = False
|
|
239
|
+
for key in list(keys):
|
|
240
|
+
if key is None:
|
|
241
|
+
continue
|
|
242
|
+
if not self.in_current(key):
|
|
243
|
+
continue
|
|
244
|
+
model = self.items_current[self.provider].get(key)
|
|
245
|
+
if model is None:
|
|
246
|
+
continue
|
|
247
|
+
# return to available list
|
|
248
|
+
self.items_available[key] = model
|
|
249
|
+
if key not in self.removed:
|
|
250
|
+
self.removed[key] = model
|
|
251
|
+
if key in self.items_current[self.provider]:
|
|
252
|
+
del self.items_current[self.provider][key]
|
|
253
|
+
if key in self.pending:
|
|
254
|
+
del self.pending[key]
|
|
255
|
+
any_removed = True
|
|
256
|
+
|
|
257
|
+
if not any_removed:
|
|
153
258
|
self.set_status(trans('models.importer.error.remove.not_exists'))
|
|
154
259
|
return
|
|
155
|
-
|
|
156
|
-
self.items_available[self.selected_current] = model
|
|
157
|
-
if self.selected_current not in self.removed:
|
|
158
|
-
self.removed[self.selected_current] = model
|
|
159
|
-
del self.items_current[self.provider][self.selected_current]
|
|
160
|
-
if self.selected_current in self.pending:
|
|
161
|
-
del self.pending[self.selected_current]
|
|
260
|
+
|
|
162
261
|
self.refresh()
|
|
163
262
|
|
|
164
263
|
def setup(self):
|
|
@@ -328,7 +427,7 @@ class Importer:
|
|
|
328
427
|
name = model.get('id')
|
|
329
428
|
if "name" in model:
|
|
330
429
|
name = model.get('name')
|
|
331
|
-
m = self.window.core.models.create_empty(append=False)
|
|
430
|
+
m, _ = self.window.core.models.create_empty(append=False)
|
|
332
431
|
m.id = id
|
|
333
432
|
m.name = name
|
|
334
433
|
m.mode = [
|
|
@@ -504,7 +603,7 @@ class Importer:
|
|
|
504
603
|
else:
|
|
505
604
|
for model in ollama_models:
|
|
506
605
|
name = model.get('name')
|
|
507
|
-
m = self.window.core.models.create_empty(append=False)
|
|
606
|
+
m, _ = self.window.core.models.create_empty(append=False)
|
|
508
607
|
m.id = name
|
|
509
608
|
m.name = name
|
|
510
609
|
m.mode = [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.
|
|
9
|
+
# Updated Date: 2025.12.27 17:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
@@ -53,7 +53,7 @@ class Painter:
|
|
|
53
53
|
"""
|
|
54
54
|
self.open(path)
|
|
55
55
|
if not self.is_active():
|
|
56
|
-
self.window.controller.ui.switch_tab(
|
|
56
|
+
self.window.controller.ui.tabs.switch_tab(Tab.TAB_TOOL_PAINTER)
|
|
57
57
|
|
|
58
58
|
def save(self):
|
|
59
59
|
"""Store current image"""
|
|
@@ -6,9 +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.12.27 21:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
+
from PySide6 import QtCore
|
|
13
|
+
|
|
12
14
|
from pygpt_net.core.types import (
|
|
13
15
|
MODE_EXPERT,
|
|
14
16
|
)
|
|
@@ -23,6 +25,8 @@ class Experts:
|
|
|
23
25
|
:param window: Window instance
|
|
24
26
|
"""
|
|
25
27
|
self.window = window
|
|
28
|
+
self.selected_available_uuids = [] # multi-selected available experts
|
|
29
|
+
self.selected_selected_uuids = [] # multi-selected selected experts
|
|
26
30
|
|
|
27
31
|
def refresh(self):
|
|
28
32
|
"""Refresh presets"""
|
|
@@ -78,13 +82,43 @@ class Experts:
|
|
|
78
82
|
|
|
79
83
|
self.update_tab()
|
|
80
84
|
|
|
85
|
+
def _selected_uuids_from_view(self, node_id: str, fallback_idx_resolver) -> list[str]:
|
|
86
|
+
"""
|
|
87
|
+
Resolve selected expert UUIDs from a given list view using stored tooltip role,
|
|
88
|
+
falling back to idx -> uuid resolver when needed.
|
|
89
|
+
|
|
90
|
+
:param node_id: ui node id
|
|
91
|
+
:param fallback_idx_resolver: callable that maps row index -> uuid
|
|
92
|
+
:return: list of selected uuids
|
|
93
|
+
"""
|
|
94
|
+
uuids = []
|
|
95
|
+
try:
|
|
96
|
+
view = self.window.ui.nodes[node_id]
|
|
97
|
+
sel_model = view.selectionModel()
|
|
98
|
+
rows = sel_model.selectedRows() if sel_model else []
|
|
99
|
+
for ix in rows:
|
|
100
|
+
uuid = ix.data(QtCore.Qt.ToolTipRole)
|
|
101
|
+
if not uuid:
|
|
102
|
+
uuid = fallback_idx_resolver(ix.row())
|
|
103
|
+
if uuid and uuid not in uuids:
|
|
104
|
+
uuids.append(uuid)
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
return uuids
|
|
108
|
+
|
|
81
109
|
def change_available(self):
|
|
82
|
-
"""Change selected expert"""
|
|
83
|
-
|
|
110
|
+
"""Change selected expert(s) in available list"""
|
|
111
|
+
self.selected_available_uuids = self._selected_uuids_from_view(
|
|
112
|
+
"preset.experts.available",
|
|
113
|
+
self.get_available_by_idx
|
|
114
|
+
)
|
|
84
115
|
|
|
85
116
|
def change_selected(self):
|
|
86
|
-
"""Change selected expert"""
|
|
87
|
-
|
|
117
|
+
"""Change selected expert(s) in selected list"""
|
|
118
|
+
self.selected_selected_uuids = self._selected_uuids_from_view(
|
|
119
|
+
"preset.experts.selected",
|
|
120
|
+
self.get_selected_by_idx
|
|
121
|
+
)
|
|
88
122
|
|
|
89
123
|
def get_current_available(self) -> str:
|
|
90
124
|
"""
|
|
@@ -138,27 +172,47 @@ class Experts:
|
|
|
138
172
|
i += 1
|
|
139
173
|
|
|
140
174
|
def add_expert(self):
|
|
141
|
-
"""Add expert"""
|
|
175
|
+
"""Add expert(s)"""
|
|
142
176
|
agent_uuid = self.get_current_agent_id()
|
|
143
177
|
if agent_uuid is None or not self.window.core.presets.exists_uuid(agent_uuid):
|
|
144
178
|
self.window.controller.presets.editor.save(close=False)
|
|
145
179
|
return
|
|
146
|
-
|
|
147
|
-
|
|
180
|
+
|
|
181
|
+
uuids = self.selected_available_uuids[:]
|
|
182
|
+
if not uuids:
|
|
183
|
+
one = self.get_current_available()
|
|
184
|
+
if one:
|
|
185
|
+
uuids = [one]
|
|
186
|
+
|
|
187
|
+
if not uuids:
|
|
148
188
|
return
|
|
149
|
-
|
|
189
|
+
|
|
190
|
+
for expert_uuid in uuids:
|
|
191
|
+
if expert_uuid and not self.is_active(expert_uuid):
|
|
192
|
+
self.window.core.presets.add_expert(agent_uuid, expert_uuid)
|
|
193
|
+
|
|
150
194
|
self.update_list()
|
|
151
195
|
|
|
152
196
|
def remove_expert(self):
|
|
153
|
-
"""Remove expert"""
|
|
197
|
+
"""Remove expert(s)"""
|
|
154
198
|
agent_uuid = self.get_current_agent_id()
|
|
155
199
|
if agent_uuid is None or not self.window.core.presets.exists_uuid(agent_uuid):
|
|
156
200
|
self.window.controller.presets.editor.save(close=False)
|
|
157
201
|
return
|
|
158
|
-
|
|
159
|
-
|
|
202
|
+
|
|
203
|
+
uuids = self.selected_selected_uuids[:]
|
|
204
|
+
if not uuids:
|
|
205
|
+
one = self.get_current_selected()
|
|
206
|
+
if one:
|
|
207
|
+
uuids = [one]
|
|
208
|
+
|
|
209
|
+
if not uuids:
|
|
160
210
|
return
|
|
161
|
-
|
|
211
|
+
|
|
212
|
+
for expert_uuid in uuids:
|
|
213
|
+
if expert_uuid and self.is_active(expert_uuid):
|
|
214
|
+
self.window.core.presets.remove_expert(agent_uuid, expert_uuid)
|
|
215
|
+
|
|
162
216
|
self.update_list()
|
|
163
217
|
|
|
164
218
|
def update_tab(self):
|
|
@@ -174,5 +228,4 @@ class Experts:
|
|
|
174
228
|
if num == 0:
|
|
175
229
|
tabs.setTabText(idx, trans("preset.tab.experts"))
|
|
176
230
|
else:
|
|
177
|
-
tabs.setTabText(idx, trans("preset.tab.experts") + f" ({num})")
|
|
178
|
-
|
|
231
|
+
tabs.setTabText(idx, trans("preset.tab.experts") + f" ({num})")
|
|
@@ -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.12.
|
|
9
|
+
# Updated Date: 2025.12.27 21:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
|
-
from typing import Optional, List, Dict
|
|
13
|
+
from typing import Optional, List, Dict, Union
|
|
14
14
|
|
|
15
15
|
from PySide6.QtCore import QTimer
|
|
16
16
|
from PySide6.QtGui import QTextCursor
|
|
@@ -536,25 +536,39 @@ class Presets:
|
|
|
536
536
|
"""
|
|
537
537
|
return _FILENAME_SANITIZE_RE.sub('_', name.lower())
|
|
538
538
|
|
|
539
|
-
def duplicate(self, idx: Optional[int] = None):
|
|
539
|
+
def duplicate(self, idx: Optional[Union[int, list]] = None):
|
|
540
540
|
"""
|
|
541
541
|
Duplicate preset
|
|
542
542
|
|
|
543
|
-
:param idx: preset
|
|
543
|
+
:param idx: preset ID (or list of IDs)
|
|
544
544
|
"""
|
|
545
|
-
if idx
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
545
|
+
ids = idx if isinstance(idx, list) else [idx]
|
|
546
|
+
last_id = None
|
|
547
|
+
updated = False
|
|
548
|
+
w = self.window
|
|
549
|
+
mode = w.core.config.get('mode')
|
|
550
|
+
for idx in ids:
|
|
551
|
+
if idx is not None:
|
|
552
|
+
if len(ids) > 1:
|
|
553
|
+
preset = w.core.presets.get_by_id(mode, idx) # by ID, PresetItem
|
|
554
|
+
preset_id = preset.filename if preset else None
|
|
555
|
+
else:
|
|
556
|
+
preset = w.core.presets.get_by_idx(idx, mode) # by index, str
|
|
557
|
+
preset_id = preset
|
|
558
|
+
if preset_id:
|
|
559
|
+
new_id = w.core.presets.duplicate(preset_id)
|
|
560
|
+
last_id = new_id
|
|
561
|
+
updated = True
|
|
562
|
+
|
|
563
|
+
if updated:
|
|
564
|
+
self.update_list()
|
|
565
|
+
self.refresh(no_scroll=True)
|
|
566
|
+
if len(ids) == 1 and idx is not None:
|
|
567
|
+
idx = w.core.presets.get_idx_by_id(mode, last_id)
|
|
568
|
+
self.editor.edit(idx)
|
|
569
|
+
self.select(idx) # switch to the new preset if only one was duplicated
|
|
570
|
+
w.update_status(trans('status.preset.duplicated'))
|
|
571
|
+
|
|
558
572
|
|
|
559
573
|
def enable(self, idx: Optional[int] = None):
|
|
560
574
|
"""
|
|
@@ -654,30 +668,49 @@ class Presets:
|
|
|
654
668
|
|
|
655
669
|
def delete(
|
|
656
670
|
self,
|
|
657
|
-
idx: Optional[int] = None,
|
|
671
|
+
idx: Optional[Union[int, list]] = None,
|
|
658
672
|
force: bool = False
|
|
659
673
|
):
|
|
660
674
|
"""
|
|
661
675
|
Delete preset
|
|
662
676
|
|
|
663
|
-
:param idx: preset index (row index)
|
|
677
|
+
:param idx: preset index (row index) or list of indices
|
|
664
678
|
:param force: force delete without confirmation
|
|
665
679
|
"""
|
|
666
|
-
if idx
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
680
|
+
ids = idx if isinstance(idx, list) else [idx]
|
|
681
|
+
w = self.window
|
|
682
|
+
mode = w.core.config.get('mode')
|
|
683
|
+
is_preset = False
|
|
684
|
+
updated = False
|
|
685
|
+
for tmp_id in ids:
|
|
686
|
+
if len(ids) > 1:
|
|
687
|
+
preset = w.core.presets.get_by_id(mode, tmp_id) # by ID, PresetItem
|
|
688
|
+
else:
|
|
689
|
+
preset = w.core.presets.get_by_idx(tmp_id, mode) # by index, str
|
|
690
|
+
if preset:
|
|
691
|
+
is_preset = True
|
|
692
|
+
break
|
|
693
|
+
|
|
694
|
+
if ids and is_preset:
|
|
695
|
+
if not force:
|
|
696
|
+
w.ui.dialogs.confirm(
|
|
697
|
+
type='preset_delete',
|
|
698
|
+
id=idx,
|
|
699
|
+
msg=trans('confirm.preset.delete'),
|
|
700
|
+
)
|
|
701
|
+
return
|
|
702
|
+
|
|
703
|
+
# Determine neighbor only if the deleted preset is currently active.
|
|
704
|
+
# This keeps API semantics untouched and prevents unexpected selection changes.
|
|
705
|
+
for idx in ids:
|
|
706
|
+
if len(ids) > 1:
|
|
707
|
+
preset = w.core.presets.get_by_id(mode, idx) # by ID, PresetItem
|
|
708
|
+
preset_id = preset.filename if preset else None
|
|
709
|
+
else:
|
|
710
|
+
preset = w.core.presets.get_by_idx(idx, mode) # by index, str
|
|
711
|
+
preset_id = preset
|
|
712
|
+
if not preset:
|
|
713
|
+
continue
|
|
681
714
|
is_current = (preset_id == w.core.config.get('preset'))
|
|
682
715
|
target_id = None
|
|
683
716
|
if is_current:
|
|
@@ -700,8 +733,11 @@ class Presets:
|
|
|
700
733
|
w.core.config.set('preset', None)
|
|
701
734
|
w.ui.nodes['preset.prompt'].setPlainText("")
|
|
702
735
|
|
|
703
|
-
|
|
704
|
-
|
|
736
|
+
updated = True
|
|
737
|
+
|
|
738
|
+
if updated:
|
|
739
|
+
self.refresh(no_scroll=True)
|
|
740
|
+
w.update_status(trans('status.preset.deleted'))
|
|
705
741
|
|
|
706
742
|
def restore(self, force: bool = False):
|
|
707
743
|
"""
|