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.
- pygpt_net/CHANGELOG.txt +11 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +9 -5
- pygpt_net/controller/__init__.py +1 -0
- pygpt_net/controller/chat/common.py +115 -6
- pygpt_net/controller/chat/input.py +4 -1
- pygpt_net/controller/presets/editor.py +442 -39
- pygpt_net/controller/presets/presets.py +121 -6
- pygpt_net/controller/settings/editor.py +0 -15
- pygpt_net/controller/theme/markdown.py +2 -5
- pygpt_net/controller/ui/ui.py +4 -7
- pygpt_net/core/agents/custom/__init__.py +281 -0
- pygpt_net/core/agents/custom/debug.py +64 -0
- pygpt_net/core/agents/custom/factory.py +109 -0
- pygpt_net/core/agents/custom/graph.py +71 -0
- pygpt_net/core/agents/custom/llama_index/__init__.py +10 -0
- pygpt_net/core/agents/custom/llama_index/factory.py +100 -0
- pygpt_net/core/agents/custom/llama_index/router_streamer.py +106 -0
- pygpt_net/core/agents/custom/llama_index/runner.py +562 -0
- pygpt_net/core/agents/custom/llama_index/stream.py +56 -0
- pygpt_net/core/agents/custom/llama_index/utils.py +253 -0
- pygpt_net/core/agents/custom/logging.py +50 -0
- pygpt_net/core/agents/custom/memory.py +51 -0
- pygpt_net/core/agents/custom/router.py +155 -0
- pygpt_net/core/agents/custom/router_streamer.py +187 -0
- pygpt_net/core/agents/custom/runner.py +455 -0
- pygpt_net/core/agents/custom/schema.py +127 -0
- pygpt_net/core/agents/custom/utils.py +193 -0
- pygpt_net/core/agents/provider.py +72 -7
- pygpt_net/core/agents/runner.py +7 -4
- pygpt_net/core/agents/runners/helpers.py +1 -1
- pygpt_net/core/agents/runners/llama_workflow.py +3 -0
- pygpt_net/core/agents/runners/openai_workflow.py +8 -1
- pygpt_net/core/db/viewer.py +11 -5
- pygpt_net/{ui/widget/builder → core/node_editor}/__init__.py +2 -2
- pygpt_net/core/{builder → node_editor}/graph.py +28 -226
- pygpt_net/core/node_editor/models.py +118 -0
- pygpt_net/core/node_editor/types.py +78 -0
- pygpt_net/core/node_editor/utils.py +17 -0
- pygpt_net/core/presets/presets.py +216 -29
- pygpt_net/core/render/markdown/parser.py +0 -2
- pygpt_net/core/render/web/renderer.py +10 -8
- pygpt_net/data/config/config.json +5 -6
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +2 -38
- pygpt_net/data/locale/locale.de.ini +64 -1
- pygpt_net/data/locale/locale.en.ini +63 -4
- pygpt_net/data/locale/locale.es.ini +64 -1
- pygpt_net/data/locale/locale.fr.ini +64 -1
- pygpt_net/data/locale/locale.it.ini +64 -1
- pygpt_net/data/locale/locale.pl.ini +65 -2
- pygpt_net/data/locale/locale.uk.ini +64 -1
- pygpt_net/data/locale/locale.zh.ini +64 -1
- pygpt_net/data/locale/plugin.cmd_system.en.ini +62 -66
- pygpt_net/item/agent.py +5 -1
- pygpt_net/item/preset.py +19 -1
- pygpt_net/provider/agents/base.py +33 -2
- pygpt_net/provider/agents/llama_index/flow_from_schema.py +92 -0
- pygpt_net/provider/agents/openai/flow_from_schema.py +96 -0
- pygpt_net/provider/core/agent/json_file.py +11 -5
- pygpt_net/provider/core/config/patch.py +10 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -6
- pygpt_net/tools/agent_builder/tool.py +233 -52
- pygpt_net/tools/agent_builder/ui/dialogs.py +172 -28
- pygpt_net/tools/agent_builder/ui/list.py +37 -10
- pygpt_net/ui/__init__.py +2 -4
- pygpt_net/ui/dialog/about.py +58 -38
- pygpt_net/ui/dialog/db.py +142 -3
- pygpt_net/ui/dialog/preset.py +62 -8
- pygpt_net/ui/layout/toolbox/presets.py +52 -16
- pygpt_net/ui/main.py +1 -1
- pygpt_net/ui/widget/dialog/db.py +0 -0
- pygpt_net/ui/widget/lists/preset.py +644 -60
- pygpt_net/{core/builder → ui/widget/node_editor}/__init__.py +2 -2
- pygpt_net/ui/widget/node_editor/command.py +373 -0
- pygpt_net/ui/widget/node_editor/config.py +157 -0
- pygpt_net/ui/widget/node_editor/editor.py +2070 -0
- pygpt_net/ui/widget/node_editor/item.py +493 -0
- pygpt_net/ui/widget/node_editor/node.py +1460 -0
- pygpt_net/ui/widget/node_editor/utils.py +17 -0
- pygpt_net/ui/widget/node_editor/view.py +364 -0
- pygpt_net/ui/widget/tabs/output.py +1 -1
- pygpt_net/ui/widget/textarea/input.py +2 -2
- pygpt_net/utils.py +114 -2
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/METADATA +80 -93
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/RECORD +88 -61
- pygpt_net/core/agents/custom.py +0 -150
- pygpt_net/ui/widget/builder/editor.py +0 -2001
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.59.dist-info → pygpt_net-2.6.61.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
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.24 00:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
# Safety: check C++ pointer validity to avoid calling methods on deleted Qt objects
|
|
13
|
+
try:
|
|
14
|
+
from shiboken6 import isValid as _qt_is_valid
|
|
15
|
+
except Exception:
|
|
16
|
+
def _qt_is_valid(obj) -> bool:
|
|
17
|
+
return obj is not None
|
|
@@ -0,0 +1,364 @@
|
|
|
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.26 03:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from typing import Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from PySide6.QtCore import Qt, QPointF, QRectF, QObject, Signal, QSize, QEvent
|
|
16
|
+
from PySide6.QtGui import QColor, QPainter, QPen, QTransform, QIcon
|
|
17
|
+
from PySide6.QtWidgets import QWidget, QGraphicsView, QGraphicsScene, QPushButton, QHBoxLayout, QLabel
|
|
18
|
+
|
|
19
|
+
from .config import EditorConfig
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ------------------------ Graphics View / Scene ------------------------
|
|
23
|
+
|
|
24
|
+
class NodeGraphicsView(QGraphicsView):
|
|
25
|
+
"""Zoomable, pannable view with a grid background.
|
|
26
|
+
|
|
27
|
+
The view renders a lightweight grid and supports:
|
|
28
|
+
- Ctrl + Mouse Wheel zooming
|
|
29
|
+
- Middle Mouse Button panning
|
|
30
|
+
- Rubber band selection
|
|
31
|
+
- Optional left-button panning only when global grab mode is enabled
|
|
32
|
+
"""
|
|
33
|
+
def __init__(self, scene: QGraphicsScene, parent: Optional[QWidget] = None):
|
|
34
|
+
super().__init__(scene, parent)
|
|
35
|
+
self.setRenderHints(self.renderHints() |
|
|
36
|
+
QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)
|
|
37
|
+
self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
|
|
38
|
+
self.setDragMode(QGraphicsView.RubberBandDrag)
|
|
39
|
+
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
|
|
40
|
+
a = QGraphicsView.AnchorViewCenter
|
|
41
|
+
self.setResizeAnchor(a)
|
|
42
|
+
self.setFocusPolicy(Qt.StrongFocus)
|
|
43
|
+
|
|
44
|
+
self._zoom = 1.0
|
|
45
|
+
self._zoom_step = 1.15
|
|
46
|
+
self._min_zoom = 0.2
|
|
47
|
+
self._max_zoom = 3.0
|
|
48
|
+
|
|
49
|
+
self._panning = False
|
|
50
|
+
self._last_pan_pos = None
|
|
51
|
+
self._global_grab_mode = False # when True, left button pans regardless of items
|
|
52
|
+
|
|
53
|
+
def drawBackground(self, painter: QPainter, rect: QRectF):
|
|
54
|
+
"""Draw the checker grid in the background."""
|
|
55
|
+
parent_editor = self.parent() # NodeEditor
|
|
56
|
+
color_back = getattr(parent_editor, "_grid_back_color", QColor(35, 35, 38))
|
|
57
|
+
color_pen = getattr(parent_editor, "_grid_pen_color", QColor(55, 55, 60))
|
|
58
|
+
painter.fillRect(rect, color_back)
|
|
59
|
+
pen = QPen(color_pen)
|
|
60
|
+
pen.setWidthF(1.0)
|
|
61
|
+
painter.setPen(pen)
|
|
62
|
+
grid = 20
|
|
63
|
+
left = int(rect.left()) - (int(rect.left()) % grid)
|
|
64
|
+
top = int(rect.top()) - (int(rect.top()) % grid)
|
|
65
|
+
x = left
|
|
66
|
+
while x < rect.right():
|
|
67
|
+
painter.drawLine(x, rect.top(), x, rect.bottom())
|
|
68
|
+
x += grid
|
|
69
|
+
y = top
|
|
70
|
+
while y < rect.bottom():
|
|
71
|
+
painter.drawLine(rect.left(), y, rect.right(), y)
|
|
72
|
+
y += grid
|
|
73
|
+
|
|
74
|
+
def enterEvent(self, e: QEvent):
|
|
75
|
+
"""Ensure cursor reflects grab mode when entering the view."""
|
|
76
|
+
if self._global_grab_mode and not self._panning:
|
|
77
|
+
self.viewport().setCursor(Qt.OpenHandCursor)
|
|
78
|
+
else:
|
|
79
|
+
self.viewport().setCursor(Qt.ArrowCursor)
|
|
80
|
+
super().enterEvent(e)
|
|
81
|
+
|
|
82
|
+
def leaveEvent(self, e: QEvent):
|
|
83
|
+
"""Restore cursor on leave."""
|
|
84
|
+
self.viewport().setCursor(Qt.ArrowCursor)
|
|
85
|
+
super().leaveEvent(e)
|
|
86
|
+
|
|
87
|
+
def keyPressEvent(self, e):
|
|
88
|
+
super().keyPressEvent(e)
|
|
89
|
+
|
|
90
|
+
def keyReleaseEvent(self, e):
|
|
91
|
+
super().keyReleaseEvent(e)
|
|
92
|
+
|
|
93
|
+
def wheelEvent(self, e):
|
|
94
|
+
"""Handle Ctrl + Wheel zoom. Otherwise, default wheel behavior (scroll)."""
|
|
95
|
+
if e.modifiers() & Qt.ControlModifier:
|
|
96
|
+
self._apply_zoom(self._zoom_step if e.angleDelta().y() > 0 else 1.0 / self._zoom_step)
|
|
97
|
+
e.accept()
|
|
98
|
+
return
|
|
99
|
+
super().wheelEvent(e)
|
|
100
|
+
|
|
101
|
+
def _begin_pan(self, e):
|
|
102
|
+
"""Start panning from current mouse event position."""
|
|
103
|
+
self._panning = True
|
|
104
|
+
self._last_pan_pos = e.position()
|
|
105
|
+
# Use 'grab' during drag
|
|
106
|
+
self.viewport().setCursor(Qt.ClosedHandCursor)
|
|
107
|
+
e.accept()
|
|
108
|
+
|
|
109
|
+
def _end_pan(self, e):
|
|
110
|
+
"""Stop panning and restore appropriate cursor."""
|
|
111
|
+
self._panning = False
|
|
112
|
+
if self._global_grab_mode:
|
|
113
|
+
self.viewport().setCursor(Qt.OpenHandCursor)
|
|
114
|
+
else:
|
|
115
|
+
self.viewport().setCursor(Qt.ArrowCursor)
|
|
116
|
+
e.accept()
|
|
117
|
+
|
|
118
|
+
def _clicked_on_empty(self, e) -> bool:
|
|
119
|
+
"""Return True if the click is on empty scene space (no items)."""
|
|
120
|
+
try:
|
|
121
|
+
item = self.itemAt(int(e.position().x()), int(e.position().y()))
|
|
122
|
+
return item is None
|
|
123
|
+
except Exception:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
def mousePressEvent(self, e):
|
|
127
|
+
"""Panning: MMB always; LMB only in global grab mode. Also clear selection on empty click."""
|
|
128
|
+
if e.button() == Qt.MiddleButton:
|
|
129
|
+
self._begin_pan(e)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
if e.button() == Qt.LeftButton:
|
|
133
|
+
if self._global_grab_mode:
|
|
134
|
+
# Global grab enabled -> pan on LMB anywhere
|
|
135
|
+
self._begin_pan(e)
|
|
136
|
+
return
|
|
137
|
+
else:
|
|
138
|
+
# No global grab: clicking empty clears selection
|
|
139
|
+
if self._clicked_on_empty(e) and self.scene():
|
|
140
|
+
self.scene().clearSelection()
|
|
141
|
+
|
|
142
|
+
super().mousePressEvent(e)
|
|
143
|
+
|
|
144
|
+
def mouseMoveEvent(self, e):
|
|
145
|
+
"""While panning, translate scrollbars. Otherwise defer."""
|
|
146
|
+
if self._panning and self._last_pan_pos is not None:
|
|
147
|
+
delta = e.position() - self._last_pan_pos
|
|
148
|
+
self._last_pan_pos = e.position()
|
|
149
|
+
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - int(delta.x()))
|
|
150
|
+
self.verticalScrollBar().setValue(self.verticalScrollBar().value() - int(delta.y()))
|
|
151
|
+
e.accept()
|
|
152
|
+
return
|
|
153
|
+
super().mouseMoveEvent(e)
|
|
154
|
+
|
|
155
|
+
def mouseReleaseEvent(self, e):
|
|
156
|
+
"""Stop panning on Middle or Left Mouse Button release; otherwise defer."""
|
|
157
|
+
if self._panning and e.button() in (Qt.MiddleButton, Qt.LeftButton):
|
|
158
|
+
self._end_pan(e)
|
|
159
|
+
return
|
|
160
|
+
super().mouseReleaseEvent(e)
|
|
161
|
+
|
|
162
|
+
def zoom_in(self):
|
|
163
|
+
"""Programmatically zoom in by a predefined step."""
|
|
164
|
+
self._apply_zoom(self._zoom_step)
|
|
165
|
+
|
|
166
|
+
def zoom_out(self):
|
|
167
|
+
"""Programmatically zoom out by a predefined step."""
|
|
168
|
+
self._apply_zoom(1.0 / self._zoom_step)
|
|
169
|
+
|
|
170
|
+
def _apply_zoom(self, factor: float):
|
|
171
|
+
"""Apply zoom scaling factor within configured bounds."""
|
|
172
|
+
new_zoom = self._zoom * factor
|
|
173
|
+
if not (self._min_zoom <= new_zoom <= self._max_zoom):
|
|
174
|
+
return
|
|
175
|
+
self._zoom = new_zoom
|
|
176
|
+
self.scale(factor, factor)
|
|
177
|
+
|
|
178
|
+
def zoom_value(self) -> float:
|
|
179
|
+
"""Return the current zoom factor."""
|
|
180
|
+
return float(self._zoom)
|
|
181
|
+
|
|
182
|
+
def set_zoom_value(self, zoom: float, keep_center: bool = False):
|
|
183
|
+
"""Set an absolute zoom factor and optionally keep the current viewport center."""
|
|
184
|
+
if zoom is None:
|
|
185
|
+
return
|
|
186
|
+
z = max(self._min_zoom, min(self._max_zoom, float(zoom)))
|
|
187
|
+
center_scene = None
|
|
188
|
+
if keep_center and self.viewport() is not None and self.viewport().rect().isValid():
|
|
189
|
+
center_scene = self.mapToScene(self.viewport().rect().center())
|
|
190
|
+
|
|
191
|
+
self.resetTransform()
|
|
192
|
+
self._zoom = 1.0
|
|
193
|
+
if abs(z - 1.0) > 1e-9:
|
|
194
|
+
self.scale(z, z)
|
|
195
|
+
self._zoom = z
|
|
196
|
+
|
|
197
|
+
if keep_center and center_scene is not None:
|
|
198
|
+
self.centerOn(center_scene)
|
|
199
|
+
|
|
200
|
+
def get_scroll_values(self) -> Tuple[int, int]:
|
|
201
|
+
h = self.horizontalScrollBar().value() if self.horizontalScrollBar() else 0
|
|
202
|
+
v = self.verticalScrollBar().value() if self.verticalScrollBar() else 0
|
|
203
|
+
return int(h), int(v)
|
|
204
|
+
|
|
205
|
+
def set_scroll_values(self, h: int, v: int):
|
|
206
|
+
if self.horizontalScrollBar():
|
|
207
|
+
self.horizontalScrollBar().setValue(int(h))
|
|
208
|
+
if self.verticalScrollBar():
|
|
209
|
+
self.verticalScrollBar().setValue(int(v))
|
|
210
|
+
|
|
211
|
+
def view_state(self) -> dict:
|
|
212
|
+
h, v = self.get_scroll_values()
|
|
213
|
+
return {"zoom": float(self._zoom), "h": h, "v": v}
|
|
214
|
+
|
|
215
|
+
def set_view_state(self, state: dict):
|
|
216
|
+
if not isinstance(state, dict):
|
|
217
|
+
return
|
|
218
|
+
z = state.get("zoom") or state.get("scale")
|
|
219
|
+
if z is not None:
|
|
220
|
+
try:
|
|
221
|
+
self.set_zoom_value(float(z), keep_center=False)
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
h = state.get("h") or state.get("hScroll") or state.get("x")
|
|
225
|
+
v = state.get("v") or state.get("vScroll") or state.get("y")
|
|
226
|
+
try:
|
|
227
|
+
if h is not None:
|
|
228
|
+
self.set_scroll_values(int(h), int(v if v is not None else 0))
|
|
229
|
+
elif v is not None:
|
|
230
|
+
self.set_scroll_values(self.get_scroll_values()[0], int(v))
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
def set_global_grab_mode(self, enabled: bool):
|
|
235
|
+
"""Enable/disable global grab mode (left click pans anywhere)."""
|
|
236
|
+
self._global_grab_mode = bool(enabled)
|
|
237
|
+
if self._global_grab_mode:
|
|
238
|
+
self.viewport().setCursor(Qt.OpenHandCursor)
|
|
239
|
+
else:
|
|
240
|
+
if not self._panning:
|
|
241
|
+
self.viewport().setCursor(Qt.ArrowCursor)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class NodeViewOverlayControls(QWidget):
|
|
245
|
+
"""Small overlay with three buttons (Grab toggle, Zoom Out, Zoom In) anchored top-right."""
|
|
246
|
+
|
|
247
|
+
grabToggled = Signal(bool)
|
|
248
|
+
zoomInClicked = Signal()
|
|
249
|
+
zoomOutClicked = Signal()
|
|
250
|
+
|
|
251
|
+
def __init__(self, parent: Optional[QWidget] = None):
|
|
252
|
+
super().__init__(parent)
|
|
253
|
+
self.setObjectName("NodeViewOverlayControls")
|
|
254
|
+
self.setAttribute(Qt.WA_StyledBackground, True)
|
|
255
|
+
|
|
256
|
+
layout = QHBoxLayout(self)
|
|
257
|
+
# Bigger spacing to visually add padding around buttons
|
|
258
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
259
|
+
layout.setSpacing(8)
|
|
260
|
+
|
|
261
|
+
cfg = self._cfg()
|
|
262
|
+
|
|
263
|
+
# Grab (toggle)
|
|
264
|
+
self.btnGrab = QPushButton(self)
|
|
265
|
+
self.btnGrab.setCheckable(True)
|
|
266
|
+
self.btnGrab.setToolTip(cfg.overlay_grab_tooltip())
|
|
267
|
+
self.btnGrab.setIcon(QIcon(":/icons/drag.svg"))
|
|
268
|
+
self.btnGrab.setIconSize(QSize(20, 20))
|
|
269
|
+
self.btnGrab.setMinimumSize(32, 32)
|
|
270
|
+
|
|
271
|
+
# Zoom Out (placed before Zoom In)
|
|
272
|
+
self.btnZoomOut = QPushButton(self)
|
|
273
|
+
self.btnZoomOut.setToolTip(cfg.overlay_zoom_out_tooltip())
|
|
274
|
+
self.btnZoomOut.setIcon(QIcon(":/icons/zoom_out.svg"))
|
|
275
|
+
self.btnZoomOut.setIconSize(QSize(20, 20))
|
|
276
|
+
self.btnZoomOut.setMinimumSize(32, 32)
|
|
277
|
+
|
|
278
|
+
# Zoom In
|
|
279
|
+
self.btnZoomIn = QPushButton(self)
|
|
280
|
+
self.btnZoomIn.setToolTip(cfg.overlay_zoom_in_tooltip())
|
|
281
|
+
self.btnZoomIn.setIcon(QIcon(":/icons/zoom_in.svg"))
|
|
282
|
+
self.btnZoomIn.setIconSize(QSize(20, 20))
|
|
283
|
+
self.btnZoomIn.setMinimumSize(32, 32)
|
|
284
|
+
|
|
285
|
+
layout.addWidget(self.btnGrab)
|
|
286
|
+
layout.addWidget(self.btnZoomIn)
|
|
287
|
+
layout.addWidget(self.btnZoomOut)
|
|
288
|
+
|
|
289
|
+
self.btnGrab.toggled.connect(self.grabToggled.emit)
|
|
290
|
+
self.btnZoomIn.clicked.connect(self.zoomInClicked.emit)
|
|
291
|
+
self.btnZoomOut.clicked.connect(self.zoomOutClicked.emit)
|
|
292
|
+
|
|
293
|
+
self.show()
|
|
294
|
+
|
|
295
|
+
def _cfg(self) -> EditorConfig:
|
|
296
|
+
p = self.parent()
|
|
297
|
+
c = getattr(p, "config", None) if p is not None else None
|
|
298
|
+
return c if isinstance(c, EditorConfig) else EditorConfig()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class NodeViewStatusLabel(QWidget):
|
|
302
|
+
"""Fixed status overlay pinned to bottom-left that shows node type counts."""
|
|
303
|
+
|
|
304
|
+
def __init__(self, parent: Optional[QWidget] = None):
|
|
305
|
+
super().__init__(parent)
|
|
306
|
+
self.setObjectName("NodeViewStatusLabel")
|
|
307
|
+
self.setAttribute(Qt.WA_StyledBackground, True)
|
|
308
|
+
|
|
309
|
+
self._lbl = QLabel(self)
|
|
310
|
+
cfg = self._cfg()
|
|
311
|
+
self._lbl.setText(cfg.status_no_nodes())
|
|
312
|
+
|
|
313
|
+
layout = QHBoxLayout(self)
|
|
314
|
+
layout.setContentsMargins(8, 4, 8, 4) # some padding around the text
|
|
315
|
+
layout.setSpacing(0)
|
|
316
|
+
layout.addWidget(self._lbl)
|
|
317
|
+
|
|
318
|
+
self.adjustSize()
|
|
319
|
+
self.show()
|
|
320
|
+
|
|
321
|
+
def _cfg(self) -> EditorConfig:
|
|
322
|
+
p = self.parent()
|
|
323
|
+
c = getattr(p, "config", None) if p is not None else None
|
|
324
|
+
return c if isinstance(c, EditorConfig) else EditorConfig()
|
|
325
|
+
|
|
326
|
+
def set_text(self, text: str):
|
|
327
|
+
self._lbl.setText(text)
|
|
328
|
+
# Safe: adjustSize() uses sizeHint(), which no longer calls adjustSize()
|
|
329
|
+
self.adjustSize()
|
|
330
|
+
|
|
331
|
+
def sizeHint(self):
|
|
332
|
+
"""Return hint based on layout/label without calling adjustSize()."""
|
|
333
|
+
if self.layout() is not None:
|
|
334
|
+
return self.layout().sizeHint()
|
|
335
|
+
return self._lbl.sizeHint()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class NodeGraphicsScene(QGraphicsScene):
|
|
339
|
+
"""Graphics scene extended with custom context menu emission."""
|
|
340
|
+
sceneContextRequested = Signal(QPointF)
|
|
341
|
+
|
|
342
|
+
def __init__(self, parent: Optional[QObject] = None):
|
|
343
|
+
"""Initialize the scene and set a very large scene rect."""
|
|
344
|
+
super().__init__(parent)
|
|
345
|
+
self.setSceneRect(-5000, -5000, 10000, 10000)
|
|
346
|
+
|
|
347
|
+
def contextMenuEvent(self, event):
|
|
348
|
+
"""Emit a scene-level context menu request when clicking empty space."""
|
|
349
|
+
transform = self.views()[0].transform() if self.views() else QTransform()
|
|
350
|
+
item = self.itemAt(event.scenePos(), transform)
|
|
351
|
+
if item is None:
|
|
352
|
+
# Respect external edit permission if available on parent editor
|
|
353
|
+
editor = self.parent()
|
|
354
|
+
allowed = True
|
|
355
|
+
try:
|
|
356
|
+
if hasattr(editor, "editing_allowed") and callable(editor.editing_allowed):
|
|
357
|
+
allowed = bool(editor.editing_allowed())
|
|
358
|
+
except Exception:
|
|
359
|
+
allowed = False
|
|
360
|
+
if allowed:
|
|
361
|
+
self.sceneContextRequested.emit(event.scenePos())
|
|
362
|
+
event.accept()
|
|
363
|
+
return
|
|
364
|
+
super().contextMenuEvent(event)
|
|
@@ -311,7 +311,7 @@ class AddButton(QPushButton):
|
|
|
311
311
|
)
|
|
312
312
|
self.setObjectName('tab-add')
|
|
313
313
|
self.setProperty('tabAdd', True)
|
|
314
|
-
self.setToolTip(trans('action.tab.add.chat'))
|
|
314
|
+
self.setToolTip(trans('action.tab.add.chat.tooltip'))
|
|
315
315
|
|
|
316
316
|
def mousePressEvent(self, event):
|
|
317
317
|
"""
|
|
@@ -94,8 +94,8 @@ class ChatInput(QTextEdit):
|
|
|
94
94
|
key="web",
|
|
95
95
|
icon=self.ICON_WEB_OFF,
|
|
96
96
|
alt_icon=self.ICON_WEB_ON,
|
|
97
|
-
tooltip=trans('icon.remote_tool.web'),
|
|
98
|
-
alt_tooltip=trans('icon.remote_tool.web'),
|
|
97
|
+
tooltip=trans('icon.remote_tool.web.disabled'),
|
|
98
|
+
alt_tooltip=trans('icon.remote_tool.web.enabled'),
|
|
99
99
|
callback=self.action_toggle_web,
|
|
100
100
|
visible=True,
|
|
101
101
|
)
|
pygpt_net/utils.py
CHANGED
|
@@ -6,15 +6,19 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.09.25 12:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
13
|
import os
|
|
14
14
|
import re
|
|
15
|
+
import math
|
|
16
|
+
|
|
15
17
|
from datetime import datetime
|
|
16
18
|
from contextlib import contextmanager
|
|
17
19
|
from typing import Any
|
|
20
|
+
from decimal import Decimal, ROUND_HALF_UP, InvalidOperation
|
|
21
|
+
from typing import Sequence
|
|
18
22
|
|
|
19
23
|
from PySide6 import QtCore, QtGui
|
|
20
24
|
from PySide6.QtWidgets import QApplication
|
|
@@ -381,4 +385,112 @@ def mem_clean(force: bool = False) -> bool:
|
|
|
381
385
|
'''
|
|
382
386
|
except Exception as e:
|
|
383
387
|
print(e)
|
|
384
|
-
return ok
|
|
388
|
+
return ok
|
|
389
|
+
|
|
390
|
+
def short_num(value,
|
|
391
|
+
*,
|
|
392
|
+
base: int = 1000,
|
|
393
|
+
suffixes: Sequence[str] = ("", "k", "M", "B", "T", "P", "E"),
|
|
394
|
+
max_decimals: int = 1,
|
|
395
|
+
decimal_sep: str = ",") -> str:
|
|
396
|
+
"""
|
|
397
|
+
Compact human-readable formatter for numbers with suffixes (k, M, B, ...).
|
|
398
|
+
|
|
399
|
+
Rules:
|
|
400
|
+
- abs(value) < base -> return the value without a suffix
|
|
401
|
+
- otherwise -> divide by base^n and append suffix
|
|
402
|
+
- decimals:
|
|
403
|
+
< 10 -> up to 2 (bounded by max_decimals)
|
|
404
|
+
< 100 -> up to 1 (bounded by max_decimals)
|
|
405
|
+
>= 100 -> 0
|
|
406
|
+
- rounding: ROUND_HALF_UP
|
|
407
|
+
- auto "carry" to the next suffix after rounding (e.g., 999.95k -> 1M)
|
|
408
|
+
|
|
409
|
+
Params:
|
|
410
|
+
- base: 1000 for general numbers, 1024 for bytes, etc.
|
|
411
|
+
- suffixes: first item must be "" (for < base)
|
|
412
|
+
- max_decimals: upper bound for fractional digits
|
|
413
|
+
- decimal_sep: decimal separator in the output
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
# --- helpers hidden inside (closure) ---
|
|
417
|
+
|
|
418
|
+
def _to_decimal(v) -> Decimal:
|
|
419
|
+
"""Convert supported inputs to Decimal; keep NaN/Inf as-is."""
|
|
420
|
+
if isinstance(v, Decimal):
|
|
421
|
+
return v
|
|
422
|
+
if isinstance(v, int):
|
|
423
|
+
return Decimal(v)
|
|
424
|
+
if isinstance(v, float):
|
|
425
|
+
if math.isnan(v):
|
|
426
|
+
return Decimal("NaN")
|
|
427
|
+
if math.isinf(v):
|
|
428
|
+
return Decimal("Infinity") if v > 0 else Decimal("-Infinity")
|
|
429
|
+
# str() to avoid binary float artefacts
|
|
430
|
+
return Decimal(str(v))
|
|
431
|
+
try:
|
|
432
|
+
return Decimal(str(v))
|
|
433
|
+
except (InvalidOperation, ValueError, TypeError):
|
|
434
|
+
raise TypeError("short_num(value): value must be numeric (int/float/Decimal) "
|
|
435
|
+
"or a string parsable to a number.")
|
|
436
|
+
|
|
437
|
+
def _decimals_for(scaled: Decimal) -> int:
|
|
438
|
+
"""Pick number of decimals based on magnitude."""
|
|
439
|
+
if max_decimals <= 0:
|
|
440
|
+
return 0
|
|
441
|
+
if scaled < 10:
|
|
442
|
+
return min(2, max_decimals)
|
|
443
|
+
if scaled < 100:
|
|
444
|
+
return min(1, max_decimals)
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
def _round_dec(d: Decimal, decimals: int) -> Decimal:
|
|
448
|
+
"""ROUND_HALF_UP to the requested decimals."""
|
|
449
|
+
if decimals <= 0:
|
|
450
|
+
return d.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
|
451
|
+
q = Decimal(1).scaleb(-decimals) # 10**(-decimals)
|
|
452
|
+
return d.quantize(q, rounding=ROUND_HALF_UP)
|
|
453
|
+
|
|
454
|
+
def _strip_trailing_zeros(s: str) -> str:
|
|
455
|
+
"""Remove trailing zeros and trailing decimal point if needed."""
|
|
456
|
+
if "." in s:
|
|
457
|
+
s = s.rstrip("0").rstrip(".")
|
|
458
|
+
return s
|
|
459
|
+
|
|
460
|
+
# --- main logic ---
|
|
461
|
+
|
|
462
|
+
d = _to_decimal(value)
|
|
463
|
+
if not d.is_finite():
|
|
464
|
+
# For NaN/Inf just echo back Python-ish text
|
|
465
|
+
return str(value)
|
|
466
|
+
|
|
467
|
+
sign = "-" if d < 0 else ""
|
|
468
|
+
d = abs(d)
|
|
469
|
+
|
|
470
|
+
# For values below base, return "as is" (normalized, no suffix)
|
|
471
|
+
if d < base:
|
|
472
|
+
s = _strip_trailing_zeros(f"{d.normalize():f}")
|
|
473
|
+
return (sign + s).replace(".", decimal_sep)
|
|
474
|
+
|
|
475
|
+
# Find initial suffix tier
|
|
476
|
+
idx = 0
|
|
477
|
+
last_idx = len(suffixes) - 1
|
|
478
|
+
while d >= base and idx < last_idx:
|
|
479
|
+
d = d / base
|
|
480
|
+
idx += 1
|
|
481
|
+
|
|
482
|
+
# Choose decimals, round, then handle possible carry to next suffix
|
|
483
|
+
decimals = _decimals_for(d)
|
|
484
|
+
d = _round_dec(d, decimals)
|
|
485
|
+
|
|
486
|
+
while d >= base and idx < last_idx:
|
|
487
|
+
# Carry over (e.g., 999.95k -> 1000.00k -> 1.00M)
|
|
488
|
+
d = d / base
|
|
489
|
+
idx += 1
|
|
490
|
+
decimals = _decimals_for(d)
|
|
491
|
+
d = _round_dec(d, decimals)
|
|
492
|
+
|
|
493
|
+
# Format, trim zeros, apply custom decimal separator, attach suffix
|
|
494
|
+
out = f"{d:.{decimals}f}"
|
|
495
|
+
out = _strip_trailing_zeros(out).replace(".", decimal_sep)
|
|
496
|
+
return f"{sign}{out}{suffixes[idx]}"
|