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,175 @@
1
+ """
2
+ カスタムプロンプト編集ダイアログ
3
+ """
4
+ import tkinter as tk
5
+ from tkinter import ttk, scrolledtext
6
+ from typing import Optional
7
+
8
+ from thonny import get_workbench
9
+ from ..i18n import tr
10
+
11
+
12
+ class CustomPromptDialog(tk.Toplevel):
13
+ """カスタムシステムプロンプトを編集するダイアログ"""
14
+
15
+ def __init__(self, parent, current_prompt: str = ""):
16
+ super().__init__(parent)
17
+
18
+ self.title(tr("Edit Custom System Prompt"))
19
+ self.geometry("600x500")
20
+
21
+ # モーダルダイアログ
22
+ self.transient(parent)
23
+
24
+ self.result: Optional[str] = None
25
+ self.current_prompt = current_prompt
26
+
27
+ self._init_ui()
28
+
29
+ # 現在のプロンプトを設定
30
+ if self.current_prompt:
31
+ self.text_editor.insert("1.0", self.current_prompt)
32
+ # プロンプトが空の場合は何も挿入しない(ユーザーがプリセットから選ぶ)
33
+
34
+ # フォーカスを設定
35
+ self.text_editor.focus_set()
36
+
37
+ # Escキーで閉じる
38
+ self.bind("<Escape>", lambda e: self.destroy())
39
+
40
+ def _init_ui(self):
41
+ """UIを初期化"""
42
+ # メインコンテナ
43
+ main_frame = ttk.Frame(self, padding="10")
44
+ main_frame.pack(fill="both", expand=True)
45
+
46
+ # 説明ラベル
47
+ description = ttk.Label(
48
+ main_frame,
49
+ text=tr("Enter your custom system prompt. This will be used to instruct the AI on how to respond."),
50
+ wraplength=550
51
+ )
52
+ description.pack(pady=(0, 10))
53
+
54
+ # ヒントフレーム
55
+ hint_frame = ttk.LabelFrame(main_frame, text=tr("Tips"), padding="5")
56
+ hint_frame.pack(fill="x", pady=(0, 10))
57
+
58
+ hints = [
59
+ tr("• Use {skill_level} to reference the user's skill level"),
60
+ tr("• Use {language} to reference the output language"),
61
+ tr("• Be specific about the coding style and explanation depth"),
62
+ tr("• Include examples of how you want the AI to respond")
63
+ ]
64
+
65
+ # 変数の説明を追加
66
+ var_explanation = ttk.Label(
67
+ hint_frame,
68
+ text=tr("Variables: {skill_level} = 'beginner/intermediate/advanced (with detailed description)', {language} = 'ja/en/zh-CN/zh-TW/auto'"),
69
+ foreground="blue",
70
+ wraplength=530
71
+ )
72
+ var_explanation.pack(anchor="w", pady=(5, 0))
73
+
74
+ for hint in hints:
75
+ ttk.Label(hint_frame, text=hint, foreground="gray").pack(anchor="w")
76
+
77
+ # テキストエディタ
78
+ editor_frame = ttk.Frame(main_frame)
79
+ editor_frame.pack(fill="both", expand=True, pady=(0, 10))
80
+
81
+ self.text_editor = scrolledtext.ScrolledText(
82
+ editor_frame,
83
+ wrap=tk.WORD,
84
+ font=("Consolas", 10),
85
+ height=15
86
+ )
87
+ self.text_editor.pack(fill="both", expand=True)
88
+
89
+ # プリセットボタンフレーム
90
+ preset_frame = ttk.Frame(main_frame)
91
+ preset_frame.pack(fill="x", pady=(0, 10))
92
+
93
+ ttk.Label(preset_frame, text=tr("Presets:")).pack(side="left", padx=(0, 10))
94
+
95
+ ttk.Button(
96
+ preset_frame,
97
+ text=tr("Default"),
98
+ command=self._insert_default_preset,
99
+ width=15
100
+ ).pack(side="left", padx=2)
101
+
102
+ ttk.Button(
103
+ preset_frame,
104
+ text=tr("Educational"),
105
+ command=self._insert_educational_preset,
106
+ width=15
107
+ ).pack(side="left", padx=2)
108
+
109
+ ttk.Button(
110
+ preset_frame,
111
+ text=tr("Professional"),
112
+ command=self._insert_professional_preset,
113
+ width=15
114
+ ).pack(side="left", padx=2)
115
+
116
+ ttk.Button(
117
+ preset_frame,
118
+ text=tr("Minimal"),
119
+ command=self._insert_minimal_preset,
120
+ width=15
121
+ ).pack(side="left", padx=2)
122
+
123
+ # ボタンフレーム
124
+ button_frame = ttk.Frame(main_frame)
125
+ button_frame.pack(fill="x")
126
+
127
+ ttk.Button(
128
+ button_frame,
129
+ text=tr("Cancel"),
130
+ command=self.destroy
131
+ ).pack(side="right", padx=(5, 0))
132
+
133
+ ttk.Button(
134
+ button_frame,
135
+ text=tr("Save"),
136
+ command=self._save_and_close,
137
+ style="Accent.TButton"
138
+ ).pack(side="right")
139
+
140
+ def _insert_default_template(self):
141
+ """デフォルトのテンプレートを挿入"""
142
+ from ..prompts import DEFAULT_SYSTEM_PROMPT_TEMPLATE
143
+
144
+ self.text_editor.delete("1.0", tk.END)
145
+ self.text_editor.insert("1.0", DEFAULT_SYSTEM_PROMPT_TEMPLATE)
146
+
147
+ def _insert_educational_preset(self):
148
+ """教育向けプリセットを挿入"""
149
+ from ..prompts import EDUCATIONAL_PRESET_TEMPLATE
150
+
151
+ self.text_editor.delete("1.0", tk.END)
152
+ self.text_editor.insert("1.0", EDUCATIONAL_PRESET_TEMPLATE)
153
+
154
+ def _insert_professional_preset(self):
155
+ """プロフェッショナル向けプリセットを挿入"""
156
+ from ..prompts import PROFESSIONAL_PRESET_TEMPLATE
157
+
158
+ self.text_editor.delete("1.0", tk.END)
159
+ self.text_editor.insert("1.0", PROFESSIONAL_PRESET_TEMPLATE)
160
+
161
+ def _insert_minimal_preset(self):
162
+ """最小限のプリセットを挿入"""
163
+ from ..prompts import MINIMAL_PRESET_TEMPLATE
164
+
165
+ self.text_editor.delete("1.0", tk.END)
166
+ self.text_editor.insert("1.0", MINIMAL_PRESET_TEMPLATE)
167
+
168
+ def _insert_default_preset(self):
169
+ """デフォルトプリセットを挿入"""
170
+ self._insert_default_template()
171
+
172
+ def _save_and_close(self):
173
+ """保存して閉じる"""
174
+ self.result = self.text_editor.get("1.0", tk.END).strip()
175
+ self.destroy()
@@ -0,0 +1,484 @@
1
+ """
2
+ Markdown renderer for chat messages
3
+ Converts markdown to HTML and provides interactive features
4
+ """
5
+ import re
6
+ from typing import Optional
7
+ import markdown
8
+ from pygments import highlight
9
+ from pygments.lexers import get_lexer_by_name, PythonLexer
10
+ from pygments.formatters import HtmlFormatter
11
+
12
+
13
+ class MarkdownRenderer:
14
+ """Markdownテキストを対話機能付きのHTMLに変換"""
15
+
16
+ def __init__(self):
17
+ # Markdownパーサーの設定
18
+ # fenced_codeとcodehiliteを除外して、独自のコードブロック処理を使用
19
+ self.md = markdown.Markdown(
20
+ extensions=[
21
+ 'markdown.extensions.tables',
22
+ 'markdown.extensions.nl2br',
23
+ 'markdown.extensions.sane_lists',
24
+ ]
25
+ )
26
+
27
+ # Pygmentsのスタイルを設定
28
+ self.formatter = HtmlFormatter(style='friendly', nowrap=False)
29
+ self.css_style = self.formatter.get_style_defs('.highlight')
30
+
31
+ # コードブロックのIDカウンター
32
+ self.code_block_id = 0
33
+
34
+ def render(self, text: str, sender: str = "assistant") -> str:
35
+ """
36
+ MarkdownテキストをHTMLに変換
37
+
38
+ Args:
39
+ text: Markdownテキスト
40
+ sender: 送信者("user", "assistant", "system")
41
+
42
+ Returns:
43
+ HTML文字列
44
+ """
45
+ # コードブロックを一時的に置換(後で処理)
46
+ code_blocks = []
47
+ # より柔軟な正規表現パターン(改行の有無に対応)
48
+ code_pattern = re.compile(r'```(\w*)\n?(.*?)```', re.DOTALL)
49
+
50
+ def replace_code_block(match):
51
+ lang = match.group(1) or 'python'
52
+ code = match.group(2).strip()
53
+ code_blocks.append((lang, code))
54
+ # 一意のプレースホルダーを生成
55
+ placeholder_id = f'CODEBLOCK{len(code_blocks)-1}CODEBLOCK'
56
+ return f'\n\n{placeholder_id}\n\n'
57
+
58
+ # コードブロックを一時的なプレースホルダーに置換
59
+ text_with_placeholders = code_pattern.sub(replace_code_block, text)
60
+
61
+ # Markdownを変換
62
+ html_content = self.md.convert(text_with_placeholders)
63
+
64
+ # コードブロックを処理して戻す
65
+ for i, (lang, code) in enumerate(code_blocks):
66
+ code_html = self._render_code_block(lang, code)
67
+ placeholder = f'CODEBLOCK{i}CODEBLOCK'
68
+ # <p>タグで囲まれている場合も考慮
69
+ html_content = html_content.replace(f'<p>{placeholder}</p>', code_html)
70
+ html_content = html_content.replace(placeholder, code_html)
71
+
72
+ # メッセージ全体をラップ
73
+ sender_class = f"message-{sender}"
74
+ full_html = f'''
75
+ <div class="message {sender_class}">
76
+ <div class="message-header">{sender.title()}</div>
77
+ <div class="message-content">
78
+ {html_content}
79
+ </div>
80
+ </div>
81
+ '''
82
+
83
+ return full_html
84
+
85
+ def _render_code_block(self, language: str, code: str) -> str:
86
+ """
87
+ コードブロックをシンタックスハイライト付きでレンダリング
88
+ Copy/Insertボタンも追加
89
+ """
90
+ self.code_block_id += 1
91
+ block_id = f"code-block-{self.code_block_id}"
92
+
93
+ # シンタックスハイライト
94
+ try:
95
+ if language:
96
+ lexer = get_lexer_by_name(language, stripall=True)
97
+ else:
98
+ lexer = PythonLexer(stripall=True)
99
+ highlighted_code = highlight(code, lexer, self.formatter)
100
+ except Exception:
101
+ # フォールバック
102
+ highlighted_code = f'<pre><code>{self._escape_html(code)}</code></pre>'
103
+
104
+ # エスケープされたコードを保存(JavaScript用)
105
+ escaped_code = self._escape_js_string(code)
106
+
107
+ # HTMLを生成
108
+ return f'''
109
+ <div class="code-block" id="{block_id}">
110
+ <div class="code-header">
111
+ <div class="code-language-wrapper">
112
+ <span class="code-language">{language or 'text'}</span>
113
+ </div>
114
+ <div class="code-buttons-wrapper">
115
+ <button class="code-button copy-button" onclick="copyCode('{block_id}')">
116
+ Copy
117
+ </button>
118
+ <button class="code-button insert-button" onclick="insertCode('{block_id}')">
119
+ Insert
120
+ </button>
121
+ </div>
122
+ </div>
123
+ <div class="code-content">
124
+ {highlighted_code}
125
+ </div>
126
+ <textarea class="code-source" style="display: none;" id="{block_id}-source">{self._escape_html(code)}</textarea>
127
+ </div>
128
+ '''
129
+
130
+ def _escape_html(self, text: str) -> str:
131
+ """HTMLエスケープ"""
132
+ return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
133
+
134
+ def _escape_js_string(self, text: str) -> str:
135
+ """JavaScript文字列用のエスケープ"""
136
+ # シングルクォート、ダブルクォート、改行、バックスラッシュをエスケープ
137
+ return (text
138
+ .replace('\\', '\\\\')
139
+ .replace("'", "\\'")
140
+ .replace('"', '\\"')
141
+ .replace('\n', '\\n')
142
+ .replace('\r', '\\r')
143
+ .replace('\t', '\\t'))
144
+
145
+ def get_full_html(self, messages: list) -> str:
146
+ """
147
+ メッセージリストから完全なHTMLを生成
148
+
149
+ Args:
150
+ messages: [(sender, text), ...] のリスト
151
+
152
+ Returns:
153
+ 完全なHTML文書
154
+ """
155
+ # コードブロックIDをリセット(完全再生成時)
156
+ self.code_block_id = 0
157
+
158
+ # メッセージをレンダリング
159
+ messages_html = []
160
+ for sender, text in messages:
161
+ messages_html.append(self.render(text, sender))
162
+
163
+
164
+ # 完全なHTML文書を生成
165
+ return f'''
166
+ <!DOCTYPE html>
167
+ <html>
168
+ <head>
169
+ <meta charset="UTF-8">
170
+ <style>
171
+ /* 基本スタイル */
172
+ body {{
173
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
174
+ margin: 0;
175
+ padding: 5px;
176
+ background-color: #f8f8f8;
177
+ font-size: 13px;
178
+ line-height: 1.4;
179
+ /* スムーズスクロールを無効化(プログラム制御のため) */
180
+ scroll-behavior: auto !important;
181
+ }}
182
+
183
+ /* メッセージスタイル */
184
+ .message {{
185
+ margin-bottom: 10px;
186
+ background: white;
187
+ border-radius: 6px;
188
+ padding: 8px 10px;
189
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
190
+ /* 更新時のちらつき防止 */
191
+ transform: translateZ(0);
192
+ will-change: contents;
193
+ }}
194
+
195
+ .message-header {{
196
+ font-weight: bold;
197
+ margin-bottom: 4px;
198
+ color: #333;
199
+ font-size: 12px;
200
+ }}
201
+
202
+ .message-user .message-header {{
203
+ color: #0066cc;
204
+ }}
205
+
206
+ .message-assistant .message-header {{
207
+ color: #006600;
208
+ }}
209
+
210
+ .message-system .message-header {{
211
+ color: #666666;
212
+ }}
213
+
214
+ .message-content {{
215
+ color: #333;
216
+ }}
217
+
218
+ /* コードブロックスタイル */
219
+ .code-block {{
220
+ margin: 6px 0;
221
+ border: 1px solid #e1e4e8;
222
+ border-radius: 4px;
223
+ overflow: hidden;
224
+ background: #f6f8fa;
225
+ }}
226
+
227
+ .code-header {{
228
+ padding: 4px 10px;
229
+ background: #f1f3f5;
230
+ border-bottom: 1px solid #e1e4e8;
231
+ min-height: 26px;
232
+ overflow: hidden;
233
+ }}
234
+
235
+ .code-header:after {{
236
+ content: "";
237
+ display: table;
238
+ clear: both;
239
+ }}
240
+
241
+ .code-language-wrapper {{
242
+ float: left;
243
+ line-height: 22px;
244
+ }}
245
+
246
+ .code-buttons-wrapper {{
247
+ float: right;
248
+ line-height: 22px;
249
+ }}
250
+
251
+ .code-language {{
252
+ font-size: 11px;
253
+ color: #586069;
254
+ font-weight: 500;
255
+ line-height: 18px;
256
+ }}
257
+
258
+ .code-button {{
259
+ display: inline-block;
260
+ }}
261
+
262
+ .code-button + .code-button {{
263
+ margin-left: 6px;
264
+ }}
265
+
266
+ .code-button {{
267
+ padding: 2px 8px;
268
+ font-size: 10px;
269
+ border: 1px solid #d1d5da;
270
+ background: white;
271
+ border-radius: 3px;
272
+ cursor: pointer;
273
+ color: #333;
274
+ font-weight: 500;
275
+ transition: background-color 0.2s;
276
+ }}
277
+
278
+ .code-button:hover {{
279
+ background: #f3f4f6;
280
+ border-color: #c2c7cd;
281
+ }}
282
+
283
+ .code-button.copy-button {{
284
+ background-color: #0066cc;
285
+ color: white;
286
+ border-color: #0066cc;
287
+ }}
288
+
289
+ .code-button.copy-button:hover {{
290
+ background-color: #0052a3;
291
+ }}
292
+
293
+ .code-button.insert-button {{
294
+ background-color: #28a745;
295
+ color: white;
296
+ border-color: #28a745;
297
+ }}
298
+
299
+ .code-button.insert-button:hover {{
300
+ background-color: #218838;
301
+ }}
302
+
303
+ .code-content {{
304
+ padding: 8px;
305
+ overflow-x: auto;
306
+ }}
307
+
308
+ .code-content pre {{
309
+ margin: 0;
310
+ font-family: "Consolas", "Monaco", "Courier New", monospace;
311
+ font-size: 12px;
312
+ line-height: 1.3;
313
+ }}
314
+
315
+ /* Pygmentsスタイル */
316
+ {self.css_style}
317
+
318
+ /* その他のMarkdown要素 */
319
+ p {{
320
+ margin: 0 0 6px 0;
321
+ }}
322
+
323
+ ul, ol {{
324
+ margin: 0 0 6px 0;
325
+ padding-left: 18px;
326
+ }}
327
+
328
+ blockquote {{
329
+ margin: 0 0 6px 0;
330
+ padding: 0 0 0 12px;
331
+ border-left: 3px solid #dfe2e5;
332
+ color: #6a737d;
333
+ }}
334
+
335
+ table {{
336
+ border-collapse: collapse;
337
+ margin-bottom: 6px;
338
+ }}
339
+
340
+ th, td {{
341
+ border: 1px solid #dfe2e5;
342
+ padding: 4px 8px;
343
+ }}
344
+
345
+ th {{
346
+ background-color: #f1f3f5;
347
+ font-weight: 600;
348
+ }}
349
+
350
+ /* コピー成功の通知 */
351
+ .copy-success {{
352
+ position: fixed;
353
+ bottom: 20px;
354
+ right: 20px;
355
+ background: #28a745;
356
+ color: white;
357
+ padding: 10px 20px;
358
+ border-radius: 4px;
359
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
360
+ animation: slideIn 0.3s ease-out;
361
+ }}
362
+
363
+ @keyframes slideIn {{
364
+ from {{
365
+ transform: translateY(100%);
366
+ opacity: 0;
367
+ }}
368
+ to {{
369
+ transform: translateY(0);
370
+ opacity: 1;
371
+ }}
372
+ }}
373
+ </style>
374
+ <script>
375
+ // コードをコピー
376
+ function copyCode(blockId) {{
377
+ var sourceElement = document.getElementById(blockId + '-source');
378
+ if (!sourceElement) {{
379
+ showNotification('Code source not found', 'error');
380
+ return;
381
+ }}
382
+ var code = sourceElement.value;
383
+
384
+ // Python関数を使用(PythonMonkey経由)
385
+ if (typeof pyCopyCode !== 'undefined') {{
386
+ try {{
387
+ var result = pyCopyCode(code);
388
+ if (result) {{
389
+ showCopySuccess();
390
+ }} else {{
391
+ showNotification('Failed to copy', 'error');
392
+ }}
393
+ }} catch (error) {{
394
+ console.error('Copy error:', error);
395
+ // フォールバック: Clipboard API
396
+ navigator.clipboard.writeText(code).then(function() {{
397
+ showCopySuccess();
398
+ }}).catch(function(error) {{
399
+ showNotification('Failed to copy', 'error');
400
+ }});
401
+ }}
402
+ }} else {{
403
+ // フォールバック: Clipboard API
404
+ navigator.clipboard.writeText(code).then(function() {{
405
+ showCopySuccess();
406
+ }}).catch(function(error) {{
407
+ showNotification('Failed to copy', 'error');
408
+ }});
409
+ }}
410
+ }}
411
+
412
+ // コードを挿入(Thonnyのエディタに)
413
+ function insertCode(blockId) {{
414
+ var sourceElement = document.getElementById(blockId + '-source');
415
+ if (!sourceElement) {{
416
+ showNotification('Code source not found', 'error');
417
+ return;
418
+ }}
419
+ var code = sourceElement.value;
420
+
421
+ // Python関数を使用(PythonMonkey経由)
422
+ if (typeof pyInsertCode !== 'undefined') {{
423
+ try {{
424
+ var result = pyInsertCode(code);
425
+ if (!result) {{
426
+ showNotification('Please open a file in the editor first', 'error');
427
+ }}
428
+ // 成功時はナビゲーションを防ぐため何もしない
429
+ return false;
430
+ }} catch (error) {{
431
+ console.error('Insert error:', error);
432
+ // フォールバック: URL経由
433
+ window.location.href = 'thonny:insert:' + encodeURIComponent(code);
434
+ }}
435
+ }} else {{
436
+ // フォールバック: URL経由でPythonに送信
437
+ window.location.href = 'thonny:insert:' + encodeURIComponent(code);
438
+ }}
439
+ }}
440
+
441
+
442
+ // コピー成功通知
443
+ function showCopySuccess() {{
444
+ showNotification('Code copied!', 'success');
445
+ }}
446
+
447
+ // 汎用的な通知関数
448
+ function showNotification(message, type) {{
449
+ var notification = document.createElement('div');
450
+ notification.className = 'copy-success';
451
+ notification.textContent = message;
452
+
453
+ // タイプに応じて色を変更
454
+ if (type === 'error') {{
455
+ notification.style.backgroundColor = '#dc3545';
456
+ }} else if (type === 'info') {{
457
+ notification.style.backgroundColor = '#17a2b8';
458
+ }}
459
+
460
+ document.body.appendChild(notification);
461
+
462
+ setTimeout(function() {{
463
+ notification.remove();
464
+ }}, 2000);
465
+ }}
466
+ </script>
467
+ </head>
468
+ <body>
469
+ <div id="messages">
470
+ {''.join(messages_html)}
471
+ </div>
472
+ <script>
473
+ // ページの準備完了を示すフラグ
474
+ window.pageReady = false;
475
+
476
+ // ページ読み込み時に最下部にスクロール
477
+ window.addEventListener('load', function() {{
478
+ window.scrollTo(0, document.body.scrollHeight);
479
+ window.pageReady = true;
480
+ }});
481
+ </script>
482
+ </body>
483
+ </html>
484
+ '''