je-editor 0.0.104__py3-none-any.whl → 0.0.228__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 (151) hide show
  1. je_editor/__init__.py +26 -21
  2. je_editor/__main__.py +1 -1
  3. je_editor/code_scan/ruff_thread.py +58 -0
  4. je_editor/code_scan/watchdog_implement.py +56 -0
  5. je_editor/code_scan/watchdog_thread.py +78 -0
  6. je_editor/git_client/commit_graph.py +77 -0
  7. je_editor/git_client/git_action.py +175 -0
  8. je_editor/git_client/git_cli.py +66 -0
  9. je_editor/pyside_ui/browser/browser_download_window.py +75 -0
  10. je_editor/pyside_ui/browser/browser_serach_lineedit.py +51 -0
  11. je_editor/pyside_ui/browser/browser_view.py +87 -0
  12. je_editor/pyside_ui/browser/browser_widget.py +103 -0
  13. je_editor/pyside_ui/browser/main_browser_widget.py +85 -0
  14. je_editor/pyside_ui/code/auto_save/auto_save_manager.py +60 -0
  15. je_editor/pyside_ui/code/auto_save/auto_save_thread.py +59 -0
  16. je_editor/pyside_ui/code/code_format/pep8_format.py +130 -0
  17. je_editor/pyside_ui/code/code_process/code_exec.py +267 -0
  18. je_editor/pyside_ui/code/plaintext_code_edit/code_edit_plaintext.py +412 -0
  19. je_editor/pyside_ui/code/running_process_manager.py +48 -0
  20. je_editor/pyside_ui/code/shell_process/shell_exec.py +236 -0
  21. je_editor/pyside_ui/code/syntax/python_syntax.py +99 -0
  22. je_editor/pyside_ui/code/syntax/syntax_setting.py +95 -0
  23. je_editor/pyside_ui/code/textedit_code_result/code_record.py +75 -0
  24. je_editor/pyside_ui/code/variable_inspector/inspector_gui.py +172 -0
  25. je_editor/pyside_ui/dialog/ai_dialog/set_ai_dialog.py +71 -0
  26. je_editor/pyside_ui/dialog/file_dialog/create_file_dialog.py +68 -0
  27. je_editor/pyside_ui/dialog/file_dialog/open_file_dialog.py +111 -0
  28. je_editor/pyside_ui/dialog/file_dialog/save_file_dialog.py +67 -0
  29. je_editor/pyside_ui/dialog/search_ui/search_error_box.py +49 -0
  30. je_editor/pyside_ui/dialog/search_ui/search_text_box.py +49 -0
  31. je_editor/pyside_ui/git_ui/code_diff_compare/code_diff_viewer_widget.py +90 -0
  32. je_editor/pyside_ui/git_ui/code_diff_compare/line_number_code_viewer.py +141 -0
  33. je_editor/pyside_ui/git_ui/code_diff_compare/multi_file_diff_viewer.py +88 -0
  34. je_editor/pyside_ui/git_ui/code_diff_compare/side_by_side_diff_widget.py +284 -0
  35. je_editor/pyside_ui/git_ui/git_client/commit_table.py +65 -0
  36. je_editor/pyside_ui/git_ui/git_client/git_branch_tree_widget.py +156 -0
  37. je_editor/pyside_ui/git_ui/git_client/git_client_gui.py +799 -0
  38. je_editor/pyside_ui/git_ui/git_client/graph_view.py +218 -0
  39. je_editor/pyside_ui/main_ui/ai_widget/ai_config.py +34 -0
  40. je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py +36 -0
  41. je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py +147 -0
  42. je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py +84 -0
  43. je_editor/pyside_ui/main_ui/console_widget/console_gui.py +162 -0
  44. je_editor/pyside_ui/main_ui/console_widget/qprocess_adapter.py +84 -0
  45. je_editor/pyside_ui/main_ui/dock/__init__.py +0 -0
  46. je_editor/pyside_ui/main_ui/dock/destroy_dock.py +50 -0
  47. je_editor/pyside_ui/main_ui/editor/__init__.py +0 -0
  48. je_editor/pyside_ui/main_ui/editor/editor_widget.py +301 -0
  49. je_editor/pyside_ui/main_ui/editor/editor_widget_dock.py +70 -0
  50. je_editor/pyside_ui/main_ui/editor/process_input.py +101 -0
  51. je_editor/pyside_ui/main_ui/ipython_widget/__init__.py +0 -0
  52. je_editor/pyside_ui/main_ui/ipython_widget/rich_jupyter.py +78 -0
  53. je_editor/pyside_ui/main_ui/main_editor.py +369 -0
  54. je_editor/pyside_ui/main_ui/menu/__init__.py +0 -0
  55. je_editor/pyside_ui/main_ui/menu/check_style_menu/__init__.py +0 -0
  56. je_editor/pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +104 -0
  57. je_editor/pyside_ui/main_ui/menu/dock_menu/__init__.py +0 -0
  58. je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py +208 -0
  59. je_editor/pyside_ui/main_ui/menu/file_menu/__init__.py +0 -0
  60. je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py +186 -0
  61. je_editor/pyside_ui/main_ui/menu/help_menu/__init__.py +0 -0
  62. je_editor/pyside_ui/main_ui/menu/help_menu/build_help_menu.py +100 -0
  63. je_editor/pyside_ui/main_ui/menu/language_menu/__init__.py +0 -0
  64. je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py +89 -0
  65. je_editor/pyside_ui/main_ui/menu/python_env_menu/__init__.py +0 -0
  66. je_editor/pyside_ui/main_ui/menu/python_env_menu/build_venv_menu.py +238 -0
  67. je_editor/pyside_ui/main_ui/menu/run_menu/__init__.py +0 -0
  68. je_editor/pyside_ui/main_ui/menu/run_menu/build_run_menu.py +160 -0
  69. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/__init__.py +0 -0
  70. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_debug_menu.py +109 -0
  71. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_program_menu.py +101 -0
  72. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_shell_menu.py +98 -0
  73. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/utils.py +41 -0
  74. je_editor/pyside_ui/main_ui/menu/set_menu_bar.py +63 -0
  75. je_editor/pyside_ui/main_ui/menu/style_menu/__init__.py +0 -0
  76. je_editor/pyside_ui/main_ui/menu/style_menu/build_style_menu.py +73 -0
  77. je_editor/pyside_ui/main_ui/menu/tab_menu/__init__.py +0 -0
  78. je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py +275 -0
  79. je_editor/pyside_ui/main_ui/menu/text_menu/__init__.py +0 -0
  80. je_editor/pyside_ui/main_ui/menu/text_menu/build_text_menu.py +135 -0
  81. je_editor/pyside_ui/main_ui/save_settings/__init__.py +0 -0
  82. je_editor/pyside_ui/main_ui/save_settings/setting_utils.py +33 -0
  83. je_editor/pyside_ui/main_ui/save_settings/user_color_setting_file.py +103 -0
  84. je_editor/pyside_ui/main_ui/save_settings/user_setting_file.py +58 -0
  85. je_editor/pyside_ui/main_ui/system_tray/__init__.py +0 -0
  86. je_editor/pyside_ui/main_ui/system_tray/extend_system_tray.py +90 -0
  87. je_editor/start_editor.py +32 -8
  88. je_editor/utils/encodings/python_encodings.py +100 -97
  89. je_editor/utils/exception/exception_tags.py +11 -11
  90. je_editor/utils/file/open/open_file.py +38 -22
  91. je_editor/utils/file/save/save_file.py +40 -16
  92. je_editor/utils/json/json_file.py +36 -15
  93. je_editor/utils/json_format/json_process.py +38 -2
  94. je_editor/utils/logging/__init__.py +0 -0
  95. je_editor/utils/logging/loggin_instance.py +57 -0
  96. je_editor/utils/multi_language/__init__.py +0 -0
  97. je_editor/utils/multi_language/english.py +221 -0
  98. je_editor/utils/multi_language/multi_language_wrapper.py +54 -0
  99. je_editor/utils/multi_language/traditional_chinese.py +214 -0
  100. je_editor/utils/redirect_manager/redirect_manager_class.py +67 -25
  101. je_editor/utils/venv_check/__init__.py +0 -0
  102. je_editor/utils/venv_check/check_venv.py +51 -0
  103. je_editor-0.0.228.dist-info/METADATA +99 -0
  104. je_editor-0.0.228.dist-info/RECORD +140 -0
  105. {je_editor-0.0.104.dist-info → je_editor-0.0.228.dist-info}/WHEEL +1 -1
  106. {je_editor-0.0.104.dist-info → je_editor-0.0.228.dist-info/licenses}/LICENSE +1 -1
  107. je_editor/pyside_ui/auto_save/auto_save_thread.py +0 -34
  108. je_editor/pyside_ui/code_editor/code_edit_plaintext.py +0 -143
  109. je_editor/pyside_ui/code_process/code_exec.py +0 -190
  110. je_editor/pyside_ui/code_result/code_record.py +0 -39
  111. je_editor/pyside_ui/colors/global_color.py +0 -4
  112. je_editor/pyside_ui/file_dialog/open_file_dialog.py +0 -27
  113. je_editor/pyside_ui/file_dialog/save_file_dialog.py +0 -24
  114. je_editor/pyside_ui/main_ui/editor_main_ui/main_editor.py +0 -183
  115. je_editor/pyside_ui/main_ui_setting/ui_setting.py +0 -36
  116. je_editor/pyside_ui/menu/menu_bar/check_style_menu/build_check_style_menu.py +0 -44
  117. je_editor/pyside_ui/menu/menu_bar/file_menu/build_file_menu.py +0 -30
  118. je_editor/pyside_ui/menu/menu_bar/help_menu/build_help_menu.py +0 -39
  119. je_editor/pyside_ui/menu/menu_bar/run_menu/build_run_menu.py +0 -102
  120. je_editor/pyside_ui/menu/menu_bar/set_menu_bar.py +0 -24
  121. je_editor/pyside_ui/menu/menu_bar/venv_menu/build_venv_menu.py +0 -74
  122. je_editor/pyside_ui/search_ui/search_error_box.py +0 -20
  123. je_editor/pyside_ui/search_ui/search_text_box.py +0 -20
  124. je_editor/pyside_ui/shell_process/shell_exec.py +0 -157
  125. je_editor/pyside_ui/syntax/python_syntax.py +0 -99
  126. je_editor/pyside_ui/treeview/project_treeview/set_project_treeview.py +0 -47
  127. je_editor/pyside_ui/user_setting/user_setting_file.py +0 -23
  128. je_editor-0.0.104.dist-info/METADATA +0 -84
  129. je_editor-0.0.104.dist-info/RECORD +0 -69
  130. /je_editor/{pyside_ui/auto_save → code_scan}/__init__.py +0 -0
  131. /je_editor/{pyside_ui/code_editor → git_client}/__init__.py +0 -0
  132. /je_editor/pyside_ui/{code_process → browser}/__init__.py +0 -0
  133. /je_editor/pyside_ui/{code_result → code}/__init__.py +0 -0
  134. /je_editor/pyside_ui/{colors → code/auto_save}/__init__.py +0 -0
  135. /je_editor/pyside_ui/{file_dialog → code/code_format}/__init__.py +0 -0
  136. /je_editor/pyside_ui/{main_ui/editor_main_ui → code/code_process}/__init__.py +0 -0
  137. /je_editor/pyside_ui/{main_ui_setting → code/plaintext_code_edit}/__init__.py +0 -0
  138. /je_editor/pyside_ui/{menu → code/shell_process}/__init__.py +0 -0
  139. /je_editor/pyside_ui/{menu/menu_bar → code/syntax}/__init__.py +0 -0
  140. /je_editor/pyside_ui/{menu/menu_bar/check_style_menu → code/textedit_code_result}/__init__.py +0 -0
  141. /je_editor/pyside_ui/{menu/menu_bar/file_menu → code/variable_inspector}/__init__.py +0 -0
  142. /je_editor/pyside_ui/{menu/menu_bar/help_menu → dialog}/__init__.py +0 -0
  143. /je_editor/pyside_ui/{menu/menu_bar/run_menu → dialog/ai_dialog}/__init__.py +0 -0
  144. /je_editor/pyside_ui/{menu/menu_bar/venv_menu → dialog/file_dialog}/__init__.py +0 -0
  145. /je_editor/pyside_ui/{search_ui → dialog/search_ui}/__init__.py +0 -0
  146. /je_editor/pyside_ui/{shell_process → git_ui}/__init__.py +0 -0
  147. /je_editor/pyside_ui/{syntax → git_ui/code_diff_compare}/__init__.py +0 -0
  148. /je_editor/pyside_ui/{treeview → git_ui/git_client}/__init__.py +0 -0
  149. /je_editor/pyside_ui/{treeview/project_treeview → main_ui/ai_widget}/__init__.py +0 -0
  150. /je_editor/pyside_ui/{user_setting → main_ui/console_widget}/__init__.py +0 -0
  151. {je_editor-0.0.104.dist-info → je_editor-0.0.228.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,218 @@
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 # 節點半徑 / node radius
19
+ ROW_HEIGHT = 28 # 每一列高度 / row height
20
+ LANE_WIDTH = 22 # 每一條 lane 的寬度 / lane width
21
+ LEFT_MARGIN = 220 # 左側 SHA/訊息文字區域保留空間 / left margin for SHA/message text
22
+
23
+ # === 顏色設定 / Colors ===
24
+ EDGE_COLOR = QColor("#888") # 邊線顏色 / edge color
25
+ TEXT_COLOR = QColor("#222") # 文字顏色 / text color
26
+ BG_COLOR = QColor("#ffffff") # 背景顏色 / background color
27
+
28
+ # Lane 顏色調色盤 / Lane color 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
+ """
45
+ 根據 lane index 回傳顏色
46
+ Return color based on lane index
47
+ """
48
+ if lane < 0:
49
+ return QColor("#999")
50
+ return LANE_COLORS[lane % len(LANE_COLORS)]
51
+
52
+
53
+ class CommitGraphView(QGraphicsView):
54
+ """
55
+ Git Commit Graph 視覺化檢視器
56
+ Git commit graph visualization view
57
+ """
58
+
59
+ def __init__(self, parent=None):
60
+ super().__init__(parent)
61
+ self.language_wrapper_get = language_wrapper.language_word_dict.get
62
+
63
+ # 設定背景與抗鋸齒 / set background and antialiasing
64
+ self.setBackgroundBrush(QBrush(BG_COLOR))
65
+ self.setRenderHints(
66
+ self.renderHints()
67
+ | QPainter.RenderHint.Antialiasing
68
+ | QPainter.RenderHint.TextAntialiasing
69
+ )
70
+
71
+ # 啟用拖曳與縮放模式 / enable drag and zoom mode
72
+ self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
73
+ self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
74
+
75
+ # 建立場景 / create scene
76
+ self._scene = QGraphicsScene(self)
77
+ self.setScene(self._scene)
78
+
79
+ self.graph: Optional[CommitGraph] = None
80
+ self._padding = 40
81
+ self._zoom = 1.0
82
+
83
+ def set_graph(self, graph: CommitGraph):
84
+ """
85
+ 設定 commit graph 並重新繪製
86
+ Set commit graph and redraw
87
+ """
88
+ self.graph = graph
89
+ self._redraw()
90
+
91
+ def _lane_x(self, lane: int) -> float:
92
+ # 計算 lane 的 X 座標 / calculate X position for lane
93
+ return lane * LANE_WIDTH + NODE_RADIUS * 2
94
+
95
+ def _row_y(self, row: int) -> float:
96
+ # 計算 row 的 Y 座標 / calculate Y position for row
97
+ return row * ROW_HEIGHT + NODE_RADIUS * 2
98
+
99
+ def _redraw(self):
100
+ """
101
+ 重新繪製整個 commit graph
102
+ Redraw the entire commit graph
103
+ """
104
+ self._scene.clear()
105
+ if not self.graph or not self.graph.nodes:
106
+ self._scene.setSceneRect(QRectF(0, 0, 800, 400))
107
+ return
108
+
109
+ # === 繪製邊線 (父子關係) / Draw edges (parent-child relationships) ===
110
+ edge_pen = QPen(EDGE_COLOR, 2)
111
+ for row, node in enumerate(self.graph.nodes):
112
+ if not node.parent_shas:
113
+ continue
114
+ x0 = self._lane_x(node.lane_index)
115
+ y0 = self._row_y(row)
116
+ for p in node.parent_shas:
117
+ if p not in self.graph.index:
118
+ continue
119
+ prow = self.graph.index[p]
120
+ parent_node = self.graph.nodes[prow]
121
+ x1 = self._lane_x(parent_node.lane_index)
122
+ y1 = self._row_y(prow)
123
+
124
+ # 使用三次貝茲曲線繪製邊線 / cubic Bezier curve for edge
125
+ path = QPainterPath(QPointF(x0, y0))
126
+ ctrl_y = (y0 + y1) / 2.0
127
+ path.cubicTo(QPointF(x0, ctrl_y), QPointF(x1, ctrl_y), QPointF(x1, y1))
128
+ edge_item = QGraphicsPathItem(path)
129
+ edge_item.setPen(edge_pen)
130
+ self._scene.addItem(edge_item)
131
+
132
+ # === 繪製節點 (commit 圓點) / Draw nodes (commit circles) ===
133
+ for row, node in enumerate(self.graph.nodes):
134
+ cx = self._lane_x(node.lane_index)
135
+ cy = self._row_y(row)
136
+
137
+ circle = QGraphicsEllipseItem(
138
+ QRectF(cx - NODE_RADIUS, cy - NODE_RADIUS, NODE_RADIUS * 2, NODE_RADIUS * 2)
139
+ )
140
+ circle.setBrush(QBrush(lane_color(node.lane_index)))
141
+ circle.setPen(QPen(Qt.PenStyle.NoPen))
142
+ # 設定 tooltip 顯示 commit 資訊 / tooltip with commit info
143
+ circle.setToolTip(self.language_wrapper_get(
144
+ "git_graph_tooltip_commit"
145
+ ).format(short=node.commit_sha[:7],
146
+ author=node.author_name,
147
+ date=node.commit_date,
148
+ msg=node.commit_message))
149
+ self._scene.addItem(circle)
150
+
151
+ # 在左側顯示行號 / show row number on the left
152
+ label_item = QGraphicsSimpleTextItem(str(row + 1))
153
+ label_item.setBrush(QBrush(TEXT_COLOR))
154
+ label_item.setPos(-30, cy - NODE_RADIUS * 2)
155
+ self._scene.addItem(label_item)
156
+
157
+ # 設定場景大小 / set scene rect
158
+ self._scene.setSceneRect(
159
+ QRectF(-40, 0,
160
+ self._lane_x(max(n.lane_index for n in self.graph.nodes) + 1) + self._padding,
161
+ self._row_y(len(self.graph.nodes)) + self._padding)
162
+ )
163
+
164
+ def _apply_initial_view(self):
165
+ """
166
+ 初始縮放設定
167
+ Apply initial zoom setting
168
+ """
169
+ self._zoom = 0.9
170
+ self._apply_zoom_transform()
171
+
172
+ # === 滑鼠滾輪縮放 / Mouse wheel zoom ===
173
+ def wheelEvent(self, event):
174
+ if not self._scene.items():
175
+ return
176
+
177
+ # Ctrl+滾輪縮放,否則正常滾動
178
+ # Ctrl+Wheel to zoom, else scroll normally
179
+ if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
180
+ angle = event.angleDelta().y()
181
+ factor = 1.0 + (0.1 if angle > 0 else -0.1)
182
+ self._zoom = max(0.1, min(3.0, self._zoom * factor))
183
+ self._apply_zoom_transform()
184
+ event.accept()
185
+ else:
186
+ super().wheelEvent(event)
187
+
188
+ def _apply_zoom_transform(self):
189
+ """
190
+ 套用縮放轉換
191
+ Apply zoom transform
192
+ """
193
+ t = QTransform()
194
+ t.scale(self._zoom, self._zoom)
195
+ self.setTransform(t)
196
+
197
+ def resizeEvent(self, event):
198
+ """
199
+ 視窗大小改變時的事件
200
+ Handle resize event (keep scene rect, no auto-fit)
201
+ """
202
+ super().resizeEvent(event)
203
+ # 保持場景大小,不自動縮放
204
+ # Keep scene rect; do not auto-fit
205
+
206
+ # === 外部控制輔助方法 / Helper for external controllers ===
207
+ def focus_row(self, row: int):
208
+ """
209
+ 將檢視器聚焦到指定的 row
210
+ Center the view around a specific row
211
+ """
212
+ if not self.graph or row < 0 or row >= len(self.graph.nodes):
213
+ return
214
+ y = self._row_y(row)
215
+ rect = QRectF(0, y - ROW_HEIGHT * 2, self._scene.width(), ROW_HEIGHT * 4)
216
+ self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
217
+ # 聚焦後恢復使用者縮放比例 / restore user zoom after focusing
218
+ self._apply_zoom_transform()
@@ -0,0 +1,34 @@
1
+ from queue import Queue
2
+
3
+
4
+ class AIConfig(object):
5
+ """
6
+ AIConfig 類別:用來管理 AI 模型的設定與訊息佇列
7
+ AIConfig class: manages AI model configuration and message queue
8
+ """
9
+
10
+ def __init__(self):
11
+ # 當前 AI 模型的系統提示詞 (system prompt)
12
+ # Current AI model system prompt
13
+ self.current_ai_model_system_prompt: str = ""
14
+
15
+ # 可選擇的 AI 模型設定字典
16
+ # Dictionary of choosable AI model configurations
17
+ # 結構: { "AI_model": { "ai_base_url": ..., "ai_api_key": ..., "chat_model": ..., "prompt_template": ... } }
18
+ self.choosable_ai: dict[str, dict[str, str]] = {
19
+ "AI_model": {
20
+ "ai_base_url": "", # AI 服務的基礎 URL / Base URL of AI service
21
+ "ai_api_key": "", # API 金鑰 / API key
22
+ "chat_model": "", # 模型名稱 / Model name
23
+ "prompt_template": "", # 提示詞模板 / Prompt template
24
+ }
25
+ }
26
+
27
+ # 訊息佇列,用來暫存與 AI 的互動訊息
28
+ # Message queue for storing AI interaction messages
29
+ self.message_queue = Queue()
30
+
31
+
32
+ # 建立全域唯一的 AIConfig 實例
33
+ # Create a global singleton instance of AIConfig
34
+ ai_config = AIConfig()
@@ -0,0 +1,36 @@
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
+ AskThread 類別:用來在背景執行緒中呼叫 AI 模型,避免阻塞主執行緒
10
+ AskThread class: runs AI model calls in a background thread to avoid blocking the main thread
11
+ """
12
+
13
+ def __init__(self, lang_chain_interface: LangChainInterface, prompt):
14
+ """
15
+ 初始化 AskThread
16
+ Initialize AskThread
17
+ :param lang_chain_interface: LangChainInterface 實例,用來呼叫 AI 模型
18
+ LangChainInterface instance for calling AI model
19
+ :param prompt: 傳給 AI 模型的提示詞
20
+ Prompt to send to the AI model
21
+ """
22
+ super().__init__()
23
+ self.lang_chain_interface = lang_chain_interface
24
+ self.prompt = prompt
25
+
26
+ def run(self):
27
+ """
28
+ 執行緒的主要邏輯:
29
+ 1. 呼叫 AI 模型並取得回應
30
+ 2. 將回應放入 ai_config 的 message_queue 中,供主程式使用
31
+ Thread main logic:
32
+ 1. Call AI model and get response
33
+ 2. Put response into ai_config.message_queue for main program to consume
34
+ """
35
+ ai_response = self.lang_chain_interface.call_ai_model(prompt=self.prompt)
36
+ ai_config.message_queue.put(ai_response)
@@ -0,0 +1,147 @@
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) # 關閉視窗時自動釋放資源 / Auto delete on close
27
+ self.main_window = main_window
28
+
29
+ # ---------------- Chat Panel 聊天面板 ----------------
30
+ self.chat_panel = QPlainTextEdit() # 顯示聊天訊息的文字框 / Text area for chat messages
31
+ self.chat_panel.setLineWrapMode(self.chat_panel.LineWrapMode.NoWrap) # 不自動換行 / Disable line wrap
32
+ self.chat_panel.setReadOnly(True) # 設為唯讀,避免使用者直接輸入 / Read-only
33
+ self.chat_panel_scroll_area = QScrollArea() # 加入滾動區域 / Scroll area for chat panel
34
+ self.chat_panel_scroll_area.setWidgetResizable(True)
35
+ self.chat_panel_scroll_area.setViewportMargins(0, 0, 0, 0)
36
+ self.chat_panel_scroll_area.setWidget(self.chat_panel)
37
+ self.chat_panel.setFont(QFontDatabase.font(self.font().family(), "", 16)) # 設定字體大小 / Set font size
38
+
39
+ # ---------------- Prompt Input 輸入框 ----------------
40
+ self.prompt_input = QLineEdit() # 使用者輸入提示詞 / Input field for prompts
41
+ self.prompt_input.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
42
+ self.prompt_input.returnPressed.connect(self.call_ai_model) # 按 Enter 時呼叫 AI / Call AI on Enter
43
+
44
+ # ---------------- Font Size Combobox 字體大小選單 ----------------
45
+ self.font_size_label = QLabel(language_wrapper.language_word_dict.get("font_size")) # 標籤 / Label
46
+ self.font_size_combobox = QComboBox() # 下拉選單 / Dropdown for font size
47
+ for font_size in range(2, 101, 2): # 提供 2~100 的字體大小選項 / Font size options
48
+ self.font_size_combobox.addItem(str(font_size))
49
+ self.font_size_combobox.setCurrentText("16") # 預設字體大小 / Default font size
50
+ self.font_size_combobox.currentTextChanged.connect(self.update_panel_text_size)
51
+
52
+ # ---------------- Buttons 按鈕 ----------------
53
+ self.set_ai_config_button = QPushButton(language_wrapper.language_word_dict.get("chat_ui_set_ai_button"))
54
+ self.set_ai_config_button.clicked.connect(self.set_ai_config) # 開啟 AI 設定視窗 / Open AI config dialog
55
+
56
+ self.load_ai_config_button = QPushButton(language_wrapper.language_word_dict.get("chat_ui_load_ai_button"))
57
+ self.load_ai_config_button.clicked.connect(
58
+ lambda: self.load_ai_config(show_load_complete=True)) # 載入設定 / Load config
59
+
60
+ self.call_ai_model_button = QPushButton(language_wrapper.language_word_dict.get("chat_ui_call_ai_model_button"))
61
+ self.call_ai_model_button.clicked.connect(self.call_ai_model) # 呼叫 AI / Call AI
62
+
63
+ # ---------------- Layout 版面配置 ----------------
64
+ self.grid_layout = QGridLayout()
65
+ self.grid_layout.addWidget(self.chat_panel_scroll_area, 0, 0, 1, 4) # 聊天面板 / Chat panel
66
+ self.grid_layout.addWidget(self.call_ai_model_button, 1, 0) # 呼叫 AI 按鈕 / Call AI button
67
+ self.grid_layout.addWidget(self.font_size_combobox, 1, 1) # 字體大小選單 / Font size combobox
68
+ self.grid_layout.addWidget(self.set_ai_config_button, 1, 2) # 設定 AI 按鈕 / Set AI config button
69
+ self.grid_layout.addWidget(self.load_ai_config_button, 1, 3) # 載入設定按鈕 / Load AI config button
70
+ self.grid_layout.addWidget(self.prompt_input, 2, 0, 1, 4) # 輸入框 / Prompt input
71
+
72
+ # ---------------- Variables 變數 ----------------
73
+ self.ai_config: AIConfig = ai_config # AI 設定物件 / AI config object
74
+ self.lang_chain_interface: Union[LangChainInterface, None] = None # LangChain 介面 / LangChain interface
75
+ self.set_ai_config_dialog = None # 設定對話框 / Config dialog
76
+
77
+ # ---------------- Timer 計時器 ----------------
78
+ self.pull_message_timer = QTimer(self) # 定時檢查訊息佇列 / Timer to pull messages
79
+ self.pull_message_timer.setInterval(1000) # 每秒檢查一次 / Check every 1 second
80
+ self.pull_message_timer.timeout.connect(self.pull_message)
81
+ self.pull_message_timer.start()
82
+
83
+ # ---------------- Set Layout 設定版面 ----------------
84
+ self.setLayout(self.grid_layout)
85
+
86
+ # ---------------- Load AI Config 載入 AI 設定 ----------------
87
+ self.load_ai_config()
88
+
89
+ # 更新聊天面板字體大小 / Update chat panel font size
90
+ def update_panel_text_size(self):
91
+ self.chat_panel.setFont(
92
+ QFontDatabase.font(self.font().family(), "", int(self.font_size_combobox.currentText())))
93
+
94
+ # 載入 AI 設定檔 / Load AI configuration file
95
+ def load_ai_config(self, show_load_complete: bool = False):
96
+ ai_config_file = Path(str(Path.cwd()) + "/" + ".jeditor/ai_config.json")
97
+ if ai_config_file.exists():
98
+ with open(ai_config_file, "r", encoding="utf-8"):
99
+ json_data: dict = read_json(str(ai_config_file))
100
+ if json_data:
101
+ # 確認 AI_model 設定存在且格式正確 / Ensure AI_model config exists and valid
102
+ if json_data.get("AI_model") and len(json_data.get("AI_model")) == 4:
103
+ ai_info: dict = json_data.get("AI_model")
104
+ if ai_info.get("ai_base_url") and ai_info.get("chat_model"):
105
+ ai_config.choosable_ai.update(json_data) # 更新全域設定 / Update global config
106
+ # 建立 LangChain 介面 / Initialize LangChain interface
107
+ self.lang_chain_interface = LangChainInterface(
108
+ main_window=self,
109
+ api_key=ai_info.get("ai_api_key"),
110
+ base_url=ai_info.get("ai_base_url"),
111
+ chat_model=ai_info.get("chat_model"),
112
+ prompt_template=ai_info.get("prompt_template"),
113
+ )
114
+ if show_load_complete:
115
+ load_complete = QMessageBox(self)
116
+ load_complete.setWindowTitle(language_wrapper.language_word_dict.get("load_ai_messagebox_title"))
117
+ load_complete.setText(language_wrapper.language_word_dict.get("load_ai_messagebox_text"))
118
+ load_complete.exec()
119
+
120
+ # 呼叫 AI 模型 / Call AI model
121
+ def call_ai_model(self):
122
+ if isinstance(self.lang_chain_interface, LangChainInterface):
123
+ # 建立新執行緒處理 AI 請求 / Start a new thread for AI request
124
+ thread = AskThread(lang_chain_interface=self.lang_chain_interface, prompt=self.prompt_input.text())
125
+ thread.start()
126
+ else:
127
+ # 若未正確設定 AI,顯示錯誤訊息 / Show error if AI not configured
128
+ ai_info = ai_config.choosable_ai.get('AI_model')
129
+ QMessageBox.warning(self,
130
+ language_wrapper.language_word_dict.get("call_ai_model_error_title"),
131
+ language_wrapper.language_word_dict.get(
132
+ f"ai_api_key: {ai_info.get('ai_api_key')}, \n"
133
+ f"ai_base_url: {ai_info.get('ai_base_url')}, \n"
134
+ f"chat_model: {ai_info.get('chat_model')}, \n"
135
+ f"prompt_template: {ai_info.get('prompt_template')}"))
136
+
137
+ # 從訊息佇列中取出 AI 回覆並顯示 / Pull AI response from queue
138
+ def pull_message(self):
139
+ if not ai_config.message_queue.empty():
140
+ ai_response = ai_config.message_queue.get_nowait()
141
+ self.chat_panel.appendPlainText(ai_response) # 顯示回覆 / Display response
142
+ self.chat_panel.appendPlainText("\n")
143
+
144
+ # 開啟 AI 設定對話框 / Open AI config dialog
145
+ def set_ai_config(self):
146
+ self.set_ai_config_dialog = SetAIDialog()
147
+ self.set_ai_config_dialog.show()
@@ -0,0 +1,84 @@
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_core.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
+
18
+ class LangChainInterface(object):
19
+ """
20
+ LangChainInterface 負責與 LangChain + OpenAI 模型互動
21
+ LangChainInterface is responsible for interacting with LangChain + OpenAI model
22
+ """
23
+
24
+ def __init__(self, main_window: ChatUI, prompt_template: str, base_url: str,
25
+ api_key: Union[SecretStr, str], chat_model: str):
26
+ """
27
+ 初始化 LangChainInterface
28
+ Initialize LangChainInterface
29
+
30
+ :param main_window: 主視窗,用於顯示錯誤訊息 / Main window, used for showing error messages
31
+ :param prompt_template: 系統提示詞模板 / System prompt template
32
+ :param base_url: OpenAI API 的基礎 URL / Base URL for OpenAI API
33
+ :param api_key: OpenAI API 金鑰 / OpenAI API key
34
+ :param chat_model: 使用的聊天模型名稱 / Chat model name
35
+ """
36
+ # 建立系統提示詞模板 / Create system message prompt template
37
+ self.system_message_prompt = SystemMessagePromptTemplate.from_template(prompt_template)
38
+
39
+ # 儲存基本設定 / Store basic settings
40
+ self.base_url = base_url
41
+ self.api_key = api_key
42
+ self.chat_model = chat_model
43
+ self.main_window = main_window
44
+
45
+ # 將設定寫入環境變數,方便其他套件讀取
46
+ # Save settings into environment variables for other packages to read
47
+ os.environ["OPENAI_BASE_URL"] = self.base_url
48
+ os.environ["OPENAI_API_KEY"] = self.api_key
49
+ os.environ["CHAT_MODEL"] = self.chat_model
50
+
51
+ # 初始化 ChatOpenAI 物件,用於呼叫模型
52
+ # Initialize ChatOpenAI object for model invocation
53
+ self.chat_ai = ChatOpenAI(base_url=self.base_url, api_key=self.api_key, model=self.chat_model)
54
+
55
+ def call_ai_model(self, prompt: str) -> str | None:
56
+ """
57
+ 呼叫 AI 模型並回傳結果
58
+ Call AI model and return response
59
+
60
+ :param prompt: 使用者輸入的提示詞 / User input prompt
61
+ :return: AI 回覆文字或 None / AI response text or None
62
+ """
63
+ message = None
64
+ try:
65
+ # 呼叫 AI 並取得回覆 / Invoke AI and get response
66
+ message = self.chat_ai.invoke(prompt).text()
67
+
68
+ # 嘗試過濾掉 <think> 標籤前的內容,只保留主要回覆
69
+ # Try to filter out content before </think>, keep only main response
70
+ match = re.search(r"</think>\s*(.*)", message, re.DOTALL)
71
+ if match:
72
+ message = match.group(1).strip()
73
+ else:
74
+ message = message
75
+
76
+ except Exception as error:
77
+ # 發生錯誤時,彈出警告視窗顯示錯誤訊息
78
+ # Show error message in a warning dialog if exception occurs
79
+ QMessageBox.warning(
80
+ self.main_window,
81
+ language_wrapper.language_word_dict.get("call_ai_model_error_title"),
82
+ str(error)
83
+ )
84
+ return message
@@ -0,0 +1,162 @@
1
+ import os
2
+
3
+ from PySide6.QtCore import Qt, QEvent
4
+ from PySide6.QtGui import QTextCursor, QColor, QKeyEvent
5
+ from PySide6.QtWidgets import (
6
+ QWidget, QVBoxLayout, QHBoxLayout, QPlainTextEdit, QLineEdit,
7
+ QPushButton, QLabel, QFileDialog, QComboBox
8
+ )
9
+
10
+ from je_editor.pyside_ui.main_ui.console_widget.qprocess_adapter import ConsoleProcessAdapter
11
+ from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
12
+
13
+
14
+ class ConsoleWidget(QWidget):
15
+ """
16
+ ConsoleWidget 提供一個互動式終端機介面,讓使用者可以輸入指令並查看輸出。
17
+ ConsoleWidget provides an interactive console interface for running commands and viewing output.
18
+ """
19
+
20
+ def __init__(self, parent=None):
21
+ super().__init__(parent)
22
+ self.language_word_dict_get = language_wrapper.language_word_dict.get
23
+
24
+ # 初始化子程序控制器 / Initialize process adapter
25
+ self.proc = ConsoleProcessAdapter(self)
26
+ self.proc.stdout.connect(self.append_text) # 標準輸出 / Standard output
27
+ self.proc.stderr.connect(lambda t: self.append_text(t, "#d33")) # 錯誤輸出 (紅色) / Error output (red)
28
+ self.proc.system.connect(
29
+ lambda t: self.append_text(f"{self.language_word_dict_get('dynamic_console_system_prefix')}{t}\n", "#888")
30
+ )
31
+ self.proc.started.connect(lambda: self.proc.system.emit(self.language_word_dict_get("dynamic_console_running")))
32
+ self.proc.finished.connect(self.on_finished)
33
+
34
+ # 指令歷史紀錄 / Command history
35
+ self.history, self.history_index = [], -1
36
+
37
+ # 輸出區域 / Output area
38
+ self.output = QPlainTextEdit(readOnly=True)
39
+ self.output.setMaximumBlockCount(10000) # 最多保留 10000 行 / Keep up to 10000 lines
40
+ self.output.setStyleSheet("font-family: Consolas, monospace;")
41
+
42
+ # 輸入框 / Input field
43
+ self.input = QLineEdit()
44
+ self.input.setPlaceholderText("Enter command") # 預設提示文字 / Placeholder text
45
+ self.input.returnPressed.connect(self.run_command) # 按 Enter 執行指令 / Run command on Enter
46
+ self.input.installEventFilter(self) # 安裝事件過濾器,用於上下鍵瀏覽歷史 / Install event filter for history navigation
47
+
48
+ # 控制按鈕 / Control buttons
49
+ self.btn_run = QPushButton(self.language_word_dict_get("dynamic_console_run"))
50
+ self.btn_run.clicked.connect(self.run_command)
51
+
52
+ self.btn_stop = QPushButton(self.language_word_dict_get("dynamic_console_stop"))
53
+ self.btn_stop.clicked.connect(self.proc.stop)
54
+
55
+ self.btn_clear = QPushButton(self.language_word_dict_get("dynamic_console_clear"))
56
+ self.btn_clear.clicked.connect(self.output.clear)
57
+
58
+ # 工作目錄顯示與選擇 / Current working directory display and picker
59
+ self.cwd_label = QLabel(f"{self.language_word_dict_get('dynamic_console_cwd')}: {os.getcwd()}")
60
+ self.btn_pick_cwd = QPushButton("…")
61
+ self.btn_pick_cwd.clicked.connect(self.pick_cwd)
62
+
63
+ # Shell 選擇下拉選單 / Shell selection combobox
64
+ self.shell_combo = QComboBox()
65
+ self.shell_combo.addItems(["auto", "cmd", "powershell", "bash", "sh"])
66
+
67
+ # 版面配置:上方 (工作目錄 + Shell 選擇) / Layout: Top (CWD + Shell selection)
68
+ top = QHBoxLayout()
69
+ top.addWidget(self.cwd_label)
70
+ top.addWidget(self.btn_pick_cwd)
71
+ top.addStretch()
72
+ top.addWidget(QLabel(self.language_word_dict_get("dynamic_console_shell")))
73
+ top.addWidget(self.shell_combo)
74
+
75
+ # 版面配置:中間 (輸入框 + 按鈕) / Layout: Middle (Input + Buttons)
76
+ mid = QHBoxLayout()
77
+ mid.addWidget(self.input, 1)
78
+ mid.addWidget(self.btn_run)
79
+ mid.addWidget(self.btn_stop)
80
+ mid.addWidget(self.btn_clear)
81
+
82
+ # 主版面配置 / Main layout
83
+ lay = QVBoxLayout(self)
84
+ lay.addLayout(top)
85
+ lay.addWidget(self.output, 1)
86
+ lay.addLayout(mid)
87
+
88
+ # 初始化狀態訊息 / Initial status message
89
+ self.proc.system.emit(self.language_word_dict_get("dynamic_console_ready"))
90
+
91
+ # 啟動互動式 shell / Start interactive shell
92
+ self.proc.start_shell(self.shell_combo.currentText())
93
+
94
+ # 事件過濾器:支援上下鍵瀏覽歷史指令
95
+ # Event filter: Support navigating command history with Up/Down keys
96
+ def eventFilter(self, obj, event):
97
+ if obj is self.input and isinstance(event, QKeyEvent) and event.type() == QEvent.Type.KeyPress:
98
+ if event.key() == Qt.Key.Key_Up:
99
+ self.history_prev()
100
+ return True
101
+ if event.key() == Qt.Key.Key_Down:
102
+ self.history_next()
103
+ return True
104
+ return super().eventFilter(obj, event)
105
+
106
+ # 瀏覽上一個歷史指令 / Navigate to previous command in history
107
+ def history_prev(self):
108
+ if not self.history: return
109
+ self.history_index = len(self.history) - 1 if self.history_index < 0 else max(0, self.history_index - 1)
110
+ self.input.setText(self.history[self.history_index])
111
+ self.input.end(False)
112
+
113
+ # 瀏覽下一個歷史指令 / Navigate to next command in history
114
+ def history_next(self):
115
+ if not self.history: return
116
+ if self.history_index < 0: return
117
+ self.history_index += 1
118
+ if self.history_index >= len(self.history):
119
+ self.history_index = -1
120
+ self.input.clear()
121
+ else:
122
+ self.input.setText(self.history[self.history_index])
123
+ self.input.end(False)
124
+
125
+ # 選擇新的工作目錄 / Pick a new working directory
126
+ def pick_cwd(self):
127
+ d = QFileDialog.getExistingDirectory(self, "Select working directory", os.getcwd())
128
+ if d:
129
+ self.proc.set_cwd(d)
130
+ self.cwd_label.setText(f"{self.language_word_dict_get('dynamic_console_cwd')}: {d}")
131
+ self.proc.system.emit(f'cd "{d}"')
132
+
133
+ # 在輸出區域新增文字 (支援顏色) / Append text to output area (with optional color)
134
+ def append_text(self, text, color=None):
135
+ cursor = self.output.textCursor()
136
+ cursor.movePosition(QTextCursor.MoveOperation.End)
137
+ fmt = self.output.currentCharFormat()
138
+ if color:
139
+ fmt.setForeground(QColor(color))
140
+ cursor.setCharFormat(fmt)
141
+ cursor.insertText(text)
142
+ self.output.setTextCursor(cursor)
143
+ self.output.ensureCursorVisible()
144
+
145
+ # 執行輸入的指令 / Run the entered command
146
+ def run_command(self):
147
+ cmd = self.input.text().strip()
148
+ if not cmd: return
149
+ if not self.history or self.history[-1] != cmd:
150
+ self.history.append(cmd)
151
+ self.history_index = -1
152
+ self.append_text(f"{self.language_word_dict_get('dynamic_console_prompt')}{cmd}\n", "#0aa")
153
+ self.proc.send_command(cmd)
154
+ self.input.clear()
155
+
156
+ # 子程序結束時的處理 / Handle process finished event
157
+ def on_finished(self, code, status):
158
+ self.append_text(
159
+ f"\n{self.language_word_dict_get('dynamic_console_done').format(code=code, status=status)}\n",
160
+ "#888"
161
+ )
162
+ self.proc.system.emit(self.language_word_dict_get("dynamic_console_ready"))