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
@@ -6,27 +6,32 @@
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.04.11 16:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt
13
- from PySide6.QtGui import QAction, QIcon, QKeySequence, QTextCursor, QFontMetrics
13
+ from PySide6.QtGui import QAction, QIcon, QKeySequence, QFontMetrics
14
14
  from PySide6.QtWidgets import QTextEdit
15
15
 
16
16
  from pygpt_net.core.text.finder import Finder
17
17
  from pygpt_net.utils import trans
18
18
 
19
- import pygpt_net.icons_rc
20
-
21
19
 
22
20
  class BaseCodeEditor(QTextEdit):
21
+
22
+ _ICON_VOLUME = QIcon(":/icons/volume.svg")
23
+ _ICON_SAVE = QIcon(":/icons/save.svg")
24
+ _ICON_SEARCH = QIcon(":/icons/search.svg")
25
+ _ICON_CLOSE = QIcon(":/icons/close.svg")
26
+ _FIND_SEQ = QKeySequence("Ctrl+F")
27
+
23
28
  def __init__(self, window=None):
24
29
  """
25
30
  Base code editor
26
31
 
27
32
  :param window: main window
28
33
  """
29
- super(BaseCodeEditor, self).__init__(window)
34
+ super().__init__(window)
30
35
  self.window = window
31
36
  self.finder = Finder(window, self)
32
37
  self.setReadOnly(True)
@@ -40,114 +45,89 @@ class BaseCodeEditor(QTextEdit):
40
45
  self.excluded_copy_to = []
41
46
  self.textChanged.connect(self.text_changed)
42
47
 
43
- # tabulation
44
48
  metrics = QFontMetrics(self.font())
45
49
  space_width = metrics.horizontalAdvance(" ")
46
50
  self.setTabStopDistance(4 * space_width)
47
51
 
48
52
  def text_changed(self):
49
- """On text changed"""
50
53
  self.finder.text_changed()
51
54
 
52
55
  def update_stylesheet(self, data: str):
53
- """
54
- Update stylesheet
55
-
56
- :param data: stylesheet CSS
57
- """
58
56
  self.setStyleSheet(self.default_stylesheet + data)
59
57
 
60
58
  def contextMenuEvent(self, event):
61
- """
62
- Context menu event
63
-
64
- :param event: Event
65
- """
66
59
  menu = self.createStandardContextMenu()
67
- selected_text = self.textCursor().selectedText()
60
+ cursor = self.textCursor()
61
+ selected_text = cursor.selectedText()
68
62
 
69
63
  if selected_text:
70
- # plain text
71
- plain_text = self.textCursor().selection().toPlainText()
64
+ plain_text = cursor.selection().toPlainText()
72
65
 
73
- # audio read
74
- action = QAction(QIcon(":/icons/volume.svg"), trans('text.context_menu.audio.read'), self)
66
+ action = QAction(self._ICON_VOLUME, trans('text.context_menu.audio.read'), menu)
75
67
  action.triggered.connect(self.audio_read_selection)
76
68
  menu.addAction(action)
77
69
 
78
- # copy to
79
70
  copy_to_menu = self.window.ui.context_menu.get_copy_to_menu(
80
- self,
71
+ menu,
81
72
  selected_text,
82
73
  excluded=self.excluded_copy_to
83
74
  )
75
+ try:
76
+ copy_to_menu.setParent(menu)
77
+ except Exception:
78
+ pass
84
79
  menu.addMenu(copy_to_menu)
85
80
 
86
- # save as (selected)
87
- action = QAction(QIcon(":/icons/save.svg"), trans('action.save_selection_as'), self)
81
+ action = QAction(self._ICON_SAVE, trans('action.save_selection_as'), menu)
88
82
  action.triggered.connect(
89
83
  lambda: self.window.controller.chat.common.save_text(plain_text))
90
84
  menu.addAction(action)
91
85
  else:
