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.
Files changed (27) hide show
  1. thonny_codemate-0.1.0.dist-info/METADATA +307 -0
  2. thonny_codemate-0.1.0.dist-info/RECORD +27 -0
  3. thonny_codemate-0.1.0.dist-info/WHEEL +5 -0
  4. thonny_codemate-0.1.0.dist-info/licenses/LICENSE +21 -0
  5. thonny_codemate-0.1.0.dist-info/top_level.txt +1 -0
  6. thonnycontrib/__init__.py +1 -0
  7. thonnycontrib/thonny_codemate/__init__.py +397 -0
  8. thonnycontrib/thonny_codemate/api.py +154 -0
  9. thonnycontrib/thonny_codemate/context_manager.py +296 -0
  10. thonnycontrib/thonny_codemate/external_providers.py +714 -0
  11. thonnycontrib/thonny_codemate/i18n.py +506 -0
  12. thonnycontrib/thonny_codemate/llm_client.py +841 -0
  13. thonnycontrib/thonny_codemate/message_virtualization.py +136 -0
  14. thonnycontrib/thonny_codemate/model_manager.py +515 -0
  15. thonnycontrib/thonny_codemate/performance_monitor.py +141 -0
  16. thonnycontrib/thonny_codemate/prompts.py +102 -0
  17. thonnycontrib/thonny_codemate/ui/__init__.py +1 -0
  18. thonnycontrib/thonny_codemate/ui/chat_view.py +687 -0
  19. thonnycontrib/thonny_codemate/ui/chat_view_html.py +1299 -0
  20. thonnycontrib/thonny_codemate/ui/custom_prompt_dialog.py +175 -0
  21. thonnycontrib/thonny_codemate/ui/markdown_renderer.py +484 -0
  22. thonnycontrib/thonny_codemate/ui/model_download_dialog.py +355 -0
  23. thonnycontrib/thonny_codemate/ui/settings_dialog.py +1218 -0
  24. thonnycontrib/thonny_codemate/utils/__init__.py +25 -0
  25. thonnycontrib/thonny_codemate/utils/constants.py +138 -0
  26. thonnycontrib/thonny_codemate/utils/error_messages.py +92 -0
  27. 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)