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,355 @@
|
|
1
|
+
"""
|
2
|
+
モデルダウンロードダイアログ
|
3
|
+
推奨モデルのダウンロードと管理UI
|
4
|
+
"""
|
5
|
+
import tkinter as tk
|
6
|
+
from tkinter import ttk, messagebox
|
7
|
+
import threading
|
8
|
+
from typing import Dict, List
|
9
|
+
|
10
|
+
from ..model_manager import ModelManager, DownloadProgress
|
11
|
+
|
12
|
+
|
13
|
+
class ModelDownloadDialog(tk.Toplevel):
|
14
|
+
"""モデルダウンロード管理ダイアログ"""
|
15
|
+
|
16
|
+
def __init__(self, parent):
|
17
|
+
super().__init__(parent)
|
18
|
+
|
19
|
+
self.title("Model Manager")
|
20
|
+
self.geometry("700x600")
|
21
|
+
self.resizable(False, True) # 横幅は固定、縦は可変
|
22
|
+
|
23
|
+
self.model_manager = ModelManager()
|
24
|
+
self.model_widgets = {}
|
25
|
+
self._download_progress = {} # ダウンロード進捗を追跡
|
26
|
+
|
27
|
+
self._init_ui()
|
28
|
+
self._refresh_model_list()
|
29
|
+
|
30
|
+
def _init_ui(self):
|
31
|
+
"""UIを初期化"""
|
32
|
+
# メインフレーム
|
33
|
+
main_frame = ttk.Frame(self, padding="10")
|
34
|
+
main_frame.grid(row=0, column=0, sticky="nsew")
|
35
|
+
main_frame.columnconfigure(0, minsize=650)
|
36
|
+
|
37
|
+
self.columnconfigure(0, weight=1)
|
38
|
+
self.rowconfigure(0, weight=1)
|
39
|
+
main_frame.columnconfigure(0, weight=1)
|
40
|
+
main_frame.rowconfigure(1, weight=1)
|
41
|
+
|
42
|
+
# ヘッダー
|
43
|
+
header_frame = ttk.Frame(main_frame)
|
44
|
+
header_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10))
|
45
|
+
|
46
|
+
ttk.Label(header_frame, text="Recommended Models", font=("", 12, "bold")).pack(side=tk.LEFT, padx=(10, 0))
|
47
|
+
|
48
|
+
ttk.Button(
|
49
|
+
header_frame,
|
50
|
+
text="Refresh",
|
51
|
+
command=self._refresh_model_list,
|
52
|
+
width=12
|
53
|
+
).pack(side=tk.RIGHT, padx=5)
|
54
|
+
|
55
|
+
# モデルリストフレーム(スクロール可能)
|
56
|
+
list_container = ttk.Frame(main_frame)
|
57
|
+
list_container.grid(row=1, column=0, sticky="nsew")
|
58
|
+
|
59
|
+
list_frame = ttk.Frame(list_container, width=650)
|
60
|
+
list_frame.pack(fill="both", expand=True)
|
61
|
+
list_frame.columnconfigure(0, weight=1)
|
62
|
+
list_frame.rowconfigure(0, weight=1)
|
63
|
+
|
64
|
+
# スクロールバー付きキャンバス
|
65
|
+
canvas = tk.Canvas(list_frame, highlightthickness=0, width=630)
|
66
|
+
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
|
67
|
+
self.scrollable_frame = ttk.Frame(canvas)
|
68
|
+
|
69
|
+
self.scrollable_frame.bind(
|
70
|
+
"<Configure>",
|
71
|
+
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
72
|
+
)
|
73
|
+
|
74
|
+
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw", width=630)
|
75
|
+
canvas.configure(yscrollcommand=scrollbar.set)
|
76
|
+
|
77
|
+
# マウスホイールのバインディング
|
78
|
+
def _on_mousewheel(event):
|
79
|
+
canvas.yview_scroll(int(-1*(event.delta/120)), "units")
|
80
|
+
|
81
|
+
def _on_enter(event):
|
82
|
+
# Windows
|
83
|
+
canvas.bind_all("<MouseWheel>", _on_mousewheel)
|
84
|
+
# Linux/macOS
|
85
|
+
canvas.bind_all("<Button-4>", lambda e: canvas.yview_scroll(-1, "units"))
|
86
|
+
canvas.bind_all("<Button-5>", lambda e: canvas.yview_scroll(1, "units"))
|
87
|
+
|
88
|
+
def _on_leave(event):
|
89
|
+
# バインディングを解除
|
90
|
+
canvas.unbind_all("<MouseWheel>")
|
91
|
+
canvas.unbind_all("<Button-4>")
|
92
|
+
canvas.unbind_all("<Button-5>")
|
93
|
+
|
94
|
+
# キャンバスにマウスが入った時/出た時のイベント
|
95
|
+
canvas.bind("<Enter>", _on_enter)
|
96
|
+
canvas.bind("<Leave>", _on_leave)
|
97
|
+
|
98
|
+
canvas.grid(row=0, column=0, sticky="nsew")
|
99
|
+
scrollbar.grid(row=0, column=1, sticky="ns")
|
100
|
+
|
101
|
+
def _refresh_model_list(self):
|
102
|
+
"""モデルリストを更新"""
|
103
|
+
# 既存のウィジェットをクリア
|
104
|
+
for widget in self.scrollable_frame.winfo_children():
|
105
|
+
widget.destroy()
|
106
|
+
self.model_widgets.clear()
|
107
|
+
|
108
|
+
# モデルリストを取得
|
109
|
+
models = self.model_manager.list_available_models()
|
110
|
+
|
111
|
+
# 各モデルのUIを作成
|
112
|
+
for i, model in enumerate(models):
|
113
|
+
self._create_model_widget(model, i)
|
114
|
+
|
115
|
+
def _create_model_widget(self, model: Dict, row: int):
|
116
|
+
"""個別のモデルウィジェットを作成"""
|
117
|
+
# モデルフレーム
|
118
|
+
model_frame = ttk.LabelFrame(
|
119
|
+
self.scrollable_frame,
|
120
|
+
text=model["name"],
|
121
|
+
padding="10"
|
122
|
+
)
|
123
|
+
model_frame.grid(row=row, column=0, sticky="ew", pady=5, padx=5)
|
124
|
+
self.scrollable_frame.columnconfigure(0, weight=1, minsize=620)
|
125
|
+
|
126
|
+
# 説明
|
127
|
+
desc_label = ttk.Label(
|
128
|
+
model_frame,
|
129
|
+
text=model["description"],
|
130
|
+
wraplength=580
|
131
|
+
)
|
132
|
+
desc_label.grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 5))
|
133
|
+
|
134
|
+
# サイズと用途と言語
|
135
|
+
languages = model.get('languages', ['en'])
|
136
|
+
lang_text = ', '.join([
|
137
|
+
{'en': 'English', 'zh': 'Chinese', 'ja': 'Japanese', 'multi': 'Multilingual'}.get(lang, lang)
|
138
|
+
for lang in languages
|
139
|
+
])
|
140
|
+
info_text = f"Size: {model['size']} | Languages: {lang_text}"
|
141
|
+
ttk.Label(model_frame, text=info_text, foreground="gray").grid(
|
142
|
+
row=1, column=0, sticky="w"
|
143
|
+
)
|
144
|
+
|
145
|
+
# ステータスとボタン
|
146
|
+
status_frame = ttk.Frame(model_frame)
|
147
|
+
status_frame.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(5, 0))
|
148
|
+
|
149
|
+
if model["installed"]:
|
150
|
+
# インストール済み
|
151
|
+
status_label = ttk.Label(status_frame, text="✓ Installed", foreground="green")
|
152
|
+
status_label.pack(side=tk.LEFT)
|
153
|
+
|
154
|
+
# 使用ボタン
|
155
|
+
use_button = ttk.Button(
|
156
|
+
status_frame,
|
157
|
+
text="Use This Model",
|
158
|
+
command=lambda: self._use_model(model),
|
159
|
+
width=18
|
160
|
+
)
|
161
|
+
use_button.pack(side=tk.LEFT, padx=5)
|
162
|
+
|
163
|
+
# 削除ボタン
|
164
|
+
delete_button = ttk.Button(
|
165
|
+
status_frame,
|
166
|
+
text="Delete",
|
167
|
+
command=lambda: self._delete_model(model),
|
168
|
+
width=12
|
169
|
+
)
|
170
|
+
delete_button.pack(side=tk.RIGHT)
|
171
|
+
|
172
|
+
elif model["downloading"]:
|
173
|
+
# ダウンロード中
|
174
|
+
status_label = ttk.Label(status_frame, text="Downloading...", foreground="orange")
|
175
|
+
status_label.pack(side=tk.LEFT)
|
176
|
+
|
177
|
+
# プログレスバー
|
178
|
+
progress_bar = ttk.Progressbar(
|
179
|
+
status_frame,
|
180
|
+
mode='determinate',
|
181
|
+
length=200,
|
182
|
+
maximum=100
|
183
|
+
)
|
184
|
+
progress_bar.pack(side=tk.LEFT, padx=5)
|
185
|
+
|
186
|
+
# 詳細情報フレーム
|
187
|
+
detail_frame = ttk.Frame(model_frame)
|
188
|
+
detail_frame.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(5, 0))
|
189
|
+
|
190
|
+
# 進捗詳細ラベル
|
191
|
+
progress_detail_label = ttk.Label(
|
192
|
+
detail_frame,
|
193
|
+
text="Preparing download...",
|
194
|
+
font=("", 9)
|
195
|
+
)
|
196
|
+
progress_detail_label.pack(side=tk.LEFT)
|
197
|
+
|
198
|
+
# 速度と残り時間ラベル
|
199
|
+
speed_eta_label = ttk.Label(
|
200
|
+
detail_frame,
|
201
|
+
text="",
|
202
|
+
font=("", 9),
|
203
|
+
foreground="gray"
|
204
|
+
)
|
205
|
+
speed_eta_label.pack(side=tk.RIGHT)
|
206
|
+
|
207
|
+
self.model_widgets[model["key"]] = {
|
208
|
+
"frame": model_frame,
|
209
|
+
"status_label": status_label,
|
210
|
+
"progress_bar": progress_bar,
|
211
|
+
"progress_detail_label": progress_detail_label,
|
212
|
+
"speed_eta_label": speed_eta_label
|
213
|
+
}
|
214
|
+
|
215
|
+
else:
|
216
|
+
# 未インストール
|
217
|
+
status_label = ttk.Label(status_frame, text="Not installed", foreground="gray")
|
218
|
+
status_label.pack(side=tk.LEFT)
|
219
|
+
|
220
|
+
# ダウンロードボタン
|
221
|
+
download_button = ttk.Button(
|
222
|
+
status_frame,
|
223
|
+
text="Download",
|
224
|
+
command=lambda: self._download_model(model),
|
225
|
+
width=15
|
226
|
+
)
|
227
|
+
download_button.pack(side=tk.LEFT, padx=5)
|
228
|
+
|
229
|
+
def _download_model(self, model: Dict):
|
230
|
+
"""モデルをダウンロード"""
|
231
|
+
model_key = model["key"]
|
232
|
+
|
233
|
+
# カスタムモデルはダウンロードできない
|
234
|
+
if model_key.startswith("custom_"):
|
235
|
+
messagebox.showinfo("Info", "This is a custom model.", parent=self)
|
236
|
+
return
|
237
|
+
|
238
|
+
# 確認ダイアログ
|
239
|
+
if not messagebox.askyesno(
|
240
|
+
"Download Model",
|
241
|
+
f"Download {model['name']}?\nSize: {model['size']}",
|
242
|
+
parent=self
|
243
|
+
):
|
244
|
+
return
|
245
|
+
|
246
|
+
# ダウンロード開始
|
247
|
+
# プログレスキューを使用してスレッドセーフに更新
|
248
|
+
import queue
|
249
|
+
progress_queue = queue.Queue()
|
250
|
+
|
251
|
+
def progress_callback(progress: DownloadProgress):
|
252
|
+
# キューに追加(スレッドセーフ)
|
253
|
+
progress_queue.put((model_key, progress))
|
254
|
+
|
255
|
+
# ダウンロード開始を記録
|
256
|
+
self._download_progress[model_key] = True
|
257
|
+
|
258
|
+
# キューをチェックする関数
|
259
|
+
def check_progress_queue():
|
260
|
+
has_items = False
|
261
|
+
try:
|
262
|
+
while True:
|
263
|
+
key, progress = progress_queue.get_nowait()
|
264
|
+
self._update_download_progress(key, progress)
|
265
|
+
has_items = True
|
266
|
+
except queue.Empty:
|
267
|
+
pass
|
268
|
+
|
269
|
+
# 次のチェックをスケジュール(ダウンロード中の場合)
|
270
|
+
if has_items or model_key in self._download_progress:
|
271
|
+
self.after(100, check_progress_queue)
|
272
|
+
|
273
|
+
# キューチェックを開始
|
274
|
+
self.after(100, check_progress_queue)
|
275
|
+
|
276
|
+
# バックグラウンドでダウンロード
|
277
|
+
import threading
|
278
|
+
download_thread = threading.Thread(
|
279
|
+
target=lambda: self.model_manager.download_model(model_key, progress_callback),
|
280
|
+
daemon=True
|
281
|
+
)
|
282
|
+
download_thread.start()
|
283
|
+
|
284
|
+
# UIを更新
|
285
|
+
self._refresh_model_list()
|
286
|
+
|
287
|
+
def _update_download_progress(self, model_key: str, progress: DownloadProgress):
|
288
|
+
"""ダウンロード進捗を更新"""
|
289
|
+
if progress.status == "completed":
|
290
|
+
# ダウンロード完了時に追跡を削除
|
291
|
+
if model_key in self._download_progress:
|
292
|
+
del self._download_progress[model_key]
|
293
|
+
messagebox.showinfo("Success", f"{progress.model_name} downloaded successfully!", parent=self)
|
294
|
+
self._refresh_model_list()
|
295
|
+
elif progress.status == "error":
|
296
|
+
# エラー時も追跡を削除
|
297
|
+
if model_key in self._download_progress:
|
298
|
+
del self._download_progress[model_key]
|
299
|
+
# エラーメッセージを短縮(最初の行のみ表示)
|
300
|
+
error_lines = progress.error_message.split('\n')
|
301
|
+
short_error = error_lines[0] if error_lines else "Unknown error"
|
302
|
+
messagebox.showerror("Download Error", f"Failed to download {progress.model_name}:\n\n{short_error}", parent=self)
|
303
|
+
self._refresh_model_list()
|
304
|
+
elif progress.status == "downloading":
|
305
|
+
# 進捗更新
|
306
|
+
if model_key in self.model_widgets:
|
307
|
+
widgets = self.model_widgets[model_key]
|
308
|
+
|
309
|
+
# プログレスバーを更新
|
310
|
+
if "progress_bar" in widgets:
|
311
|
+
widgets["progress_bar"]["value"] = progress.percentage
|
312
|
+
|
313
|
+
# 詳細ラベルを更新
|
314
|
+
if "progress_detail_label" in widgets:
|
315
|
+
widgets["progress_detail_label"]["text"] = f"{progress.size_str} ({progress.percentage:.1f}%)"
|
316
|
+
|
317
|
+
# 速度と残り時間を更新
|
318
|
+
if "speed_eta_label" in widgets:
|
319
|
+
if progress.speed > 0:
|
320
|
+
widgets["speed_eta_label"]["text"] = f"{progress.speed_str} | ETA: {progress.eta_str}"
|
321
|
+
else:
|
322
|
+
widgets["speed_eta_label"]["text"] = "Connecting..."
|
323
|
+
|
324
|
+
def _use_model(self, model: Dict):
|
325
|
+
"""モデルを使用"""
|
326
|
+
from thonny import get_workbench
|
327
|
+
workbench = get_workbench()
|
328
|
+
|
329
|
+
# 設定を更新
|
330
|
+
workbench.set_option("llm.model_path", model["path"])
|
331
|
+
|
332
|
+
messagebox.showinfo(
|
333
|
+
"Model Selected",
|
334
|
+
f"{model['name']} is now the active model.\n\nRestart the LLM Assistant to use this model.",
|
335
|
+
parent=self
|
336
|
+
)
|
337
|
+
|
338
|
+
# 親ダイアログの設定変更フラグを立てる
|
339
|
+
if hasattr(self.master, 'settings_changed'):
|
340
|
+
self.master.settings_changed = True
|
341
|
+
|
342
|
+
def _delete_model(self, model: Dict):
|
343
|
+
"""モデルを削除"""
|
344
|
+
if not messagebox.askyesno(
|
345
|
+
"Delete Model",
|
346
|
+
f"Delete {model['name']}?\nThis action cannot be undone.",
|
347
|
+
parent=self
|
348
|
+
):
|
349
|
+
return
|
350
|
+
|
351
|
+
if self.model_manager.delete_model(model["path"]):
|
352
|
+
messagebox.showinfo("Success", "Model deleted successfully.", parent=self)
|
353
|
+
self._refresh_model_list()
|
354
|
+
else:
|
355
|
+
messagebox.showerror("Error", "Failed to delete model.", parent=self)
|