je-editor 0.0.215__py3-none-any.whl → 0.0.217__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.
- je_editor/code_scan/watchdog_thread.py +1 -1
- je_editor/git_client/__init__.py +0 -0
- je_editor/git_client/commit_graph.py +88 -0
- je_editor/git_client/git.py +181 -0
- je_editor/git_client/git_cli.py +66 -0
- je_editor/git_client/github.py +50 -0
- je_editor/pyside_ui/browser/browser_view.py +0 -3
- je_editor/pyside_ui/git_ui/__init__.py +0 -0
- je_editor/pyside_ui/git_ui/commit_table.py +27 -0
- je_editor/pyside_ui/git_ui/git_branch_tree_widget.py +119 -0
- je_editor/pyside_ui/git_ui/git_client_gui.py +291 -0
- je_editor/pyside_ui/git_ui/graph_view.py +174 -0
- je_editor/pyside_ui/main_ui/ai_widget/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/ai_widget/ai_config.py +19 -0
- je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py +17 -0
- je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py +130 -0
- je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py +45 -0
- je_editor/pyside_ui/main_ui/main_editor.py +1 -1
- je_editor/pyside_ui/main_ui/menu/check_style_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +81 -0
- je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py +2 -2
- je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py +2 -2
- je_editor/utils/multi_language/english.py +1 -1
- je_editor/utils/multi_language/traditional_chinese.py +1 -1
- {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/METADATA +2 -1
- {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/RECORD +29 -12
- {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/WHEEL +0 -0
- {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/licenses/LICENSE +0 -0
- {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,291 @@
|
|
1
|
+
import traceback
|
2
|
+
|
3
|
+
from PySide6.QtGui import QTextOption
|
4
|
+
from PySide6.QtWidgets import QWidget, QLabel, QPushButton, QComboBox, QHBoxLayout, QListWidget, QPlainTextEdit, \
|
5
|
+
QSizePolicy, QSplitter, QLineEdit, QVBoxLayout, QMessageBox, QFileDialog, QInputDialog
|
6
|
+
|
7
|
+
from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
|
8
|
+
from je_editor.git_client.git import Worker, GitService
|
9
|
+
from je_editor.git_client.github import GitCloneHandler
|
10
|
+
|
11
|
+
|
12
|
+
class Gitgui(QWidget):
|
13
|
+
|
14
|
+
def __init__(self):
|
15
|
+
super().__init__()
|
16
|
+
self.git = GitService()
|
17
|
+
self.clone_handler = GitCloneHandler()
|
18
|
+
self.language_wrapper_get = language_wrapper.language_word_dict.get
|
19
|
+
self._init_ui()
|
20
|
+
|
21
|
+
def _init_ui(self):
|
22
|
+
# Top controls
|
23
|
+
self.repo_label = QLabel(self.language_wrapper_get("label_repo_initial"))
|
24
|
+
self.btn_open = QPushButton(self.language_wrapper_get("btn_open_repo"))
|
25
|
+
self.branch_combo = QComboBox()
|
26
|
+
self.btn_checkout = QPushButton(self.language_wrapper_get("btn_switch_branch"))
|
27
|
+
self.btn_pull = QPushButton(self.language_wrapper_get("btn_pull"))
|
28
|
+
self.btn_push = QPushButton(self.language_wrapper_get("btn_push"))
|
29
|
+
self.remote_combo = QComboBox()
|
30
|
+
self.clone_button = QPushButton(self.language_wrapper_get("btn_clone_remote"))
|
31
|
+
self.clone_button.clicked.connect(self._on_clone_remote_repo)
|
32
|
+
|
33
|
+
top = QHBoxLayout()
|
34
|
+
top.addWidget(self.repo_label, 2)
|
35
|
+
top.addWidget(self.btn_open)
|
36
|
+
top.addWidget(QLabel(self.language_wrapper_get("label_remote")))
|
37
|
+
top.addWidget(self.remote_combo)
|
38
|
+
top.addWidget(QLabel(self.language_wrapper_get("label_branch")))
|
39
|
+
top.addWidget(self.branch_combo, 1)
|
40
|
+
top.addWidget(self.btn_checkout)
|
41
|
+
top.addWidget(self.btn_pull)
|
42
|
+
top.addWidget(self.btn_push)
|
43
|
+
top.addWidget(self.clone_button)
|
44
|
+
|
45
|
+
# Left commits list
|
46
|
+
self.commit_list = QListWidget()
|
47
|
+
self.commit_list.setMinimumWidth(380)
|
48
|
+
|
49
|
+
# Right diff viewer
|
50
|
+
self.diff_view = QPlainTextEdit()
|
51
|
+
self.diff_view.setReadOnly(True)
|
52
|
+
self.diff_view.setWordWrapMode(QTextOption.WrapMode.NoWrap)
|
53
|
+
self.diff_view.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
54
|
+
self.diff_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
55
|
+
mono = self.font()
|
56
|
+
mono.setFamily("Consolas")
|
57
|
+
mono.setPointSize(10)
|
58
|
+
self.diff_view.setFont(mono)
|
59
|
+
|
60
|
+
splitter = QSplitter()
|
61
|
+
splitter.addWidget(self.commit_list)
|
62
|
+
splitter.addWidget(self.diff_view)
|
63
|
+
splitter.setStretchFactor(0, 0)
|
64
|
+
splitter.setStretchFactor(1, 1)
|
65
|
+
|
66
|
+
# Bottom commit box
|
67
|
+
self.msg_edit = QLineEdit()
|
68
|
+
self.msg_edit.setPlaceholderText(self.language_wrapper_get("placeholder_commit_message"))
|
69
|
+
self.btn_stage_all = QPushButton(self.language_wrapper_get("btn_stage_all"))
|
70
|
+
self.btn_commit = QPushButton(self.language_wrapper_get("btn_commit"))
|
71
|
+
|
72
|
+
bottom = QHBoxLayout()
|
73
|
+
bottom.addWidget(QLabel(self.language_wrapper_get("label_message")))
|
74
|
+
bottom.addWidget(self.msg_edit, 1)
|
75
|
+
bottom.addWidget(self.btn_stage_all)
|
76
|
+
bottom.addWidget(self.btn_commit)
|
77
|
+
|
78
|
+
# Layout
|
79
|
+
center_layout = QVBoxLayout()
|
80
|
+
center_layout.addLayout(top)
|
81
|
+
center_layout.addWidget(splitter, 1)
|
82
|
+
center_layout.addLayout(bottom)
|
83
|
+
self.setLayout(center_layout)
|
84
|
+
|
85
|
+
# Events
|
86
|
+
self.btn_open.clicked.connect(self.on_open_repo)
|
87
|
+
self.branch_combo.currentTextChanged.connect(self.on_branch_changed)
|
88
|
+
self.btn_checkout.clicked.connect(self.on_checkout)
|
89
|
+
self.commit_list.itemSelectionChanged.connect(self.on_commit_selected)
|
90
|
+
self.btn_stage_all.clicked.connect(self.on_stage_all)
|
91
|
+
self.btn_commit.clicked.connect(self.on_commit)
|
92
|
+
self.btn_pull.clicked.connect(self.on_pull)
|
93
|
+
self.btn_push.clicked.connect(self.on_push)
|
94
|
+
|
95
|
+
self._update_controls(enabled=False)
|
96
|
+
|
97
|
+
# ------------- UI helpers -------------
|
98
|
+
def _update_controls(self, enabled: bool):
|
99
|
+
for w in [
|
100
|
+
self.branch_combo, self.btn_checkout, self.commit_list,
|
101
|
+
self.btn_stage_all, self.btn_commit, self.btn_pull, self.btn_push, self.remote_combo
|
102
|
+
]:
|
103
|
+
w.setEnabled(enabled)
|
104
|
+
|
105
|
+
def _error(self, title: str, err: Exception):
|
106
|
+
traceback.print_exc()
|
107
|
+
QMessageBox.critical(self, title, f"{title}\n\n{err}")
|
108
|
+
|
109
|
+
def _info(self, title: str, msg: str):
|
110
|
+
QMessageBox.information(self, title, msg)
|
111
|
+
|
112
|
+
# ------------- Event handlers -------------
|
113
|
+
def on_open_repo(self):
|
114
|
+
path = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("dialog_choose_repo"))
|
115
|
+
if not path:
|
116
|
+
return
|
117
|
+
try:
|
118
|
+
self.git.open_repo(path)
|
119
|
+
self.repo_label.setText(f"Repo: {path}")
|
120
|
+
self._refresh_branches()
|
121
|
+
self._refresh_remotes()
|
122
|
+
self._update_controls(enabled=True)
|
123
|
+
self._load_commits_async()
|
124
|
+
except Exception as e:
|
125
|
+
self._error(self.language_wrapper_get("err_open_repo"), e)
|
126
|
+
|
127
|
+
def _refresh_remotes(self):
|
128
|
+
self.remote_combo.clear()
|
129
|
+
try:
|
130
|
+
remotes = self.git.remotes()
|
131
|
+
self.remote_combo.addItems(remotes if remotes else [self.language_wrapper_get("default_remote")])
|
132
|
+
except Exception:
|
133
|
+
self.remote_combo.addItem(self.language_wrapper_get("default_remote"))
|
134
|
+
|
135
|
+
def _refresh_branches(self):
|
136
|
+
self.branch_combo.blockSignals(True)
|
137
|
+
self.branch_combo.clear()
|
138
|
+
try:
|
139
|
+
branches = self.git.list_branches()
|
140
|
+
self.branch_combo.addItems(branches)
|
141
|
+
cur = self.git.current_branch()
|
142
|
+
idx = self.branch_combo.findText(cur)
|
143
|
+
if idx >= 0:
|
144
|
+
self.branch_combo.setCurrentIndex(idx)
|
145
|
+
except Exception as e:
|
146
|
+
self._error(self.language_wrapper_get("err_load_branches"), e)
|
147
|
+
finally:
|
148
|
+
self.branch_combo.blockSignals(False)
|
149
|
+
|
150
|
+
def on_branch_changed(self, branch: str):
|
151
|
+
if not branch:
|
152
|
+
return
|
153
|
+
self._load_commits_async(branch)
|
154
|
+
|
155
|
+
def _load_commits_async(self, branch: str | None = None):
|
156
|
+
if branch is None:
|
157
|
+
try:
|
158
|
+
branch = self.git.current_branch()
|
159
|
+
except Exception:
|
160
|
+
return
|
161
|
+
self.commit_list.clear()
|
162
|
+
self.diff_view.setPlainText("")
|
163
|
+
self.worker = Worker(self.git.list_commits, branch, 200)
|
164
|
+
self.worker.done.connect(self._on_commits_loaded)
|
165
|
+
self.worker.start()
|
166
|
+
|
167
|
+
def _on_commits_loaded(self, result, error):
|
168
|
+
if error:
|
169
|
+
self._error(self.language_wrapper_get("err_load_commits"), error)
|
170
|
+
return
|
171
|
+
for c in result:
|
172
|
+
self.commit_list.addItem(f"{c['hexsha'][:8]} {c['date']} {c['author']} {c['summary']}")
|
173
|
+
self.commit_list.setProperty("commit_data", result)
|
174
|
+
|
175
|
+
def on_checkout(self):
|
176
|
+
branch = self.branch_combo.currentText()
|
177
|
+
if not branch:
|
178
|
+
return
|
179
|
+
|
180
|
+
def after(res, err):
|
181
|
+
if err:
|
182
|
+
self._error(self.language_wrapper_get("err_checkout"), err)
|
183
|
+
else:
|
184
|
+
self._refresh_branches()
|
185
|
+
self._load_commits_async(branch)
|
186
|
+
self._info(
|
187
|
+
self.language_wrapper_get("info_checkout_title"),
|
188
|
+
f"{self.language_wrapper_get("info_checkout_msg")} {branch}"
|
189
|
+
)
|
190
|
+
self.worker = Worker(self.git.checkout, branch)
|
191
|
+
self.worker.done.connect(after)
|
192
|
+
self.worker.start()
|
193
|
+
|
194
|
+
def on_commit_selected(self):
|
195
|
+
items = self.commit_list.selectedIndexes()
|
196
|
+
if not items:
|
197
|
+
return
|
198
|
+
idx = items[0].row()
|
199
|
+
data = self.commit_list.property("commit_data") or []
|
200
|
+
if idx >= len(data):
|
201
|
+
return
|
202
|
+
sha = data[idx]["hexsha"]
|
203
|
+
|
204
|
+
def after(res, err):
|
205
|
+
if err:
|
206
|
+
self._error(self.language_wrapper_get("err_read_diff"), err)
|
207
|
+
else:
|
208
|
+
self.diff_view.setPlainText(res)
|
209
|
+
|
210
|
+
self.worker = Worker(self.git.show_diff_of_commit, sha)
|
211
|
+
self.worker.done.connect(after)
|
212
|
+
self.worker.start()
|
213
|
+
|
214
|
+
def on_stage_all(self):
|
215
|
+
def after(res, err):
|
216
|
+
if err:
|
217
|
+
self._error(self.language_wrapper_get("err_stage"), err)
|
218
|
+
else:
|
219
|
+
self._info(self.language_wrapper_get("info_stage_title"), self.language_wrapper_get("info_stage_msg"))
|
220
|
+
|
221
|
+
self.worker = Worker(self.git.stage_all)
|
222
|
+
self.worker.done.connect(after)
|
223
|
+
self.worker.start()
|
224
|
+
|
225
|
+
def on_commit(self):
|
226
|
+
msg = self.msg_edit.text()
|
227
|
+
|
228
|
+
def after(res, err):
|
229
|
+
if err:
|
230
|
+
self._error(self.language_wrapper_get("err_commit"), err)
|
231
|
+
else:
|
232
|
+
self.msg_edit.clear()
|
233
|
+
self._load_commits_async()
|
234
|
+
self._info(self.language_wrapper_get("info_commit_title"), self.language_wrapper_get("info_commit_msg"))
|
235
|
+
|
236
|
+
self.worker = Worker(self.git.commit, msg)
|
237
|
+
self.worker.done.connect(after)
|
238
|
+
self.worker.start()
|
239
|
+
|
240
|
+
def on_pull(self):
|
241
|
+
remote = self.remote_combo.currentText() or self.language_wrapper_get("default_remote")
|
242
|
+
branch = self.branch_combo.currentText()
|
243
|
+
|
244
|
+
def after(res, err):
|
245
|
+
if err:
|
246
|
+
self._error(self.language_wrapper_get("err_pull"), err)
|
247
|
+
else:
|
248
|
+
self._load_commits_async()
|
249
|
+
self._info(self.language_wrapper_get("info_pull_title"), str(res))
|
250
|
+
|
251
|
+
self.worker = Worker(self.git.pull, remote, branch)
|
252
|
+
self.worker.done.connect(after)
|
253
|
+
self.worker.start()
|
254
|
+
|
255
|
+
def on_push(self):
|
256
|
+
remote = self.remote_combo.currentText() or self.language_wrapper_get("default_remote")
|
257
|
+
branch = self.branch_combo.currentText()
|
258
|
+
|
259
|
+
def after(res, err):
|
260
|
+
if err:
|
261
|
+
self._error(self.language_wrapper_get("err_push"), err)
|
262
|
+
else:
|
263
|
+
self._info(self.language_wrapper_get("info_push_title"), str(res))
|
264
|
+
|
265
|
+
self.worker = Worker(self.git.push, remote, branch)
|
266
|
+
self.worker.done.connect(after)
|
267
|
+
self.worker.start()
|
268
|
+
|
269
|
+
def _on_clone_remote_repo(self):
|
270
|
+
"""
|
271
|
+
UI handler for cloning a remote repository.
|
272
|
+
"""
|
273
|
+
url, ok = QInputDialog.getText(self,
|
274
|
+
self.language_wrapper_get("dialog_clone_title"),
|
275
|
+
self.language_wrapper_get("dialog_clone_prompt"))
|
276
|
+
if not ok or not url.strip():
|
277
|
+
return
|
278
|
+
|
279
|
+
local_dir = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("dialog_select_folder"))
|
280
|
+
if not local_dir:
|
281
|
+
return
|
282
|
+
|
283
|
+
try:
|
284
|
+
repo_path = self.clone_handler.clone_repo(url.strip(), local_dir)
|
285
|
+
self.git.open_repo(repo_path)
|
286
|
+
QMessageBox.information(self,
|
287
|
+
self.language_wrapper_get("info_clone_success_title"),
|
288
|
+
f"{self.language_wrapper_get("info_clone_success_msg")} {repo_path}")
|
289
|
+
except Exception as e:
|
290
|
+
QMessageBox.critical(self, self.language_wrapper_get("err_clone_failed_title"), str(e))
|
291
|
+
|
@@ -0,0 +1,174 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from PySide6.QtCore import Qt, QRectF, QPointF
|
6
|
+
from PySide6.QtGui import QPainter, QPainterPath, QPen, QColor, QBrush, QTransform
|
7
|
+
from PySide6.QtWidgets import (
|
8
|
+
QGraphicsView,
|
9
|
+
QGraphicsScene,
|
10
|
+
QGraphicsEllipseItem,
|
11
|
+
QGraphicsPathItem, QGraphicsSimpleTextItem,
|
12
|
+
)
|
13
|
+
|
14
|
+
from je_editor.git_client.commit_graph import CommitGraph
|
15
|
+
from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
|
16
|
+
|
17
|
+
# Layout constants
|
18
|
+
NODE_RADIUS = 6
|
19
|
+
ROW_HEIGHT = 28
|
20
|
+
LANE_WIDTH = 22
|
21
|
+
LEFT_MARGIN = 220 # space for SHA/message text area on the left
|
22
|
+
|
23
|
+
# Colors
|
24
|
+
EDGE_COLOR = QColor("#888")
|
25
|
+
TEXT_COLOR = QColor("#222")
|
26
|
+
BG_COLOR = QColor("#ffffff")
|
27
|
+
|
28
|
+
# A small, repeatable lane palette
|
29
|
+
LANE_COLORS = [
|
30
|
+
QColor("#4C78A8"),
|
31
|
+
QColor("#F58518"),
|
32
|
+
QColor("#E45756"),
|
33
|
+
QColor("#72B7B2"),
|
34
|
+
QColor("#54A24B"),
|
35
|
+
QColor("#EECA3B"),
|
36
|
+
QColor("#B279A2"),
|
37
|
+
QColor("#FF9DA6"),
|
38
|
+
QColor("#9D755D"),
|
39
|
+
QColor("#BAB0AC"),
|
40
|
+
]
|
41
|
+
|
42
|
+
|
43
|
+
def lane_color(lane: int) -> QColor:
|
44
|
+
if lane < 0:
|
45
|
+
return QColor("#999")
|
46
|
+
return LANE_COLORS[lane % len(LANE_COLORS)]
|
47
|
+
|
48
|
+
|
49
|
+
class CommitGraphView(QGraphicsView):
|
50
|
+
def __init__(self, parent=None):
|
51
|
+
super().__init__(parent)
|
52
|
+
self.language_wrapper_get = language_wrapper.language_word_dict.get
|
53
|
+
self.setBackgroundBrush(QBrush(BG_COLOR))
|
54
|
+
self.setRenderHints(
|
55
|
+
self.renderHints()
|
56
|
+
| QPainter.RenderHint.Antialiasing
|
57
|
+
| QPainter.RenderHint.TextAntialiasing
|
58
|
+
)
|
59
|
+
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
60
|
+
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
61
|
+
|
62
|
+
self._scene = QGraphicsScene(self)
|
63
|
+
self.setScene(self._scene)
|
64
|
+
|
65
|
+
self.graph: Optional[CommitGraph] = None
|
66
|
+
self._padding = 40
|
67
|
+
self._zoom = 1.0
|
68
|
+
|
69
|
+
def set_graph(self, graph: CommitGraph):
|
70
|
+
self.graph = graph
|
71
|
+
self._redraw()
|
72
|
+
|
73
|
+
def _lane_x(self, lane: int) -> float:
|
74
|
+
return lane * LANE_WIDTH + NODE_RADIUS * 2
|
75
|
+
|
76
|
+
def _row_y(self, row: int) -> float:
|
77
|
+
return row * ROW_HEIGHT + NODE_RADIUS * 2
|
78
|
+
|
79
|
+
def _redraw(self):
|
80
|
+
self._scene.clear()
|
81
|
+
if not self.graph or not self.graph.nodes:
|
82
|
+
self._scene.setSceneRect(QRectF(0, 0, 800, 400))
|
83
|
+
return
|
84
|
+
|
85
|
+
edge_pen = QPen(EDGE_COLOR, 2)
|
86
|
+
for row, node in enumerate(self.graph.nodes):
|
87
|
+
if not node.parent_shas:
|
88
|
+
continue
|
89
|
+
x0 = self._lane_x(node.lane_index)
|
90
|
+
y0 = self._row_y(row)
|
91
|
+
for p in node.parent_shas:
|
92
|
+
if p not in self.graph.index:
|
93
|
+
continue
|
94
|
+
prow = self.graph.index[p]
|
95
|
+
parent_node = self.graph.nodes[prow]
|
96
|
+
x1 = self._lane_x(parent_node.lane_index)
|
97
|
+
y1 = self._row_y(prow)
|
98
|
+
path = QPainterPath(QPointF(x0, y0))
|
99
|
+
ctrl_y = (y0 + y1) / 2.0
|
100
|
+
path.cubicTo(QPointF(x0, ctrl_y), QPointF(x1, ctrl_y), QPointF(x1, y1))
|
101
|
+
edge_item = QGraphicsPathItem(path)
|
102
|
+
edge_item.setPen(edge_pen)
|
103
|
+
self._scene.addItem(edge_item)
|
104
|
+
|
105
|
+
for row, node in enumerate(self.graph.nodes):
|
106
|
+
cx = self._lane_x(node.lane_index)
|
107
|
+
cy = self._row_y(row)
|
108
|
+
|
109
|
+
circle = QGraphicsEllipseItem(
|
110
|
+
QRectF(cx - NODE_RADIUS, cy - NODE_RADIUS, NODE_RADIUS * 2, NODE_RADIUS * 2)
|
111
|
+
)
|
112
|
+
circle.setBrush(QBrush(lane_color(node.lane_index)))
|
113
|
+
circle.setPen(QPen(Qt.PenStyle.NoPen))
|
114
|
+
circle.setToolTip(self.language_wrapper_get(
|
115
|
+
"git_graph_tooltip_commit"
|
116
|
+
).format(short=node.commit_sha[:7],
|
117
|
+
author=node.author_name,
|
118
|
+
date=node.commit_date,
|
119
|
+
msg=node.commit_message))
|
120
|
+
self._scene.addItem(circle)
|
121
|
+
|
122
|
+
label_item = QGraphicsSimpleTextItem(str(row + 1))
|
123
|
+
label_item.setBrush(QBrush(TEXT_COLOR))
|
124
|
+
label_item.setPos(-30, cy - NODE_RADIUS * 2)
|
125
|
+
self._scene.addItem(label_item)
|
126
|
+
|
127
|
+
self._scene.setSceneRect(
|
128
|
+
QRectF(-40, 0,
|
129
|
+
self._lane_x(max(n.lane_index for n in self.graph.nodes) + 1) + self._padding,
|
130
|
+
self._row_y(len(self.graph.nodes)) + self._padding)
|
131
|
+
)
|
132
|
+
|
133
|
+
def _apply_initial_view(self):
|
134
|
+
# Start with a mild zoom to fit rows comfortably
|
135
|
+
self._zoom = 0.9
|
136
|
+
self._apply_zoom_transform()
|
137
|
+
|
138
|
+
# Zoom and interaction
|
139
|
+
def wheelEvent(self, event):
|
140
|
+
if not self._scene.items():
|
141
|
+
return
|
142
|
+
|
143
|
+
# Ctrl+Wheel to zoom, else scroll normally
|
144
|
+
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
145
|
+
angle = event.angleDelta().y()
|
146
|
+
factor = 1.0 + (0.1 if angle > 0 else -0.1)
|
147
|
+
self._zoom = max(0.1, min(3.0, self._zoom * factor))
|
148
|
+
self._apply_zoom_transform()
|
149
|
+
event.accept()
|
150
|
+
else:
|
151
|
+
super().wheelEvent(event)
|
152
|
+
|
153
|
+
def _apply_zoom_transform(self):
|
154
|
+
t = QTransform()
|
155
|
+
t.scale(self._zoom, self._zoom)
|
156
|
+
self.setTransform(t)
|
157
|
+
|
158
|
+
def resizeEvent(self, event):
|
159
|
+
super().resizeEvent(event)
|
160
|
+
# Keep scene rect; do not auto-fit. Users control zoom.
|
161
|
+
# Nothing else needed here.
|
162
|
+
|
163
|
+
# Optional helpers for external controllers
|
164
|
+
def focus_row(self, row: int):
|
165
|
+
"""
|
166
|
+
Center the view around a specific row.
|
167
|
+
"""
|
168
|
+
if not self.graph or row < 0 or row >= len(self.graph.nodes):
|
169
|
+
return
|
170
|
+
y = self._row_y(row)
|
171
|
+
rect = QRectF(0, y - ROW_HEIGHT * 2, self._scene.width(), ROW_HEIGHT * 4)
|
172
|
+
self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
|
173
|
+
# Restore user zoom preference after focusing
|
174
|
+
self._apply_zoom_transform()
|
File without changes
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from queue import Queue
|
2
|
+
|
3
|
+
|
4
|
+
class AIConfig(object):
|
5
|
+
|
6
|
+
def __init__(self):
|
7
|
+
self.current_ai_model_system_prompt: str = ""
|
8
|
+
self.choosable_ai: dict[str, dict[str, str]] = {
|
9
|
+
"AI_model": {
|
10
|
+
"ai_base_url": "",
|
11
|
+
"ai_api_key": "",
|
12
|
+
"chat_model": "",
|
13
|
+
"prompt_template": "",
|
14
|
+
}
|
15
|
+
}
|
16
|
+
self.message_queue = Queue()
|
17
|
+
|
18
|
+
|
19
|
+
ai_config = AIConfig()
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from threading import Thread
|
2
|
+
|
3
|
+
from je_editor.pyside_ui.main_ui.ai_widget.ai_config import ai_config
|
4
|
+
from je_editor.pyside_ui.main_ui.ai_widget.langchain_interface import LangChainInterface
|
5
|
+
|
6
|
+
|
7
|
+
class AskThread(Thread):
|
8
|
+
|
9
|
+
def __init__(self, lang_chain_interface: LangChainInterface, prompt):
|
10
|
+
super().__init__()
|
11
|
+
self.lang_chain_interface = lang_chain_interface
|
12
|
+
self.prompt = prompt
|
13
|
+
|
14
|
+
|
15
|
+
def run(self):
|
16
|
+
ai_response = self.lang_chain_interface.call_ai_model(prompt=self.prompt)
|
17
|
+
ai_config.message_queue.put(ai_response)
|
@@ -0,0 +1,130 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import TYPE_CHECKING, Union
|
5
|
+
|
6
|
+
from PySide6.QtCore import Qt, QTimer
|
7
|
+
from PySide6.QtGui import QFontDatabase
|
8
|
+
from PySide6.QtWidgets import QWidget, QPlainTextEdit, QScrollArea, QLabel, QComboBox, QGridLayout, QPushButton, \
|
9
|
+
QMessageBox, QSizePolicy, QLineEdit
|
10
|
+
|
11
|
+
from je_editor.pyside_ui.dialog.ai_dialog.set_ai_dialog import SetAIDialog
|
12
|
+
from je_editor.pyside_ui.main_ui.ai_widget.ai_config import AIConfig, ai_config
|
13
|
+
from je_editor.pyside_ui.main_ui.ai_widget.ask_thread import AskThread
|
14
|
+
from je_editor.pyside_ui.main_ui.ai_widget.langchain_interface import LangChainInterface
|
15
|
+
from je_editor.utils.json.json_file import read_json
|
16
|
+
from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
|
17
|
+
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
from je_editor.pyside_ui.main_ui.main_editor import EditorMain
|
20
|
+
|
21
|
+
|
22
|
+
class ChatUI(QWidget):
|
23
|
+
|
24
|
+
def __init__(self, main_window: EditorMain):
|
25
|
+
super().__init__()
|
26
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
27
|
+
self.main_window = main_window
|
28
|
+
# Chat panel
|
29
|
+
self.chat_panel = QPlainTextEdit()
|
30
|
+
self.chat_panel.setLineWrapMode(self.chat_panel.LineWrapMode.NoWrap)
|
31
|
+
self.chat_panel.setReadOnly(True)
|
32
|
+
self.chat_panel_scroll_area = QScrollArea()
|
33
|
+
self.chat_panel_scroll_area.setWidgetResizable(True)
|
34
|
+
self.chat_panel_scroll_area.setViewportMargins(0, 0, 0, 0)
|
35
|
+
self.chat_panel_scroll_area.setWidget(self.chat_panel)
|
36
|
+
self.chat_panel.setFont(QFontDatabase.font(self.font().family(), "", 16))
|
37
|
+
# Prompt input
|
38
|
+
self.prompt_input = QLineEdit()
|
39
|
+
self.prompt_input.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
40
|
+
self.prompt_input.returnPressed.connect(self.call_ai_model)
|
41
|
+
# Font size combobox
|
42
|
+
self.font_size_label = QLabel(language_wrapper.language_word_dict.get("font_size"))
|
43
|
+
self.font_size_combobox = QComboBox()
|
44
|
+
for font_size in range(2, 101, 2):
|
45
|
+
self.font_size_combobox.addItem(str(font_size))
|
46
|
+
self.font_size_combobox.setCurrentText("16")
|
47
|
+
self.font_size_combobox.currentTextChanged.connect(self.update_panel_text_size)
|
48
|
+
# Buttons
|
49
|
+
self.set_ai_config_button = QPushButton(language_wrapper.language_word_dict.get("chat_ui_set_ai_button"))
|
50
|
+
self.set_ai_config_button.clicked.connect(self.set_ai_config)
|
51
|
+
self.load_ai_config_button = QPushButton(language_wrapper.language_word_dict.get("chat_ui_load_ai_button"))
|
52
|
+
self.load_ai_config_button.clicked.connect(lambda: self.load_ai_config(show_load_complete=True))
|
53
|
+
self.call_ai_model_button = QPushButton(language_wrapper.language_word_dict.get("chat_ui_call_ai_model_button"))
|
54
|
+
self.call_ai_model_button.clicked.connect(self.call_ai_model)
|
55
|
+
# Add to layout
|
56
|
+
self.grid_layout = QGridLayout()
|
57
|
+
self.grid_layout.addWidget(self.chat_panel_scroll_area, 0, 0, 1, 4)
|
58
|
+
self.grid_layout.addWidget(self.call_ai_model_button, 1, 0)
|
59
|
+
self.grid_layout.addWidget(self.font_size_combobox, 1, 1)
|
60
|
+
self.grid_layout.addWidget(self.set_ai_config_button, 1, 2)
|
61
|
+
self.grid_layout.addWidget(self.load_ai_config_button, 1, 3)
|
62
|
+
self.grid_layout.addWidget(self.prompt_input, 2, 0, 1, 4)
|
63
|
+
|
64
|
+
# Variable
|
65
|
+
self.ai_config: AIConfig = ai_config
|
66
|
+
self.lang_chain_interface: Union[LangChainInterface, None] = None
|
67
|
+
self.set_ai_config_dialog = None
|
68
|
+
# Timer to pop queue
|
69
|
+
self.pull_message_timer = QTimer(self)
|
70
|
+
self.pull_message_timer.setInterval(1000)
|
71
|
+
self.pull_message_timer.timeout.connect(self.pull_message)
|
72
|
+
self.pull_message_timer.start()
|
73
|
+
|
74
|
+
# Set layout
|
75
|
+
self.setLayout(self.grid_layout)
|
76
|
+
|
77
|
+
self.load_ai_config()
|
78
|
+
|
79
|
+
def update_panel_text_size(self):
|
80
|
+
self.chat_panel.setFont(
|
81
|
+
QFontDatabase.font(self.font().family(), "", int(self.font_size_combobox.currentText())))
|
82
|
+
|
83
|
+
def load_ai_config(self, show_load_complete: bool = False):
|
84
|
+
ai_config_file = Path(str(Path.cwd()) + "/" + ".jeditor/ai_config.json")
|
85
|
+
if ai_config_file.exists():
|
86
|
+
with open(ai_config_file, "r", encoding="utf-8"):
|
87
|
+
json_data: dict = read_json(str(ai_config_file))
|
88
|
+
if json_data:
|
89
|
+
if json_data.get("AI_model") and len(json_data.get("AI_model")) == 4:
|
90
|
+
ai_info: dict = json_data.get("AI_model")
|
91
|
+
if ai_info.get("ai_base_url") and ai_info.get("chat_model"):
|
92
|
+
ai_config.choosable_ai.update(json_data)
|
93
|
+
self.lang_chain_interface = LangChainInterface(
|
94
|
+
main_window=self,
|
95
|
+
api_key=ai_info.get("ai_api_key"),
|
96
|
+
base_url=ai_info.get("ai_base_url"),
|
97
|
+
chat_model=ai_info.get("chat_model"),
|
98
|
+
prompt_template=ai_info.get("prompt_template"),
|
99
|
+
)
|
100
|
+
if show_load_complete:
|
101
|
+
load_complete = QMessageBox(self)
|
102
|
+
load_complete.setWindowTitle(language_wrapper.language_word_dict.get("load_ai_messagebox_title"))
|
103
|
+
load_complete.setText(language_wrapper.language_word_dict.get("load_ai_messagebox_text"))
|
104
|
+
load_complete.exec()
|
105
|
+
|
106
|
+
|
107
|
+
def call_ai_model(self):
|
108
|
+
if isinstance(self.lang_chain_interface, LangChainInterface):
|
109
|
+
thread = AskThread(lang_chain_interface=self.lang_chain_interface, prompt=self.prompt_input.text())
|
110
|
+
thread.start()
|
111
|
+
else:
|
112
|
+
ai_info = ai_config.choosable_ai.get('AI_model')
|
113
|
+
QMessageBox.warning(self,
|
114
|
+
language_wrapper.language_word_dict.get("call_ai_model_error_title"),
|
115
|
+
language_wrapper.language_word_dict.get(
|
116
|
+
f"ai_api_key: {ai_info.get('ai_api_key')}, \n"
|
117
|
+
f"ai_base_url: {ai_info.get('ai_base_url')}, \n"
|
118
|
+
f"chat_model: {ai_info.get('chat_model')}, \n"
|
119
|
+
f"prompt_template: {ai_info.get('prompt_template')}"))
|
120
|
+
|
121
|
+
def pull_message(self):
|
122
|
+
if not ai_config.message_queue.empty():
|
123
|
+
ai_response = ai_config.message_queue.get_nowait()
|
124
|
+
self.chat_panel.appendPlainText(ai_response)
|
125
|
+
self.chat_panel.appendPlainText("\n")
|
126
|
+
|
127
|
+
def set_ai_config(self):
|
128
|
+
# Set and output AI a config file
|
129
|
+
self.set_ai_config_dialog = SetAIDialog()
|
130
|
+
self.set_ai_config_dialog.show()
|
@@ -0,0 +1,45 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
import re
|
5
|
+
from typing import Union, TYPE_CHECKING
|
6
|
+
|
7
|
+
from PySide6.QtWidgets import QMessageBox
|
8
|
+
from langchain.prompts.chat import SystemMessagePromptTemplate
|
9
|
+
from langchain_openai import ChatOpenAI
|
10
|
+
from pydantic import SecretStr
|
11
|
+
|
12
|
+
from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from je_editor.pyside_ui.main_ui.ai_widget.chat_ui import ChatUI
|
16
|
+
|
17
|
+
class LangChainInterface(object):
|
18
|
+
|
19
|
+
def __init__(self, main_window: ChatUI, prompt_template: str, base_url: str, api_key: Union[SecretStr, str],
|
20
|
+
chat_model: str):
|
21
|
+
self.system_message_prompt = SystemMessagePromptTemplate.from_template(prompt_template)
|
22
|
+
self.base_url = base_url
|
23
|
+
self.api_key = api_key
|
24
|
+
self.chat_model = chat_model
|
25
|
+
self.main_window = main_window
|
26
|
+
os.environ["OPENAI_BASE_URL"] = self.base_url
|
27
|
+
os.environ["OPENAI_API_KEY"] = self.api_key
|
28
|
+
os.environ["CHAT_MODEL"] = self.chat_model
|
29
|
+
self.chat_ai = ChatOpenAI(base_url=self.base_url, api_key=self.api_key, model=self.chat_model)
|
30
|
+
|
31
|
+
def call_ai_model(self, prompt: str) -> str | None:
|
32
|
+
message = None
|
33
|
+
try:
|
34
|
+
message = self.chat_ai.invoke(prompt).text()
|
35
|
+
match = re.search(r"</think>\s*(.*)", message, re.DOTALL)
|
36
|
+
if match:
|
37
|
+
message = match.group(1).strip()
|
38
|
+
else:
|
39
|
+
message = message
|
40
|
+
|
41
|
+
except Exception as error:
|
42
|
+
QMessageBox.warning(self.main_window,
|
43
|
+
language_wrapper.language_word_dict.get("call_ai_model_error_title"),
|
44
|
+
str(error))
|
45
|
+
return message
|
@@ -7,7 +7,7 @@ from typing import Dict, Type
|
|
7
7
|
import jedi.settings
|
8
8
|
from PySide6.QtCore import QTimer, QEvent
|
9
9
|
from PySide6.QtGui import QFontDatabase, QIcon, Qt, QTextCharFormat
|
10
|
-
from PySide6.QtWidgets import QMainWindow, QWidget, QTabWidget
|
10
|
+
from PySide6.QtWidgets import QMainWindow, QWidget, QTabWidget
|
11
11
|
from frontengine import FrontEngineMainUI
|
12
12
|
from qt_material import QtStyleTools
|
13
13
|
|