pygpt-net 2.6.21__py3-none-any.whl → 2.6.23__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 (160) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +3 -1
  4. pygpt_net/controller/__init__.py +4 -8
  5. pygpt_net/controller/access/voice.py +2 -2
  6. pygpt_net/controller/agent/llama.py +3 -0
  7. pygpt_net/controller/assistant/batch.py +2 -3
  8. pygpt_net/controller/assistant/editor.py +2 -2
  9. pygpt_net/controller/assistant/files.py +2 -3
  10. pygpt_net/controller/assistant/store.py +2 -2
  11. pygpt_net/controller/audio/audio.py +2 -2
  12. pygpt_net/controller/chat/response.py +4 -0
  13. pygpt_net/controller/ctx/ctx.py +2 -1
  14. pygpt_net/controller/files/files.py +24 -55
  15. pygpt_net/controller/idx/indexer.py +85 -76
  16. pygpt_net/controller/lang/lang.py +52 -34
  17. pygpt_net/controller/model/importer.py +2 -2
  18. pygpt_net/controller/notepad/notepad.py +86 -84
  19. pygpt_net/controller/plugins/settings.py +3 -4
  20. pygpt_net/controller/settings/profile.py +105 -124
  21. pygpt_net/controller/theme/menu.py +154 -57
  22. pygpt_net/controller/theme/nodes.py +51 -44
  23. pygpt_net/controller/theme/theme.py +33 -9
  24. pygpt_net/controller/tools/tools.py +2 -2
  25. pygpt_net/controller/ui/tabs.py +2 -3
  26. pygpt_net/core/agents/observer/evaluation.py +2 -2
  27. pygpt_net/core/agents/runners/loop.py +1 -0
  28. pygpt_net/core/bridge/bridge.py +2 -0
  29. pygpt_net/core/ctx/container.py +13 -12
  30. pygpt_net/core/ctx/output.py +7 -4
  31. pygpt_net/core/debug/console/console.py +2 -2
  32. pygpt_net/core/filesystem/actions.py +1 -2
  33. pygpt_net/core/filesystem/opener.py +261 -0
  34. pygpt_net/core/filesystem/url.py +13 -10
  35. pygpt_net/core/platforms/platforms.py +5 -4
  36. pygpt_net/core/render/plain/helpers.py +2 -5
  37. pygpt_net/core/render/plain/renderer.py +26 -30
  38. pygpt_net/core/render/web/body.py +1 -1
  39. pygpt_net/core/settings/settings.py +43 -13
  40. pygpt_net/core/tabs/tabs.py +20 -13
  41. pygpt_net/data/config/config.json +4 -4
  42. pygpt_net/data/config/models.json +3 -3
  43. pygpt_net/data/css/web-blocks.dark.css +7 -1
  44. pygpt_net/data/css/web-blocks.light.css +5 -2
  45. pygpt_net/data/css/web-chatgpt.dark.css +7 -1
  46. pygpt_net/data/css/web-chatgpt.light.css +3 -0
  47. pygpt_net/data/css/web-chatgpt_wide.dark.css +7 -1
  48. pygpt_net/data/css/web-chatgpt_wide.light.css +3 -0
  49. pygpt_net/data/locale/locale.de.ini +5 -1
  50. pygpt_net/data/locale/locale.en.ini +5 -1
  51. pygpt_net/data/locale/locale.es.ini +5 -1
  52. pygpt_net/data/locale/locale.fr.ini +5 -1
  53. pygpt_net/data/locale/locale.it.ini +5 -1
  54. pygpt_net/data/locale/locale.pl.ini +6 -4
  55. pygpt_net/data/locale/locale.uk.ini +5 -1
  56. pygpt_net/data/locale/locale.zh.ini +5 -1
  57. pygpt_net/plugin/twitter/plugin.py +2 -2
  58. pygpt_net/provider/core/config/patch.py +12 -1
  59. pygpt_net/tools/audio_transcriber/ui/dialogs.py +44 -54
  60. pygpt_net/tools/code_interpreter/body.py +1 -2
  61. pygpt_net/tools/code_interpreter/tool.py +7 -4
  62. pygpt_net/tools/code_interpreter/ui/html.py +1 -3
  63. pygpt_net/tools/code_interpreter/ui/widgets.py +2 -3
  64. pygpt_net/tools/html_canvas/ui/widgets.py +1 -3
  65. pygpt_net/tools/image_viewer/ui/dialogs.py +40 -37
  66. pygpt_net/tools/indexer/ui/widgets.py +2 -4
  67. pygpt_net/tools/media_player/tool.py +2 -5
  68. pygpt_net/tools/media_player/ui/widgets.py +60 -36
  69. pygpt_net/tools/text_editor/ui/widgets.py +18 -19
  70. pygpt_net/tools/translator/ui/widgets.py +39 -35
  71. pygpt_net/ui/base/context_menu.py +9 -4
  72. pygpt_net/ui/dialog/db.py +1 -3
  73. pygpt_net/ui/dialog/models.py +1 -3
  74. pygpt_net/ui/dialog/models_importer.py +2 -4
  75. pygpt_net/ui/dialogs.py +34 -30
  76. pygpt_net/ui/layout/chat/attachments.py +72 -84
  77. pygpt_net/ui/layout/chat/attachments_ctx.py +40 -44
  78. pygpt_net/ui/layout/chat/attachments_uploaded.py +36 -39
  79. pygpt_net/ui/layout/chat/calendar.py +100 -70
  80. pygpt_net/ui/layout/chat/chat.py +23 -17
  81. pygpt_net/ui/layout/chat/input.py +95 -118
  82. pygpt_net/ui/layout/chat/output.py +100 -162
  83. pygpt_net/ui/layout/chat/painter.py +89 -61
  84. pygpt_net/ui/layout/ctx/ctx_list.py +43 -52
  85. pygpt_net/ui/layout/status.py +23 -14
  86. pygpt_net/ui/layout/toolbox/agent.py +27 -38
  87. pygpt_net/ui/layout/toolbox/agent_llama.py +41 -45
  88. pygpt_net/ui/layout/toolbox/assistants.py +42 -38
  89. pygpt_net/ui/layout/toolbox/computer_env.py +32 -23
  90. pygpt_net/ui/layout/toolbox/footer.py +13 -16
  91. pygpt_net/ui/layout/toolbox/image.py +18 -21
  92. pygpt_net/ui/layout/toolbox/indexes.py +46 -89
  93. pygpt_net/ui/layout/toolbox/mode.py +20 -7
  94. pygpt_net/ui/layout/toolbox/model.py +12 -10
  95. pygpt_net/ui/layout/toolbox/presets.py +68 -52
  96. pygpt_net/ui/layout/toolbox/prompt.py +31 -58
  97. pygpt_net/ui/layout/toolbox/toolbox.py +25 -21
  98. pygpt_net/ui/layout/toolbox/vision.py +20 -22
  99. pygpt_net/ui/main.py +2 -4
  100. pygpt_net/ui/menu/about.py +64 -84
  101. pygpt_net/ui/menu/audio.py +87 -63
  102. pygpt_net/ui/menu/config.py +121 -127
  103. pygpt_net/ui/menu/debug.py +69 -76
  104. pygpt_net/ui/menu/file.py +32 -35
  105. pygpt_net/ui/menu/menu.py +2 -3
  106. pygpt_net/ui/menu/plugins.py +69 -33
  107. pygpt_net/ui/menu/theme.py +45 -46
  108. pygpt_net/ui/menu/tools.py +56 -60
  109. pygpt_net/ui/menu/video.py +20 -25
  110. pygpt_net/ui/tray.py +1 -2
  111. pygpt_net/ui/widget/audio/bar.py +1 -3
  112. pygpt_net/ui/widget/audio/input_button.py +3 -4
  113. pygpt_net/ui/widget/calendar/select.py +1 -2
  114. pygpt_net/ui/widget/dialog/base.py +12 -9
  115. pygpt_net/ui/widget/dialog/editor_file.py +20 -23
  116. pygpt_net/ui/widget/dialog/find.py +25 -24
  117. pygpt_net/ui/widget/dialog/profile.py +57 -53
  118. pygpt_net/ui/widget/draw/painter.py +62 -93
  119. pygpt_net/ui/widget/element/button.py +42 -30
  120. pygpt_net/ui/widget/element/checkbox.py +23 -15
  121. pygpt_net/ui/widget/element/group.py +6 -5
  122. pygpt_net/ui/widget/element/labels.py +1 -2
  123. pygpt_net/ui/widget/filesystem/explorer.py +93 -102
  124. pygpt_net/ui/widget/image/display.py +1 -2
  125. pygpt_net/ui/widget/lists/assistant.py +1 -2
  126. pygpt_net/ui/widget/lists/attachment.py +1 -2
  127. pygpt_net/ui/widget/lists/attachment_ctx.py +1 -2
  128. pygpt_net/ui/widget/lists/context.py +2 -4
  129. pygpt_net/ui/widget/lists/index.py +1 -2
  130. pygpt_net/ui/widget/lists/model.py +1 -2
  131. pygpt_net/ui/widget/lists/model_editor.py +1 -2
  132. pygpt_net/ui/widget/lists/model_importer.py +1 -2
  133. pygpt_net/ui/widget/lists/preset.py +1 -2
  134. pygpt_net/ui/widget/lists/preset_plugins.py +1 -2
  135. pygpt_net/ui/widget/lists/profile.py +1 -2
  136. pygpt_net/ui/widget/lists/uploaded.py +1 -2
  137. pygpt_net/ui/widget/option/checkbox.py +2 -4
  138. pygpt_net/ui/widget/option/checkbox_list.py +1 -4
  139. pygpt_net/ui/widget/option/cmd.py +1 -4
  140. pygpt_net/ui/widget/option/dictionary.py +25 -28
  141. pygpt_net/ui/widget/option/input.py +1 -3
  142. pygpt_net/ui/widget/tabs/Input.py +16 -12
  143. pygpt_net/ui/widget/tabs/body.py +5 -3
  144. pygpt_net/ui/widget/tabs/layout.py +41 -28
  145. pygpt_net/ui/widget/tabs/output.py +442 -85
  146. pygpt_net/ui/widget/textarea/calendar_note.py +1 -2
  147. pygpt_net/ui/widget/textarea/editor.py +41 -73
  148. pygpt_net/ui/widget/textarea/find.py +11 -10
  149. pygpt_net/ui/widget/textarea/html.py +3 -6
  150. pygpt_net/ui/widget/textarea/input.py +134 -69
  151. pygpt_net/ui/widget/textarea/notepad.py +54 -38
  152. pygpt_net/ui/widget/textarea/output.py +65 -54
  153. pygpt_net/ui/widget/textarea/search_input.py +5 -4
  154. pygpt_net/ui/widget/textarea/web.py +2 -4
  155. pygpt_net/ui/widget/vision/camera.py +2 -31
  156. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/METADATA +38 -174
  157. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/RECORD +160 -159
  158. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/LICENSE +0 -0
  159. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/WHEEL +0 -0
  160. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,261 @@
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.08.25 18:00:00 #
10
+ # ================================================== #
11
+
12
+ import os
13
+ import sys
14
+ import shutil
15
+ import subprocess
16
+
17
+ from PySide6.QtCore import QUrl
18
+ from PySide6.QtGui import QDesktopServices
19
+
20
+ # QtDBus is optional and may not be available on all systems
21
+ try:
22
+ from PySide6.QtDBus import QDBusInterface, QDBusConnection, QDBusMessage
23
+ HAS_QT_DBUS = True
24
+ except Exception:
25
+ HAS_QT_DBUS = False
26
+
27
+ IS_WINDOWS = sys.platform.startswith("win")
28
+ IS_MAC = sys.platform == "darwin"
29
+ IS_LINUX = sys.platform.startswith("linux")
30
+
31
+ class Opener:
32
+ # ===== public API =====
33
+ @staticmethod
34
+ def open_path(path: str, reveal: bool = False) -> bool:
35
+ """
36
+ Open file or directory in the system file manager or default application.
37
+
38
+ :param path: Path to file or directory
39
+ :param reveal: If True and path is a file, reveal it in the file manager
40
+ :return: True if successful, False otherwise
41
+ """
42
+ p = os.path.abspath(path)
43
+
44
+ if IS_WINDOWS:
45
+ if reveal and os.path.isfile(p):
46
+ return Opener._reveal_windows(p)
47
+ if os.path.isdir(p):
48
+ return Opener._open_dir_windows(p)
49
+ return Opener._open_file_windows(p)
50
+
51
+ if IS_MAC:
52
+ if reveal and os.path.isfile(p):
53
+ return Opener._reveal_mac(p)
54
+ if os.path.isdir(p):
55
+ return Opener._open_dir_mac(p)
56
+ return Opener._open_file_mac(p)
57
+
58
+ # Linux
59
+ if reveal and os.path.isfile(p):
60
+ if Opener._reveal_linux(p):
61
+ return True
62
+ # fallback:
63
+ return Opener._open_dir_linux(os.path.dirname(p) or "/")
64
+ if os.path.isdir(p):
65
+ return Opener._open_dir_linux(p)
66
+ return Opener._open_file_linux(p)
67
+
68
+ # ===== Windows =====
69
+ @staticmethod
70
+ def _open_dir_windows(path: str) -> bool:
71
+ """
72
+ Open directory in Windows Explorer
73
+
74
+ :param path: Path to directory
75
+ :return: True if successful, False otherwise
76
+ """
77
+ try:
78
+ os.startfile(path)
79
+ return True
80
+ except Exception:
81
+ try:
82
+ subprocess.Popen(["explorer", path])
83
+ return True
84
+ except Exception:
85
+ return False
86
+
87
+ @staticmethod
88
+ def _reveal_windows(path: str) -> bool:
89
+ """
90
+ Reveal file in Windows Explorer
91
+
92
+ :param path: Path to file
93
+ :return: True if successful, False otherwise
94
+ """
95
+ try:
96
+ subprocess.Popen(["explorer", "/select,", os.path.normpath(path)])
97
+ return True
98
+ except Exception:
99
+ return Opener._open_dir_windows(os.path.dirname(path) or "C:\\")
100
+
101
+ @staticmethod
102
+ def _open_file_windows(path: str) -> bool:
103
+ """
104
+ Open file with default application
105
+
106
+ :param path: Path to file
107
+ :return: True if successful, False otherwise
108
+ """
109
+ try:
110
+ os.startfile(path)
111
+ return True
112
+ except Exception:
113
+ # Qt
114
+ return QDesktopServices.openUrl(QUrl.fromLocalFile(path))
115
+
116
+ # ===== macOS =====
117
+ @staticmethod
118
+ def _open_dir_mac(path: str) -> bool:
119
+ """
120
+ Open directory in Finder
121
+
122
+ :param path: Path to directory
123
+ :return: True if successful, False otherwise
124
+ """
125
+ try:
126
+ subprocess.Popen(["open", path])
127
+ return True
128
+ except Exception:
129
+ return QDesktopServices.openUrl(QUrl.fromLocalFile(path))
130
+
131
+ @staticmethod
132
+ def _reveal_mac(path: str) -> bool:
133
+ """
134
+ Reveal file in Finder
135
+
136
+ :param path: Path to file
137
+ :return: True if successful, False otherwise
138
+ """
139
+ try:
140
+ subprocess.Popen(["open", "-R", path])
141
+ return True
142
+ except Exception:
143
+ return Opener._open_dir_mac(os.path.dirname(path) or "/")
144
+
145
+ @staticmethod
146
+ def _open_file_mac(path: str) -> bool:
147
+ """
148
+ Open file with default application
149
+
150
+ :param path: Path to file
151
+ :return: True if successful, False otherwise
152
+ """
153
+ try:
154
+ subprocess.Popen(["open", path])
155
+ return True
156
+ except Exception:
157
+ return QDesktopServices.openUrl(QUrl.fromLocalFile(path))
158
+
159
+ # ===== Linux =====
160
+ @staticmethod
161
+ def _fm_iface():
162
+ """
163
+ Get FileManager1 D-Bus interface if available
164
+
165
+ :return: QDBusInterface or None
166
+ """
167
+ if not (IS_LINUX and HAS_QT_DBUS):
168
+ return None
169
+ bus = QDBusConnection.sessionBus()
170
+ iface = QDBusInterface(
171
+ "org.freedesktop.FileManager1",
172
+ "/org/freedesktop/FileManager1",
173
+ "org.freedesktop.FileManager1",
174
+ bus,
175
+ )
176
+ return iface if iface.isValid() else None
177
+
178
+ @staticmethod
179
+ def _show_folders_dbus(paths):
180
+ """
181
+ Show folders using D-Bus FileManager1 interface
182
+
183
+ :param paths: List of folder paths
184
+ :return: True if successful, False otherwise
185
+ """
186
+ iface = Opener._fm_iface()
187
+ if not iface:
188
+ return False
189
+ uris = [QUrl.fromLocalFile(p).toString() for p in paths]
190
+ reply = iface.call("ShowFolders", uris, "")
191
+ return reply.type() != QDBusMessage.ErrorMessage
192
+
193
+ @staticmethod
194
+ def _show_items_dbus(paths):
195
+ """
196
+ Show items using D-Bus FileManager1 interface
197
+
198
+ :param paths: List of item paths
199
+ :return: True if successful, False otherwise
200
+ """
201
+ iface = Opener._fm_iface()
202
+ if not iface:
203
+ return False
204
+ uris = [QUrl.fromLocalFile(p).toString() for p in paths]
205
+ reply = iface.call("ShowItems", uris, "")
206
+ return reply.type() != QDBusMessage.ErrorMessage
207
+
208
+ @staticmethod
209
+ def _open_with_cli_linux(path):
210
+ """
211
+ Open path using common CLI tools (xdg-open, gio)
212
+
213
+ :param path: Path to file or directory
214
+ :return: True if successful, False otherwise
215
+ """
216
+ for cmd in (["xdg-open", path], ["gio", "open", path]):
217
+ if shutil.which(cmd[0]):
218
+ try:
219
+ subprocess.Popen(cmd)
220
+ return True
221
+ except Exception:
222
+ pass
223
+ return False
224
+
225
+ @staticmethod
226
+ def _open_dir_linux(path: str) -> bool:
227
+ """
228
+ Open directory in default file manager
229
+
230
+ :param path: Path to directory
231
+ :return: True if successful, False otherwise
232
+ """
233
+ if Opener._show_folders_dbus([path]):
234
+ return True
235
+ if QDesktopServices.openUrl(QUrl.fromLocalFile(path)):
236
+ return True
237
+ return Opener._open_with_cli_linux(path)
238
+
239
+ @staticmethod
240
+ def _reveal_linux(path: str) -> bool:
241
+ """
242
+ Reveal file in default file manager
243
+
244
+ :param path: Path to file
245
+ :return: True if successful, False otherwise
246
+ """
247
+ if Opener._show_items_dbus([path]):
248
+ return True
249
+ return False
250
+
251
+ @staticmethod
252
+ def _open_file_linux(path: str) -> bool:
253
+ """
254
+ Open file with default application
255
+
256
+ :param path: Path to file
257
+ :return: True if successful, False otherwise
258
+ """
259
+ if QDesktopServices.openUrl(QUrl.fromLocalFile(path)):
260
+ return True
261
+ return Opener._open_with_cli_linux(path)
@@ -23,19 +23,12 @@ class Url:
23
23
 