92
- # save as (all)
93
- action = QAction(QIcon(":/icons/save.svg"), trans('action.save_as'), self)
86
+ action = QAction(self._ICON_SAVE, trans('action.save_as'), menu)
94
87
  action.triggered.connect(
95
88
  lambda: self.window.controller.chat.common.save_text(self.toPlainText()))
96
89
  menu.addAction(action)
97
90
 
98
- action = QAction(QIcon(":/icons/search.svg"), trans('text.context_menu.find'), self)
91
+ action = QAction(self._ICON_SEARCH, trans('text.context_menu.find'), menu)
99
92
  action.triggered.connect(self.find_open)
100
- action.setShortcut(QKeySequence("Ctrl+F"))
93
+ action.setShortcut(self._FIND_SEQ)
101
94
  menu.addAction(action)
102
95
 
103
- # clear
104
- action = QAction(QIcon(":/icons/close.svg"), trans('action.clear'), self)
105
- action.triggered.connect(
106
- lambda: self.clear_content())
96
+ action = QAction(self._ICON_CLOSE, trans('action.clear'), menu)
97
+ action.triggered.connect(self.clear_content)
107
98
  menu.addAction(action)
108
99
 
109
- menu.exec_(event.globalPos())
100
+ menu.exec(event.globalPos())
101
+ menu.deleteLater()
110
102
 
111
103
  def clear_content(self):
112
- """Clear content"""
113
- cursor = self.textCursor()
114
- cursor.select(QTextCursor.Document)
115
- cursor.removeSelectedText()
104
+ self.clear()
116
105
 
117
106
  def audio_read_selection(self):
118
- """Read selected text (audio)"""
119
107
  self.window.controller.audio.read_text(self.textCursor().selectedText())
120
108
 
121
109
  def find_open(self):
122
- """Open find dialog"""
123
110
  self.window.controller.finder.open(self.finder)
124
111
 
125
112
  def on_update(self):
126
- """On content update"""
127
- self.finder.clear() # clear finder
113
+ self.finder.clear()
128
114
 
129
115
  def on_destroy(self):
130
- """On destroy"""
131
- self.window.controller.finder.unset(self.finder) # unregister finder from memory
116
+ try:
117
+ self.textChanged.disconnect(self.text_changed)
118
+ except Exception:
119
+ pass
120
+ self.window.controller.finder.unset(self.finder)
132
121
 
133
122
  def keyPressEvent(self, e):
134
- """
135
- Key press event
136
-
137
- :param e: Event
138
- """
139
123
  if e.key() == Qt.Key_F and e.modifiers() & Qt.ControlModifier:
140
124
  self.find_open()
141
125
  else:
142
- super(BaseCodeEditor, self).keyPressEvent(e)
126
+ super().keyPressEvent(e)
143
127
 
144
128
  def wheelEvent(self, event):
145
- """
146
- Wheel event: set font size
147
-
148
- :param event: Event
149
- """
150
129
  if event.modifiers() & Qt.ControlModifier:
130
+ prev = self.value
151
131
  if event.angleDelta().y() > 0:
152
132
  if self.value < self.max_font_size:
153
133
  self.value += 1
@@ -155,18 +135,14 @@ class BaseCodeEditor(QTextEdit):
155
135
  if self.value > self.min_font_size:
156
136
  self.value -= 1
157
137
 
158
- self.update_stylesheet(f"QTextEdit {{ font-size: {self.value}px }};")
138
+ if self.value != prev:
139
+ self.update_stylesheet(f"QTextEdit {{ font-size: {self.value}px }};")
159
140
  event.accept()
160
141
  else:
161
- super(BaseCodeEditor, self).wheelEvent(event)
142
+ super().wheelEvent(event)
162
143
 
163
144
  def focusInEvent(self, e):
164
- """
165
- Focus in event
166
-
167
- :param e: focus event
168
- """
169
- super(BaseCodeEditor, self).focusInEvent(e)
145
+ super().focusInEvent(e)
170
146
  self.window.controller.finder.focus_in(self.finder)
171
147
 
172
148
 
@@ -177,12 +153,4 @@ class CodeEditor(BaseCodeEditor):
177
153
 
178
154
  :param window: main window
