pygpt-net 2.6.61__py3-none-any.whl → 2.6.63__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygpt_net/CHANGELOG.txt +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/chat/response.py +8 -2
- pygpt_net/controller/presets/editor.py +65 -1
- pygpt_net/controller/settings/profile.py +16 -4
- pygpt_net/controller/settings/workdir.py +30 -5
- pygpt_net/controller/theme/common.py +4 -2
- pygpt_net/controller/theme/markdown.py +2 -2
- pygpt_net/controller/theme/theme.py +2 -1
- pygpt_net/controller/ui/ui.py +31 -3
- pygpt_net/core/agents/custom/llama_index/runner.py +30 -52
- pygpt_net/core/agents/custom/runner.py +199 -76
- pygpt_net/core/agents/runners/llama_workflow.py +122 -12
- pygpt_net/core/agents/runners/openai_workflow.py +2 -1
- pygpt_net/core/node_editor/types.py +13 -1
- pygpt_net/core/render/web/renderer.py +76 -11
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/presets/agent_openai_b2b.json +1 -15
- pygpt_net/data/config/presets/agent_openai_coder.json +1 -15
- pygpt_net/data/config/presets/agent_openai_evolve.json +1 -23
- pygpt_net/data/config/presets/agent_openai_planner.json +1 -21
- pygpt_net/data/config/presets/agent_openai_researcher.json +1 -21
- pygpt_net/data/config/presets/agent_openai_supervisor.json +1 -13
- pygpt_net/data/config/presets/agent_openai_writer.json +1 -15
- pygpt_net/data/config/presets/agent_supervisor.json +1 -11
- pygpt_net/data/css/style.dark.css +18 -0
- pygpt_net/data/css/style.light.css +20 -1
- pygpt_net/data/js/app/runtime.js +4 -1
- pygpt_net/data/js/app.min.js +3 -2
- pygpt_net/data/locale/locale.de.ini +2 -0
- pygpt_net/data/locale/locale.en.ini +7 -0
- pygpt_net/data/locale/locale.es.ini +2 -0
- pygpt_net/data/locale/locale.fr.ini +2 -0
- pygpt_net/data/locale/locale.it.ini +2 -0
- pygpt_net/data/locale/locale.pl.ini +3 -1
- pygpt_net/data/locale/locale.uk.ini +2 -0
- pygpt_net/data/locale/locale.zh.ini +2 -0
- pygpt_net/item/ctx.py +23 -1
- pygpt_net/js_rc.py +13 -10
- pygpt_net/provider/agents/base.py +0 -0
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +0 -0
- pygpt_net/provider/agents/llama_index/workflow/codeact.py +9 -6
- pygpt_net/provider/agents/llama_index/workflow/openai.py +38 -11
- pygpt_net/provider/agents/llama_index/workflow/planner.py +248 -28
- pygpt_net/provider/agents/llama_index/workflow/supervisor.py +60 -10
- pygpt_net/provider/agents/openai/agent.py +3 -1
- pygpt_net/provider/agents/openai/agent_b2b.py +17 -13
- pygpt_net/provider/agents/openai/agent_planner.py +617 -258
- pygpt_net/provider/agents/openai/agent_with_experts.py +4 -1
- pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +8 -6
- pygpt_net/provider/agents/openai/agent_with_feedback.py +8 -6
- pygpt_net/provider/agents/openai/evolve.py +12 -8
- pygpt_net/provider/agents/openai/flow_from_schema.py +0 -0
- pygpt_net/provider/agents/openai/supervisor.py +292 -37
- pygpt_net/provider/api/openai/agents/response.py +1 -0
- pygpt_net/provider/api/x_ai/__init__.py +0 -0
- pygpt_net/provider/core/agent/__init__.py +0 -0
- pygpt_net/provider/core/agent/base.py +0 -0
- pygpt_net/provider/core/agent/json_file.py +0 -0
- pygpt_net/provider/core/config/patch.py +8 -0
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -0
- pygpt_net/provider/llms/base.py +0 -0
- pygpt_net/provider/llms/deepseek_api.py +0 -0
- pygpt_net/provider/llms/google.py +0 -0
- pygpt_net/provider/llms/hugging_face_api.py +0 -0
- pygpt_net/provider/llms/hugging_face_router.py +0 -0
- pygpt_net/provider/llms/mistral.py +0 -0
- pygpt_net/provider/llms/perplexity.py +0 -0
- pygpt_net/provider/llms/x_ai.py +0 -0
- pygpt_net/tools/agent_builder/tool.py +6 -0
- pygpt_net/tools/agent_builder/ui/dialogs.py +0 -41
- pygpt_net/ui/layout/toolbox/presets.py +14 -2
- pygpt_net/ui/main.py +2 -2
- pygpt_net/ui/widget/dialog/confirm.py +55 -5
- pygpt_net/ui/widget/draw/painter.py +90 -1
- pygpt_net/ui/widget/lists/preset.py +289 -25
- pygpt_net/ui/widget/node_editor/editor.py +53 -15
- pygpt_net/ui/widget/node_editor/node.py +82 -104
- pygpt_net/ui/widget/node_editor/view.py +4 -5
- pygpt_net/ui/widget/textarea/input.py +155 -21
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.63.dist-info}/METADATA +22 -8
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.63.dist-info}/RECORD +70 -70
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.63.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.63.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.61.dist-info → pygpt_net-2.6.63.dist-info}/entry_points.txt +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.
|
|
9
|
+
# Updated Date: 2025.09.26 12:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -143,18 +143,6 @@ class NodeEditor(QWidget):
|
|
|
143
143
|
# Centralized strings
|
|
144
144
|
self.config: EditorConfig = config if isinstance(config, EditorConfig) else EditorConfig()
|
|
145
145
|
|
|
146
|
-
# Theme and palette
|
|
147
|
-
"""
|
|
148
|
-
if parent and hasattr(parent, "window"):
|
|
149
|
-
theme = parent.window.core.config.get("theme")
|
|
150
|
-
if theme.startswith("light"):
|
|
151
|
-
print("[NodeEditor] Detected light theme from parent")
|
|
152
|
-
self._grid_back_color = QColor(255, 255, 255)
|
|
153
|
-
self._grid_pen_color = QColor(230, 230, 230)
|
|
154
|
-
self.gridBackColor = Property(QColor, lambda self: self._grid_back_color, lambda self, v: self._q_set("_grid_back_color", v))
|
|
155
|
-
self.gridPenColor = Property(QColor, lambda self: self._grid_pen_color, lambda self, v: self._q_set("_grid_pen_color", v))
|
|
156
|
-
"""
|
|
157
|
-
|
|
158
146
|
self.graph = NodeGraph(registry)
|
|
159
147
|
self.scene = NodeGraphicsScene(self)
|
|
160
148
|
self.view = NodeGraphicsView(self.scene, self)
|
|
@@ -422,7 +410,12 @@ class NodeEditor(QWidget):
|
|
|
422
410
|
|
|
423
411
|
def add_node(self, type_name: str, scene_pos: QPointF):
|
|
424
412
|
"""Add a new node of the given type at scene_pos (undoable)."""
|
|
413
|
+
# Enforce optional per-type limit configured in registry.
|
|
414
|
+
if not self._can_add_node_of_type(type_name):
|
|
415
|
+
self._dbg(f"add_node blocked: type='{type_name}' reached its max per-layout limit")
|
|
416
|
+
return False
|
|
425
417
|
self._undo.push(AddNodeCommand(self, type_name, scene_pos))
|
|
418
|
+
return True
|
|
426
419
|
|
|
427
420
|
def clear(self, ask_user: bool = True):
|
|
428
421
|
"""Clear the entire editor (undoable), optionally asking the user for confirmation."""
|
|
@@ -438,6 +431,7 @@ class NodeEditor(QWidget):
|
|
|
438
431
|
if reply != QMessageBox.Yes:
|
|
439
432
|
return False
|
|
440
433
|
self._undo.push(ClearGraphCommand(self))
|
|
434
|
+
self._update_status_label()
|
|
441
435
|
return True
|
|
442
436
|
|
|
443
437
|
def undo(self):
|
|
@@ -668,6 +662,35 @@ class NodeEditor(QWidget):
|
|
|
668
662
|
vp_pt = self.view.mapFromScene(scene_pos)
|
|
669
663
|
return self.view.viewport().mapToGlobal(vp_pt)
|
|
670
664
|
|
|
665
|
+
# ---------- Per-type limit helpers ----------
|
|
666
|
+
|
|
667
|
+
def _type_limit(self, type_name: str) -> Optional[int]:
|
|
668
|
+
"""Return configured max_num for type or None if unlimited."""
|
|
669
|
+
spec = self.graph.registry.get(type_name) if self.graph and self.graph.registry else None
|
|
670
|
+
if not spec:
|
|
671
|
+
return None
|
|
672
|
+
try:
|
|
673
|
+
limit = getattr(spec, "max_num", None)
|
|
674
|
+
if isinstance(limit, int) and limit <= 0:
|
|
675
|
+
return None
|
|
676
|
+
return limit if isinstance(limit, int) else None
|
|
677
|
+
except Exception:
|
|
678
|
+
return None
|
|
679
|
+
|
|
680
|
+
def _count_nodes_of_type(self, type_name: str) -> int:
|
|
681
|
+
"""Count current nodes of a given type in the live graph."""
|
|
682
|
+
try:
|
|
683
|
+
return sum(1 for n in self.graph.nodes.values() if getattr(n, "type", None) == type_name)
|
|
684
|
+
except Exception:
|
|
685
|
+
return 0
|
|
686
|
+
|
|
687
|
+
def _can_add_node_of_type(self, type_name: str) -> bool:
|
|
688
|
+
"""Check whether adding another node of given type is allowed by max_num."""
|
|
689
|
+
limit = self._type_limit(type_name)
|
|
690
|
+
if limit is None:
|
|
691
|
+
return True
|
|
692
|
+
return self._count_nodes_of_type(type_name) < int(limit)
|
|
693
|
+
|
|
671
694
|
def _on_scene_context_menu(self, scene_pos: QPointF):
|
|
672
695
|
"""Show context menu for adding nodes and undo/redo/clear at empty scene position."""
|
|
673
696
|
try:
|
|
@@ -685,7 +708,21 @@ class NodeEditor(QWidget):
|
|
|
685
708
|
add_menu = menu.addMenu(self.config.menu_add())
|
|
686
709
|
action_by_type: Dict[QAction, str] = {}
|
|
687
710
|
for tname in self.graph.registry.types():
|
|
688
|
-
|
|
711
|
+
# Prefer UI-only display name; fallback to identifier (type_name)
|
|
712
|
+
try:
|
|
713
|
+
label = self.graph.registry.display_name(tname)
|
|
714
|
+
except Exception:
|
|
715
|
+
label = tname
|
|
716
|
+
act = add_menu.addAction(label)
|
|
717
|
+
# Show the underlying identifier in tooltip to make UI/ID relation explicit
|
|
718
|
+
if label != tname:
|
|
719
|
+
try:
|
|
720
|
+
act.setToolTip(tname)
|
|
721
|
+
except Exception:
|
|
722
|
+
pass
|
|
723
|
+
# Disable when limit reached (evaluated in real-time, per layout)
|
|
724
|
+
if not self._can_add_node_of_type(tname):
|
|
725
|
+
act.setEnabled(False)
|
|
689
726
|
action_by_type[act] = tname
|
|
690
727
|
|
|
691
728
|
menu.addSeparator()
|
|
@@ -711,7 +748,8 @@ class NodeEditor(QWidget):
|
|
|
711
748
|
self.clear(ask_user=True)
|
|
712
749
|
elif chosen in action_by_type:
|
|
713
750
|
type_name = action_by_type[chosen]
|
|
714
|
-
|
|
751
|
+
# Route through editor.add_node() to enforce limit guards
|
|
752
|
+
self.add_node(type_name, scene_pos)
|
|
715
753
|
|
|
716
754
|
# ---------- Z-order helpers ----------
|
|
717
755
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.
|
|
9
|
+
# Updated Date: 2025.09.26 10:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -44,6 +44,9 @@ class SingleLineTextEdit(QTextEdit):
|
|
|
44
44
|
self.setTabChangesFocus(True)
|
|
45
45
|
self.setFrameStyle(QFrame.NoFrame)
|
|
46
46
|
self.document().setDocumentMargin(2)
|
|
47
|
+
# Enforce line-edit-like behavior with no context menu
|
|
48
|
+
# (widget menus are disabled by design; only node menu remains)
|
|
49
|
+
self.setContextMenuPolicy(Qt.NoContextMenu)
|
|
47
50
|
self._update_height()
|
|
48
51
|
|
|
49
52
|
def _update_height(self):
|
|
@@ -86,58 +89,13 @@ class SingleLineTextEdit(QTextEdit):
|
|
|
86
89
|
c.setPosition(min(pos, len(t2)))
|
|
87
90
|
self.setTextCursor(c)
|
|
88
91
|
|
|
89
|
-
def _apply_menu_theme(self, menu: QMenu):
|
|
90
|
-
"""Apply app/window stylesheet + palette + font to context menu."""
|
|
91
|
-
try:
|
|
92
|
-
wnd = self.window()
|
|
93
|
-
except Exception:
|
|
94
|
-
wnd = None
|
|
95
|
-
stylesheet = ""
|
|
96
|
-
pal = None
|
|
97
|
-
font = None
|
|
98
|
-
try:
|
|
99
|
-
if wnd:
|
|
100
|
-
stylesheet = wnd.styleSheet() or ""
|
|
101
|
-
pal = wnd.palette()
|
|
102
|
-
font = wnd.font()
|
|
103
|
-
except Exception:
|
|
104
|
-
pass
|
|
105
|
-
try:
|
|
106
|
-
app = QApplication.instance()
|
|
107
|
-
if app:
|
|
108
|
-
if not stylesheet and app.styleSheet():
|
|
109
|
-
stylesheet = app.styleSheet()
|
|
110
|
-
if pal is None:
|
|
111
|
-
pal = app.palette()
|
|
112
|
-
if font is None:
|
|
113
|
-
font = app.font()
|
|
114
|
-
except Exception:
|
|
115
|
-
pass
|
|
116
|
-
try:
|
|
117
|
-
if pal:
|
|
118
|
-
menu.setPalette(pal)
|
|
119
|
-
if font:
|
|
120
|
-
menu.setFont(font)
|
|
121
|
-
if stylesheet:
|
|
122
|
-
menu.setStyleSheet(stylesheet)
|
|
123
|
-
menu.ensurePolished()
|
|
124
|
-
except Exception:
|
|
125
|
-
pass
|
|
126
|
-
|
|
127
92
|
def contextMenuEvent(self, e):
|
|
128
|
-
"""
|
|
93
|
+
"""Widget-level context menu is intentionally disabled."""
|
|
129
94
|
try:
|
|
130
|
-
|
|
95
|
+
e.ignore()
|
|
131
96
|
except Exception:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
try:
|
|
135
|
-
menu.exec(e.globalPos())
|
|
136
|
-
finally:
|
|
137
|
-
try:
|
|
138
|
-
menu.deleteLater()
|
|
139
|
-
except Exception:
|
|
140
|
-
pass
|
|
97
|
+
pass
|
|
98
|
+
return
|
|
141
99
|
|
|
142
100
|
|
|
143
101
|
class NodeContentWidget(QWidget):
|
|
@@ -229,6 +187,8 @@ class NodeContentWidget(QWidget):
|
|
|
229
187
|
elif pm.type == "text":
|
|
230
188
|
te = QTextEdit()
|
|
231
189
|
te.setFocusPolicy(Qt.StrongFocus)
|
|
190
|
+
# Disable widget-level context menu completely (only node menu is available)
|
|
191
|
+
te.setContextMenuPolicy(Qt.NoContextMenu)
|
|
232
192
|
if pm.value is not None:
|
|
233
193
|
te.setPlainText(str(pm.value))
|
|
234
194
|
te.setReadOnly(not editable)
|
|
@@ -238,8 +198,6 @@ class NodeContentWidget(QWidget):
|
|
|
238
198
|
except Exception:
|
|
239
199
|
pass
|
|
240
200
|
te.textChanged.connect(lambda pid=pid, te=te: self.valueChanged.emit(pid, te.toPlainText()))
|
|
241
|
-
# Ensure context menu follows global (Material) style
|
|
242
|
-
self._install_styled_context_menu(te)
|
|
243
201
|
w = te
|
|
244
202
|
|
|
245
203
|
elif pm.type == "int":
|
|
@@ -314,57 +272,6 @@ class NodeContentWidget(QWidget):
|
|
|
314
272
|
except Exception:
|
|
315
273
|
pass
|
|
316
274
|
|
|
317
|
-
def _install_styled_context_menu(self, te: QTextEdit):
|
|
318
|
-
"""Install a custom context menu handler that applies global styles."""
|
|
319
|
-
try:
|
|
320
|
-
te.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
321
|
-
te.customContextMenuRequested.connect(
|
|
322
|
-
lambda pos, _te=te: self._show_styled_standard_menu(_te, pos)
|
|
323
|
-
)
|
|
324
|
-
except Exception:
|
|
325
|
-
pass
|
|
326
|
-
|
|
327
|
-
def _show_styled_standard_menu(self, te: QTextEdit, pos):
|
|
328
|
-
"""Create standard menu and apply app/window stylesheet + palette + font."""
|
|
329
|
-
try:
|
|
330
|
-
menu = te.createStandardContextMenu()
|
|
331
|
-
except Exception:
|
|
332
|
-
return
|
|
333
|
-
stylesheet = ""
|
|
334
|
-
pal = None
|
|
335
|
-
font = None
|
|
336
|
-
try:
|
|
337
|
-
# Prefer editor helpers (consistent with the rest of NodeEditor)
|
|
338
|
-
stylesheet = self.editor._current_stylesheet()
|
|
339
|
-
pal = self.editor._current_palette()
|
|
340
|
-
font = self.editor._current_font()
|
|
341
|
-
except Exception:
|
|
342
|
-
try:
|
|
343
|
-
wnd = te.window()
|
|
344
|
-
if wnd:
|
|
345
|
-
stylesheet = wnd.styleSheet() or ""
|
|
346
|
-
pal = wnd.palette()
|
|
347
|
-
font = wnd.font()
|
|
348
|
-
except Exception:
|
|
349
|
-
pass
|
|
350
|
-
try:
|
|
351
|
-
if pal:
|
|
352
|
-
menu.setPalette(pal)
|
|
353
|
-
if font:
|
|
354
|
-
menu.setFont(font)
|
|
355
|
-
if stylesheet:
|
|
356
|
-
menu.setStyleSheet(stylesheet)
|
|
357
|
-
menu.ensurePolished()
|
|
358
|
-
except Exception:
|
|
359
|
-
pass
|
|
360
|
-
try:
|
|
361
|
-
menu.exec(te.mapToGlobal(pos))
|
|
362
|
-
finally:
|
|
363
|
-
try:
|
|
364
|
-
menu.deleteLater()
|
|
365
|
-
except Exception:
|
|
366
|
-
pass
|
|
367
|
-
|
|
368
275
|
def event(self, e):
|
|
369
276
|
"""
|
|
370
277
|
Filter ShortcutOverride so editor keystrokes are not eaten by the scene.
|
|
@@ -825,7 +732,7 @@ class NodeItem(QGraphicsWidget):
|
|
|
825
732
|
def _effective_hit_margin(self) -> float:
|
|
826
733
|
"""Return the effective resize-grip 'hit' margin (visual margin minus inset)."""
|
|
827
734
|
margin = float(getattr(self.editor, "_resize_grip_margin", 12.0) or 12.0)
|
|
828
|
-
inset = float(getattr(self.editor, "_resize_grip_hit_inset",
|
|
735
|
+
inset = float(getattr(self.editor, "_resize_grip_hit_inset", 5.0) or 0.0)
|
|
829
736
|
hit = max(4.0, margin - inset)
|
|
830
737
|
return hit
|
|
831
738
|
|
|
@@ -1216,6 +1123,77 @@ class NodeItem(QGraphicsWidget):
|
|
|
1216
1123
|
"""Filter events on proxy/content to keep hover/ports/overlay in sync."""
|
|
1217
1124
|
et = e.type()
|
|
1218
1125
|
try:
|
|
1126
|
+
# Forward RMB on the proxy (covers all embedded editors) to node menu
|
|
1127
|
+
if obj is self._proxy:
|
|
1128
|
+
if et == QEvent.GraphicsSceneMousePress:
|
|
1129
|
+
try:
|
|
1130
|
+
if e.button() == Qt.RightButton:
|
|
1131
|
+
# Use screenPos if available; fallback to view mapping
|
|
1132
|
+
gp = e.screenPos() if hasattr(e, "screenPos") else None
|
|
1133
|
+
if gp is None:
|
|
1134
|
+
sp = e.scenePos()
|
|
1135
|
+
vp = self.editor.view.mapFromScene(sp)
|
|
1136
|
+
gp = self.editor.view.viewport().mapToGlobal(vp)
|
|
1137
|
+
# Reuse node menu logic here for consistency
|
|
1138
|
+
menu = QMenu(self.editor.window())
|
|
1139
|
+
ss = self.editor.window().styleSheet()
|
|
1140
|
+
if ss:
|
|
1141
|
+
menu.setStyleSheet(ss)
|
|
1142
|
+
act_rename = QAction(self.editor.config.node_context_rename(), menu)
|
|
1143
|
+
act_delete = QAction(self.editor.config.node_context_delete(), menu)
|
|
1144
|
+
menu.addAction(act_rename)
|
|
1145
|
+
menu.addSeparator()
|
|
1146
|
+
menu.addAction(act_delete)
|
|
1147
|
+
chosen = menu.exec(gp.toPoint() if hasattr(gp, "toPoint") else gp)
|
|
1148
|
+
if chosen == act_rename:
|
|
1149
|
+
from PySide6.QtWidgets import QInputDialog
|
|
1150
|
+
new_name, ok = QInputDialog.getText(
|
|
1151
|
+
self.editor.window(),
|
|
1152
|
+
self.editor.config.rename_dialog_title(),
|
|
1153
|
+
self.editor.config.rename_dialog_label(),
|
|
1154
|
+
text=self.node.name
|
|
1155
|
+
)
|
|
1156
|
+
if ok and new_name:
|
|
1157
|
+
self.node.name = new_name
|
|
1158
|
+
self.update()
|
|
1159
|
+
elif chosen == act_delete:
|
|
1160
|
+
self.editor._delete_node_item(self)
|
|
1161
|
+
e.accept()
|
|
1162
|
+
return True
|
|
1163
|
+
except Exception:
|
|
1164
|
+
pass
|
|
1165
|
+
|
|
1166
|
+
if et == QEvent.GraphicsSceneContextMenu:
|
|
1167
|
+
try:
|
|
1168
|
+
gp = e.screenPos() if hasattr(e, "screenPos") else None
|
|
1169
|
+
menu = QMenu(self.editor.window())
|
|
1170
|
+
ss = self.editor.window().styleSheet()
|
|
1171
|
+
if ss:
|
|
1172
|
+
menu.setStyleSheet(ss)
|
|
1173
|
+
act_rename = QAction(self.editor.config.node_context_rename(), menu)
|
|
1174
|
+
act_delete = QAction(self.editor.config.node_context_delete(), menu)
|
|
1175
|
+
menu.addAction(act_rename)
|
|
1176
|
+
menu.addSeparator()
|
|
1177
|
+
menu.addAction(act_delete)
|
|
1178
|
+
chosen = menu.exec(gp)
|
|
1179
|
+
if chosen == act_rename:
|
|
1180
|
+
from PySide6.QtWidgets import QInputDialog
|
|
1181
|
+
new_name, ok = QInputDialog.getText(
|
|
1182
|
+
self.editor.window(),
|
|
1183
|
+
self.editor.config.rename_dialog_title(),
|
|
1184
|
+
self.editor.config.rename_dialog_label(),
|
|
1185
|
+
text=self.node.name
|
|
1186
|
+
)
|
|
1187
|
+
if ok and new_name:
|
|
1188
|
+
self.node.name = new_name
|
|
1189
|
+
self.update()
|
|
1190
|
+
elif chosen == act_delete:
|
|
1191
|
+
self.editor._delete_node_item(self)
|
|
1192
|
+
e.accept()
|
|
1193
|
+
return True
|
|
1194
|
+
except Exception:
|
|
1195
|
+
pass
|
|
1196
|
+
|
|
1219
1197
|
if obj is self._proxy and et in (QEvent.GraphicsSceneMouseMove, QEvent.GraphicsSceneHoverMove):
|
|
1220
1198
|
local = self.mapFromScene(e.scenePos())
|
|
1221
1199
|
self._apply_hover_from_pos(local)
|
|
@@ -254,7 +254,6 @@ class NodeViewOverlayControls(QWidget):
|
|
|
254
254
|
self.setAttribute(Qt.WA_StyledBackground, True)
|
|
255
255
|
|
|
256
256
|
layout = QHBoxLayout(self)
|
|
257
|
-
# Bigger spacing to visually add padding around buttons
|
|
258
257
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
259
258
|
layout.setSpacing(8)
|
|
260
259
|
|
|
@@ -266,21 +265,21 @@ class NodeViewOverlayControls(QWidget):
|
|
|
266
265
|
self.btnGrab.setToolTip(cfg.overlay_grab_tooltip())
|
|
267
266
|
self.btnGrab.setIcon(QIcon(":/icons/drag.svg"))
|
|
268
267
|
self.btnGrab.setIconSize(QSize(20, 20))
|
|
269
|
-
self.btnGrab.setMinimumSize(
|
|
268
|
+
self.btnGrab.setMinimumSize(25, 25)
|
|
270
269
|
|
|
271
|
-
# Zoom Out
|
|
270
|
+
# Zoom Out
|
|
272
271
|
self.btnZoomOut = QPushButton(self)
|
|
273
272
|
self.btnZoomOut.setToolTip(cfg.overlay_zoom_out_tooltip())
|
|
274
273
|
self.btnZoomOut.setIcon(QIcon(":/icons/zoom_out.svg"))
|
|
275
274
|
self.btnZoomOut.setIconSize(QSize(20, 20))
|
|
276
|
-
self.btnZoomOut.setMinimumSize(
|
|
275
|
+
self.btnZoomOut.setMinimumSize(25, 25)
|
|
277
276
|
|
|
278
277
|
# Zoom In
|
|
279
278
|
self.btnZoomIn = QPushButton(self)
|
|
280
279
|
self.btnZoomIn.setToolTip(cfg.overlay_zoom_in_tooltip())
|
|
281
280
|
self.btnZoomIn.setIcon(QIcon(":/icons/zoom_in.svg"))
|
|
282
281
|
self.btnZoomIn.setIconSize(QSize(20, 20))
|
|
283
|
-
self.btnZoomIn.setMinimumSize(
|
|
282
|
+
self.btnZoomIn.setMinimumSize(25, 25)
|
|
284
283
|
|
|
285
284
|
layout.addWidget(self.btnGrab)
|
|
286
285
|
layout.addWidget(self.btnZoomIn)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.09.
|
|
9
|
+
# Updated Date: 2025.09.26 12:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from typing import Optional
|
|
@@ -62,8 +62,8 @@ class ChatInput(QTextEdit):
|
|
|
62
62
|
self._icons_margin = 6 # inner left/right padding around the bar
|
|
63
63
|
self._icons_spacing = 4 # spacing between buttons
|
|
64
64
|
self._icons_offset_y = -4 # small upward shift (visual alignment)
|
|
65
|
-
self._icon_size = QSize(18, 18) # icon size (matches
|
|
66
|
-
self._btn_size = QSize(24, 24) # button size (w x h), matches
|
|
65
|
+
self._icon_size = QSize(18, 18) # icon size (matches original)
|
|
66
|
+
self._btn_size = QSize(24, 24) # button size (w x h), matches QPushButton
|
|
67
67
|
|
|
68
68
|
# Storage for icon buttons and metadata
|
|
69
69
|
self._icons = {} # key -> QPushButton
|
|
@@ -124,6 +124,9 @@ class ChatInput(QTextEdit):
|
|
|
124
124
|
self._tokens_timer.timeout.connect(self.window.controller.ui.update_tokens)
|
|
125
125
|
self.textChanged.connect(self._on_text_changed_tokens)
|
|
126
126
|
|
|
127
|
+
# Paste/input safety limits
|
|
128
|
+
self._paste_max_chars = 1000000000 # hard cap to prevent pathological pastes from freezing/crashing
|
|
129
|
+
|
|
127
130
|
def _on_text_changed_tokens(self):
|
|
128
131
|
"""Schedule token count update with debounce."""
|
|
129
132
|
self._tokens_timer.start()
|
|
@@ -142,15 +145,133 @@ class ChatInput(QTextEdit):
|
|
|
142
145
|
self._text_top_padding = max(0, int(px))
|
|
143
146
|
self._apply_margins()
|
|
144
147
|
|
|
148
|
+
def canInsertFromMimeData(self, source) -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Restrict accepted MIME types to safe, explicitly handled ones.
|
|
151
|
+
This prevents Qt from trying to parse unknown/broken formats.
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
if source is None:
|
|
155
|
+
return False
|
|
156
|
+
return source.hasText() or source.hasUrls() or source.hasImage()
|
|
157
|
+
except Exception:
|
|
158
|
+
return False
|
|
159
|
+
|
|
145
160
|
def insertFromMimeData(self, source):
|
|
146
161
|
"""
|
|
147
162
|
Insert from mime data
|
|
148
163
|
|
|
149
164
|
:param source: source
|
|
150
165
|
"""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
# Always process attachments first; never break input pipeline on errors.
|
|
167
|
+
try:
|
|
168
|
+
self.handle_clipboard(source)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
try:
|
|
171
|
+
self.window.core.debug.log(e)
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
# If an image is present, we treat it as attachment-only and do not insert textual representation.
|
|
176
|
+
try:
|
|
177
|
+
if source and source.hasImage():
|
|
178
|
+
return
|
|
179
|
+
except Exception:
|
|
180
|
+
# fallback to text extraction below
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
# Insert only sanitized plain text (no HTML, no custom formats).
|
|
184
|
+
try:
|
|
185
|
+
text = self._safe_text_from_mime(source)
|
|
186
|
+
if text:
|
|
187
|
+
self.insertPlainText(text)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
try:
|
|
190
|
+
self.window.core.debug.log(e)
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
def _safe_text_from_mime(self, source) -> str:
|
|
195
|
+
"""
|
|
196
|
+
Extracts plain text from QMimeData safely, normalizes and sanitizes it.
|
|
197
|
+
Falls back to URLs joined by space if textual content is not provided.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
if source is None:
|
|
201
|
+
return ""
|
|
202
|
+
if source.hasText():
|
|
203
|
+
return self._sanitize_text(source.text())
|
|
204
|
+
if source.hasUrls():
|
|
205
|
+
parts = []
|
|
206
|
+
for url in source.urls():
|
|
207
|
+
try:
|
|
208
|
+
if url.isLocalFile():
|
|
209
|
+
parts.append(url.toLocalFile())
|
|
210
|
+
else:
|
|
211
|
+
parts.append(url.toString())
|
|
212
|
+
except Exception:
|
|
213
|
+
continue
|
|
214
|
+
return self._sanitize_text(" ".join([p for p in parts if p]))
|
|
215
|
+
except Exception as e:
|
|
216
|
+
try:
|
|
217
|
+
self.window.core.debug.log(e)
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
return ""
|
|
221
|
+
|
|
222
|
+
def _sanitize_text(self, text: str) -> str:
|
|
223
|
+
"""
|
|
224
|
+
Sanitize pasted text:
|
|
225
|
+
- normalize newlines
|
|
226
|
+
- remove NUL and most control chars except tab/newline
|
|
227
|
+
- strip zero-width and bidi control characters
|
|
228
|
+
- hard-cap maximum length to avoid UI freeze
|
|
229
|
+
"""
|
|
230
|
+
if not text:
|
|
231
|
+
return ""
|
|
232
|
+
if not isinstance(text, str):
|
|
233
|
+
try:
|
|
234
|
+
text = str(text)
|
|
235
|
+
except Exception:
|
|
236
|
+
return ""
|
|
237
|
+
|
|
238
|
+
# Normalize line breaks
|
|
239
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
240
|
+
|
|
241
|
+
# Remove disallowed control chars, keep tab/newline
|
|
242
|
+
out = []
|
|
243
|
+
for ch in text:
|
|
244
|
+
code = ord(ch)
|
|
245
|
+
if code == 0:
|
|
246
|
+
continue # NUL
|
|
247
|
+
if code < 32:
|
|
248
|
+
if ch in ("\n", "\t"):
|
|
249
|
+
out.append(ch)
|
|
250
|
+
else:
|
|
251
|
+
out.append(" ")
|
|
252
|
+
continue
|
|
253
|
+
if code == 0x7F:
|
|
254
|
+
continue # DEL
|
|
255
|
+
# Remove zero-width and bidi controls
|
|
256
|
+
if (0x200B <= code <= 0x200F) or (0x202A <= code <= 0x202E) or (0x2066 <= code <= 0x2069):
|
|
257
|
+
continue
|
|
258
|
+
out.append(ch)
|
|
259
|
+
|
|
260
|
+
s = "".join(out)
|
|
261
|
+
|
|
262
|
+
# Cap very large pastes
|
|
263
|
+
try:
|
|
264
|
+
limit = int(self._paste_max_chars)
|
|
265
|
+
except Exception:
|
|
266
|
+
limit = 250000
|
|
267
|
+
if limit > 0 and len(s) > limit:
|
|
268
|
+
s = s[:limit]
|
|
269
|
+
try:
|
|
270
|
+
self.window.core.debug.log(f"Input paste truncated to {limit} chars")
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
return s
|
|
154
275
|
|
|
155
276
|
def handle_clipboard(self, source):
|
|
156
277
|
"""
|
|
@@ -158,20 +279,33 @@ class ChatInput(QTextEdit):
|
|
|
158
279
|
|
|
159
280
|
:param source: source
|
|
160
281
|
"""
|
|
161
|
-
if source
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
282
|
+
if source is None:
|
|
283
|
+
return
|
|
284
|
+
try:
|
|
285
|
+
if source.hasImage():
|
|
286
|
+
image = source.imageData()
|
|
287
|
+
if isinstance(image, QImage):
|
|
288
|
+
self.window.controller.attachment.from_clipboard_image(image)
|
|
289
|
+
elif source.hasUrls():
|
|
290
|
+
urls = source.urls()
|
|
291
|
+
for url in urls:
|
|
292
|
+
try:
|
|
293
|
+
if url.isLocalFile():
|
|
294
|
+
local_path = url.toLocalFile()
|
|
295
|
+
self.window.controller.attachment.from_clipboard_url(local_path)
|
|
296
|
+
except Exception:
|
|
297
|
+
# Ignore broken URL entries
|
|
298
|
+
continue
|
|
299
|
+
elif source.hasText():
|
|
300
|
+
text = self._sanitize_text(source.text())
|
|
301
|
+
if text:
|
|
302
|
+
self.window.controller.attachment.from_clipboard_text(text)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
# Never propagate clipboard errors to UI thread
|
|
305
|
+
try:
|
|
306
|
+
self.window.core.debug.log(e)
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
175
309
|
|
|
176
310
|
def contextMenuEvent(self, event):
|
|
177
311
|
"""
|
|
@@ -395,7 +529,7 @@ class ChatInput(QTextEdit):
|
|
|
395
529
|
btn.setCursor(Qt.PointingHandCursor)
|
|
396
530
|
btn.setToolTip(tooltip or key)
|
|
397
531
|
btn.setFocusPolicy(Qt.NoFocus)
|
|
398
|
-
btn.setFlat(True) # flat button style
|
|
532
|
+
btn.setFlat(True) # flat button style
|
|
399
533
|
# optional: no text
|
|
400
534
|
btn.setText("")
|
|
401
535
|
|