24
24
  def handle(self, url: QUrl):
25
25
  """
26
- Handle URL
26
+ Handle URL, bridge action or local file
27
27
 
28
28
  :param url: url
29
29
  """
30
- extra_schemes = [
31
- 'extra-audio-read',
32
- 'extra-code-copy',
33
- 'extra-copy',
34
- 'extra-delete',
35
- 'extra-edit',
36
- 'extra-join',
37
- 'extra-replay',
38
- ]
30
+ if not url.toString().strip():
31
+ return
39
32
 
40
33
  # JS bridge
41
34
  if url.toString().startswith('bridge://open_find'):
@@ -50,8 +43,18 @@ class Url:
50
43
  pid = self.window.controller.ui.tabs.get_current_pid()
51
44
  if pid in self.window.ui.nodes['output']:
52
45
  self.window.ui.nodes['output'][pid].on_focus_js()
46
+ return
53
47
 
54
48
  # -------------
49
+ extra_schemes = (
50
+ 'extra-audio-read',
51
+ 'extra-code-copy',
52
+ 'extra-copy',
53
+ 'extra-delete',
54
+ 'extra-edit',
55
+ 'extra-join',
56
+ 'extra-replay'
57
+ )
55
58
 
56
59
  # local file
57
60
  if not url.scheme().startswith('http') and url.scheme() not in extra_schemes:
@@ -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.07.18 17:00:00 #
9
+ # Updated Date: 2025.08.25 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import platform
13
13
  import os
14
+ import sys
14
15
 
15
16
  from PySide6 import QtCore, QtWidgets
16
17
  from PySide6.QtSvg import QSvgRenderer
@@ -87,7 +88,7 @@ class Platforms:
87
88
 
88
89
  :return: True if OS is Linux
89
90
  """
90
- return self.get_os() == 'Linux'
91
+ return self.get_os() == 'Linux' or sys.platform.startswith("linux")
91
92
 
92
93
  def is_mac(self) -> bool:
93
94
  """
@@ -95,7 +96,7 @@ class Platforms:
95
96
 
96
97
  :return: True if OS is MacOS
97
98
  """
98
- return self.get_os() == 'Darwin'
99
+ return self.get_os() == 'Darwin' or sys.platform == "darwin"
99
100
 
100
101
  def is_windows(self) -> bool:
101
102
  """
@@ -103,7 +104,7 @@ class Platforms:
103
104
 
104
105
  :return: True if OS is Windows
105
106
  """
106
- return self.get_os() == 'Windows'
107
+ return self.get_os() == 'Windows' or sys.platform.startswith("win")
107
108
 
108
109
  def is_snap(self) -> bool:
109
110
  """