179
155
  """
180
- super(CodeEditor, self).__init__(window)
181
- self.window = window
182
- self.setReadOnly(True)
183
- self.value = 12
184
- self.max_font_size = 42
185
- self.min_font_size = 8
186
- self.setProperty('class', 'code-editor')
187
- self.default_stylesheet = ""
188
- self.setStyleSheet(self.default_stylesheet)
156
+ super().__init__(window)
@@ -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.04.10 23:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6 import QtCore
@@ -21,13 +21,15 @@ class FindInput(QLineEdit):
21
21
  :param window: main window
22
22
  :param id: info window id
23
23
  """
24
- super(FindInput, self).__init__(window)
24
+ super().__init__(window)
25
25
 
26
26
  self.window = window
27
27
  self.id = id
28
- self.textChanged.connect(
29
- lambda: self.window.controller.finder.search_text_changed(self.text()),
30
- )
28
+ self.textChanged.connect(self._on_text_changed)
29
+
30
+ @QtCore.Slot(str)
31
+ def _on_text_changed(self, text):
32
+ self.window.controller.finder.search_text_changed(text)
31
33
 
32
34
  def keyPressEvent(self, event):
33
35
  """
@@ -35,10 +37,10 @@ class FindInput(QLineEdit):
35
37
 
36
38
  :param event: key event
37
39
  """
38
- super(FindInput, self).keyPressEvent(event)
40
+ super().keyPressEvent(event)
39
41
 
40
42
  # update on Enter
41
- if event.key() == QtCore.Qt.Key_Return or event.key() == QtCore.Qt.Key_Enter:
43
+ if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
42
44
  self.window.controller.finder.focus_input(self.text())
43
45
 
44
46
  def focusInEvent(self, e):
@@ -47,6 +49,5 @@ class FindInput(QLineEdit):
47
49
 
48
50
  :param e: focus event
49
51
  """
50
- super(FindInput, self).focusInEvent(e)
51
- self.window.controller.finder.focus_input(self.text())
52
-
52
+ super().focusInEvent(e)
53
+ self.window.controller.finder.focus_input(self.text())
@@ -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.19 07:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import re
@@ -15,16 +15,13 @@ from PySide6.QtCore import Qt, QObject, Signal, Slot, QEvent
15
15
  from PySide6.QtWebChannel import QWebChannel
16
16
  from PySide6.QtWebEngineCore import QWebEngineSettings, QWebEnginePage
17
17
  from PySide6.QtWebEngineWidgets import QWebEngineView
18
- from PySide6.QtGui import QAction, QIcon, QKeySequence
18
+ from PySide6.QtGui import QAction, QIcon
19
19
  from PySide6.QtWidgets import QMenu
20
20
 
21
- from pygpt_net.core.events import RenderEvent
22
21
  from pygpt_net.item.ctx import CtxMeta
23
22
  from pygpt_net.core.text.web_finder import WebFinder
24
23
  from pygpt_net.utils import trans
25
24
 
26
- import pygpt_net.icons_rc
27
-
28
25
 
29
26
  class HtmlOutput(QWebEngineView):
30
27
  def __init__(self, window=None):
@@ -142,7 +139,7 @@ class HtmlOutput(QWebEngineView):
142
139
  menu.addAction(action)
143
140
 
144
141
  # copy to
145
- copy_to_menu = self.window.ui.context_menu.get_copy_to_menu(self, selected_text)
142
+ copy_to_menu = self.window.ui.context_menu.get_copy_to_menu(menu, selected_text)
146
143
  menu.addMenu(copy_to_menu)
147
144
 
148
145
  # save as (selected)
@@ -6,35 +6,53 @@
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.22 15:00:00 #
9
+ # Updated Date: 2025.08.25 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from PySide6 import QtCore
13
- from PySide6.QtCore import Qt
12
+ from PySide6.QtCore import Qt, QSize
14
13
  from PySide6.QtGui import QAction, QIcon, QImage
15
- from PySide6.QtWidgets import QTextEdit, QApplication
14
+ from PySide6.QtWidgets import QTextEdit, QApplication, QPushButton
16
15
 
17
16
  from pygpt_net.utils import trans
18
- import pygpt_net.icons_rc
19
-
20
17
 
21
18
  class ChatInput(QTextEdit):
19
+
20
+ ICON_PASTE = QIcon(":/icons/paste.svg")
21
+ ICON_VOLUME = QIcon(":/icons/volume.svg")
22
+ ICON_SAVE = QIcon(":/icons/save.svg")
23
+ ICON_ATTACHMENT = QIcon(":/icons/add.svg")
24
+
22
25
  def __init__(self, window=None):
23
26
  """
