pygpt-net 2.6.59__py3-none-any.whl → 2.6.61__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 (91) hide show
  1. pygpt_net/CHANGELOG.txt +11 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +9 -5
  4. pygpt_net/controller/__init__.py +1 -0
  5. pygpt_net/controller/chat/common.py +115 -6
  6. pygpt_net/controller/chat/input.py +4 -1
  7. pygpt_net/controller/presets/editor.py +442 -39
  8. pygpt_net/controller/presets/presets.py +121 -6
  9. pygpt_net/controller/settings/editor.py +0 -15
  10. pygpt_net/controller/theme/markdown.py +2 -5
  11. pygpt_net/controller/ui/ui.py +4 -7
  12. pygpt_net/core/agents/custom/__init__.py +281 -0
  13. pygpt_net/core/agents/custom/debug.py +64 -0
  14. pygpt_net/core/agents/custom/factory.py +109 -0
  15. pygpt_net/core/agents/custom/graph.py +71 -0
  16. pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
  17. pygpt_net/core/agents/custom/llama_index/factory.py +100 -0
  18. pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
  19. pygpt_net/core/agents/custom/llama_index/runner.py +562 -0
  20. pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
  21. pygpt_net/core/agents/custom/llama_index/utils.py +253 -0
  22. pygpt_net/core/agents/custom/logging.py +50 -0
  23. pygpt_net/core/agents/custom/memory.py +51 -0
  24. pygpt_net/core/agents/custom/router.py +155 -0
  25. pygpt_net/core/agents/custom/router_streamer.py +187 -0
  26. pygpt_net/core/agents/custom/runner.py +455 -0
  27. pygpt_net/core/agents/custom/schema.py +127 -0
  28. pygpt_net/core/agents/custom/utils.py +193 -0
  29. pygpt_net/core/agents/provider.py +72 -7
  30. pygpt_net/core/agents/runner.py +7 -4
  31. pygpt_net/core/agents/runners/helpers.py +1 -1
  32. pygpt_net/core/agents/runners/llama_workflow.py +3 -0
  33. pygpt_net/core/agents/runners/openai_workflow.py +8 -1
  34. pygpt_net/core/db/viewer.py +11 -5
  35. pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
  36. pygpt_net/core/{builder → node_editor}/graph.py +28 -226
  37. pygpt_net/core/node_editor/models.py +118 -0
  38. pygpt_net/core/node_editor/types.py +78 -0
  39. pygpt_net/core/node_editor/utils.py +17 -0
  40. pygpt_net/core/presets/presets.py +216 -29
  41. pygpt_net/core/render/markdown/parser.py +0 -2
  42. pygpt_net/core/render/web/renderer.py +10 -8
  43. pygpt_net/data/config/config.json +5 -6
  44. pygpt_net/data/config/models.json +3 -3
  45. pygpt_net/data/config/settings.json +2 -38
  46. pygpt_net/data/locale/locale.de.ini +64 -1
  47. pygpt_net/data/locale/locale.en.ini +63 -4
  48. pygpt_net/data/locale/locale.es.ini +64 -1
  49. pygpt_net/data/locale/locale.fr.ini +64 -1
  50. pygpt_net/data/locale/locale.it.ini +64 -1
  51. pygpt_net/data/locale/locale.pl.ini +65 -2
  52. pygpt_net/data/locale/locale.uk.ini +64 -1
  53. pygpt_net/data/locale/locale.zh.ini +64 -1
  54. pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
  55. pygpt_net/item/agent.py +5 -1
  56. pygpt_net/item/preset.py +19 -1
  57. pygpt_net/provider/agents/base.py +33 -2
  58. pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
  59. pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
  60. pygpt_net/provider/core/agent/json_file.py +11 -5
  61. pygpt_net/provider/core/config/patch.py +10 -1
  62. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
  63. pygpt_net/tools/agent_builder/tool.py +233 -52
  64. pygpt_net/tools/agent_builder/ui/dialogs.py +172 -28
  65. pygpt_net/tools/agent_builder/ui/list.py +37 -10
  66. pygpt_net/ui/__init__.py +2 -4
  67. pygpt_net/ui/dialog/about.py +58 -38
  68. pygpt_net/ui/dialog/db.py +142 -3
  69. pygpt_net/ui/dialog/preset.py +62 -8
  70. pygpt_net/ui/layout/toolbox/presets.py +52 -16
  71. pygpt_net/ui/main.py +1 -1
  72. pygpt_net/ui/widget/dialog/db.py +0 -0
  73. pygpt_net/ui/widget/lists/preset.py +644 -60
  74. pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
  75. pygpt_net/ui/widget/node_editor/command.py +373 -0
  76. pygpt_net/ui/widget/node_editor/config.py +157 -0
  77. pygpt_net/ui/widget/node_editor/editor.py +2070 -0
  78. pygpt_net/ui/widget/node_editor/item.py +493 -0
  79. pygpt_net/ui/widget/node_editor/node.py +1460 -0
  80. pygpt_net/ui/widget/node_editor/utils.py +17 -0
  81. pygpt_net/ui/widget/node_editor/view.py +364 -0
  82. pygpt_net/ui/widget/tabs/output.py +1 -1
  83. pygpt_net/ui/widget/textarea/input.py +2 -2
  84. pygpt_net/utils.py +114 -2
  85. {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +80 -93
  86. {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +88 -61
  87. pygpt_net/core/agents/custom.py +0 -150
  88. pygpt_net/ui/widget/builder/editor.py +0 -2001
  89. {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
  90. {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
  91. {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.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.19 00:00:00 #
9
+ # Updated Date: 2025.09.24 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from .graph import NodeGraph, NodeTypeRegistry, NodeModel, PropertyModel, ConnectionModel
12
+ from .editor import NodeEditor
@@ -0,0 +1,373 @@
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.25 11:30:00 #
10
+ # ================================================== #
11
+
12
+ from __future__ import annotations
13
+ from typing import Optional, List, Dict
14
+
15
+ from PySide6.QtCore import QPointF, QSizeF
16
+ from PySide6.QtGui import (QUndoCommand)
17
+
18
+ from pygpt_net.core.node_editor.models import ConnectionModel
19
+
20
+ from .item import PortItem
21
+
22
+
23
+ # ------------------------ Undo/Redo Commands ------------------------
24
+
25
+ class AddNodeCommand(QUndoCommand):
26
+ """Undoable command that adds a node of a given type at a scene position."""
27
+
28
+ def __init__(self, editor: "NodeEditor", type_name: str, scene_pos: QPointF):
29
+ """Prepare command.
30
+
31
+ Args:
32
+ editor: Owning NodeEditor.
33
+ type_name: Registered node type name.
34
+ scene_pos: Target scene position to place the node.
35
+ """
36
+ super().__init__(editor.config.cmd_add_node(type_name))
37
+ self.editor = editor
38
+ self.type_name = type_name
39
+ self.scene_pos = scene_pos
40
+ self._node_uuid: Optional[str] = None
41
+
42
+ def redo(self):
43
+ """Create or re-add the node and its item to the scene.
44
+
45
+ Behavior:
46
+ - First invocation creates a fresh NodeModel from registry, applies defaults,
47
+ and inserts it via editor._add_node_model().
48
+ - Subsequent redos re-instantiate the NodeItem for the preserved model UUID.
49
+ """
50
+ if self._node_uuid is None:
51
+ node = self.editor.graph.create_node_from_type(self.type_name)
52
+ self.editor._prepare_new_node_defaults(node)
53
+ self._node_uuid = node.uuid
54
+ self.editor._add_node_model(node, self.scene_pos)
55
+ else:
56
+ node = self.editor._model_by_uuid(self._node_uuid)
57
+ self.editor._add_node_item(node, self.scene_pos)
58
+
59
+ def undo(self):
60
+ """Remove the created node by its UUID."""
61
+ if self._node_uuid:
62
+ self.editor._remove_node_by_uuid(self._node_uuid)
63
+
64
+
65
+ class MoveNodeCommand(QUndoCommand):
66
+ """Undoable command to move a node item from old_pos to new_pos."""
67
+
68
+ def __init__(self, item: "NodeItem", old_pos: QPointF, new_pos: QPointF):
69
+ """Store references and positions for undo/redo."""
70
+ super().__init__(item.editor.config.cmd_move_node())
71
+ self.item = item
72
+ self.old_pos = old_pos
73
+ self.new_pos = new_pos
74
+
75
+ def redo(self):
76
+ """Apply the new position."""
77
+ self.item.setPos(self.new_pos)
78
+
79
+ def undo(self):
80
+ """Restore the old position."""
81
+ self.item.setPos(self.old_pos)
82
+
83
+
84
+ class ResizeNodeCommand(QUndoCommand):
85
+ """Undoable command to resize a node item from old_size to new_size."""
86
+
87
+ def __init__(self, item: "NodeItem", old_size: QSizeF, new_size: QSizeF):
88
+ """Store sizes for undo/redo (QSizeF copies kept)."""
89
+ super().__init__(item.editor.config.cmd_resize_node())
90
+ self.item = item
91
+ self.old_size = QSizeF(old_size)
92
+ self.new_size = QSizeF(new_size)
93
+
94
+ def redo(self):
95
+ """Apply the new size, clamped to content constraints."""
96
+ self.item._apply_resize(self.new_size, clamp=True)
97
+
98
+ def undo(self):
99
+ """Restore the previous size, clamped to content constraints."""
100
+ self.item._apply_resize(self.old_size, clamp=True)
101
+
102
+
103
+ class ConnectCommand(QUndoCommand):
104
+ """Undoable command that creates a connection between two ports."""
105
+
106
+ def __init__(self, editor: "NodeEditor", src: PortItem, dst: PortItem):
107
+ """Keep enough data to restore the connection across undo/redo.
108
+
109
+ Args:
110
+ editor: Owning NodeEditor.
111
+ src: Source PortItem (output).
112
+ dst: Destination PortItem (input).
113
+ """
114
+ super().__init__(editor.config.cmd_connect())
115
+ self.editor = editor
116
+ self.src_port = src
117
+ self.dst_port = dst
118
+ self.src_node = src.node_item.node.uuid
119
+ self.src_prop = src.prop_id
120
+ self.dst_node = dst.node_item.node.uuid
121
+ self.dst_prop = dst.prop_id
122
+ self._conn_uuid: Optional[str] = None
123
+
124
+ def redo(self):
125
+ """Create the connection or re-add it by UUID."""
126
+ if self._conn_uuid is None:
127
+ ok, reason, conn = self.editor.graph.connect(
128
+ (self.src_node, self.src_prop), (self.dst_node, self.dst_prop)
129
+ )
130
+ self.editor._dbg(f"ConnectCommand.redo (new) -> ok={ok}, reason='{reason}', new_uuid={(conn.uuid if ok else None)}")
131
+ if ok:
132
+ self._conn_uuid = conn.uuid
133
+ else:
134
+ conn = ConnectionModel(
135
+ uuid=self._conn_uuid,
136
+ src_node=self.src_node, src_prop=self.src_prop,
137
+ dst_node=self.dst_node, dst_prop=self.dst_prop
138
+ )
139
+ ok, reason = self.editor.graph.add_connection(conn)
140
+ self.editor._dbg(f"ConnectCommand.redo (restore) -> ok={ok}, reason='{reason}', uuid={self._conn_uuid}")
141
+
142
+ def undo(self):
143
+ """Remove the connection by UUID."""
144
+ if self._conn_uuid:
145
+ self.editor._dbg(f"ConnectCommand.undo -> remove uuid={self._conn_uuid}")
146
+ self.editor._remove_connection_by_uuid(self._conn_uuid)
147
+
148
+
149
+ class RewireConnectionCommand(QUndoCommand):
150
+ """Undoable command that rewires an existing connection or deletes it."""
151
+
152
+ def __init__(self, editor: "NodeEditor",
153
+ old_conn: ConnectionModel,
154
+ new_src: Optional[PortItem],
155
+ new_dst: Optional[PortItem]):
156
+ """Prepare rewire/delete action.
157
+
158
+ Args:
159
+ editor: Owning NodeEditor.
160
+ old_conn: Existing connection model to replace.
161
+ new_src: New source port (or None to delete).
162
+ new_dst: New destination port (or None to delete).
163
+ """
164
+ title = editor.config.cmd_delete_connection() if (new_src is None or new_dst is None) else editor.config.cmd_rewire_connection()
165
+ super().__init__(title)
166
+ self.editor = editor
167
+ self.old_conn_data = old_conn.to_dict()
168
+ self.old_uuid = old_conn.uuid
169
+ self.new_src = new_src
170
+ self.new_dst = new_dst
171
+ self._new_uuid: Optional[str] = None
172
+ self._applied = False
173
+
174
+ def redo(self):
175
+ """Apply rewire or deletion, restoring the old connection on failure."""
176
+ self.editor._dbg(f"RewireCommand.redo -> remove old={self.old_uuid}, new={'DELETE' if (self.new_src is None or self.new_dst is None) else 'CONNECT'}")
177
+ self.editor._remove_connection_by_uuid(self.old_uuid)
178
+ if self.new_src is not None and self.new_dst is not None:
179
+ ok, reason, conn = self.editor.graph.connect(
180
+ (self.new_src.node_item.node.uuid, self.new_src.prop_id),
181
+ (self.new_dst.node_item.node.uuid, self.new_dst.prop_id)
182
+ )
183
+ self.editor._dbg(f"RewireCommand.redo connect -> ok={ok}, reason='{reason}', new_uuid={(conn.uuid if ok else None)}")
184
+ if not ok:
185
+ old = ConnectionModel.from_dict(self.old_conn_data)
186
+ self.editor.graph.add_connection(old)
187
+ self._applied = False
188
+ return
189
+ self._new_uuid = conn.uuid
190
+ self._applied = True
191
+
192
+ def undo(self):
193
+ """Revert the rewire/delete by restoring the original connection."""
194
+ self.editor._dbg(f"RewireCommand.undo -> restore old={self.old_uuid}, remove new={self._new_uuid}")
195
+ if not self._applied:
196
+ return
197
+ if self._new_uuid:
198
+ self._editor = self.editor
199
+ self._editor._remove_connection_by_uuid(self._new_uuid)
200
+ old = ConnectionModel.from_dict(self.old_conn_data)
201
+ self.editor.graph.add_connection(old)
202
+
203
+
204
+ class ClearGraphCommand(QUndoCommand):
205
+ """Undoable command that clears the entire graph and scene with a snapshot."""
206
+
207
+ def __init__(self, editor: "NodeEditor"):
208
+ """Take an internal snapshot on first redo to enable undo restoration."""
209
+ super().__init__(editor.config.cmd_clear())
210
+ self.editor = editor
211
+ self._snapshot: Optional[dict] = None
212
+
213
+ def redo(self):
214
+ """Clear the scene and the graph, taking a snapshot if not already taken."""
215
+ if self._snapshot is None:
216
+ self._snapshot = self.editor.graph.to_dict()
217
+ self.editor._dbg("ClearGraph.redo -> clearing scene+graph")
218
+ self.editor._clear_scene_and_graph()
219
+
220
+ def undo(self):
221
+ """Restore the previous snapshot."""
222
+ self._dbg = self.editor._dbg
223
+ self._dbg("ClearGraph.undo -> restoring snapshot")
224
+ if self._snapshot:
225
+ self.editor.load_layout(self._snapshot)
226
+
227
+ class DeleteConnectionCommand(QUndoCommand):
228
+ """Undoable command that deletes a single existing connection and can restore it."""
229
+
230
+ def __init__(self, editor: "NodeEditor", conn: ConnectionModel):
231
+ """Snapshot connection for reliable restore.
232
+
233
+ Args:
234
+ editor: Owning NodeEditor.
235
+ conn: Existing ConnectionModel to delete/restore.
236
+ """
237
+ super().__init__(editor.config.cmd_delete_connection())
238
+ self.editor = editor
239
+ self.conn_uuid: Optional[str] = conn.uuid
240
+ # Keep a serializable snapshot for undo
241
+ self.conn_data: Dict = conn.to_dict() if hasattr(conn, "to_dict") else {
242
+ "uuid": conn.uuid,
243
+ "src_node": conn.src_node, "src_prop": conn.src_prop,
244
+ "dst_node": conn.dst_node, "dst_prop": conn.dst_prop,
245
+ }
246
+
247
+ def redo(self):
248
+ """Remove the connection by UUID (no-op if already gone)."""
249
+ if self.conn_uuid and self.conn_uuid in self.editor.graph.connections:
250
+ self.editor._remove_connection_by_uuid(self.conn_uuid)
251
+
252
+ def undo(self):
253
+ """Recreate the exact same connection (UUID preserved)."""
254
+ if not self.conn_uuid:
255
+ return
256
+ try:
257
+ cm = ConnectionModel.from_dict(self.conn_data)
258
+ except Exception:
259
+ cm = ConnectionModel(
260
+ uuid=self.conn_data.get("uuid"),
261
+ src_node=self.conn_data.get("src_node"),
262
+ src_prop=self.conn_data.get("src_prop"),
263
+ dst_node=self.conn_data.get("dst_node"),
264
+ dst_prop=self.conn_data.get("dst_prop"),
265
+ )
266
+ # Best-effort restore; if add_connection fails, try connect() as fallback
267
+ ok, _ = self.editor.graph.add_connection(cm)
268
+ if not ok:
269
+ self.editor.graph.connect(
270
+ (cm.src_node, cm.src_prop),
271
+ (cm.dst_node, cm.dst_prop),
272
+ )
273
+ class DeleteNodeCommand(QUndoCommand):
274
+ """Undoable command that deletes a node and all its connections, and can restore them."""
275
+
276
+ def __init__(self, editor: "NodeEditor", item: "NodeItem"):
277
+ """Snapshot node type, uuid, name, property values, position, size and related connections.
278
+
279
+ Args:
280
+ editor: Owning NodeEditor.
281
+ item: NodeItem to delete (used only to take a snapshot at construction time).
282
+ """
283
+ super().__init__(editor.config.cmd_delete_node())
284
+ self.editor = editor
285
+ node = item.node
286
+ self.node_uuid: str = node.uuid
287
+ self.node_type: str = node.type
288
+ self.node_name: str = node.name
289
+ # Snapshot property values
290
+ self.prop_values: Dict[str, object] = {pid: pm.value for pid, pm in node.properties.items()}
291
+ # Snapshot UI geometry
292
+ self.pos = QPointF(item.pos())
293
+ self.size = QSizeF(item.size())
294
+ # Snapshot all connections touching this node
295
+ self._connections: List[Dict] = []
296
+ for conn in editor.graph.connections.values():
297
+ if conn.src_node == self.node_uuid or conn.dst_node == self.node_uuid:
298
+ self._connections.append(conn.to_dict() if hasattr(conn, "to_dict") else {
299
+ "uuid": conn.uuid,
300
+ "src_node": conn.src_node, "src_prop": conn.src_prop,
301
+ "dst_node": conn.dst_node, "dst_prop": conn.dst_prop,
302
+ })
303
+
304
+ def redo(self):
305
+ """Delete the node (graph will remove its connections and UI will sync via signals)."""
306
+ self.editor._remove_node_by_uuid(self.node_uuid)
307
+
308
+ def undo(self):
309
+ """Recreate the node with the same UUID, restore pos/size and re-add all its connections."""
310
+ # 1) Recreate node from registry using saved type and UUID
311
+ try:
312
+ node = self.editor.graph.create_node_from_type(self.node_type)
313
+ except Exception:
314
+ return
315
+ try:
316
+ node.uuid = self.node_uuid
317
+ except Exception:
318
+ pass
319
+ try:
320
+ if isinstance(self.node_name, str) and self.node_name.strip():
321
+ node.name = self.node_name
322
+ except Exception:
323
+ pass
324
+
325
+ # 2) Restore property values (casted to current spec types)
326
+ for pid, pm in list(node.properties.items()):
327
+ if pid in self.prop_values:
328
+ try:
329
+ pm.value = self.editor._coerce_value_for_property(pm, self.prop_values[pid])
330
+ except Exception:
331
+ pm.value = self.prop_values[pid]
332
+
333
+ # 3) Place node back at original position; scene item will be created by graph signal
334
+ self.editor._pending_node_positions[self.node_uuid] = QPointF(self.pos)
335
+ self.editor.graph.add_node(node)
336
+
337
+ # 4) Apply size after item exists
338
+ item = self.editor._uuid_to_item.get(self.node_uuid)
339
+ if item:
340
+ try:
341
+ item._apply_resize(QSizeF(self.size), clamp=True)
342
+ except Exception:
343
+ pass
344
+
345
+ # 5) Restore connections (skip duplicates and validate ports)
346
+ for cd in self._connections:
347
+ cuuid = cd.get("uuid")
348
+ # Skip if already present (could be restored by another command in a macro)
349
+ if cuuid and cuuid in self.editor.graph.connections:
350
+ continue
351
+ s_n = cd.get("src_node"); s_p = cd.get("src_prop")
352
+ d_n = cd.get("dst_node"); d_p = cd.get("dst_prop")
353
+
354
+ # Validate node/port presence in current graph/spec
355
+ if s_n not in self.editor.graph.nodes or d_n not in self.editor.graph.nodes:
356
+ continue
357
+ s_node = self.editor.graph.nodes[s_n]
358
+ d_node = self.editor.graph.nodes[d_n]
359
+ if s_p not in s_node.properties or d_p not in d_node.properties:
360
+ continue
361
+
362
+ try:
363
+ cm = ConnectionModel(
364
+ uuid=cuuid if isinstance(cuuid, str) and cuuid else None,
365
+ src_node=s_n, src_prop=s_p,
366
+ dst_node=d_n, dst_prop=d_p
367
+ )
368
+ ok, _ = self.editor.graph.add_connection(cm)
369
+ if not ok:
370
+ self.editor.graph.connect((s_n, s_p), (d_n, d_p))
371
+ except Exception:
372
+ # Best-effort: ignore a single failing connection to not break the undo sequence
373
+ pass
@@ -0,0 +1,157 @@
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.25 11:30:00 #
10
+ # ================================================== #
11
+
12
+ from __future__ import annotations
13
+
14
+ try:
15
+ from pygpt_net.utils import trans as _trans
16
+ except Exception:
17
+ _trans = None
18
+
19
+
20
+ class EditorConfig:
21
+ """Central place for all user-facing strings used by NodeEditor.
22
+
23
+ Each method returns a final string. If a translator (pygpt_net.utils.trans)
24
+ is available, keys are looked up; otherwise English defaults are used.
25
+ """
26
+
27
+ def _t(self, key: str, default: str) -> str:
28
+ try:
29
+ if _trans is None:
30
+ return default
31
+ val = _trans(key)
32
+ # Fallback to default when translator returns key or empty
33
+ if not val or val == key:
34
+ return default
35
+ return val
36
+ except Exception:
37
+ return default
38
+
39
+ # ----- Commands / Undo text -----
40
+
41
+ def cmd_add_node(self, type_name: str) -> str:
42
+ return f"{self._t('node.editor.cmd.add', 'Add')} {type_name}"
43
+
44
+ def cmd_move_node(self) -> str:
45
+ return self._t('node.editor.cmd.move_node', 'Move Node')
46
+
47
+ def cmd_resize_node(self) -> str:
48
+ return self._t('node.editor.cmd.resize_node', 'Resize Node')
49
+
50
+ def cmd_connect(self) -> str:
51
+ return self._t('node.editor.cmd.connect', 'Connect')
52
+
53
+ def cmd_rewire_connection(self) -> str:
54
+ return self._t('node.editor.cmd.rewire_connection', 'Rewire Connection')
55
+
56
+ def cmd_delete_connection(self) -> str:
57
+ return self._t('node.editor.cmd.delete_connection', 'Delete Connection')
58
+
59
+ def cmd_delete_node(self) -> str:
60
+ return self._t('node.editor.cmd.delete_node', 'Delete Node')
61
+
62
+ def cmd_clear(self) -> str:
63
+ return self._t('node.editor.cmd.clear', 'Clear')
64
+
65
+ def macro_delete_selection(self) -> str:
66
+ return self._t('node.editor.macro.delete_selection', 'Delete selection')
67
+
68
+ # ----- Scene / Context menus -----
69
+
70
+ def menu_add(self) -> str:
71
+ return self._t('node.editor.menu.add', 'Add')
72
+
73
+ def menu_undo(self) -> str:
74
+ return self._t('node.editor.menu.undo', 'Undo')
75
+
76
+ def menu_redo(self) -> str:
77
+ return self._t('node.editor.menu.redo', 'Redo')
78
+
79
+ def menu_clear(self) -> str:
80
+ return self._t('node.editor.menu.clear', 'Clear')
81
+
82
+ def edge_context_delete(self) -> str:
83
+ return self._t('node.editor.edge.delete', 'Delete connection')
84
+
85
+ def node_context_rename(self) -> str:
86
+ return self._t('node.editor.node.rename', 'Rename')
87
+
88
+ def node_context_delete(self) -> str:
89
+ return self._t('node.editor.node.delete', 'Delete')
90
+
91
+ def rename_dialog_title(self) -> str:
92
+ return self._t('node.editor.rename.title', 'Rename Node')
93
+
94
+ def rename_dialog_label(self) -> str:
95
+ return self._t('node.editor.rename.label', 'Name:')
96
+
97
+ # ----- Overlays / Status -----
98
+
99
+ def overlay_grab_tooltip(self) -> str:
100
+ return self._t('node.editor.overlay.grab', 'Grab: toggle global panning with Left Mouse Button')
101
+
102
+ def overlay_zoom_in_tooltip(self) -> str:
103
+ return self._t('node.editor.overlay.zoom_in', 'Zoom In')
104
+
105
+ def overlay_zoom_out_tooltip(self) -> str:
106
+ return self._t('node.editor.overlay.zoom_out', 'Zoom Out')
107
+
108
+ def status_no_nodes(self) -> str:
109
+ return self._t('node.editor.status.no_nodes', 'No nodes')
110
+
111
+ def type_unknown(self) -> str:
112
+ return self._t('node.editor.type.unknown', 'Unknown')
113
+
114
+ # ----- Labels / Tooltips -----
115
+
116
+ def label_id(self) -> str:
117
+ return self._t('node.editor.label.id', 'ID')
118
+
119
+ def lbl_node(self) -> str:
120
+ return self._t('node.editor.lbl.node', 'Node:')
121
+
122
+ def lbl_port(self) -> str:
123
+ return self._t('node.editor.lbl.port', 'Port:')
124
+
125
+ def lbl_allowed_connections(self) -> str:
126
+ return self._t('node.editor.lbl.allowed', 'Allowed connections:')
127
+
128
+ def cap_unlimited(self) -> str:
129
+ return self._t('node.editor.cap.unlimited', 'unlimited (∞)')
130
+
131
+ def cap_na(self) -> str:
132
+ return self._t('node.editor.cap.na', 'n/a')
133
+
134
+ def side_label(self, side: str) -> str:
135
+ if side == "input":
136
+ return self._t('node.editor.side.input', 'Input')
137
+ if side == "output":
138
+ return self._t('node.editor.side.output', 'Output')
139
+ return side
140
+
141
+ def hint_click_start(self) -> str:
142
+ return self._t('node.editor.hint.click_start', 'Click: start a new connection')
143
+
144
+ def hint_ctrl_click_rewire(self) -> str:
145
+ return self._t('node.editor.hint.ctrl_rewire', 'Ctrl+Click: rewire/detach existing')
146
+
147
+ def port_tooltip(self, node_name: str, side_label: str, prop_id: str, cap_str: str) -> str:
148
+ return (
149
+ f"{self.lbl_node()} {node_name}\n"
150
+ f"{self.lbl_port()} {side_label} • {prop_id}\n"
151
+ f"{self.lbl_allowed_connections()} {cap_str}\n\n"
152
+ f"{self.hint_click_start()}\n"
153
+ f"{self.hint_ctrl_click_rewire()}"
154
+ )
155
+
156
+ def port_capacity_tooltip(self, cap_text: str) -> str:
157
+ return f"{self.lbl_allowed_connections()} (IN/OUT): {cap_text}"