@@ -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.07.14 00:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
 
@@ -27,10 +27,7 @@ class Helpers:
27
27
  :param text: text to format
28
28
  :return: formatted text
29
29
  """
30
- text = text.strip()
31
- #text = text.replace("#~###~", "~###~") # fix for #~###~ in text (previous versions)
32
- #text = text.replace("# ~###~", "~###~") # fix for # ~###~ in text (previous versions)
33
- return text
30
+ return text.strip()
34
31
 
35
32
  def post_format_text(self, text: str) -> str:
36
33
  """
@@ -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.21 00:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from datetime import datetime
@@ -178,11 +178,9 @@ class Renderer(BaseRenderer):
178
178
  if clear:
179
179
  self.clear_output(meta)
180
180
 
181
- i = 0
182
- for item in items:
181
+ for i, item in enumerate(items):
183
182
  item.idx = i
184
183
  self.append_context_item(meta, item)
185
- i += 1
186
184
 
187
185
  def append_input(
188
186
  self,
@@ -251,20 +249,23 @@ class Renderer(BaseRenderer):
251
249
  :param item: context item
252
250
  :param footer: True if it is a footer
253
251
  """
254
- appended = []
252
+ appended = set()
255
253
  pid = self.get_or_create_pid(meta)
256
254
 
257
255
  # images
258
256
  c = len(item.images)
259
257
  if c > 0:
260
258
  n = 1
259
+ pd = self.pids[pid]
260
+ already = set(pd.images_appended)
261
261
  for image in item.images:
262
- if image in appended or image in self.pids[pid].images_appended:
262
+ if image in appended or image in already:
263
263
  continue
264
264
  try:
265
- appended.append(image)
265
+ appended.add(image)
266
266
  self.append_raw(meta, item, self.body.get_image_html(image, n, c))
267
- self.pids[pid].images_appended.append(image)
267
+ pd.images_appended.append(image)
268
+ already.add(image)
268
269
  n += 1
269
270
  except Exception as e:
270
271
  pass
@@ -277,7 +278,7 @@ class Renderer(BaseRenderer):
277
278
  if file in appended:
278
279
  continue
279
280
  try:
280
- appended.append(file)
281
+ appended.add(file)
281
282
  self.append_raw(meta, item, self.body.get_file_html(file, n, c))
282
283
  n += 1
283
284
  except Exception as e:
@@ -288,13 +289,16 @@ class Renderer(BaseRenderer):
288
289
  if c > 0:
289
290
  urls_str = []
290
291
  n = 1
292
+ pd = self.pids[pid]
293
+ already = set(pd.urls_appended)
291
294
  for url in item.urls:
292
- if url in appended or url in self.pids[pid].urls_appended:
295
+ if url in appended or url in already:
293
296
  continue
294
297
  try:
295
- appended.append(url)
298
+ appended.add(url)
296
299
  urls_str.append(self.body.get_url_html(url, n, c))
297
- self.pids[pid].urls_appended.append(url)
300
+ pd.urls_appended.append(url)
301
+ already.add(url)
298
302
  n += 1
299
303
  except Exception as e:
300
304
  pass
@@ -337,8 +341,9 @@ class Renderer(BaseRenderer):
337
341
  raw_chunk = str(text_chunk)
338
342
 
339
343
  if begin:
340
- self.pids[pid].buffer = "" # reset buffer
341
- self.pids[pid].is_cmd = False # reset command flag
344
+ pd = self.pids[pid]
345
+ pd.buffer = ""
346
+ pd.is_cmd = False
342
347
 
343
348
  if self.is_timestamp_enabled() and item.output_timestamp is not None:
344
349
  name = ""
@@ -386,13 +391,12 @@ class Renderer(BaseRenderer):
386
391
  :param text: text to append
387
392
  """
388
393
  node = self.get_output_node(meta)
389
- prev_text = node.toPlainText()
390
- if prev_text != "":
391
- prev_text += "\n\n"
392
- new_text = f"{prev_text}{text.strip()}"
393
- node.setPlainText(new_text)
394
- cur = node.textCursor() # Move cursor to end of text
394
+ cur = node.textCursor()
395
395
  cur.movePosition(QTextCursor.End)
396
+ if not node.document().isEmpty():
397
+ cur.insertText("\n\n")
398
+ cur.insertText(text.strip())
399
+ node.setTextCursor(cur)
396
400
 
397
401
  def append_chunk_start(self, meta: CtxMeta, ctx: CtxItem):
398
402
  """
@@ -437,14 +441,9 @@ class Renderer(BaseRenderer):
437
441
  :param end: end of the line character
438
442
  """
439
443
  node = self.get_output_node(meta)
440
- cur = node.textCursor() # Move cursor to end of text
444
+ cur = node.textCursor()
441
445
  cur.movePosition(QTextCursor.End)
442
- s = f"{str(text)}{end}"
443
- while s:
444
- head, sep, s = s.partition("\n") # Split line at LF
445
- cur.insertText(head)
446
- if sep:
447
- cur.insertText("\n")
446
+ cur.insertText(f"{str(text)}{end}")
448
447
  node.setTextCursor(cur)
449
448
 
450
449
  def append_timestamp(
@@ -549,9 +548,6 @@ class Renderer(BaseRenderer):
549
548
  for node in self.get_all_nodes():
550
549
  try:
551
550
  node.clear()
552
- node.document().setMarkdown("")
553
- node.document().setHtml("")
554
- node.setPlainText("")
555
551
  except Exception as e:
556
552
  pass
557
553
 
@@ -881,7 +881,7 @@ class Body:
881
881
  }
882
882
  });