24
27
  Chat input
25
28
 
26
29
  :param window: main window
27
30
  """
28
- super(ChatInput, self).__init__(window)
31
+ super().__init__(window)
29
32
  self.window = window
30
33
  self.setAcceptRichText(False)
31
34
  self.setFocus()
32
35
  self.value = self.window.core.config.data['font_size.input']
33
36
  self.max_font_size = 42
34
37
  self.min_font_size = 8
38
+ self._text_top_padding = 10
35
39
  self.textChanged.connect(self.window.controller.ui.update_tokens)
36
40
  self.setProperty('class', 'layout-input')
37
41
 
42
+ # Add a "+" button in the top-left corner to add attachments
43
+ self._init_attachment_button()
44
+ self._apply_text_top_padding()
45
+
46
+ def _apply_text_top_padding(self):
47
+ """Apply extra top padding inside the text area by using viewport margins."""
48
+ m = self.viewportMargins()
49
+ self.setViewportMargins(m.left(), self._text_top_padding, m.right(), m.bottom())
50
+
51
+ def set_text_top_padding(self, px: int):
52
+ """Public helper to adjust top padding at runtime."""
53
+ self._text_top_padding = max(0, int(px))
54
+ self._apply_text_top_padding()
55
+
38
56
  def insertFromMimeData(self, source):
39
57
  """
40
58
  Insert from mime data
@@ -63,7 +81,8 @@ class ChatInput(QTextEdit):
63
81
  self.window.controller.attachment.from_clipboard_url(local_path)
64
82
  elif source.hasText():
65
83
  text = source.text()
66
- self.window.controller.attachment.from_clipboard_text(text)
84
+ if text:
85
+ self.window.controller.attachment.from_clipboard_text(text)
67
86
 
68
87
  def contextMenuEvent(self, event):
69
88
  """
@@ -72,51 +91,45 @@ class ChatInput(QTextEdit):
72
91
  :param event: event
73
92
  """
74
93
  menu = self.createStandardContextMenu()
75
-
76
- # paste attachment
77
- if self.window.controller.attachment.clipboard_has_attachment():
78
- action = QAction(QIcon(":/icons/paste.svg"), trans("action.use.attachment"), self)
79
- action.triggered.connect(self.action_from_clipboard)
80
- menu.addAction(action)
81
-
82
- selected_text = self.textCursor().selectedText()
83
- if selected_text:
84
- # plain text
85
- plain_text = self.textCursor().selection().toPlainText()
86
-
87
- # audio read
88
- action = QAction(QIcon(":/icons/volume.svg"), trans('text.context_menu.audio.read'), self)
89
- action.triggered.connect(self.audio_read_selection)
90
- menu.addAction(action)
91
-
92
- # copy to
93
- copy_to_menu = self.window.ui.context_menu.get_copy_to_menu(self, selected_text, excluded=["input"])
94
- menu.addMenu(copy_to_menu)
95
-
96
- # save as (selected)
97
- action = QAction(QIcon(":/icons/save.svg"), trans('action.save_selection_as'), self)
98
- action.triggered.connect(
99
- lambda: self.window.controller.chat.common.save_text(plain_text))
100
- menu.addAction(action)
101
- else:
102
- # save as (all)
103
- action = QAction(QIcon(":/icons/save.svg"), trans('action.save_as'), self)
104
- action.triggered.connect(
105
- lambda: self.window.controller.chat.common.save_text(self.toPlainText()))
106
- menu.addAction(action)
107
-
108
94
  try:
