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.
Files changed (29) hide show
  1. je_editor/code_scan/watchdog_thread.py +1 -1
  2. je_editor/git_client/__init__.py +0 -0
  3. je_editor/git_client/commit_graph.py +88 -0
  4. je_editor/git_client/git.py +181 -0
  5. je_editor/git_client/git_cli.py +66 -0
  6. je_editor/git_client/github.py +50 -0
  7. je_editor/pyside_ui/browser/browser_view.py +0 -3
  8. je_editor/pyside_ui/git_ui/__init__.py +0 -0
  9. je_editor/pyside_ui/git_ui/commit_table.py +27 -0
  10. je_editor/pyside_ui/git_ui/git_branch_tree_widget.py +119 -0
  11. je_editor/pyside_ui/git_ui/git_client_gui.py +291 -0
  12. je_editor/pyside_ui/git_ui/graph_view.py +174 -0
  13. je_editor/pyside_ui/main_ui/ai_widget/__init__.py +0 -0
  14. je_editor/pyside_ui/main_ui/ai_widget/ai_config.py +19 -0
  15. je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py +17 -0
  16. je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py +130 -0
  17. je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py +45 -0
  18. je_editor/pyside_ui/main_ui/main_editor.py +1 -1
  19. je_editor/pyside_ui/main_ui/menu/check_style_menu/__init__.py +0 -0
  20. je_editor/pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +81 -0
  21. je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py +2 -2
  22. je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py +2 -2
  23. je_editor/utils/multi_language/english.py +1 -1
  24. je_editor/utils/multi_language/traditional_chinese.py +1 -1
  25. {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/METADATA +2 -1
  26. {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/RECORD +29 -12
  27. {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/WHEEL +0 -0
  28. {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/licenses/LICENSE +0 -0
  29. {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, QMessageBox
10
+ from PySide6.QtWidgets import QMainWindow, QWidget, QTabWidget
11
11
  from frontengine import FrontEngineMainUI
12
12
  from qt_material import QtStyleTools
13
13