883
883
  });
884
- setTimeout(cycleTips, 60000); // after 60 seconds
884
+ setTimeout(cycleTips, 10000); // after 10 seconds
885
885
  </script>
886
886
  </head>
887
887
  <body """
@@ -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: 2024.12.14 22:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -15,7 +15,10 @@ import os
15
15
  import shutil
16
16
  from typing import Optional, Dict, Any, List
17
17
 
18
+ from PySide6.QtWidgets import QApplication
19
+
18
20
  from pygpt_net.core.events import RenderEvent
21
+ from pygpt_net.utils import trans
19
22
 
20
23
 
21
24
  class Settings:
@@ -116,7 +119,7 @@ class Settings:
116
119
  """Load defaults from file"""
117
120
  file = self.window.ui.dialog['config.editor'].file
118
121
  self.load_editor(file)
119
- self.window.update_status("Restored from user file: {}".format(file))
122
+ self.window.update_status(f"Restored from user file: {file}")
120
123
 
121
124
  def load_default_editor_app(self):
122
125
  """Load defaults from file (app)"""
@@ -125,11 +128,11 @@ class Settings:
125
128
  if basename.endswith(".css"):
126
129
  path = str(os.path.join(self.window.core.config.get_app_path(), "data", "css", basename))
127
130
  self.load_editor(file, path)
128
- self.window.update_status("Restored from app defaults: {}".format(basename))
131
+ self.window.update_status(f"Restored from app defaults: {basename}")
129
132
  elif basename.endswith(".json"):
130
133
  path = str(os.path.join(self.window.core.config.get_app_path(), "data", "config", basename))
131
134
  self.load_editor(file, path)
132
- self.window.update_status("Restored from app defaults: {}".format(basename))
135
+ self.window.update_status(f"Restored from app defaults: {basename}")
133
136
 
134
137
  def load_editor(
135
138
  self,
@@ -151,13 +154,14 @@ class Settings:
151
154
  self.window.ui.paths['config'].setText(path)
152
155
 
153
156
  self.window.ui.dialog['config.editor'].file = file
157
+
154
158
  try:
155
159
  with open(path, 'r', encoding="utf-8") as f:
156
160
  txt = f.read()
157
161
  self.window.ui.editor['config'].setPlainText(txt)
158
162
  except Exception as e:
159
163
  self.window.core.debug.log(e)
160
- self.window.update_status("Error loading file: {}".format(e))
164
+ self.window.update_status(f"Error loading file: {e}")
161
165
 
162
166
  def save_editor(self):
163
167
  """Save file to disk"""
@@ -170,8 +174,8 @@ class Settings:
170
174
  try:
171
175
  json.loads(data)
172
176
  except Exception as e:
173
- self.window.update_status("This is not a valid JSON: {}".format(e))
174
- self.window.ui.dialogs.alert("This is not a valid JSON: {}".format(e))
177
+ self.window.update_status(f"This is not a valid JSON: {e}")
178
+ self.window.ui.dialogs.alert(f"This is not a valid JSON: {e}")
175
179
  return
176
180
  path = os.path.join(self.window.core.config.get_user_path(), file)
177
181
  elif file.endswith('.css'):
@@ -189,22 +193,48 @@ class Settings:
189
193
  backup_path = os.path.join(self.window.core.config.get_user_path(), backup_file)
190
194
  if os.path.isfile(path):
191
195
  shutil.copyfile(path, backup_path)
192
- self.window.update_status("Created backup file: {}".format(backup_file))
196
+ self.window.update_status(f"Created backup file: {backup_file}")
197
+
198
+ prev_content = None
199
+ if os.path.isfile(path):
200
+ try:
201
+ with open(path, 'r', encoding="utf-8") as f:
202
+ prev_content = f.read()
203
+ except Exception as e:
204
+ pass
193
205
 
194
206
  # save changes to current file
195
207
  try:
196
208
  with open(path, 'w', encoding="utf-8") as f:
197
209
  f.write(data)
198
- self.window.update_status("Saved file: {}".format(path))
199
- self.window.ui.dialogs.alert("Saved file: {}".format(path))
210
+ self.window.update_status(f"Saved file: {path}")
211
+ except Exception as e:
212
+ self.window.core.debug.log(e)
213
+ self.window.update_status(f"Error saving file: {path}")
214
+ return # abort if error
215
+
216
+ if prev_content == data:
217
+ self.window.update_status(f"Saved file: {path}")
218
+ self.window.ui.dialogs.alert(f"Saved file: {path}")
219
+ return # no changes made, no need to reload
220
+
221
+ if file in ("config.json", "models.json") or file.endswith('.css'):
222
+ self.window.update_status(trans("status.reloading"))
223
+
224
+ QApplication.processEvents() # process events to update UI
225
+
226
+ try:
200
227
  if file == "config.json":
201
228
  self.window.core.config.load_config() # reload config
202
229
  elif file == "models.json":
203
230
  self.window.core.models.load() # reload models
204
231
  elif file.endswith('.css'):
205
- event = RenderEvent(RenderEvent.ON_THEME_CHANGE)
206
- self.window.dispatch(event)
232
+ self.window.dispatch(RenderEvent(RenderEvent.ON_THEME_CHANGE))
207
233
  self.window.controller.theme.reload(force=True) # reload theme
234
+ self.window.update_status(f"Saved file: {path}")
235
+ self.window.ui.dialogs.alert(f"Saved file: {path}")
208
236
  except Exception as e:
209
237
  self.window.core.debug.log(e)
210
- self.window.update_status("Error saving file: {}".format(path))
238
+ self.window.update_status(f"Error reloading saved file: {path}")
239
+
240
+