109
- self.window.core.prompt.template.to_menu_options(menu, "input")
110
- self.window.core.prompt.custom.to_menu_options(menu, "input")
111
- except Exception as e:
112
- self.window.core.debug.log(e)
113
-
114
- # save current prompt
115
- action = QAction(QIcon(":/icons/save.svg"), trans('preset.prompt.save_custom'), self)
116
- action.triggered.connect(self.window.controller.presets.save_prompt)
117
- menu.addAction(action)
95
+ if self.window.controller.attachment.clipboard_has_attachment():
96
+ action = QAction(self.ICON_PASTE, trans("action.use.attachment"), menu)
97
+ action.triggered.connect(self.action_from_clipboard)
98
+ menu.addAction(action)
99
+
100
+ cursor = self.textCursor()
101
+ selected_text = cursor.selectedText()
102
+ if selected_text:
103
+ plain_text = cursor.selection().toPlainText()
104
+
105
+ action = QAction(self.ICON_VOLUME, trans('text.context_menu.audio.read'), menu)
106
+ action.triggered.connect(self.audio_read_selection)
107
+ menu.addAction(action)
108
+
109
+ copy_to_menu = self.window.ui.context_menu.get_copy_to_menu(menu, selected_text, excluded=["input"])
110
+ menu.addMenu(copy_to_menu)
111
+
112
+ action = QAction(self.ICON_SAVE, trans('action.save_selection_as'), menu)
113
+ action.triggered.connect(lambda: self.window.controller.chat.common.save_text(plain_text))
114
+ menu.addAction(action)
115
+ else:
116
+ action = QAction(self.ICON_SAVE, trans('action.save_as'), menu)
117
+ action.triggered.connect(lambda: self.window.controller.chat.common.save_text(self.toPlainText()))
118
+ menu.addAction(action)
119
+
120
+ try:
121
+ self.window.core.prompt.template.to_menu_options(menu, "input")
122
+ self.window.core.prompt.custom.to_menu_options(menu, "input")
123
+ except Exception as e:
124
+ self.window.core.debug.log(e)
125
+
126
+ action = QAction(self.ICON_SAVE, trans('preset.prompt.save_custom'), menu)
127
+ action.triggered.connect(self.window.controller.presets.save_prompt)
128
+ menu.addAction(action)
118
129
 
119
- menu.exec_(event.globalPos())
130
+ menu.exec(event.globalPos())
131
+ finally:
132
+ menu.deleteLater()
120
133
 
121
134
  def action_from_clipboard(self):
122
135
  """
@@ -137,28 +150,26 @@ class ChatInput(QTextEdit):
137
150
  :param event: key event
138
151
  """
139
152
  handled = False
140
- if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
153
+ key = event.key()
154
+ if key in (Qt.Key_Return, Qt.Key_Enter):
141
155
  mode = self.window.core.config.get('send_mode')
142
- if mode > 0: # Enter or Shift + Enter
143
- if mode == 2: # Shift + Enter
144
- modifiers = QApplication.keyboardModifiers()
145
- if modifiers == QtCore.Qt.ShiftModifier or modifiers == QtCore.Qt.ControlModifier:
156
+ if mode > 0:
157
+ modifiers = event.modifiers()
158
+ if mode == 2:
159
+ if modifiers == Qt.ShiftModifier or modifiers == Qt.ControlModifier:
146
160
  self.window.controller.chat.input.send_input()
147
161
  handled = True
148
- else: # Enter
149
- modifiers = QApplication.keyboardModifiers()
150
- if modifiers != QtCore.Qt.ShiftModifier and modifiers != QtCore.Qt.ControlModifier:
162
+ else:
163
+ if modifiers != Qt.ShiftModifier and modifiers != Qt.ControlModifier:
151
164
  self.window.controller.chat.input.send_input()
152
165
  handled = True
153
166
  self.setFocus()
