pygpt-net 2.6.64__py3-none-any.whl → 2.6.66__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 +21 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +5 -1
- pygpt_net/controller/chat/chat.py +0 -0
- pygpt_net/controller/chat/handler/openai_stream.py +137 -7
- pygpt_net/controller/chat/render.py +0 -0
- pygpt_net/controller/config/field/checkbox_list.py +34 -1
- pygpt_net/controller/files/files.py +71 -2
- pygpt_net/controller/media/media.py +20 -1
- pygpt_net/controller/presets/editor.py +137 -22
- pygpt_net/controller/presets/presets.py +4 -1
- pygpt_net/controller/ui/mode.py +14 -10
- pygpt_net/controller/ui/ui.py +18 -1
- 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/image/image.py +34 -1
- pygpt_net/core/render/web/renderer.py +33 -11
- pygpt_net/core/tabs/tabs.py +0 -0
- pygpt_net/core/types/image.py +61 -3
- pygpt_net/data/config/config.json +4 -3
- pygpt_net/data/config/models.json +629 -41
- 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.de.ini +4 -0
- pygpt_net/data/locale/locale.en.ini +8 -0
- pygpt_net/data/locale/locale.es.ini +4 -0
- pygpt_net/data/locale/locale.fr.ini +4 -0
- pygpt_net/data/locale/locale.it.ini +4 -0
- pygpt_net/data/locale/locale.pl.ini +4 -0
- pygpt_net/data/locale/locale.uk.ini +4 -0
- pygpt_net/data/locale/locale.zh.ini +4 -0
- pygpt_net/icons.qrc +4 -0
- pygpt_net/icons_rc.py +274 -137
- pygpt_net/item/model.py +15 -19
- pygpt_net/js_rc.py +2038 -2075
- pygpt_net/provider/agents/openai/agent.py +0 -0
- pygpt_net/provider/api/google/__init__.py +20 -9
- pygpt_net/provider/api/google/image.py +161 -28
- pygpt_net/provider/api/google/video.py +73 -36
- pygpt_net/provider/api/openai/__init__.py +21 -11
- pygpt_net/provider/api/openai/agents/client.py +0 -0
- pygpt_net/provider/api/openai/video.py +562 -0
- pygpt_net/provider/core/config/patch.py +15 -0
- pygpt_net/provider/core/model/patch.py +29 -3
- pygpt_net/provider/vector_stores/qdrant.py +117 -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/layout/toolbox/raw.py +7 -1
- 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/checkbox_list.py +14 -2
- pygpt_net/ui/widget/textarea/input.py +71 -17
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/METADATA +76 -25
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/RECORD +63 -55
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
@@ -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.25 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import re
|
|
@@ -497,8 +497,10 @@ class Presets:
|
|
|
497
497
|
|
|
498
498
|
:param no_scroll: do not scroll to current
|
|
499
499
|
"""
|
|
500
|
+
self.locked = True
|
|
500
501
|
w = self.window
|
|
501
502
|
if w.core.config.get('mode') == MODE_ASSISTANT:
|
|
503
|
+
self.locked = False
|
|
502
504
|
return
|
|
503
505
|
if no_scroll:
|
|
504
506
|
w.ui.nodes['preset.presets'].store_scroll_position()
|
|
@@ -511,6 +513,7 @@ class Presets:
|
|
|
511
513
|
if no_scroll:
|
|
512
514
|
w.ui.nodes['preset.presets'].restore_scroll_position()
|
|
513
515
|
self.on_changed()
|
|
516
|
+
self.locked = False
|
|
514
517
|
|
|
515
518
|
def update_list(self):
|
|
516
519
|
"""Update presets list"""
|
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.
|
|
9
|
+
# Updated Date: 2025.12.25 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from pygpt_net.core.types import (
|
|
@@ -39,7 +39,8 @@ class Mode:
|
|
|
39
39
|
def update(self):
|
|
40
40
|
"""Update mode, model, preset and rest of the toolbox"""
|
|
41
41
|
|
|
42
|
-
mode = self.window.core.config.
|
|
42
|
+
mode = self.window.core.config.get("mode")
|
|
43
|
+
model = self.window.core.config.get("model")
|
|
43
44
|
|
|
44
45
|
ui_nodes = self.window.ui.nodes
|
|
45
46
|
ui_tabs = self.window.ui.tabs
|
|
@@ -53,7 +54,7 @@ class Mode:
|
|
|
53
54
|
is_agent_llama = mode == MODE_AGENT_LLAMA
|
|
54
55
|
is_agent_openai = mode == MODE_AGENT_OPENAI
|
|
55
56
|
is_expert = mode == MODE_EXPERT
|
|
56
|
-
|
|
57
|
+
is_media = mode == MODE_IMAGE
|
|
57
58
|
is_llama_index = mode == MODE_LLAMA_INDEX
|
|
58
59
|
is_completion = mode == MODE_COMPLETION
|
|
59
60
|
is_audio = mode == MODE_AUDIO
|
|
@@ -161,15 +162,18 @@ class Mode:
|
|
|
161
162
|
ui_nodes['preset.editor.modes'].setVisible(True)
|
|
162
163
|
ui_tabs['preset.editor.extra'].setTabText(0, trans("preset.prompt"))
|
|
163
164
|
|
|
164
|
-
#
|
|
165
|
-
if
|
|
165
|
+
# media options visibility
|
|
166
|
+
if is_media:
|
|
166
167
|
ui_nodes['media.raw'].setVisible(True)
|
|
167
|
-
if ctrl.media.is_video_model():
|
|
168
|
+
if ctrl.media.is_video_model() and ctrl.media.get_mode() == "video":
|
|
168
169
|
ui_nodes['video.options'].setVisible(True)
|
|
169
170
|
ui_nodes['dalle.options'].setVisible(False)
|
|
170
|
-
elif ctrl.media.is_image_model():
|
|
171
|
+
elif ctrl.media.is_image_model() and ctrl.media.get_mode() == "image":
|
|
171
172
|
ui_nodes['dalle.options'].setVisible(True)
|
|
172
173
|
ui_nodes['video.options'].setVisible(False)
|
|
174
|
+
elif ctrl.media.get_mode() == "music":
|
|
175
|
+
ui_nodes['dalle.options'].setVisible(False)
|
|
176
|
+
ui_nodes['video.options'].setVisible(False)
|
|
173
177
|
else:
|
|
174
178
|
ui_nodes['media.raw'].setVisible(False)
|
|
175
179
|
ui_nodes['dalle.options'].setVisible(False)
|
|
@@ -199,7 +203,7 @@ class Mode:
|
|
|
199
203
|
else:
|
|
200
204
|
ui_nodes['idx.options'].setVisible(False)
|
|
201
205
|
|
|
202
|
-
if
|
|
206
|
+
if is_media:
|
|
203
207
|
ui_nodes['input.stream'].setVisible(False)
|
|
204
208
|
else:
|
|
205
209
|
ui_nodes['input.stream'].setVisible(True)
|
|
@@ -213,13 +217,13 @@ class Mode:
|
|
|
213
217
|
ui_tabs['input'].setTabVisible(1, show)
|
|
214
218
|
|
|
215
219
|
# remote tools icon visibility
|
|
216
|
-
if not
|
|
220
|
+
if not is_media and not is_completion:
|
|
217
221
|
self.window.ui.nodes['input'].set_icon_visible("web", True)
|
|
218
222
|
else:
|
|
219
223
|
self.window.ui.nodes['input'].set_icon_visible("web", False)
|
|
220
224
|
|
|
221
225
|
ui_tabs['input'].setTabVisible(2, is_assistant)
|
|
222
|
-
ui_tabs['input'].setTabVisible(3, (not is_assistant) and (not
|
|
226
|
+
ui_tabs['input'].setTabVisible(3, (not is_assistant) and (not is_media))
|
|
223
227
|
|
|
224
228
|
presets_editor.toggle_extra_options()
|
|
225
229
|
|
pygpt_net/controller/ui/ui.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.
|
|
9
|
+
# Updated Date: 2025.12.25 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from typing import Optional
|
|
@@ -68,6 +68,7 @@ class UI:
|
|
|
68
68
|
self.update_tokens()
|
|
69
69
|
self.vision.update()
|
|
70
70
|
self.window.controller.agent.legacy.update()
|
|
71
|
+
self.img_update_available_modes()
|
|
71
72
|
self.img_update_available_resolutions()
|
|
72
73
|
|
|
73
74
|
def handle(self, event: BaseEvent):
|
|
@@ -258,4 +259,20 @@ class UI:
|
|
|
258
259
|
key="img_resolution",
|
|
259
260
|
option=self.window.core.image.get_resolution_option(),
|
|
260
261
|
value=current,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def img_update_available_modes(self):
|
|
265
|
+
"""Update available modes for images"""
|
|
266
|
+
mode = self.window.core.config.get('mode')
|
|
267
|
+
if mode != MODE_IMAGE:
|
|
268
|
+
return
|
|
269
|
+
model = self.window.core.config.get('model')
|
|
270
|
+
keys = self.window.core.image.get_available_modes(model)
|
|
271
|
+
current = self.window.core.config.get('img_mode', 'image')
|
|
272
|
+
self.window.ui.config['global']['img_mode'].set_keys(keys, lock=False)
|
|
273
|
+
self.window.controller.config.apply_value(
|
|
274
|
+
parent_id="global",
|
|
275
|
+
key="img_mode",
|
|
276
|
+
option=self.window.core.image.get_mode_option(),
|
|
277
|
+
value=current,
|
|
261
278
|
)
|
|
@@ -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
|
pygpt_net/core/image/image.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.
|
|
9
|
+
# Updated Date: 2025.12.25 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import uuid
|
|
@@ -158,6 +158,39 @@ class Image(QObject):
|
|
|
158
158
|
"keys": self.get_available_resolutions(),
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
def get_mode_option(self) -> dict:
|
|
162
|
+
"""
|
|
163
|
+
Get image mode option for UI
|
|
164
|
+
|
|
165
|
+
:return: dict
|
|
166
|
+
"""
|
|
167
|
+
return {
|
|
168
|
+
"type": "combo",
|
|
169
|
+
"slider": True,
|
|
170
|
+
"label": "img_mode",
|
|
171
|
+
"value": "image",
|
|
172
|
+
"keys": self.get_available_modes(),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
def get_available_modes(self, model_name: str = None) -> Dict[str, str]:
|
|
176
|
+
"""
|
|
177
|
+
Get available modes (image / video)
|
|
178
|
+
|
|
179
|
+
:param model_name: model name
|
|
180
|
+
:return: dict of available modes
|
|
181
|
+
"""
|
|
182
|
+
options = {}
|
|
183
|
+
if model_name:
|
|
184
|
+
model = self.window.core.models.get(model_name)
|
|
185
|
+
if model:
|
|
186
|
+
if model.is_image_output():
|
|
187
|
+
options["image"] = trans("mode.img.image")
|
|
188
|
+
if model.is_video_output():
|
|
189
|
+
options["video"] = trans("mode.img.video")
|
|
190
|
+
if model.is_music_output():
|
|
191
|
+
options["music"] = trans("mode.img.music")
|
|
192
|
+
return options
|
|
193
|
+
|
|
161
194
|
def get_available_resolutions(self, model: str = None) -> Dict[str, str]:
|
|
162
195
|
"""
|
|
163
196
|
Get available image resolutions
|