pygpt-net 2.6.63__py3-none-any.whl → 2.6.65__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/app.py +3 -1
- pygpt_net/controller/attachment/attachment.py +17 -8
- pygpt_net/controller/camera/camera.py +4 -4
- pygpt_net/controller/files/files.py +71 -2
- pygpt_net/controller/lang/custom.py +2 -2
- pygpt_net/controller/presets/editor.py +137 -22
- pygpt_net/controller/ui/mode.py +18 -3
- pygpt_net/core/agents/custom/__init__.py +18 -2
- pygpt_net/core/agents/custom/runner.py +2 -2
- pygpt_net/core/attachments/clipboard.py +146 -0
- pygpt_net/core/render/web/renderer.py +44 -11
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/presets/agent_openai_coder.json +15 -1
- pygpt_net/data/css/style.dark.css +12 -0
- pygpt_net/data/css/style.light.css +12 -0
- pygpt_net/data/icons/pin2.svg +1 -0
- pygpt_net/data/icons/pin3.svg +3 -0
- pygpt_net/data/icons/point.svg +1 -0
- pygpt_net/data/icons/target.svg +1 -0
- pygpt_net/data/js/app/runtime.js +11 -4
- pygpt_net/data/js/app/scroll.js +14 -0
- pygpt_net/data/js/app/ui.js +19 -2
- pygpt_net/data/js/app/user.js +22 -54
- pygpt_net/data/js/app.min.js +13 -14
- pygpt_net/data/locale/locale.de.ini +32 -0
- pygpt_net/data/locale/locale.en.ini +38 -2
- pygpt_net/data/locale/locale.es.ini +32 -0
- pygpt_net/data/locale/locale.fr.ini +32 -0
- pygpt_net/data/locale/locale.it.ini +32 -0
- pygpt_net/data/locale/locale.pl.ini +34 -2
- pygpt_net/data/locale/locale.uk.ini +32 -0
- pygpt_net/data/locale/locale.zh.ini +32 -0
- pygpt_net/icons.qrc +4 -0
- pygpt_net/icons_rc.py +274 -137
- pygpt_net/js_rc.py +8262 -8230
- pygpt_net/provider/agents/llama_index/planner_workflow.py +15 -3
- pygpt_net/provider/agents/llama_index/workflow/planner.py +69 -41
- pygpt_net/provider/agents/openai/agent_planner.py +57 -35
- pygpt_net/provider/agents/openai/evolve.py +0 -3
- pygpt_net/provider/api/google/__init__.py +9 -3
- pygpt_net/provider/api/google/image.py +11 -1
- pygpt_net/provider/api/google/music.py +375 -0
- pygpt_net/provider/core/config/patch.py +8 -0
- pygpt_net/ui/__init__.py +6 -1
- pygpt_net/ui/dialog/preset.py +9 -4
- pygpt_net/ui/layout/chat/attachments.py +18 -1
- pygpt_net/ui/layout/status.py +3 -3
- pygpt_net/ui/widget/element/status.py +55 -0
- pygpt_net/ui/widget/filesystem/explorer.py +116 -2
- pygpt_net/ui/widget/lists/context.py +26 -16
- pygpt_net/ui/widget/option/combo.py +149 -11
- pygpt_net/ui/widget/textarea/input.py +71 -17
- pygpt_net/ui/widget/textarea/web.py +1 -1
- pygpt_net/ui/widget/vision/camera.py +135 -12
- {pygpt_net-2.6.63.dist-info → pygpt_net-2.6.65.dist-info}/METADATA +18 -2
- {pygpt_net-2.6.63.dist-info → pygpt_net-2.6.65.dist-info}/RECORD +62 -55
- {pygpt_net-2.6.63.dist-info → pygpt_net-2.6.65.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.63.dist-info → pygpt_net-2.6.65.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.63.dist-info → pygpt_net-2.6.65.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
2.6.65 (2025-09-28)
|
|
2
|
+
|
|
3
|
+
- Added drag and drop functionality for files and directories from the filesystem in attachments and file explorer.
|
|
4
|
+
- Added automatic thumbnail generation when uploading avatars.
|
|
5
|
+
- Added a last status timer.
|
|
6
|
+
- Added a fade effect to collapsed user messages.
|
|
7
|
+
- Added a scroll area to the agent options in the presets editor.
|
|
8
|
+
- Added a hover effect to lists.
|
|
9
|
+
- Improved UI/UX.
|
|
10
|
+
|
|
11
|
+
2.6.64 (2025-09-27)
|
|
12
|
+
|
|
13
|
+
- Added translations to agent headers.
|
|
14
|
+
- Improved presets tabs.
|
|
15
|
+
- Added support for music (Lyria) in both image and video modes (beta).
|
|
16
|
+
|
|
1
17
|
2.6.63 (2025-09-27)
|
|
2
18
|
|
|
3
19
|
- Improved agents' workflows.
|
pygpt_net/__init__.py
CHANGED
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.
|
|
9
|
+
# Updated Date: 2025.09.28 00:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
__author__ = "Marcin Szczygliński"
|
|
13
13
|
__copyright__ = "Copyright 2025, Marcin Szczygliński"
|
|
14
14
|
__credits__ = ["Marcin Szczygliński"]
|
|
15
15
|
__license__ = "MIT"
|
|
16
|
-
__version__ = "2.6.
|
|
17
|
-
__build__ = "2025-09-
|
|
16
|
+
__version__ = "2.6.65"
|
|
17
|
+
__build__ = "2025-09-28"
|
|
18
18
|
__maintainer__ = "Marcin Szczygliński"
|
|
19
19
|
__github__ = "https://github.com/szczyglis-dev/py-gpt"
|
|
20
20
|
__report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
|
pygpt_net/app.py
CHANGED
|
@@ -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.09.
|
|
9
|
+
# Updated Date: 2025.09.28 09:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
@@ -20,6 +20,8 @@ from pygpt_net.utils import set_env
|
|
|
20
20
|
|
|
21
21
|
# app env
|
|
22
22
|
set_env("PYGPT_APP_ENV", "prod", allow_overwrite=True) # dev | prod
|
|
23
|
+
# IF dev, JS will be loaded from `data/js/app/*` [js_rc.py], not from `data/js/app.min.js`
|
|
24
|
+
# recompile js_rc.py with: bin/resources.sh, minify to app.min.js with: bin/minify.sh
|
|
23
25
|
|
|
24
26
|
# debug
|
|
25
27
|
# set_env("QTWEBENGINE_REMOTE_DEBUGGING", 9222)
|
|
@@ -579,19 +579,28 @@ class Attachment:
|
|
|
579
579
|
if not os.path.exists(url):
|
|
580
580
|
return
|
|
581
581
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
582
|
+
is_image = False
|
|
583
|
+
image_ext = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff']
|
|
584
|
+
ext = os.path.splitext(url)[1].lower()
|
|
585
|
+
if ext in image_ext:
|
|
586
|
+
is_image = True
|
|
587
|
+
|
|
588
|
+
if not all and not is_image:
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
if is_image:
|
|
592
|
+
title = "attachments.paste.img"
|
|
593
|
+
status = "painter.capture.manual.captured.success"
|
|
594
|
+
else:
|
|
595
|
+
title = "attachments.paste.file"
|
|
596
|
+
status = "attachments.paste.success"
|
|
587
597
|
|
|
588
598
|
mode = self.window.core.config.get('mode')
|
|
589
|
-
title
|
|
590
|
-
self.window.core.attachments.new(mode, title, url, False)
|
|
599
|
+
self.window.core.attachments.new(mode, trans(title), url, False)
|
|
591
600
|
self.window.core.attachments.save()
|
|
592
601
|
self.window.controller.attachment.update()
|
|
593
602
|
event = KernelEvent(KernelEvent.STATUS, {
|
|
594
|
-
'status': trans(
|
|
603
|
+
'status': trans(status) + ' ' + os.path.basename(url),
|
|
595
604
|
})
|
|
596
605
|
self.window.dispatch(event)
|
|
597
606
|
|
|
@@ -67,9 +67,9 @@ class Camera(QObject):
|
|
|
67
67
|
|
|
68
68
|
# update label
|
|
69
69
|
if not self.window.core.config.get('vision.capture.auto'):
|
|
70
|
-
self.window.ui.nodes['video.preview'].
|
|
70
|
+
self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.label"))
|
|
71
71
|
else:
|
|
72
|
-
self.window.ui.nodes['video.preview'].
|
|
72
|
+
self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.auto.label"))
|
|
73
73
|
|
|
74
74
|
def update(self):
|
|
75
75
|
"""Update camera frame"""
|
|
@@ -381,7 +381,7 @@ class Camera(QObject):
|
|
|
381
381
|
{'value': True}
|
|
382
382
|
)
|
|
383
383
|
"""
|
|
384
|
-
self.window.ui.nodes['video.preview'].
|
|
384
|
+
self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.auto.label"))
|
|
385
385
|
|
|
386
386
|
if not self.window.core.config.get('vision.capture.enabled'):
|
|
387
387
|
self.enable_capture()
|
|
@@ -403,7 +403,7 @@ class Camera(QObject):
|
|
|
403
403
|
{'value': False}
|
|
404
404
|
)
|
|
405
405
|
"""
|
|
406
|
-
self.window.ui.nodes['video.preview'].
|
|
406
|
+
self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.label"))
|
|
407
407
|
|
|
408
408
|
def toggle_auto(self, state: bool):
|
|
409
409
|
"""
|
|
@@ -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.09.28 08:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
@@ -270,6 +270,75 @@ class Files:
|
|
|
270
270
|
self.window.update_status(f"[OK] Uploaded: {num} files.")
|
|
271
271
|
self.update_explorer()
|
|
272
272
|
|
|
273
|
+
def upload_paths(
|
|
274
|
+
self,
|
|
275
|
+
paths: list,
|
|
276
|
+
target_directory: Optional[str] = None
|
|
277
|
+
):
|
|
278
|
+
"""
|
|
279
|
+
Upload provided local paths (files or directories) into target directory.
|
|
280
|
+
- Directories are copied recursively.
|
|
281
|
+
- Name collisions are resolved using timestamp prefix, consistent with upload_local().
|
|
282
|
+
- Skips copying directory into itself or its subdirectory.
|
|
283
|
+
|
|
284
|
+
:param paths: list of absolute local paths
|
|
285
|
+
:param target_directory: destination directory (defaults to user 'data' dir)
|
|
286
|
+
"""
|
|
287
|
+
if not paths:
|
|
288
|
+
return
|
|
289
|
+
if target_directory is None:
|
|
290
|
+
target_directory = self.window.core.config.get_user_dir('data')
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
if not os.path.exists(target_directory):
|
|
294
|
+
os.makedirs(target_directory, exist_ok=True)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
self.window.core.debug.log(e)
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
copied = 0
|
|
300
|
+
|
|
301
|
+
def unique_dest(dest_path: str) -> str:
|
|
302
|
+
if not os.path.exists(dest_path):
|
|
303
|
+
return dest_path
|
|
304
|
+
base_dir = os.path.dirname(dest_path)
|
|
305
|
+
name = os.path.basename(dest_path)
|
|
306
|
+
new_name = self.make_ts_prefix() + "_" + name
|
|
307
|
+
return os.path.join(base_dir, new_name)
|
|
308
|
+
|
|
309
|
+
for src in paths:
|
|
310
|
+
try:
|
|
311
|
+
if not src or not os.path.exists(src):
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
# Prevent copying a directory into itself or its subdirectory
|
|
315
|
+
try:
|
|
316
|
+
if os.path.isdir(src):
|
|
317
|
+
common = os.path.commonpath([os.path.abspath(src), os.path.abspath(target_directory)])
|
|
318
|
+
if common == os.path.abspath(src):
|
|
319
|
+
# target is inside src; skip
|
|
320
|
+
self.window.core.debug.log(f"Skipped copying directory into itself: {src} -> {target_directory}")
|
|
321
|
+
continue
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
dest_base = os.path.join(target_directory, os.path.basename(src))
|
|
326
|
+
dest_path = unique_dest(dest_base)
|
|
327
|
+
|
|
328
|
+
if os.path.isdir(src):
|
|
329
|
+
shutil.copytree(src, dest_path)
|
|
330
|
+
copied += 1
|
|
331
|
+
else:
|
|
332
|
+
copy2(src, dest_path)
|
|
333
|
+
copied += 1
|
|
334
|
+
except Exception as e:
|
|
335
|
+
self.window.core.debug.log(e)
|
|
336
|
+
print(f"Error uploading path {src}: {e}")
|
|
337
|
+
|
|
338
|
+
if copied > 0:
|
|
339
|
+
self.window.update_status(f"[OK] Uploaded: {copied} files.")
|
|
340
|
+
self.update_explorer()
|
|
341
|
+
|
|
273
342
|
def rename(self, path: str):
|
|
274
343
|
"""
|
|
275
344
|
Rename file or directory
|
|
@@ -480,4 +549,4 @@ class Files:
|
|
|
480
549
|
|
|
481
550
|
def reload(self):
|
|
482
551
|
"""Reload files"""
|
|
483
|
-
self.update_explorer(reload=True)
|
|
552
|
+
self.update_explorer(reload=True)
|
|
@@ -68,9 +68,9 @@ class Custom:
|
|
|
68
68
|
|
|
69
69
|
# camera capture
|
|
70
70
|
if not self.window.core.config.get('vision.capture.auto'):
|
|
71
|
-
self.window.ui.nodes['video.preview'].
|
|
71
|
+
self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.label"))
|
|
72
72
|
else:
|
|
73
|
-
self.window.ui.nodes['video.preview'].
|
|
73
|
+
self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.auto.label"))
|
|
74
74
|
|
|
75
75
|
# files / indexes
|
|
76
76
|
self.window.ui.nodes['output_files'].btn_upload.setText(trans('files.local.upload'))
|
|
@@ -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.09.
|
|
9
|
+
# Updated Date: 2025.09.28 09:35:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
@@ -14,8 +14,9 @@ import os
|
|
|
14
14
|
import shutil
|
|
15
15
|
from typing import Any, Optional, Dict
|
|
16
16
|
|
|
17
|
-
from PySide6.QtCore import Slot
|
|
18
|
-
from PySide6.QtWidgets import QVBoxLayout, QWidget, QHBoxLayout
|
|
17
|
+
from PySide6.QtCore import Slot, Qt
|
|
18
|
+
from PySide6.QtWidgets import QVBoxLayout, QWidget, QHBoxLayout, QScrollArea, QFrame
|
|
19
|
+
from PySide6.QtGui import QImageReader
|
|
19
20
|
|
|
20
21
|
from pygpt_net.core.types import (
|
|
21
22
|
MODE_AGENT,
|
|
@@ -573,10 +574,19 @@ class Editor:
|
|
|
573
574
|
layout.addStretch(1)
|
|
574
575
|
layout.addLayout(checkbox_layout)
|
|
575
576
|
|
|
576
|
-
#
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
577
|
+
# wrap the tab content in a scroll area to avoid vertical overlaps
|
|
578
|
+
tab_content = QWidget()
|
|
579
|
+
tab_content.setLayout(layout)
|
|
580
|
+
|
|
581
|
+
scroll = QScrollArea()
|
|
582
|
+
scroll.setWidgetResizable(True)
|
|
583
|
+
scroll.setFrameShape(QFrame.NoFrame)
|
|
584
|
+
scroll.setWidget(tab_content)
|
|
585
|
+
# Attach metadata on the tab widget itself for later mapping.
|
|
586
|
+
scroll.setProperty("agent_id", id)
|
|
587
|
+
scroll.setProperty("option_tab_id", option_tab_id)
|
|
588
|
+
|
|
589
|
+
tabs.addTab(scroll, title)
|
|
580
590
|
|
|
581
591
|
# store mapping: agent id -> [tab index]
|
|
582
592
|
if id not in self.tab_options_idx:
|
|
@@ -747,15 +757,20 @@ class Editor:
|
|
|
747
757
|
layout.addStretch(1)
|
|
748
758
|
layout.addLayout(checkbox_layout)
|
|
749
759
|
|
|
750
|
-
# Assemble tab widget
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
760
|
+
# Assemble tab widget wrapped into a scroll area.
|
|
761
|
+
tab_content = QWidget()
|
|
762
|
+
tab_content.setLayout(layout)
|
|
763
|
+
|
|
764
|
+
scroll = QScrollArea()
|
|
765
|
+
scroll.setWidgetResizable(True)
|
|
766
|
+
scroll.setFrameShape(QFrame.NoFrame)
|
|
767
|
+
scroll.setWidget(tab_content)
|
|
768
|
+
scroll.setProperty('agent_id', agent_id)
|
|
769
|
+
scroll.setProperty('option_tab_id', option_tab_id)
|
|
755
770
|
|
|
756
771
|
# Insert at a stable anchor to preserve general ordering between agents.
|
|
757
772
|
insertion_index = min(insertion_index, tabs.count())
|
|
758
|
-
tabs.insertTab(insertion_index,
|
|
773
|
+
tabs.insertTab(insertion_index, scroll, title)
|
|
759
774
|
new_indices.append(insertion_index)
|
|
760
775
|
insertion_index += 1
|
|
761
776
|
|
|
@@ -1297,17 +1312,26 @@ class Editor:
|
|
|
1297
1312
|
os.remove(avatar_path)
|
|
1298
1313
|
if os.path.exists(file_path):
|
|
1299
1314
|
shutil.copy(file_path, avatar_path)
|
|
1315
|
+
|
|
1316
|
+
# create thumbnail next to original (max 350px on the longest side, prefix: thumb_)
|
|
1317
|
+
thumb_path = self._create_avatar_thumbnail(avatar_path)
|
|
1318
|
+
|
|
1300
1319
|
if preset:
|
|
1301
1320
|
preset.ai_avatar = store_name
|
|
1302
1321
|
else:
|
|
1303
1322
|
self.tmp_avatar = store_name
|
|
1323
|
+
|
|
1324
|
+
# keep storing original filename in config/preset (UI will prefer thumbnail only for preview if available)
|
|
1304
1325
|
self.window.controller.config.apply_value(
|
|
1305
1326
|
parent_id=self.id,
|
|
1306
1327
|
key="ai_avatar",
|
|
1307
1328
|
option=self.options["ai_avatar"],
|
|
1308
1329
|
value=store_name,
|
|
1309
1330
|
)
|
|
1310
|
-
|
|
1331
|
+
|
|
1332
|
+
# prefer thumbnail in UI preview if it exists, fall back to original
|
|
1333
|
+
preview_path = thumb_path if thumb_path and os.path.exists(thumb_path) else avatar_path
|
|
1334
|
+
avatar_widget.load_avatar(preview_path)
|
|
1311
1335
|
avatar_widget.enable_remove_button(True)
|
|
1312
1336
|
return avatar_path
|
|
1313
1337
|
|
|
@@ -1325,11 +1349,19 @@ class Editor:
|
|
|
1325
1349
|
"avatars",
|
|
1326
1350
|
avatar_path,
|
|
1327
1351
|
)
|
|
1352
|
+
# prefer thumbnail in UI preview if available
|
|
1353
|
+
thumb_path = os.path.join(
|
|
1354
|
+
self.window.core.config.get_user_dir("presets"),
|
|
1355
|
+
"avatars",
|
|
1356
|
+
f"thumb_{avatar_path}",
|
|
1357
|
+
)
|
|
1358
|
+
preview_path = thumb_path if os.path.exists(thumb_path) else file_path
|
|
1359
|
+
|
|
1328
1360
|
if not os.path.exists(file_path):
|
|
1329
1361
|
avatar_widget.remove_avatar()
|
|
1330
1362
|
print("Avatar file does not exist:", file_path)
|
|
1331
1363
|
return
|
|
1332
|
-
avatar_widget.load_avatar(
|
|
1364
|
+
avatar_widget.load_avatar(preview_path)
|
|
1333
1365
|
avatar_widget.enable_remove_button(True)
|
|
1334
1366
|
else:
|
|
1335
1367
|
avatar_widget.remove_avatar()
|
|
@@ -1354,10 +1386,25 @@ class Editor:
|
|
|
1354
1386
|
presets_dir = self.window.core.config.get_user_dir("presets")
|
|
1355
1387
|
avatars_dir = os.path.join(presets_dir, "avatars")
|
|
1356
1388
|
avatar_path = os.path.join(avatars_dir, current)
|
|
1389
|
+
thumb_path = os.path.join(avatars_dir, f"thumb_{current}")
|
|
1390
|
+
# remove original
|
|
1357
1391
|
if os.path.exists(avatar_path):
|
|
1358
1392
|
os.remove(avatar_path)
|
|
1393
|
+
# remove thumbnail (if exists)
|
|
1394
|
+
if os.path.exists(thumb_path):
|
|
1395
|
+
os.remove(thumb_path)
|
|
1359
1396
|
preset.ai_avatar = ""
|
|
1360
1397
|
else:
|
|
1398
|
+
# if not yet persisted, also drop any staged temp avatar
|
|
1399
|
+
if self.tmp_avatar:
|
|
1400
|
+
presets_dir = self.window.core.config.get_user_dir("presets")
|
|
1401
|
+
avatars_dir = os.path.join(presets_dir, "avatars")
|
|
1402
|
+
avatar_path = os.path.join(avatars_dir, self.tmp_avatar)
|
|
1403
|
+
thumb_path = os.path.join(avatars_dir, f"thumb_{self.tmp_avatar}")
|
|
1404
|
+
if os.path.exists(avatar_path):
|
|
1405
|
+
os.remove(avatar_path)
|
|
1406
|
+
if os.path.exists(thumb_path):
|
|
1407
|
+
os.remove(thumb_path)
|
|
1361
1408
|
self.tmp_avatar = None
|
|
1362
1409
|
|
|
1363
1410
|
self.window.ui.nodes['preset.editor.avatar'].remove_avatar()
|
|
@@ -1500,15 +1547,20 @@ class Editor:
|
|
|
1500
1547
|
layout.addStretch(1)
|
|
1501
1548
|
layout.addLayout(checkbox_layout)
|
|
1502
1549
|
|
|
1503
|
-
# Create tab widget and tag with metadata
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1550
|
+
# Create tab widget wrapped in a scroll area and tag with metadata
|
|
1551
|
+
tab_content = QWidget()
|
|
1552
|
+
tab_content.setLayout(layout)
|
|
1553
|
+
|
|
1554
|
+
scroll = QScrollArea()
|
|
1555
|
+
scroll.setWidgetResizable(True)
|
|
1556
|
+
scroll.setFrameShape(QFrame.NoFrame)
|
|
1557
|
+
scroll.setWidget(tab_content)
|
|
1558
|
+
scroll.setProperty('agent_id', a_id)
|
|
1559
|
+
scroll.setProperty('option_tab_id', option_tab_id)
|
|
1508
1560
|
|
|
1509
1561
|
# Insert tab and advance anchor
|
|
1510
1562
|
insertion_index = min(insertion_index, tabs.count())
|
|
1511
|
-
tabs.insertTab(insertion_index,
|
|
1563
|
+
tabs.insertTab(insertion_index, scroll, title)
|
|
1512
1564
|
insertion_index += 1
|
|
1513
1565
|
|
|
1514
1566
|
# Apply saved values (if present) or defaults
|
|
@@ -1592,4 +1644,67 @@ class Editor:
|
|
|
1592
1644
|
widget.set_value(default_val)
|
|
1593
1645
|
except Exception:
|
|
1594
1646
|
# Silent fallback; apply_value above should already handle most cases
|
|
1595
|
-
pass
|
|
1647
|
+
pass
|
|
1648
|
+
|
|
1649
|
+
# ---------- Avatar thumbnail helpers ----------
|
|
1650
|
+
|
|
1651
|
+
def _create_avatar_thumbnail(self, src_path: str, max_px: int = 350) -> Optional[str]:
|
|
1652
|
+
"""
|
|
1653
|
+
Create a thumbnail next to the original avatar file with prefix 'thumb_'.
|
|
1654
|
+
|
|
1655
|
+
Notes:
|
|
1656
|
+
- Respects EXIF orientation via QImageReader.setAutoTransform(True).
|
|
1657
|
+
- Keeps aspect ratio; the longer side is limited to max_px.
|
|
1658
|
+
- Saves using the same extension as the original (derived from destination filename).
|
|
1659
|
+
- Returns full path to the thumbnail or None on failure.
|
|
1660
|
+
"""
|
|
1661
|
+
try:
|
|
1662
|
+
if not os.path.exists(src_path):
|
|
1663
|
+
return None
|
|
1664
|
+
|
|
1665
|
+
dir_name, base_name = os.path.split(src_path)
|
|
1666
|
+
thumb_name = f"thumb_{base_name}"
|
|
1667
|
+
thumb_path = os.path.join(dir_name, thumb_name)
|
|
1668
|
+
|
|
1669
|
+
# Clean previous thumbnail (if any) to avoid stale previews
|
|
1670
|
+
if os.path.exists(thumb_path):
|
|
1671
|
+
try:
|
|
1672
|
+
os.remove(thumb_path)
|
|
1673
|
+
except Exception:
|
|
1674
|
+
pass
|
|
1675
|
+
|
|
1676
|
+
reader = QImageReader(src_path)
|
|
1677
|
+
reader.setAutoTransform(True) # apply EXIF rotation if present
|
|
1678
|
+
image = reader.read()
|
|
1679
|
+
if image.isNull():
|
|
1680
|
+
# If reading failed, fallback to copying original (not resized)
|
|
1681
|
+
shutil.copy(src_path, thumb_path)
|
|
1682
|
+
return thumb_path
|
|
1683
|
+
|
|
1684
|
+
w = image.width()
|
|
1685
|
+
h = image.height()
|
|
1686
|
+
if w <= 0 or h <= 0:
|
|
1687
|
+
return None
|
|
1688
|
+
|
|
1689
|
+
# Scale down only when needed; always keep aspect ratio and smooth transformation
|
|
1690
|
+
if max(w, h) > max_px:
|
|
1691
|
+
scaled = image.scaled(max_px, max_px, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
1692
|
+
ok = scaled.save(thumb_path)
|
|
1693
|
+
else:
|
|
1694
|
+
ok = image.save(thumb_path)
|
|
1695
|
+
|
|
1696
|
+
if ok:
|
|
1697
|
+
return thumb_path
|
|
1698
|
+
|
|
1699
|
+
# Last resort: copy original
|
|
1700
|
+
shutil.copy(src_path, thumb_path)
|
|
1701
|
+
return thumb_path
|
|
1702
|
+
except Exception as e:
|
|
1703
|
+
# As a safe fallback try to copy the original; ignore errors silently
|
|
1704
|
+
try:
|
|
1705
|
+
dir_name, base_name = os.path.split(src_path)
|
|
1706
|
+
thumb_path = os.path.join(dir_name, f"thumb_{base_name}")
|
|
1707
|
+
shutil.copy(src_path, thumb_path)
|
|
1708
|
+
return thumb_path
|
|
1709
|
+
except Exception:
|
|
1710
|
+
return None
|
pygpt_net/controller/ui/mode.py
CHANGED
|
@@ -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.09.
|
|
9
|
+
# Updated Date: 2025.09.27 15:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from pygpt_net.core.types import (
|
|
@@ -58,6 +58,19 @@ class Mode:
|
|
|
58
58
|
is_completion = mode == MODE_COMPLETION
|
|
59
59
|
is_audio = mode == MODE_AUDIO
|
|
60
60
|
|
|
61
|
+
# enable/disable system prompt edit - disable in agents (prompts are defined per agent in presets)
|
|
62
|
+
if not is_agent_openai and not is_agent_llama:
|
|
63
|
+
presets_editor.toggle_tab("personalize", True)
|
|
64
|
+
if 'preset.prompt' in ui_nodes and ui_nodes['preset.prompt'].isReadOnly():
|
|
65
|
+
ui_nodes['preset.prompt'].setReadOnly(False)
|
|
66
|
+
ui_nodes['preset.prompt'].setPlaceholderText("")
|
|
67
|
+
else:
|
|
68
|
+
presets_editor.toggle_tab("personalize", False)
|
|
69
|
+
if 'preset.prompt' in ui_nodes and not ui_nodes['preset.prompt'].isReadOnly():
|
|
70
|
+
ui_nodes['preset.prompt'].setReadOnly(True)
|
|
71
|
+
ui_nodes['preset.prompt'].setPlaceholderText(trans("toolbox.agent.preset.placeholder"))
|
|
72
|
+
|
|
73
|
+
# audio options visibility
|
|
61
74
|
if not is_audio:
|
|
62
75
|
ui_nodes['audio.auto_turn'].setVisible(False)
|
|
63
76
|
ui_nodes["audio.loop"].setVisible(False)
|
|
@@ -71,6 +84,7 @@ class Mode:
|
|
|
71
84
|
else:
|
|
72
85
|
ctrl.audio.toggle_output_icon(False)
|
|
73
86
|
|
|
87
|
+
# presets/assistants visibility
|
|
74
88
|
if not is_assistant:
|
|
75
89
|
ui_nodes['presets.widget'].setVisible(True)
|
|
76
90
|
else:
|
|
@@ -81,6 +95,7 @@ class Mode:
|
|
|
81
95
|
else:
|
|
82
96
|
ui_nodes['env.widget'].setVisible(True)
|
|
83
97
|
|
|
98
|
+
# agents/experts/presets label visibility
|
|
84
99
|
show_agents_label = is_agent or is_agent_llama or is_agent_openai
|
|
85
100
|
if show_agents_label:
|
|
86
101
|
ui_nodes['preset.agents.label'].setVisible(True)
|
|
@@ -112,6 +127,7 @@ class Mode:
|
|
|
112
127
|
else:
|
|
113
128
|
ui_nodes['preset.editor.agent_provider_openai'].setVisible(False)
|
|
114
129
|
|
|
130
|
+
# prompt editor toolbox visibility
|
|
115
131
|
if is_agent:
|
|
116
132
|
presets_editor.toggle_tab("experts", True)
|
|
117
133
|
ui_nodes['preset.editor.temperature'].setVisible(True)
|
|
@@ -145,6 +161,7 @@ class Mode:
|
|
|
145
161
|
ui_nodes['preset.editor.modes'].setVisible(True)
|
|
146
162
|
ui_tabs['preset.editor.extra'].setTabText(0, trans("preset.prompt"))
|
|
147
163
|
|
|
164
|
+
# image options visibility
|
|
148
165
|
if is_image:
|
|
149
166
|
ui_nodes['media.raw'].setVisible(True)
|
|
150
167
|
if ctrl.media.is_video_model():
|
|
@@ -198,10 +215,8 @@ class Mode:
|
|
|
198
215
|
# remote tools icon visibility
|
|
199
216
|
if not is_image and not is_completion:
|
|
200
217
|
self.window.ui.nodes['input'].set_icon_visible("web", True)
|
|
201
|
-
# ui_nodes['icon.remote_tool.web'].setVisible(True)
|
|
202
218
|
else:
|
|
203
219
|
self.window.ui.nodes['input'].set_icon_visible("web", False)
|
|
204
|
-
# ui_nodes['icon.remote_tool.web'].setVisible(False)
|
|
205
220
|
|
|
206
221
|
ui_tabs['input'].setTabVisible(2, is_assistant)
|
|
207
222
|
ui_tabs['input'].setTabVisible(3, (not is_assistant) and (not is_image))
|
|
@@ -184,8 +184,9 @@ class Custom:
|
|
|
184
184
|
tab["label"] = slots["name"]
|
|
185
185
|
if "role" in slots:
|
|
186
186
|
opts["role"] = {
|
|
187
|
-
"type": "
|
|
188
|
-
"label": trans("agent.option.role"),
|
|
187
|
+
"type": "text",
|
|
188
|
+
"label": trans("agent.option.role.label"),
|
|
189
|
+
"description": trans("agent.option.role"),
|
|
189
190
|
"default": slots["role"],
|
|
190
191
|
}
|
|
191
192
|
if "instruction" in slots:
|
|
@@ -213,6 +214,21 @@ class Custom:
|
|
|
213
214
|
except Exception as e:
|
|
214
215
|
self.window.core.debug.log(f"Failed to build options for custom agent '{agent_id}': {e}")
|
|
215
216
|
continue
|
|
217
|
+
|
|
218
|
+
# debug tab - trace_id, etc.
|
|
219
|
+
"""
|
|
220
|
+
options["debug"] = {
|
|
221
|
+
"label": trans("agent.tab.debug"),
|
|
222
|
+
"options": {
|
|
223
|
+
"trace_id": {
|
|
224
|
+
"type": "text",
|
|
225
|
+
"label": trans("agent.option.debug.trace_id"),
|
|
226
|
+
"description": trans("agent.option.debug.trace_id.desc"),
|
|
227
|
+
"default": "",
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
"""
|
|
216
232
|
return options
|
|
217
233
|
|
|
218
234
|
def new_agent(self, name: str):
|
|
@@ -337,8 +337,8 @@ class FlowOrchestrator:
|
|
|
337
337
|
"input": prepared_items,
|
|
338
338
|
"max_turns": int(agent_kwargs.get("max_iterations", max_iterations)),
|
|
339
339
|
}
|
|
340
|
-
if trace_id:
|
|
341
|
-
run_kwargs["trace_id"] = trace_id
|
|
340
|
+
# if trace_id:
|
|
341
|
+
# run_kwargs["trace_id"] = trace_id
|
|
342
342
|
|
|
343
343
|
# Header for UI
|
|
344
344
|
ctx.set_agent_name(agent.name)
|