154
-
155
- # cancel edit
156
- elif event.key() == Qt.Key_Escape and self.window.controller.ctx.extra.is_editing():
167
+ elif key == Qt.Key_Escape and self.window.controller.ctx.extra.is_editing():
157
168
  self.window.controller.ctx.extra.edit_cancel()
158
169
  handled = True
159
170
 
160
171
  if not handled:
161
- super(ChatInput, self).keyPressEvent(event)
172
+ super().keyPressEvent(event)
162
173
 
163
174
  def wheelEvent(self, event):
164
175
  """
@@ -167,16 +178,70 @@ class ChatInput(QTextEdit):
167
178
  :param event: Event
168
179
  """
169
180
  if event.modifiers() & Qt.ControlModifier:
170
- if event.angleDelta().y() > 0:
181
+ prev = self.value
182
+ dy = event.angleDelta().y()
183
+ if dy > 0:
171
184
  if self.value < self.max_font_size:
172
185
  self.value += 1
173
186
  else:
174
187
  if self.value > self.min_font_size:
175
188
  self.value -= 1
176
189
 
177
- self.window.core.config.data['font_size.input'] = self.value
178
- self.window.core.config.save()
179
- self.window.controller.ui.update_font_size()
190
+ if self.value != prev:
191
+ self.window.core.config.data['font_size.input'] = self.value
192
+ self.window.core.config.save()
193
+ self.window.controller.ui.update_font_size()
180
194
  event.accept()
181
- else:
182
- super(ChatInput, self).wheelEvent(event)
195
+ return
196
+ super().wheelEvent(event)
197
+
198
+ # --- Added: attachment button (top-left) ---------------------------------
199
+
200
+ def _init_attachment_button(self):
201
+ """Create and place the '+' attachment button pinned in the top-left corner."""
202
+ self._attach_margin = 6 # inner padding around the button
203
+ self._attach_offset_y = -6 # shift the button 2px up
204
+
205
+ self._attach_btn = QPushButton(self)
206
+ self._attach_btn.setObjectName("chatInputAttachBtn")
207
+ self._attach_btn.setIconSize(QSize(18, 18)) # icon size (slightly larger for visibility)
208
+ self._attach_btn.setFixedSize(24, 24) # full button size
209
+ self._attach_btn.setCursor(Qt.PointingHandCursor)
210
+ self._attach_btn.setToolTip(trans("attachments.btn.input.add"))
211
+ self._attach_btn.setFocusPolicy(Qt.NoFocus)
212
+ self._attach_btn.setFlat(True) # flat button style
213
+
214
+ self._attach_btn.setIcon(self.ICON_ATTACHMENT)
215
+ self._attach_btn.clicked.connect(self.action_add_attachment)
216
+ self._update_viewport_margins_for_attachment()
217
+ self._reposition_attachment_button()
218
+
219
+ def _update_viewport_margins_for_attachment(self):
220
+ """Reserve space for the attachment button on the left and apply top text padding."""
221
+ top = self._text_top_padding
222
+ left = self._attach_btn.width() + self._attach_margin * 2 if hasattr(self, "_attach_btn") else self.viewportMargins().left()
223
+ self.setViewportMargins(left, top, 0, 0)
224
+
225
+ def _reposition_attachment_button(self):
226
+ """Keep the attachment button pinned to the top-left corner."""
227
+ if hasattr(self, "_attach_btn"):
228
+ fw = self.frameWidth()
229
+ x = fw + self._attach_margin
230
+ y = fw + self._attach_margin + self._attach_offset_y # shift up by ~2px
231
+ if y < 0:
232
+ y = 0
233
+ self._attach_btn.move(x, y)
234
+ self._attach_btn.raise_()
235
+
236
+ def resizeEvent(self, event):
237
+ """Resize event keeps the attachment button in place."""
238
+ super().resizeEvent(event)
239
+ # Keep the attachment button pinned when resizing
240
+ try:
241
+ self._reposition_attachment_button()
242
+ except Exception:
243
+ pass
244
+
245
+ def action_add_attachment(self):
246
+ """Add attachment (button click)."""
247
+ self.window.controller.attachment.open_add()