pygpt-net 2.6.64__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 +10 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +3 -1
- pygpt_net/controller/files/files.py +71 -2
- pygpt_net/controller/presets/editor.py +137 -22
- 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 +33 -11
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- 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/ui.js +19 -2
- pygpt_net/data/js/app/user.js +22 -54
- pygpt_net/data/js/app.min.js +7 -9
- pygpt_net/data/locale/locale.en.ini +4 -0
- pygpt_net/icons.qrc +4 -0
- pygpt_net/icons_rc.py +274 -137
- pygpt_net/js_rc.py +2038 -2075
- 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/textarea/input.py +71 -17
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.65.dist-info}/METADATA +12 -2
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.65.dist-info}/RECORD +38 -32
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.65.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.65.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.65.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
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
|
+
|
|
1
11
|
2.6.64 (2025-09-27)
|
|
2
12
|
|
|
3
13
|
- Added translations to agent headers.
|
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)
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.09.28 08:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from PySide6.QtCore import QObject, QEvent, Qt
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AttachmentDropHandler(QObject):
|
|
16
|
+
"""
|
|
17
|
+
Generic drag & drop handler for attaching files/images/urls/text.
|
|
18
|
+
|
|
19
|
+
Policies:
|
|
20
|
+
- SWALLOW_ALL: always consume the drop (e.g., attachments list).
|
|
21
|
+
- INPUT_MIX : for ChatInput; process attachments and:
|
|
22
|
+
* swallow image payloads (no text insert),
|
|
23
|
+
* allow default handling for non-image payloads so paths/text get inserted.
|
|
24
|
+
"""
|
|
25
|
+
SWALLOW_ALL = 0
|
|
26
|
+
INPUT_MIX = 1
|
|
27
|
+
|
|
28
|
+
def __init__(self, window, target_widget, policy=SWALLOW_ALL):
|
|
29
|
+
super().__init__(target_widget)
|
|
30
|
+
self.window = window
|
|
31
|
+
self._target = target_widget
|
|
32
|
+
self._policy = policy
|
|
33
|
+
|
|
34
|
+
# Accept drops on target and its viewport (important for QTextEdit/QAbstractScrollArea)
|
|
35
|
+
self._enable_drops(self._target)
|
|
36
|
+
vp = self._get_viewport(self._target)
|
|
37
|
+
if vp is not None:
|
|
38
|
+
self._enable_drops(vp)
|
|
39
|
+
|
|
40
|
+
# Install filters on both
|
|
41
|
+
self._target.installEventFilter(self)
|
|
42
|
+
if vp is not None:
|
|
43
|
+
vp.installEventFilter(self)
|
|
44
|
+
|
|
45
|
+
def _enable_drops(self, w):
|
|
46
|
+
try:
|
|
47
|
+
w.setAcceptDrops(True)
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def _get_viewport(self, w):
|
|
52
|
+
try:
|
|
53
|
+
vp = getattr(w, "viewport", None)
|
|
54
|
+
if callable(vp):
|
|
55
|
+
return vp()
|
|
56
|
+
return None
|
|
57
|
+
except Exception:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def _mime_supported(self, md) -> bool:
|
|
61
|
+
try:
|
|
62
|
+
if md is None:
|
|
63
|
+
return False
|
|
64
|
+
return md.hasUrls() or md.hasImage() or md.hasText()
|
|
65
|
+
except Exception:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def _process_drop(self, md):
|
|
69
|
+
"""
|
|
70
|
+
Route to ChatInput.handle_clipboard() to reuse existing attach pipeline.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
chat_input = self.window.ui.nodes.get('input')
|
|
74
|
+
except Exception:
|
|
75
|
+
chat_input = None
|
|
76
|
+
|
|
77
|
+
if chat_input is not None and hasattr(chat_input, 'handle_clipboard'):
|
|
78
|
+
try:
|
|
79
|
+
chat_input.handle_clipboard(md)
|
|
80
|
+
return chat_input
|
|
81
|
+
except Exception as e:
|
|
82
|
+
try:
|
|
83
|
+
self.window.core.debug.log(e)
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def _allow_default_text_insert_for_non_image(self, md) -> bool:
|
|
89
|
+
try:
|
|
90
|
+
return not (md and md.hasImage())
|
|
91
|
+
except Exception:
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def eventFilter(self, obj, event):
|
|
95
|
+
# Only handle events coming to the target or its viewport
|
|
96
|
+
if obj is not self._target and obj is not self._get_viewport(self._target):
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
et = event.type()
|
|
100
|
+
|
|
101
|
+
if et in (QEvent.DragEnter, QEvent.DragMove):
|
|
102
|
+
md = getattr(event, 'mimeData', lambda: None)()
|
|
103
|
+
if self._mime_supported(md):
|
|
104
|
+
try:
|
|
105
|
+
event.setDropAction(Qt.CopyAction)
|
|
106
|
+
event.acceptProposedAction()
|
|
107
|
+
except Exception:
|
|
108
|
+
event.accept()
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
if et == QEvent.Drop:
|
|
113
|
+
md = getattr(event, 'mimeData', lambda: None)()
|
|
114
|
+
if not self._mime_supported(md):
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
chat_input = self._process_drop(md)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
event.setDropAction(Qt.CopyAction)
|
|
121
|
+
event.acceptProposedAction()
|
|
122
|
+
except Exception:
|
|
123
|
+
event.accept()
|
|
124
|
+
|
|
125
|
+
# Policy decision:
|
|
126
|
+
if self._policy == self.SWALLOW_ALL:
|
|
127
|
+
# Consume the event; nothing else should handle it.
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
if self._policy == self.INPUT_MIX:
|
|
131
|
+
# For non-image payloads we allow default to insert text/paths into input.
|
|
132
|
+
# To avoid duplicate attachments (insertFromMimeData calls handle_clipboard),
|
|
133
|
+
# set a one-shot guard flag.
|
|
134
|
+
if chat_input is not None and self._allow_default_text_insert_for_non_image(md):
|
|
135
|
+
try:
|
|
136
|
+
chat_input._skip_clipboard_on_next_insert = True
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
return False # let default drop insert text/paths
|
|
140
|
+
else:
|
|
141
|
+
return True # swallow images
|
|
142
|
+
|
|
143
|
+
# Default: swallow
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
return False
|
|
@@ -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 10:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
@@ -1216,11 +1216,10 @@ class Renderer(BaseRenderer):
|
|
|
1216
1216
|
if preset.ai_name:
|
|
1217
1217
|
output_name = preset.ai_name
|
|
1218
1218
|
if preset.ai_avatar:
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
avatar_html = f"<img src=\"{self._file_prefix}{avatar_path}\" class=\"avatar\"> "
|
|
1219
|
+
# prefer thumbnail "thumb_<name>" when available
|
|
1220
|
+
avatar_fs = self._resolve_avatar_fs_path(preset.ai_avatar)
|
|
1221
|
+
if avatar_fs:
|
|
1222
|
+
avatar_html = f"<img src=\"{self._file_prefix}{avatar_fs}\" class=\"avatar\"> "
|
|
1224
1223
|
|
|
1225
1224
|
if not output_name and not avatar_html:
|
|
1226
1225
|
return ""
|
|
@@ -1962,6 +1961,30 @@ class Renderer(BaseRenderer):
|
|
|
1962
1961
|
pass
|
|
1963
1962
|
return None
|
|
1964
1963
|
|
|
1964
|
+
def _resolve_avatar_fs_path(self, basename: str) -> Optional[str]:
|
|
1965
|
+
"""
|
|
1966
|
+
Resolve avatar file system path preferring a local thumbnail named 'thumb_<basename>'.
|
|
1967
|
+
|
|
1968
|
+
Returns:
|
|
1969
|
+
- Absolute path to thumbnail when exists,
|
|
1970
|
+
- Otherwise absolute path to original when exists,
|
|
1971
|
+
- Otherwise None.
|
|
1972
|
+
"""
|
|
1973
|
+
if not basename:
|
|
1974
|
+
return None
|
|
1975
|
+
try:
|
|
1976
|
+
presets_dir = self.window.core.config.get_user_dir("presets")
|
|
1977
|
+
avatars_dir = os.path.join(presets_dir, "avatars")
|
|
1978
|
+
thumb = os.path.join(avatars_dir, f"thumb_{basename}")
|
|
1979
|
+
original = os.path.join(avatars_dir, basename)
|
|
1980
|
+
if os.path.exists(thumb):
|
|
1981
|
+
return thumb
|
|
1982
|
+
if os.path.exists(original):
|
|
1983
|
+
return original
|
|
1984
|
+
except Exception:
|
|
1985
|
+
pass
|
|
1986
|
+
return None
|
|
1987
|
+
|
|
1965
1988
|
def _output_identity(self, ctx: CtxItem) -> Tuple[str, Optional[str], bool]:
|
|
1966
1989
|
"""
|
|
1967
1990
|
Resolve output identity (name, avatar file:// path) based on preset or ctx-provided agent name.
|
|
@@ -1999,11 +2022,10 @@ class Renderer(BaseRenderer):
|
|
|
1999
2022
|
name = preset.ai_name or default_name
|
|
2000
2023
|
avatar = None
|
|
2001
2024
|
if preset.ai_avatar:
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
avatar = f"{self._file_prefix}{avatar_path}"
|
|
2025
|
+
# prefer thumbnail URL if available
|
|
2026
|
+
avatar_fs = self._resolve_avatar_fs_path(preset.ai_avatar)
|
|
2027
|
+
if avatar_fs:
|
|
2028
|
+
avatar = f"{self._file_prefix}{avatar_fs}"
|
|
2007
2029
|
return name, avatar, True
|
|
2008
2030
|
|
|
2009
2031
|
def _build_render_block(
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"__meta__": {
|
|
3
|
-
"version": "2.6.
|
|
4
|
-
"app.version": "2.6.
|
|
5
|
-
"updated_at": "2025-09-
|
|
3
|
+
"version": "2.6.65",
|
|
4
|
+
"app.version": "2.6.65",
|
|
5
|
+
"updated_at": "2025-09-28T00:00:00"
|
|
6
6
|
},
|
|
7
7
|
"access.audio.event.speech": false,
|
|
8
8
|
"access.audio.event.speech.disabled": [],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"__meta__": {
|
|
3
|
-
"version": "2.6.
|
|
4
|
-
"app.version": "2.6.
|
|
5
|
-
"updated_at": "2025-09-
|
|
3
|
+
"version": "2.6.65",
|
|
4
|
+
"app.version": "2.6.65",
|
|
5
|
+
"updated_at": "2025-09-28T00:00:00"
|
|
6
6
|
},
|
|
7
7
|
"items": {
|
|
8
8
|
"SpeakLeash/bielik-11b-v2.3-instruct:Q4_K_M": {
|