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.
Files changed (62) hide show
  1. pygpt_net/CHANGELOG.txt +16 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +3 -1
  4. pygpt_net/controller/attachment/attachment.py +17 -8
  5. pygpt_net/controller/camera/camera.py +4 -4
  6. pygpt_net/controller/files/files.py +71 -2
  7. pygpt_net/controller/lang/custom.py +2 -2
  8. pygpt_net/controller/presets/editor.py +137 -22
  9. pygpt_net/controller/ui/mode.py +18 -3
  10. pygpt_net/core/agents/custom/__init__.py +18 -2
  11. pygpt_net/core/agents/custom/runner.py +2 -2
  12. pygpt_net/core/attachments/clipboard.py +146 -0
  13. pygpt_net/core/render/web/renderer.py +44 -11
  14. pygpt_net/data/config/config.json +3 -3
  15. pygpt_net/data/config/models.json +3 -3
  16. pygpt_net/data/config/presets/agent_openai_coder.json +15 -1
  17. pygpt_net/data/css/style.dark.css +12 -0
  18. pygpt_net/data/css/style.light.css +12 -0
  19. pygpt_net/data/icons/pin2.svg +1 -0
  20. pygpt_net/data/icons/pin3.svg +3 -0
  21. pygpt_net/data/icons/point.svg +1 -0
  22. pygpt_net/data/icons/target.svg +1 -0
  23. pygpt_net/data/js/app/runtime.js +11 -4
  24. pygpt_net/data/js/app/scroll.js +14 -0
  25. pygpt_net/data/js/app/ui.js +19 -2
  26. pygpt_net/data/js/app/user.js +22 -54
  27. pygpt_net/data/js/app.min.js +13 -14
  28. pygpt_net/data/locale/locale.de.ini +32 -0
  29. pygpt_net/data/locale/locale.en.ini +38 -2
  30. pygpt_net/data/locale/locale.es.ini +32 -0
  31. pygpt_net/data/locale/locale.fr.ini +32 -0
  32. pygpt_net/data/locale/locale.it.ini +32 -0
  33. pygpt_net/data/locale/locale.pl.ini +34 -2
  34. pygpt_net/data/locale/locale.uk.ini +32 -0
  35. pygpt_net/data/locale/locale.zh.ini +32 -0
  36. pygpt_net/icons.qrc +4 -0
  37. pygpt_net/icons_rc.py +274 -137
  38. pygpt_net/js_rc.py +8262 -8230
  39. pygpt_net/provider/agents/llama_index/planner_workflow.py +15 -3
  40. pygpt_net/provider/agents/llama_index/workflow/planner.py +69 -41
  41. pygpt_net/provider/agents/openai/agent_planner.py +57 -35
  42. pygpt_net/provider/agents/openai/evolve.py +0 -3
  43. pygpt_net/provider/api/google/__init__.py +9 -3
  44. pygpt_net/provider/api/google/image.py +11 -1
  45. pygpt_net/provider/api/google/music.py +375 -0
  46. pygpt_net/provider/core/config/patch.py +8 -0
  47. pygpt_net/ui/__init__.py +6 -1
  48. pygpt_net/ui/dialog/preset.py +9 -4
  49. pygpt_net/ui/layout/chat/attachments.py +18 -1
  50. pygpt_net/ui/layout/status.py +3 -3
  51. pygpt_net/ui/widget/element/status.py +55 -0
  52. pygpt_net/ui/widget/filesystem/explorer.py +116 -2
  53. pygpt_net/ui/widget/lists/context.py +26 -16
  54. pygpt_net/ui/widget/option/combo.py +149 -11
  55. pygpt_net/ui/widget/textarea/input.py +71 -17
  56. pygpt_net/ui/widget/textarea/web.py +1 -1
  57. pygpt_net/ui/widget/vision/camera.py +135 -12
  58. {pygpt_net-2.6.63.dist-info → pygpt_net-2.6.65.dist-info}/METADATA +18 -2
  59. {pygpt_net-2.6.63.dist-info → pygpt_net-2.6.65.dist-info}/RECORD +62 -55
  60. {pygpt_net-2.6.63.dist-info → pygpt_net-2.6.65.dist-info}/LICENSE +0 -0
  61. {pygpt_net-2.6.63.dist-info → pygpt_net-2.6.65.dist-info}/WHEEL +0 -0
  62. {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.27 00:00:00 #
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.63"
17
- __build__ = "2025-09-27"
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.22 19:00:00 #
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
- if not all:
583
- image_ext = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff']
584
- ext = os.path.splitext(url)[1].lower()
585
- if ext not in image_ext:
586
- return
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 = "Clipboard image"
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("painter.capture.manual.captured.success") + ' ' + os.path.basename(url),
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'].label.setText(trans("vision.capture.label"))
70
+ self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.label"))
71
71
  else:
72
- self.window.ui.nodes['video.preview'].label.setText(trans("vision.capture.auto.label"))
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'].label.setText(trans("vision.capture.auto.label"))
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'].label.setText(trans("vision.capture.label"))
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.08.25 18:00:00 #
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'].label.setText(trans("vision.capture.label"))
71
+ self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.label"))
72
72
  else:
73
- self.window.ui.nodes['video.preview'].label.setText(trans("vision.capture.auto.label"))
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.27 00:00:00 #
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
- # as tab
577
- tab_widget = QWidget()
578
- tab_widget.setLayout(layout)
579
- tabs.addTab(tab_widget, title)
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 and tag it with metadata.
751
- tab_widget = QWidget()
752
- tab_widget.setLayout(layout)
753
- tab_widget.setProperty('agent_id', agent_id)
754
- tab_widget.setProperty('option_tab_id', option_tab_id)
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, tab_widget, title)
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
- avatar_widget.load_avatar(avatar_path)
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(file_path)
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
- tab_widget = QWidget()
1505
- tab_widget.setLayout(layout)
1506
- tab_widget.setProperty('agent_id', a_id)
1507
- tab_widget.setProperty('option_tab_id', option_tab_id)
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, tab_widget, title)
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.09.22 12:00:00 #
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": "str",
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)