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.
Files changed (68) hide show
  1. pygpt_net/CHANGELOG.txt +21 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +5 -1
  4. pygpt_net/controller/chat/chat.py +0 -0
  5. pygpt_net/controller/chat/handler/openai_stream.py +137 -7
  6. pygpt_net/controller/chat/render.py +0 -0
  7. pygpt_net/controller/config/field/checkbox_list.py +34 -1
  8. pygpt_net/controller/files/files.py +71 -2
  9. pygpt_net/controller/media/media.py +20 -1
  10. pygpt_net/controller/presets/editor.py +137 -22
  11. pygpt_net/controller/presets/presets.py +4 -1
  12. pygpt_net/controller/ui/mode.py +14 -10
  13. pygpt_net/controller/ui/ui.py +18 -1
  14. pygpt_net/core/agents/custom/__init__.py +18 -2
  15. pygpt_net/core/agents/custom/runner.py +2 -2
  16. pygpt_net/core/attachments/clipboard.py +146 -0
  17. pygpt_net/core/image/image.py +34 -1
  18. pygpt_net/core/render/web/renderer.py +33 -11
  19. pygpt_net/core/tabs/tabs.py +0 -0
  20. pygpt_net/core/types/image.py +61 -3
  21. pygpt_net/data/config/config.json +4 -3
  22. pygpt_net/data/config/models.json +629 -41
  23. pygpt_net/data/css/style.dark.css +12 -0
  24. pygpt_net/data/css/style.light.css +12 -0
  25. pygpt_net/data/icons/pin2.svg +1 -0
  26. pygpt_net/data/icons/pin3.svg +3 -0
  27. pygpt_net/data/icons/point.svg +1 -0
  28. pygpt_net/data/icons/target.svg +1 -0
  29. pygpt_net/data/js/app/ui.js +19 -2
  30. pygpt_net/data/js/app/user.js +22 -54
  31. pygpt_net/data/js/app.min.js +7 -9
  32. pygpt_net/data/locale/locale.de.ini +4 -0
  33. pygpt_net/data/locale/locale.en.ini +8 -0
  34. pygpt_net/data/locale/locale.es.ini +4 -0
  35. pygpt_net/data/locale/locale.fr.ini +4 -0
  36. pygpt_net/data/locale/locale.it.ini +4 -0
  37. pygpt_net/data/locale/locale.pl.ini +4 -0
  38. pygpt_net/data/locale/locale.uk.ini +4 -0
  39. pygpt_net/data/locale/locale.zh.ini +4 -0
  40. pygpt_net/icons.qrc +4 -0
  41. pygpt_net/icons_rc.py +274 -137
  42. pygpt_net/item/model.py +15 -19
  43. pygpt_net/js_rc.py +2038 -2075
  44. pygpt_net/provider/agents/openai/agent.py +0 -0
  45. pygpt_net/provider/api/google/__init__.py +20 -9
  46. pygpt_net/provider/api/google/image.py +161 -28
  47. pygpt_net/provider/api/google/video.py +73 -36
  48. pygpt_net/provider/api/openai/__init__.py +21 -11
  49. pygpt_net/provider/api/openai/agents/client.py +0 -0
  50. pygpt_net/provider/api/openai/video.py +562 -0
  51. pygpt_net/provider/core/config/patch.py +15 -0
  52. pygpt_net/provider/core/model/patch.py +29 -3
  53. pygpt_net/provider/vector_stores/qdrant.py +117 -0
  54. pygpt_net/ui/__init__.py +6 -1
  55. pygpt_net/ui/dialog/preset.py +9 -4
  56. pygpt_net/ui/layout/chat/attachments.py +18 -1
  57. pygpt_net/ui/layout/status.py +3 -3
  58. pygpt_net/ui/layout/toolbox/raw.py +7 -1
  59. pygpt_net/ui/widget/element/status.py +55 -0
  60. pygpt_net/ui/widget/filesystem/explorer.py +116 -2
  61. pygpt_net/ui/widget/lists/context.py +26 -16
  62. pygpt_net/ui/widget/option/checkbox_list.py +14 -2
  63. pygpt_net/ui/widget/textarea/input.py +71 -17
  64. {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/METADATA +76 -25
  65. {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/RECORD +63 -55
  66. {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/LICENSE +0 -0
  67. {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/WHEEL +0 -0
  68. {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,117 @@
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.30 13:00:00 #
10
+ # ================================================== #
11
+
12
+ import datetime
13
+ import os.path
14
+ from typing import Optional
15
+
16
+ from llama_index.core.indices.base import BaseIndex
17
+ from llama_index.core import StorageContext
18
+
19
+ from pygpt_net.utils import parse_args
20
+ from .base import BaseStore
21
+
22
+
23
+ class QdrantProvider(BaseStore):
24
+ def __init__(self, *args, **kwargs):
25
+ super(QdrantProvider, self).__init__(*args, **kwargs)
26
+ """
27
+ Qdrant vector store provider
28
+
29
+ :param args: args
30
+ :param kwargs: kwargs
31
+ """
32
+ self.window = kwargs.get('window', None)
33
+ self.id = "QdrantVectorStore"
34
+ self.prefix = "qdrant_" # prefix for index directory
35
+ self.indexes = {}
36
+
37
+ def create(self, id: str):
38
+ """
39
+ Create empty index
40
+
41
+ :param id: index name
42
+ """
43
+ path = self.get_path(id)
44
+ if not os.path.exists(path):
45
+ os.makedirs(path, exist_ok=True)
46
+ self.store(id)
47
+
48
+ def get_qdrant_store(self, id: str):
49
+ """
50
+ Get Qdrant vector store
51
+
52
+ :param id: index name
53
+ :return: QdrantVectorStore instance
54
+ """
55
+ from llama_index.vector_stores.qdrant import QdrantVectorStore
56
+
57
+ additional_args = parse_args(
58
+ self.window.core.config.get('llama.idx.storage.args', []),
59
+ )
60
+
61
+ url = additional_args.get('url', 'http://localhost:6333')
62
+ api_key = additional_args.get('api_key', '')
63
+
64
+ store_args = {k: v for k, v in additional_args.items() if k not in ['url', 'api_key', 'collection_name']}
65
+
66
+ return QdrantVectorStore(
67
+ url=url,
68
+ api_key=api_key,
69
+ collection_name=id,
70
+ **store_args
71
+ )
72
+
73
+ def get(
74
+ self,
75
+ id: str,
76
+ llm: Optional = None,
77
+ embed_model: Optional = None,
78
+ ) -> BaseIndex:
79
+ """
80
+ Get index
81
+
82
+ :param id: index name
83
+ :param llm: LLM instance
84
+ :param embed_model: Embedding model instance
85
+ :return: index instance
86
+ """
87
+ if not self.exists(id):
88
+ self.create(id)
89
+ vector_store = self.get_qdrant_store(id)
90
+ storage_context = StorageContext.from_defaults(
91
+ vector_store=vector_store,
92
+ )
93
+ self.indexes[id] = self.index_from_store(
94
+ vector_store=vector_store,
95
+ storage_context=storage_context,
96
+ llm=llm,
97
+ embed_model=embed_model,
98
+ )
99
+ return self.indexes[id]
100
+
101
+ def store(
102
+ self,
103
+ id: str,
104
+ index: Optional[BaseIndex] = None
105
+ ):
106
+ """
107
+ Store index
108
+
109
+ :param id: index name
110
+ :param index: index instance
111
+ """
112
+ path = self.get_path(id)
113
+ os.makedirs(path, exist_ok=True)
114
+ lock_file = os.path.join(path, 'store.lock')
115
+ with open(lock_file, 'w') as f:
116
+ f.write(id + ': ' + str(datetime.datetime.now()))
117
+ self.indexes[id] = index
pygpt_net/ui/__init__.py CHANGED
@@ -113,6 +113,8 @@ class UI:
113
113
  """Set default sizes"""
114
114
  def set_initial_splitter_height():
115
115
  """Set initial splitter height"""
116
+ if 'main.output' not in self.window.ui.splitters:
117
+ return
116
118
  total_height = self.window.ui.splitters['main.output'].size().height()
117
119
  if total_height > 0:
118
120
  size_output = int(total_height * 0.9)
@@ -124,6 +126,8 @@ class UI:
124
126
 
125
127
  def set_initial_splitter_width():
126
128
  """Set initial splitter width"""
129
+ if 'main' not in self.window.ui.splitters:
130
+ return
127
131
  total_width = self.window.ui.splitters['main'].size().width()
128
132
  if total_width > 0:
129
133
  size_output = int(total_width * 0.75)
@@ -139,7 +143,8 @@ class UI:
139
143
  suffix = self.window.core.platforms.get_env_suffix()
140
144
  profile_name = self.window.core.config.profile.get_current_name()
141
145
  self.window.setWindowTitle(
142
- f"PyGPT - Desktop AI Assistant {self.window.meta['version']} | build {self.window.meta['build'].replace('.', '-')}{suffix} ({profile_name})"
146
+ f"PyGPT - Desktop AI Assistant {self.window.meta['version']} | "
147
+ f"build {self.window.meta['build'].replace('.', '-')}{suffix} ({profile_name})"
143
148
  )
144
149
 
145
150
  def post_setup(self):
@@ -6,13 +6,13 @@
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.25 13:04:51 #
9
+ # Updated Date: 2025.09.28 08:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt
13
13
  from PySide6.QtGui import QIcon
14
14
  from PySide6.QtWidgets import QPushButton, QHBoxLayout, QLabel, QVBoxLayout, QSplitter, QWidget, QSizePolicy, \
15
- QTabWidget, QFileDialog
15
+ QTabWidget, QFileDialog, QScrollArea, QFrame
16
16
 
17
17
  from pygpt_net.core.types import (
18
18
  MODE_AGENT,
@@ -169,7 +169,6 @@ class Preset(BaseConfigDialog):
169
169
  desc.setContentsMargins(0, 5, 0, 5)
170
170
  self.window.ui.nodes['preset.editor.description'] = desc
171
171
 
172
-
173
172
  # prompt + extra options
174
173
  prompt_layout = QVBoxLayout()
175
174
  prompt_layout.addWidget(widgets['prompt'])
@@ -334,8 +333,14 @@ class Preset(BaseConfigDialog):
334
333
  self.window.ui.nodes['preset.editor.extra'] = {}
335
334
 
336
335
  tabs = QTabWidget()
336
+
337
+ # Make the prompt tab scrollable to avoid vertical overlap in narrow layouts.
338
+ scroll_prompt = QScrollArea()
339
+ scroll_prompt.setWidget(prompt_widget)
340
+ scroll_prompt.setWidgetResizable(True)
341
+ scroll_prompt.setFrameShape(QFrame.NoFrame)
337
342
  tabs.addTab(
338
- prompt_widget,
343
+ scroll_prompt,
339
344
  trans("preset.prompt"),
340
345
  )
341
346
  tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
@@ -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.24 23:00:00 #
9
+ # Updated Date: 2025.09.28 08:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -19,8 +19,10 @@ from PySide6.QtWidgets import QVBoxLayout, QPushButton, QHBoxLayout, QCheckBox,
19
19
  from pygpt_net.item.attachment import AttachmentItem
20
20
  from pygpt_net.ui.widget.element.labels import HelpLabel
21
21
  from pygpt_net.ui.widget.lists.attachment import AttachmentList
22
+ from pygpt_net.core.attachments.clipboard import AttachmentDropHandler
22
23
  from pygpt_net.utils import trans
23
24
 
25
+
24
26
  class Attachments:
25
27
  def __init__(self, window=None):
26
28
  """
@@ -30,6 +32,8 @@ class Attachments:
30
32
  """
31
33
  self.window = window
32
34
  self.id = 'attachments'
35
+ # Keep a strong reference to DnD handler(s)
36
+ self._dnd_handlers = {}
33
37
 
34
38
  def setup(self) -> QVBoxLayout:
35
39
  """
@@ -132,6 +136,19 @@ class Attachments:
132
136
  self.window.ui.models[self.id] = self.create_model(self.window)
133
137
  self.window.ui.nodes[self.id].setModel(self.window.ui.models[self.id])
134
138
 
139
+ # Drag & Drop: allow dropping files/images/urls/text directly onto the list
140
+ try:
141
+ self._dnd_handlers[self.id] = AttachmentDropHandler(
142
+ self.window,
143
+ self.window.ui.nodes[self.id],
144
+ policy=AttachmentDropHandler.SWALLOW_ALL,
145
+ )
146
+ except Exception as e:
147
+ try:
148
+ self.window.core.debug.log(e)
149
+ except Exception:
150
+ pass
151
+
135
152
  def create_model(self, parent) -> QStandardItemModel:
136
153
  """
137
154
  Create list model
@@ -14,6 +14,7 @@ from PySide6.QtWidgets import QLabel, QHBoxLayout, QSizePolicy, QPushButton
14
14
 
15
15
  from pygpt_net.ui.widget.element.labels import HelpLabel
16
16
  from pygpt_net.ui.widget.anims.loader import Loader
17
+ from pygpt_net.ui.widget.element.status import BottomStatus
17
18
  from pygpt_net.utils import trans
18
19
 
19
20
 
@@ -34,8 +35,7 @@ class Status:
34
35
  """
35
36
  nodes = self.window.ui.nodes
36
37
 
37
- nodes['status'] = QLabel(trans('status.started'))
38
- nodes['status'].setParent(self.window)
38
+ nodes['status'] = BottomStatus(window=self.window)
39
39
 
40
40
  nodes['status.agent'] = HelpLabel("")
41
41
  nodes['status.agent'].setParent(self.window)
@@ -53,7 +53,7 @@ class Status:
53
53
  layout = QHBoxLayout()
54
54
  layout.addWidget(nodes['anim.loading.status'])
55
55
  layout.addWidget(nodes['status.agent'])
56
- layout.addWidget(nodes['status'])
56
+ layout.addWidget(nodes['status'].setup())
57
57
  layout.addWidget(nodes['global.stop'])
58
58
  layout.setAlignment(Qt.AlignLeft)
59
59
 
@@ -6,11 +6,12 @@
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.01 23:00:00 #
9
+ # Updated Date: 2025.12.25 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QCheckBox
13
13
 
14
+ from pygpt_net.ui.widget.option.combo import OptionCombo
14
15
  from pygpt_net.utils import trans
15
16
 
16
17
 
@@ -39,7 +40,12 @@ class Raw:
39
40
  conf_global['img_raw'] = QCheckBox(trans("img.raw"), parent=container)
40
41
  conf_global['img_raw'].toggled.connect(self.window.controller.media.toggle_raw)
41
42
 
43
+ conf_global = ui.config['global']
44
+ option_modes = self.window.core.image.get_available_modes()
45
+ conf_global['img_mode'] = OptionCombo(self.window, 'global', 'img_mode', option_modes)
46
+
42
47
  cols = QHBoxLayout()
48
+ cols.addWidget(conf_global['img_mode'])
43
49
  cols.addWidget(conf_global['img_raw'])
44
50
 
45
51
  rows = QVBoxLayout()
@@ -0,0 +1,55 @@
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 00:00:00 #
10
+ # ================================================== #
11
+
12
+ from PySide6.QtWidgets import QLabel, QHBoxLayout, QWidget
13
+
14
+ from datetime import datetime
15
+ from pygpt_net.utils import trans
16
+
17
+
18
+ class BottomStatus:
19
+ def __init__(self, window=None):
20
+ self.window = window
21
+ self.timer = QLabel(parent=self.window)
22
+ self.timer.setObjectName("StatusBarTimer")
23
+ self.msg = QLabel(parent=self.window)
24
+ self.msg.setObjectName("StatusBarMessage")
25
+ self.set_text(trans('status.started'))
26
+
27
+ def set_text(self, text):
28
+ """Set status text"""
29
+ self.msg.setText(text)
30
+ if text:
31
+ now = datetime.now()
32
+ self.timer.setText(now.strftime("%H:%M"))
33
+ else:
34
+ self.timer.setText("")
35
+
36
+ def setText(self, text):
37
+ """Fallback for set_text method"""
38
+ self.set_text(text)
39
+
40
+ def text(self) -> str:
41
+ """Get status text"""
42
+ return self.msg.text()
43
+
44
+ def setup(self):
45
+ """Setup status bar widget"""
46
+ self.timer.setText("00:00")
47
+ layout = QHBoxLayout()
48
+ layout.setContentsMargins(0, 0, 0, 0)
49
+ layout.setSpacing(5)
50
+ layout.addWidget(self.timer)
51
+ layout.addWidget(self.msg)
52
+ layout.addStretch()
53
+ widget = QWidget(self.window)
54
+ widget.setLayout(layout)
55
+ return widget
@@ -6,13 +6,13 @@
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.24 23:00:00 #
9
+ # Updated Date: 2025.09.28 08:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
13
13
  import os
14
14
 
15
- from PySide6.QtCore import Qt, QModelIndex, QDir
15
+ from PySide6.QtCore import Qt, QModelIndex, QDir, QObject, QEvent
16
16
  from PySide6.QtGui import QAction, QIcon, QCursor, QResizeEvent
17
17
  from PySide6.QtWidgets import QTreeView, QMenu, QWidget, QVBoxLayout, QFileSystemModel, QLabel, QHBoxLayout, \
18
18
  QPushButton, QSizePolicy
@@ -23,6 +23,117 @@ from pygpt_net.ui.widget.element.labels import HelpLabel
23
23
  from pygpt_net.utils import trans
24
24
 
25
25
 
26
+ class ExplorerDropHandler(QObject):
27
+ """
28
+ Drag & drop handler for FileExplorer (uploads into target directory).
29
+ - Accepts local file and directory URLs.
30
+ - Determines target directory from drop position:
31
+ * directory item -> that directory
32
+ * file item -> parent directory
33
+ * empty area -> explorer root directory
34
+ - Uses Files controller to perform the actual copy and refresh view.
35
+ """
36
+ def __init__(self, explorer):
37
+ super().__init__(explorer)
38
+ self.explorer = explorer
39
+ self.view = explorer.treeView
40
+
41
+ # Enable drops on both the view and its viewport
42
+ try:
43
+ self.view.setAcceptDrops(True)
44
+ except Exception:
45
+ pass
46
+ vp = self.view.viewport()
47
+ if vp is not None:
48
+ try:
49
+ vp.setAcceptDrops(True)
50
+ except Exception:
51
+ pass
52
+ vp.installEventFilter(self)
53
+ self.view.installEventFilter(self)
54
+
55
+ def _mime_has_local_urls(self, md) -> bool:
56
+ try:
57
+ if md and md.hasUrls():
58
+ for url in md.urls():
59
+ if url.isLocalFile():
60
+ return True
61
+ except Exception:
62
+ pass
63
+ return False
64
+
65
+ def _local_paths_from_mime(self, md) -> list:
66
+ out = []
67
+ try:
68
+ if not (md and md.hasUrls()):
69
+ return out
70
+ for url in md.urls():
71
+ try:
72
+ if url.isLocalFile():
73
+ p = url.toLocalFile()
74
+ if p:
75
+ out.append(p)
76
+ except Exception:
77
+ continue
78
+ except Exception:
79
+ pass
80
+ return out
81
+
82
+ def _target_dir_from_pos(self, event) -> str:
83
+ # QDropEvent in Qt6: use position() -> QPointF
84
+ try:
85
+ pos = event.position().toPoint()
86
+ except Exception:
87
+ pos = event.pos()
88
+ idx = self.view.indexAt(pos)
89
+ if idx.isValid():
90
+ path = self.explorer.model.filePath(idx)
91
+ if os.path.isdir(path):
92
+ return path
93
+ return os.path.dirname(path)
94
+ # Fallback: explorer root directory
95
+ return self.explorer.directory
96
+
97
+ def eventFilter(self, obj, event):
98
+ et = event.type()
99
+
100
+ if et in (QEvent.DragEnter, QEvent.DragMove):
101
+ md = getattr(event, 'mimeData', lambda: None)()
102
+ if self._mime_has_local_urls(md):
103
+ try:
104
+ event.setDropAction(Qt.CopyAction)
105
+ event.acceptProposedAction()
106
+ except Exception:
107
+ event.accept()
108
+ return True
109
+ return False
110
+
111
+ if et == QEvent.Drop:
112
+ md = getattr(event, 'mimeData', lambda: None)()
113
+ if not self._mime_has_local_urls(md):
114
+ return False
115
+
116
+ paths = self._local_paths_from_mime(md)
117
+ target_dir = self._target_dir_from_pos(event)
118
+ try:
119
+ self.explorer.window.controller.files.upload_paths(paths, target_dir)
120
+ except Exception as e:
121
+ try:
122
+ self.explorer.window.core.debug.log(e)
123
+ except Exception:
124
+ pass
125
+
126
+ try:
127
+ event.setDropAction(Qt.CopyAction)
128
+ event.acceptProposedAction()
129
+ except Exception:
130
+ event.accept()
131
+ # Swallow so the view does not try to handle the drop itself
132
+ return True
133
+
134
+ return False
135
+
136
+
26
137
  class FileExplorer(QWidget):
27
138
  def __init__(self, window, directory, index_data):
28
139
  """
@@ -138,6 +249,9 @@ class FileExplorer(QWidget):
138
249
  'db': QIcon(":/icons/db.svg"),
139
250
  }
140
251
 
252
+ # Drag & Drop upload support
253
+ self._dnd_handler = ExplorerDropHandler(self)
254
+
141
255
  def eventFilter(self, source, event):
142
256
  """
143
257
  Focus event filter
@@ -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.15 22:00:00 #
9
+ # Updated Date: 2025.09.28 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -40,7 +40,7 @@ class ContextList(BaseList):
40
40
  'chat': QIcon(":/icons/chat.svg"),
41
41
  'copy': QIcon(":/icons/copy.svg"),
42
42
  'close': QIcon(":/icons/close.svg"),
43
- 'pin': QIcon(":/icons/pin.svg"),
43
+ 'pin': QIcon(":/icons/pin3.svg"),
44
44
  'clock': QIcon(":/icons/clock.svg"),
45
45
  'db': QIcon(":/icons/db.svg"),
46
46
  'folder': QIcon(":/icons/folder_filled.svg"),
@@ -49,7 +49,8 @@ class ContextList(BaseList):
49
49
  self._color_icon_cache = {}
50
50
 
51
51
  # Use a custom delegate for labels/pinned/attachment indicators and group border indicator
52
- self.setItemDelegate(ImportantItemDelegate(self, self._icons['attachment']))
52
+ # Pass both: attachment icon and pin icon (pin2.svg) for pinned indicator rendering
53
+ self.setItemDelegate(ImportantItemDelegate(self, self._icons['attachment'], self._icons['pin']))
53
54
 
54
55
  # Ensure context menu works as before
55
56
  self.setContextMenuPolicy(Qt.CustomContextMenu)
@@ -467,15 +468,17 @@ class ImportantItemDelegate(QtWidgets.QStyledItemDelegate):
467
468
  """
468
469
  Item delegate that paints:
469
470
  - Attachment icon on the right side (centered vertically),
470
- - Pinned indicator (small circle) in the top-right corner (overlays if needed),
471
+ - Pinned indicator (pin.svg icon) in the top-right corner (overlays if needed),
471
472
  - Label color as a full-height vertical bar on the left for labeled items,
472
473
  - Group enclosure indicator for expanded groups:
473
474
  - thin vertical bar (default 2 px) on the left side of child rows area,
474
475
  - thin horizontal bar (default 2 px) at the bottom of the last child row.
475
476
  """
476
- def __init__(self, parent=None, attachment_icon: QIcon = None):
477
+ def __init__(self, parent=None, attachment_icon: QIcon = None, pin_icon: QIcon = None):
477
478
  super().__init__(parent)
478
479
  self._attachment_icon = attachment_icon or QIcon(":/icons/attachment.svg")
480
+ # Use provided pin icon (transparent background) as pinned indicator
481
+ self._pin_icon = pin_icon or QIcon(":/icons/pin.svg")
479
482
 
480
483
  # Predefined label colors (status -> QColor)
481
484
  self._status_colors = {
@@ -490,8 +493,8 @@ class ImportantItemDelegate(QtWidgets.QStyledItemDelegate):
490
493
  }
491
494
 
492
495
  # Visual tuning constants
493
- self._pin_pen = QtGui.QPen(QtCore.Qt.black, 0.5, QtCore.Qt.SolidLine)
494
- self._pin_diameter = 4 # Small pinned circle diameter
496
+ self._pin_pen = QtGui.QPen(QtCore.Qt.black, 0.5, QtCore.Qt.SolidLine) # kept for compatibility
497
+ self._pin_diameter = 4 # legacy circle diameter (not used anymore)
495
498
  self._pin_margin = 3 # Margin from top and right edges
496
499
  self._attach_spacing = 4 # Kept for potential future layout tweaks
497
500
  self._label_bar_width = 4 # Full-height label bar width (left side)
@@ -507,6 +510,10 @@ class ImportantItemDelegate(QtWidgets.QStyledItemDelegate):
507
510
  self._group_indicator_gap = 6 # gap between child content left and the vertical bar
508
511
  self._group_indicator_bottom_offset = 6
509
512
 
513
+ # Pinned icon sizing (kept deliberately small, similar to previous yellow dot)
514
+ # The actual painted size is min(max_size, availableHeightWithMargins)
515
+ self._pin_icon_max_size = 12 # px
516
+
510
517
  # Try to load customization from application config (safe if missing)
511
518
  self._init_group_indicator_from_config()
512
519
 
@@ -665,20 +672,23 @@ class ImportantItemDelegate(QtWidgets.QStyledItemDelegate):
665
672
  )
666
673
  self._attachment_icon.paint(painter, icon_rect, QtCore.Qt.AlignCenter)
667
674
 
668
- # Pinned indicator (small circle) kept at a fixed top-right position.
669
- # It does not shift left when the attachment is present; it overlays above it.
675
+ # Pinned indicator: small pin.svg painted at fixed top-right position.
676
+ # It overlays above any other right-side icons.
670
677
  if is_important:
671
678
  painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
672
679
  painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver)
673
- color = self.get_color_for_status(3)
674
680
 
675
- x = option.rect.x() + option.rect.width() - self._pin_margin - self._pin_diameter
676
- y = option.rect.y() + self._pin_margin
677
- pin_rect = QtCore.QRect(x, y, self._pin_diameter, self._pin_diameter)
681
+ # Compute a compact size similar in footprint to previous circle,
682
+ # but readable for vector icon; clamp to available height.
683
+ available = max(8, option.rect.height() - 2 * self._pin_margin)
684
+ pin_size = min(self._pin_icon_max_size, available)
678
685
 
679
- painter.setBrush(color)
680
- painter.setPen(self._pin_pen)
681
- painter.drawEllipse(pin_rect)
686
+ x = option.rect.right() - self._pin_margin - pin_size
687
+ y = option.rect.top() + self._pin_margin
688
+ pin_rect = QtCore.QRect(x, y, pin_size, pin_size)
689
+
690
+ # Paint the pin icon (transparent background)
691
+ self._pin_icon.paint(painter, pin_rect, QtCore.Qt.AlignCenter)
682
692
 
683
693
  # Label bar on the left with 3px vertical margins
684
694
  if label > 0:
@@ -6,10 +6,10 @@
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.24 23:00:00 #
9
+ # Updated Date: 2025.12.25 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from PySide6.QtWidgets import QCheckBox, QWidget
12
+ from PySide6.QtWidgets import QCheckBox, QWidget, QPushButton
13
13
 
14
14
  from pygpt_net.ui.base.flow_layout import FlowLayout
15
15
  from pygpt_net.utils import trans
@@ -84,6 +84,18 @@ class OptionCheckboxList(QWidget):
84
84
  for widget in widgets:
85
85
  self.layout.addWidget(widget)
86
86
 
87
+ # select/unselect all button
88
+ btn_select = QPushButton("X", self)
89
+ btn_select.setToolTip(trans("action.select_unselect_all"))
90
+ btn_select.clicked.connect(
91
+ lambda: self.window.controller.config.checkbox_list.on_select_all(
92
+ self.parent_id,
93
+ self.id,
94
+ self.option
95
+ )
96
+ )
97
+ self.layout.addWidget(btn_select)
98
+
87
99
  self.layout.setContentsMargins(0, 0, 0, 0)
88
100
  self.setLayout(self.layout)
89
101