thonny-codemate 0.1.0__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.
- thonny_codemate-0.1.0.dist-info/METADATA +307 -0
- thonny_codemate-0.1.0.dist-info/RECORD +27 -0
- thonny_codemate-0.1.0.dist-info/WHEEL +5 -0
- thonny_codemate-0.1.0.dist-info/licenses/LICENSE +21 -0
- thonny_codemate-0.1.0.dist-info/top_level.txt +1 -0
- thonnycontrib/__init__.py +1 -0
- thonnycontrib/thonny_codemate/__init__.py +397 -0
- thonnycontrib/thonny_codemate/api.py +154 -0
- thonnycontrib/thonny_codemate/context_manager.py +296 -0
- thonnycontrib/thonny_codemate/external_providers.py +714 -0
- thonnycontrib/thonny_codemate/i18n.py +506 -0
- thonnycontrib/thonny_codemate/llm_client.py +841 -0
- thonnycontrib/thonny_codemate/message_virtualization.py +136 -0
- thonnycontrib/thonny_codemate/model_manager.py +515 -0
- thonnycontrib/thonny_codemate/performance_monitor.py +141 -0
- thonnycontrib/thonny_codemate/prompts.py +102 -0
- thonnycontrib/thonny_codemate/ui/__init__.py +1 -0
- thonnycontrib/thonny_codemate/ui/chat_view.py +687 -0
- thonnycontrib/thonny_codemate/ui/chat_view_html.py +1299 -0
- thonnycontrib/thonny_codemate/ui/custom_prompt_dialog.py +175 -0
- thonnycontrib/thonny_codemate/ui/markdown_renderer.py +484 -0
- thonnycontrib/thonny_codemate/ui/model_download_dialog.py +355 -0
- thonnycontrib/thonny_codemate/ui/settings_dialog.py +1218 -0
- thonnycontrib/thonny_codemate/utils/__init__.py +25 -0
- thonnycontrib/thonny_codemate/utils/constants.py +138 -0
- thonnycontrib/thonny_codemate/utils/error_messages.py +92 -0
- thonnycontrib/thonny_codemate/utils/unified_error_handler.py +310 -0
@@ -0,0 +1,687 @@
|
|
1
|
+
"""
|
2
|
+
LLMチャットビュー
|
3
|
+
GitHub Copilot風の右側パネルUIを提供
|
4
|
+
"""
|
5
|
+
import tkinter as tk
|
6
|
+
from tkinter import ttk, scrolledtext, messagebox
|
7
|
+
import threading
|
8
|
+
import queue
|
9
|
+
import logging
|
10
|
+
import json
|
11
|
+
from pathlib import Path
|
12
|
+
from typing import Optional
|
13
|
+
|
14
|
+
from thonny import get_workbench
|
15
|
+
|
16
|
+
# 安全なロガーを使用
|
17
|
+
try:
|
18
|
+
from .. import get_safe_logger
|
19
|
+
logger = get_safe_logger(__name__)
|
20
|
+
except ImportError:
|
21
|
+
# フォールバック
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
logger.addHandler(logging.NullHandler())
|
24
|
+
|
25
|
+
|
26
|
+
class LLMChatView(ttk.Frame):
|
27
|
+
"""
|
28
|
+
LLMとのチャットインターフェースを提供するビュー
|
29
|
+
Thonnyの右側に表示される
|
30
|
+
"""
|
31
|
+
|
32
|
+
def __init__(self, master):
|
33
|
+
super().__init__(master)
|
34
|
+
|
35
|
+
self.llm_client = None
|
36
|
+
self._init_ui()
|
37
|
+
self._init_llm()
|
38
|
+
|
39
|
+
# メッセージキュー(スレッド間通信用)
|
40
|
+
self.message_queue = queue.Queue()
|
41
|
+
self._processing = False
|
42
|
+
self._first_token = True # ストリーミング用のフラグ
|
43
|
+
self._stop_generation = False # 生成を停止するフラグ
|
44
|
+
|
45
|
+
# 定期的にキューをチェック
|
46
|
+
self._queue_check_id = self.after(100, self._process_queue)
|
47
|
+
|
48
|
+
# ウィンドウ閉じるイベントをバインド
|
49
|
+
self.bind("<Destroy>", self._on_destroy)
|
50
|
+
|
51
|
+
# チャット履歴を格納
|
52
|
+
self.chat_history = []
|
53
|
+
|
54
|
+
# 会話履歴を読み込む
|
55
|
+
self._load_chat_history()
|
56
|
+
|
57
|
+
def _init_ui(self):
|
58
|
+
"""UIコンポーネントを初期化"""
|
59
|
+
# メインコンテナ
|
60
|
+
self.columnconfigure(0, weight=1)
|
61
|
+
self.rowconfigure(1, weight=1)
|
62
|
+
|
63
|
+
# ヘッダー
|
64
|
+
header_frame = ttk.Frame(self)
|
65
|
+
header_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
66
|
+
|
67
|
+
ttk.Label(header_frame, text="LLM Assistant", font=("", 12, "bold")).pack(side=tk.LEFT)
|
68
|
+
|
69
|
+
# Clearボタン
|
70
|
+
self.clear_button = ttk.Button(
|
71
|
+
header_frame,
|
72
|
+
text="Clear",
|
73
|
+
command=self._clear_chat,
|
74
|
+
width=8
|
75
|
+
)
|
76
|
+
self.clear_button.pack(side=tk.LEFT, padx=10)
|
77
|
+
|
78
|
+
# 設定ボタン
|
79
|
+
self.settings_button = ttk.Button(
|
80
|
+
header_frame,
|
81
|
+
text="⚙",
|
82
|
+
width=3,
|
83
|
+
command=self._show_settings
|
84
|
+
)
|
85
|
+
self.settings_button.pack(side=tk.RIGHT, padx=2)
|
86
|
+
|
87
|
+
# ステータスフレーム(モデル名とステータス)
|
88
|
+
status_frame = ttk.Frame(header_frame)
|
89
|
+
status_frame.pack(side=tk.RIGHT, padx=5)
|
90
|
+
|
91
|
+
self.status_label = ttk.Label(status_frame, text="Not loaded", foreground="gray")
|
92
|
+
self.status_label.pack(side=tk.RIGHT)
|
93
|
+
|
94
|
+
# チャット表示エリア
|
95
|
+
chat_frame = ttk.Frame(self)
|
96
|
+
chat_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
|
97
|
+
chat_frame.columnconfigure(0, weight=1)
|
98
|
+
chat_frame.rowconfigure(0, weight=1)
|
99
|
+
|
100
|
+
self.chat_display = scrolledtext.ScrolledText(
|
101
|
+
chat_frame,
|
102
|
+
wrap=tk.WORD,
|
103
|
+
width=40,
|
104
|
+
height=20,
|
105
|
+
font=("Consolas", 10),
|
106
|
+
state=tk.DISABLED,
|
107
|
+
background="#f8f8f8"
|
108
|
+
)
|
109
|
+
self.chat_display.grid(row=0, column=0, sticky="nsew")
|
110
|
+
|
111
|
+
# タグの設定
|
112
|
+
self.chat_display.tag_config("user", foreground="#0066cc", font=("Consolas", 10, "bold"))
|
113
|
+
self.chat_display.tag_config("assistant", foreground="#006600")
|
114
|
+
self.chat_display.tag_config("error", foreground="#cc0000")
|
115
|
+
self.chat_display.tag_config("code", background="#e8e8e8", font=("Consolas", 9))
|
116
|
+
|
117
|
+
# 入力エリア
|
118
|
+
input_frame = ttk.Frame(self)
|
119
|
+
input_frame.grid(row=2, column=0, sticky="ew", padx=5, pady=5)
|
120
|
+
input_frame.columnconfigure(0, weight=1)
|
121
|
+
|
122
|
+
# 入力テキスト
|
123
|
+
self.input_text = tk.Text(
|
124
|
+
input_frame,
|
125
|
+
height=3,
|
126
|
+
font=("Consolas", 10),
|
127
|
+
wrap=tk.WORD
|
128
|
+
)
|
129
|
+
self.input_text.grid(row=0, column=0, sticky="ew", pady=(0, 5))
|
130
|
+
|
131
|
+
# ボタンフレーム
|
132
|
+
button_frame = ttk.Frame(input_frame)
|
133
|
+
button_frame.grid(row=1, column=0, sticky="ew")
|
134
|
+
|
135
|
+
# send button
|
136
|
+
self.send_button = ttk.Button(
|
137
|
+
button_frame,
|
138
|
+
text="Send",
|
139
|
+
command=self._handle_send_button,
|
140
|
+
state=tk.DISABLED
|
141
|
+
)
|
142
|
+
self.send_button.pack(side=tk.RIGHT, padx=2)
|
143
|
+
|
144
|
+
# Ctrl+Enterのヒントラベル
|
145
|
+
hint_label = ttk.Label(
|
146
|
+
button_frame,
|
147
|
+
text="Ctrl+Enter to send",
|
148
|
+
foreground="gray",
|
149
|
+
font=("", 9)
|
150
|
+
)
|
151
|
+
hint_label.pack(side=tk.RIGHT, padx=5)
|
152
|
+
|
153
|
+
|
154
|
+
# プリセットボタン(幅を指定して文字が切れないようにする)
|
155
|
+
ttk.Button(
|
156
|
+
button_frame,
|
157
|
+
text="Explain Error",
|
158
|
+
command=self._explain_last_error,
|
159
|
+
width=15 # 幅を指定
|
160
|
+
).pack(side=tk.LEFT, padx=2)
|
161
|
+
|
162
|
+
|
163
|
+
# コンテキストボタン
|
164
|
+
self.context_var = tk.BooleanVar(value=False)
|
165
|
+
self.context_check = ttk.Checkbutton(
|
166
|
+
button_frame,
|
167
|
+
text="Include Context",
|
168
|
+
variable=self.context_var,
|
169
|
+
command=self._toggle_context
|
170
|
+
)
|
171
|
+
self.context_check.pack(side=tk.LEFT, padx=10)
|
172
|
+
|
173
|
+
# コンテキストマネージャー
|
174
|
+
self.context_manager = None
|
175
|
+
|
176
|
+
# キーバインディング
|
177
|
+
self.input_text.bind("<Control-Return>", lambda e: self._handle_send_button())
|
178
|
+
self.input_text.bind("<Shift-Return>", lambda e: "break") # 改行を許可
|
179
|
+
|
180
|
+
# Escapeキーで生成を停止
|
181
|
+
self.bind_all("<Escape>", lambda e: self._stop_if_processing())
|
182
|
+
|
183
|
+
def _init_llm(self):
|
184
|
+
"""LLMクライアントを初期化"""
|
185
|
+
try:
|
186
|
+
from .. import get_llm_client
|
187
|
+
from ..model_manager import ModelManager
|
188
|
+
|
189
|
+
self.llm_client = get_llm_client()
|
190
|
+
|
191
|
+
workbench = get_workbench()
|
192
|
+
provider = workbench.get_option("llm.provider", "local")
|
193
|
+
|
194
|
+
if provider == "local":
|
195
|
+
# ローカルモデルの場合
|
196
|
+
model_path = workbench.get_option("llm.model_path", "")
|
197
|
+
|
198
|
+
if not model_path or not Path(model_path).exists():
|
199
|
+
# モデルがない場合、利用可能なモデルをチェック
|
200
|
+
manager = ModelManager()
|
201
|
+
available_model = manager.get_model_path("llama3.2-1b") or manager.get_model_path("llama3.2-3b")
|
202
|
+
|
203
|
+
if available_model:
|
204
|
+
# 利用可能なモデルがある場合は自動設定
|
205
|
+
workbench.set_option("llm.model_path", available_model)
|
206
|
+
model_path = available_model
|
207
|
+
else:
|
208
|
+
# モデルがない場合はダウンロードを促す
|
209
|
+
self.status_label.config(text="No model loaded", foreground="red")
|
210
|
+
self._append_message(
|
211
|
+
"System",
|
212
|
+
"No model found. Please download a model from Settings → Download Models.",
|
213
|
+
"error"
|
214
|
+
)
|
215
|
+
|
216
|
+
# 設定ダイアログを開くボタンを表示
|
217
|
+
# 親ウィンドウを指定してダイアログを表示
|
218
|
+
if messagebox.askyesno(
|
219
|
+
"No Model Found",
|
220
|
+
"No LLM model found. Would you like to download recommended models?",
|
221
|
+
parent=self
|
222
|
+
):
|
223
|
+
self._show_settings()
|
224
|
+
return
|
225
|
+
|
226
|
+
# 非同期でモデルをロード
|
227
|
+
# モデル名を表示しながらロード
|
228
|
+
model_name = Path(model_path).name if model_path else "model"
|
229
|
+
self.status_label.config(text=f"Loading {model_name}...", foreground="orange")
|
230
|
+
self.llm_client.load_model_async(callback=self._on_model_loaded)
|
231
|
+
else:
|
232
|
+
# 外部プロバイダーの場合、モデル名も含める
|
233
|
+
external_model = workbench.get_option("llm.external_model", "")
|
234
|
+
display_text = f"{external_model} ({provider})" if external_model else f"Using {provider}"
|
235
|
+
self.status_label.config(text=display_text, foreground="blue")
|
236
|
+
# config取得で外部プロバイダーが設定される
|
237
|
+
self.llm_client.get_config()
|
238
|
+
self.send_button.config(state=tk.NORMAL)
|
239
|
+
self._append_message(
|
240
|
+
"System",
|
241
|
+
f"Connected to {provider.upper()} API. Ready to chat!",
|
242
|
+
"assistant"
|
243
|
+
)
|
244
|
+
|
245
|
+
except Exception as e:
|
246
|
+
logger.error(f"Failed to initialize LLM client: {e}")
|
247
|
+
self.status_label.config(text="Error loading model", foreground="red")
|
248
|
+
self._append_message("System", f"Failed to initialize LLM: {str(e)}", "error")
|
249
|
+
|
250
|
+
def _on_model_loaded(self, success: bool, error: Optional[Exception]):
|
251
|
+
"""モデル読み込み完了時のコールバック"""
|
252
|
+
def update_ui():
|
253
|
+
if success:
|
254
|
+
# モデル名を取得
|
255
|
+
model_path = get_workbench().get_option("llm.model_path", "")
|
256
|
+
model_name = Path(model_path).name if model_path else "Unknown"
|
257
|
+
self.status_label.config(text=f"{model_name} | Ready", foreground="green")
|
258
|
+
self.send_button.config(state=tk.NORMAL)
|
259
|
+
self._append_message("System", "LLM model loaded successfully!", "assistant")
|
260
|
+
else:
|
261
|
+
self.status_label.config(text="Load failed", foreground="red")
|
262
|
+
self._append_message("System", f"Failed to load model: {error}", "error")
|
263
|
+
|
264
|
+
# UIスレッドで更新
|
265
|
+
self.after(0, update_ui)
|
266
|
+
|
267
|
+
def _append_message(self, sender: str, message: str, tag: str = None):
|
268
|
+
"""チャット表示にメッセージを追加"""
|
269
|
+
self.chat_display.config(state=tk.NORMAL)
|
270
|
+
|
271
|
+
# 送信者を表示
|
272
|
+
self.chat_display.insert(tk.END, f"\n{sender}: ", tag or "user")
|
273
|
+
|
274
|
+
# メッセージを表示
|
275
|
+
if tag == "code" or "```" in message:
|
276
|
+
# コードブロックを検出して整形
|
277
|
+
parts = message.split("```")
|
278
|
+
for i, part in enumerate(parts):
|
279
|
+
if i % 2 == 0:
|
280
|
+
self.chat_display.insert(tk.END, part, tag or "assistant")
|
281
|
+
else:
|
282
|
+
# コードブロック
|
283
|
+
lines = part.split('\n')
|
284
|
+
if lines[0]: # 言語指定がある場合
|
285
|
+
self.chat_display.insert(tk.END, f"\n[{lines[0]}]\n", "assistant")
|
286
|
+
code = '\n'.join(lines[1:])
|
287
|
+
else:
|
288
|
+
code = part
|
289
|
+
self.chat_display.insert(tk.END, code, "code")
|
290
|
+
else:
|
291
|
+
self.chat_display.insert(tk.END, message, tag or "assistant")
|
292
|
+
|
293
|
+
self.chat_display.insert(tk.END, "\n")
|
294
|
+
self.chat_display.see(tk.END)
|
295
|
+
self.chat_display.config(state=tk.DISABLED)
|
296
|
+
|
297
|
+
# 履歴に追加(システムメッセージ以外)
|
298
|
+
if sender not in ["System"] and not message.startswith("["):
|
299
|
+
self.chat_history.append({"sender": sender, "message": message})
|
300
|
+
# ユーザーとアシスタントのメッセージのみ保存
|
301
|
+
if sender in ["User", "Assistant"]:
|
302
|
+
self._save_chat_history()
|
303
|
+
|
304
|
+
def _handle_send_button(self):
|
305
|
+
"""送信/停止ボタンのハンドラー"""
|
306
|
+
if self._processing:
|
307
|
+
# 生成中の場合は停止
|
308
|
+
self._stop_generation = True
|
309
|
+
self.send_button.config(text="Stopping...")
|
310
|
+
self._append_message("System", "Stopping generation...", "info")
|
311
|
+
else:
|
312
|
+
# 通常の送信処理
|
313
|
+
self._send_message()
|
314
|
+
|
315
|
+
def _stop_if_processing(self):
|
316
|
+
"""処理中の場合は生成を停止"""
|
317
|
+
if self._processing:
|
318
|
+
self._stop_generation = True
|
319
|
+
self.send_button.config(text="Stopping...")
|
320
|
+
self._append_message("System", "Stopping generation... (ESC pressed)", "info")
|
321
|
+
|
322
|
+
def _send_message(self):
|
323
|
+
"""メッセージを送信"""
|
324
|
+
message = self.input_text.get("1.0", tk.END).strip()
|
325
|
+
if not message:
|
326
|
+
return
|
327
|
+
|
328
|
+
# UIをクリア
|
329
|
+
self.input_text.delete("1.0", tk.END)
|
330
|
+
|
331
|
+
# コンテキスト情報を取得
|
332
|
+
context_info = None
|
333
|
+
if self.context_var.get() and self.context_manager:
|
334
|
+
workbench = get_workbench()
|
335
|
+
editor = workbench.get_editor_notebook().get_current_editor()
|
336
|
+
if editor:
|
337
|
+
current_file = editor.get_filename()
|
338
|
+
text_widget = editor.get_text_widget()
|
339
|
+
|
340
|
+
if text_widget.tag_ranges("sel"):
|
341
|
+
# 選択範囲がある場合
|
342
|
+
start_line = int(text_widget.index("sel.first").split(".")[0])
|
343
|
+
end_line = int(text_widget.index("sel.last").split(".")[0])
|
344
|
+
file_name = Path(current_file).name if current_file else "Unknown"
|
345
|
+
context_info = f"[Context: {file_name} (lines {start_line}-{end_line})]"
|
346
|
+
elif current_file:
|
347
|
+
# ファイル全体の場合
|
348
|
+
file_name = Path(current_file).name
|
349
|
+
context_info = f"[Context: {file_name} (entire file)]"
|
350
|
+
|
351
|
+
# ユーザーメッセージを追加(コンテキスト情報付き)
|
352
|
+
if context_info:
|
353
|
+
display_message = f"{message}\n\n{context_info}"
|
354
|
+
else:
|
355
|
+
display_message = message
|
356
|
+
|
357
|
+
self._append_message("You", display_message, "user")
|
358
|
+
|
359
|
+
# 処理中フラグ
|
360
|
+
self._processing = True
|
361
|
+
self._first_token = True # ストリーミング用フラグをリセット
|
362
|
+
self._stop_generation = False # 停止フラグをリセット
|
363
|
+
self.send_button.config(text="Stop", state=tk.NORMAL) # ボタンを停止モードに変更
|
364
|
+
|
365
|
+
# バックグラウンドで処理
|
366
|
+
thread = threading.Thread(
|
367
|
+
target=self._generate_response,
|
368
|
+
args=(message,), # 元のメッセージを渡す(コンテキスト情報なし)
|
369
|
+
daemon=True
|
370
|
+
)
|
371
|
+
thread.start()
|
372
|
+
|
373
|
+
def _generate_response(self, message: str):
|
374
|
+
"""バックグラウンドで応答を生成"""
|
375
|
+
try:
|
376
|
+
# コンテキストを含める場合
|
377
|
+
if self.context_var.get() and self.context_manager:
|
378
|
+
# 現在のエディタのファイルパスを取得
|
379
|
+
workbench = get_workbench()
|
380
|
+
editor = workbench.get_editor_notebook().get_current_editor()
|
381
|
+
current_file = None
|
382
|
+
selected_text = None
|
383
|
+
selection_info = None
|
384
|
+
|
385
|
+
if editor:
|
386
|
+
current_file = editor.get_filename()
|
387
|
+
text_widget = editor.get_text_widget()
|
388
|
+
|
389
|
+
# 選択範囲があるかチェック
|
390
|
+
if text_widget.tag_ranges("sel"):
|
391
|
+
selected_text = text_widget.get("sel.first", "sel.last")
|
392
|
+
# 選択範囲の行番号を取得
|
393
|
+
start_line = int(text_widget.index("sel.first").split(".")[0])
|
394
|
+
end_line = int(text_widget.index("sel.last").split(".")[0])
|
395
|
+
selection_info = f"Selected lines: {start_line}-{end_line}"
|
396
|
+
|
397
|
+
if selected_text:
|
398
|
+
# 選択範囲がある場合はそれをコンテキストとして使用
|
399
|
+
# ファイル拡張子から言語を判定
|
400
|
+
file_ext = Path(current_file).suffix.lower() if current_file else '.py'
|
401
|
+
lang_map = {'.py': 'python', '.js': 'javascript', '.java': 'java', '.cpp': 'cpp', '.c': 'c'}
|
402
|
+
lang = lang_map.get(file_ext, 'python')
|
403
|
+
|
404
|
+
context_str = f"""File: {Path(current_file).name if current_file else 'Unknown'}
|
405
|
+
{selection_info}
|
406
|
+
|
407
|
+
```{lang}
|
408
|
+
{selected_text}
|
409
|
+
```"""
|
410
|
+
# コンテキスト使用メッセージは不要
|
411
|
+
elif editor and current_file:
|
412
|
+
# 選択範囲がない場合は、ファイル全体を取得
|
413
|
+
full_text = text_widget.get("1.0", tk.END).strip()
|
414
|
+
if full_text:
|
415
|
+
# ファイル拡張子から言語を判定
|
416
|
+
file_ext = Path(current_file).suffix.lower()
|
417
|
+
lang_map = {'.py': 'python', '.js': 'javascript', '.java': 'java', '.cpp': 'cpp', '.c': 'c'}
|
418
|
+
lang = lang_map.get(file_ext, 'python')
|
419
|
+
|
420
|
+
context_str = f"""File: {Path(current_file).name}
|
421
|
+
Full file content:
|
422
|
+
|
423
|
+
```{lang}
|
424
|
+
{full_text}
|
425
|
+
```"""
|
426
|
+
# コンテキスト使用メッセージは不要
|
427
|
+
else:
|
428
|
+
context_str = None
|
429
|
+
else:
|
430
|
+
context_str = None
|
431
|
+
|
432
|
+
if context_str:
|
433
|
+
# コンテキスト付きで生成
|
434
|
+
full_prompt = f"""Here is the context from the current project:
|
435
|
+
|
436
|
+
{context_str}
|
437
|
+
|
438
|
+
Based on this context, {message}"""
|
439
|
+
|
440
|
+
for token in self.llm_client.generate_stream(full_prompt):
|
441
|
+
if self._stop_generation:
|
442
|
+
self.message_queue.put(("info", "\n[Generation stopped by user]"))
|
443
|
+
break
|
444
|
+
self.message_queue.put(("token", token))
|
445
|
+
else:
|
446
|
+
# 通常の生成
|
447
|
+
for token in self.llm_client.generate_stream(message):
|
448
|
+
if self._stop_generation:
|
449
|
+
self.message_queue.put(("info", "\n[Generation stopped by user]"))
|
450
|
+
break
|
451
|
+
self.message_queue.put(("token", token))
|
452
|
+
else:
|
453
|
+
# 通常の生成
|
454
|
+
for token in self.llm_client.generate_stream(message):
|
455
|
+
if self._stop_generation:
|
456
|
+
self.message_queue.put(("info", "\n[Generation stopped by user]"))
|
457
|
+
break
|
458
|
+
self.message_queue.put(("token", token))
|
459
|
+
|
460
|
+
# 完了
|
461
|
+
self.message_queue.put(("complete", None))
|
462
|
+
|
463
|
+
except Exception as e:
|
464
|
+
logger.error(f"Error generating response: {e}")
|
465
|
+
self.message_queue.put(("error", str(e)))
|
466
|
+
|
467
|
+
def _process_queue(self):
|
468
|
+
"""メッセージキューを処理"""
|
469
|
+
try:
|
470
|
+
# キューから全てのメッセージを処理
|
471
|
+
while True:
|
472
|
+
msg_type, content = self.message_queue.get_nowait()
|
473
|
+
|
474
|
+
if msg_type == "token":
|
475
|
+
if self._first_token:
|
476
|
+
# 最初のトークンの時だけAssistantラベルを追加
|
477
|
+
self.chat_display.config(state=tk.NORMAL)
|
478
|
+
self.chat_display.insert(tk.END, "\nAssistant: ", "role")
|
479
|
+
self.chat_display.config(state=tk.DISABLED)
|
480
|
+
self._first_token = False
|
481
|
+
|
482
|
+
# トークンを追加(ラベルなしで)
|
483
|
+
self.chat_display.config(state=tk.NORMAL)
|
484
|
+
self.chat_display.insert(tk.END, content, "assistant")
|
485
|
+
self.chat_display.see(tk.END)
|
486
|
+
self.chat_display.config(state=tk.DISABLED)
|
487
|
+
|
488
|
+
elif msg_type == "complete":
|
489
|
+
self._processing = False
|
490
|
+
self._stop_generation = False # 停止フラグをリセット
|
491
|
+
self.send_button.config(text="Send", state=tk.NORMAL) # ボタンを送信モードに戻す
|
492
|
+
self._first_token = True # 次のメッセージ用にリセット
|
493
|
+
|
494
|
+
elif msg_type == "error":
|
495
|
+
self._append_message("System", f"Error: {content}", "error")
|
496
|
+
self._processing = False
|
497
|
+
self._stop_generation = False # 停止フラグをリセット
|
498
|
+
self.send_button.config(text="Send", state=tk.NORMAL) # ボタンを送信モードに戻す
|
499
|
+
self._first_token = True # 次のメッセージ用にリセット
|
500
|
+
|
501
|
+
elif msg_type == "info":
|
502
|
+
self._append_message("System", content, "assistant")
|
503
|
+
self._first_token = True # 次のメッセージ用にリセット
|
504
|
+
|
505
|
+
except queue.Empty:
|
506
|
+
pass
|
507
|
+
|
508
|
+
# 次のチェックをスケジュール
|
509
|
+
self.after(50, self._process_queue)
|
510
|
+
|
511
|
+
def explain_code(self, code: str):
|
512
|
+
"""コードを説明(外部から呼ばれる)"""
|
513
|
+
# 現在のスキルレベルを取得
|
514
|
+
workbench = get_workbench()
|
515
|
+
skill_level = workbench.get_option("llm.skill_level", "beginner")
|
516
|
+
|
517
|
+
# 現在のファイルから言語を検出
|
518
|
+
editor = workbench.get_editor_notebook().get_current_editor()
|
519
|
+
lang = 'python' # デフォルト
|
520
|
+
if editor:
|
521
|
+
filename = editor.get_filename()
|
522
|
+
if filename:
|
523
|
+
file_ext = Path(filename).suffix.lower()
|
524
|
+
lang_map = {'.py': 'python', '.js': 'javascript', '.java': 'java', '.cpp': 'cpp', '.c': 'c'}
|
525
|
+
lang = lang_map.get(file_ext, 'python')
|
526
|
+
|
527
|
+
# メッセージを作成
|
528
|
+
message = f"Please explain this code:\n```{lang}\n{code}\n```"
|
529
|
+
|
530
|
+
# 入力欄に設定して送信
|
531
|
+
self.input_text.delete("1.0", tk.END)
|
532
|
+
self.input_text.insert("1.0", message)
|
533
|
+
self._send_message()
|
534
|
+
|
535
|
+
def _explain_last_error(self):
|
536
|
+
"""最後のエラーを説明"""
|
537
|
+
try:
|
538
|
+
# シェルビューを取得
|
539
|
+
shell_view = get_workbench().get_view("ShellView")
|
540
|
+
if not shell_view:
|
541
|
+
messagebox.showinfo("No Shell", "Shell view not found.")
|
542
|
+
return
|
543
|
+
|
544
|
+
# シェルのテキストウィジェットを取得
|
545
|
+
shell_text = shell_view.text
|
546
|
+
|
547
|
+
# シェルの内容を取得(最後の部分)
|
548
|
+
shell_content = shell_text.get("1.0", tk.END)
|
549
|
+
lines = shell_content.strip().split('\n')
|
550
|
+
|
551
|
+
# エラーを探す(後ろから検索)
|
552
|
+
error_lines = []
|
553
|
+
error_found = False
|
554
|
+
|
555
|
+
for i in range(len(lines) - 1, -1, -1):
|
556
|
+
line = lines[i]
|
557
|
+
|
558
|
+
# エラーの終わりを検出
|
559
|
+
if error_found and (line.startswith(">>>") or line.startswith("===") or not line.strip()):
|
560
|
+
break
|
561
|
+
|
562
|
+
# エラーパターンを検出
|
563
|
+
if any(error_type in line for error_type in ["Error", "Exception", "Traceback"]):
|
564
|
+
error_found = True
|
565
|
+
|
566
|
+
if error_found:
|
567
|
+
error_lines.insert(0, line)
|
568
|
+
|
569
|
+
if not error_lines:
|
570
|
+
messagebox.showinfo("No Error", "No recent error found in shell.")
|
571
|
+
return
|
572
|
+
|
573
|
+
# エラーメッセージを結合
|
574
|
+
error_message = '\n'.join(error_lines)
|
575
|
+
|
576
|
+
# 関連するコードも取得(エディタから)
|
577
|
+
code = ""
|
578
|
+
editor = get_workbench().get_editor_notebook().get_current_editor()
|
579
|
+
if editor:
|
580
|
+
try:
|
581
|
+
code = editor.get_text_widget().get("1.0", tk.END).strip()
|
582
|
+
except:
|
583
|
+
pass
|
584
|
+
|
585
|
+
# プロンプトを作成
|
586
|
+
if code:
|
587
|
+
prompt = f"I encountered this error:\n```\n{error_message}\n```\n\nIn this code:\n```python\n{code}\n```\n\nPlease explain what causes this error and how to fix it."
|
588
|
+
else:
|
589
|
+
prompt = f"I encountered this error:\n```\n{error_message}\n```\n\nPlease explain what causes this error and provide a solution."
|
590
|
+
|
591
|
+
# 入力欄に設定して送信
|
592
|
+
self.input_text.delete("1.0", tk.END)
|
593
|
+
self.input_text.insert("1.0", prompt)
|
594
|
+
self._send_message()
|
595
|
+
|
596
|
+
except Exception as e:
|
597
|
+
logger.error(f"Error in _explain_last_error: {e}")
|
598
|
+
messagebox.showerror("Error", f"Failed to get error information: {str(e)}")
|
599
|
+
|
600
|
+
def _show_settings(self):
|
601
|
+
"""設定ダイアログを表示"""
|
602
|
+
from .settings_dialog import SettingsDialog
|
603
|
+
dialog = SettingsDialog(self)
|
604
|
+
dialog.grab_set()
|
605
|
+
self.wait_window(dialog)
|
606
|
+
|
607
|
+
# 設定が変更された可能性があるので再初期化
|
608
|
+
if hasattr(dialog, 'settings_changed') and dialog.settings_changed:
|
609
|
+
self._init_llm()
|
610
|
+
|
611
|
+
def _clear_chat(self):
|
612
|
+
"""チャットをクリア"""
|
613
|
+
self.chat_display.config(state=tk.NORMAL)
|
614
|
+
self.chat_display.delete("1.0", tk.END)
|
615
|
+
self.chat_display.config(state=tk.DISABLED)
|
616
|
+
|
617
|
+
# 履歴もクリア
|
618
|
+
self.chat_history.clear()
|
619
|
+
self._save_chat_history()
|
620
|
+
|
621
|
+
def _toggle_context(self):
|
622
|
+
"""コンテキストの有効/無効を切り替え"""
|
623
|
+
if self.context_var.get():
|
624
|
+
# コンテキストマネージャーを初期化
|
625
|
+
if not self.context_manager:
|
626
|
+
from ..context_manager import ContextManager
|
627
|
+
self.context_manager = ContextManager()
|
628
|
+
|
629
|
+
def _get_chat_history_path(self) -> Path:
|
630
|
+
"""チャット履歴ファイルのパスを取得"""
|
631
|
+
workbench = get_workbench()
|
632
|
+
data_dir = Path(workbench.get_configuration_directory())
|
633
|
+
llm_dir = data_dir / "llm_assistant"
|
634
|
+
llm_dir.mkdir(exist_ok=True)
|
635
|
+
return llm_dir / "chat_history_text.json"
|
636
|
+
|
637
|
+
def _save_chat_history(self):
|
638
|
+
"""チャット履歴を保存"""
|
639
|
+
try:
|
640
|
+
# 最新の100メッセージのみ保存
|
641
|
+
messages_to_save = self.chat_history[-100:]
|
642
|
+
|
643
|
+
history_path = self._get_chat_history_path()
|
644
|
+
with open(history_path, 'w', encoding='utf-8') as f:
|
645
|
+
json.dump(messages_to_save, f, ensure_ascii=False, indent=2)
|
646
|
+
|
647
|
+
except Exception as e:
|
648
|
+
logger.error(f"Failed to save chat history: {e}")
|
649
|
+
|
650
|
+
def _load_chat_history(self):
|
651
|
+
"""チャット履歴を読み込む"""
|
652
|
+
try:
|
653
|
+
history_path = self._get_chat_history_path()
|
654
|
+
if history_path.exists():
|
655
|
+
with open(history_path, 'r', encoding='utf-8') as f:
|
656
|
+
saved_messages = json.load(f)
|
657
|
+
|
658
|
+
# 保存されたメッセージを復元
|
659
|
+
for msg in saved_messages:
|
660
|
+
self._append_message(msg["sender"], msg["message"])
|
661
|
+
# _append_messageがさらに履歴に追加するのを防ぐため、最後のものを削除
|
662
|
+
if self.chat_history and self.chat_history[-1] == msg:
|
663
|
+
self.chat_history.pop()
|
664
|
+
|
665
|
+
self.chat_history = saved_messages
|
666
|
+
|
667
|
+
if self.chat_history:
|
668
|
+
self._append_message("System", "Previous conversation restored", "info")
|
669
|
+
|
670
|
+
except Exception as e:
|
671
|
+
logger.error(f"Failed to load chat history: {e}")
|
672
|
+
|
673
|
+
def _on_destroy(self, event):
|
674
|
+
"""ウィンドウが破棄される時のクリーンアップ"""
|
675
|
+
# チャット履歴を保存
|
676
|
+
self._save_chat_history()
|
677
|
+
|
678
|
+
# キューチェックを停止
|
679
|
+
if hasattr(self, '_queue_check_id'):
|
680
|
+
self.after_cancel(self._queue_check_id)
|
681
|
+
|
682
|
+
# 生成を停止
|
683
|
+
self._stop_generation = True
|
684
|
+
|
685
|
+
# LLMクライアントをシャットダウン
|
686
|
+
if self.llm_client:
|
687
|
+
self.llm_client.shutdown()
|