File size: 45,790 Bytes
ca44502
 
65ed2e4
a8ffc25
 
 
ca44502
7a21f64
a8ffc25
7a21f64
 
 
ca44502
 
 
a8ffc25
ca44502
 
 
 
a8ffc25
 
 
 
 
 
ca44502
 
 
 
7a21f64
65ed2e4
 
 
 
 
ca44502
65ed2e4
 
7a21f64
a8ffc25
 
 
65ed2e4
a8ffc25
 
 
 
 
65ed2e4
a8ffc25
 
 
 
 
6003912
65ed2e4
a8ffc25
6003912
a8ffc25
 
6003912
a8ffc25
 
 
a5e26cc
a8ffc25
 
 
 
 
a5e26cc
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
a5e26cc
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a5e26cc
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
a5e26cc
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ca44502
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ca44502
a8ffc25
 
65ed2e4
a8ffc25
 
 
6003912
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6003912
a8ffc25
 
6003912
a8ffc25
 
 
65ed2e4
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
65ed2e4
a8ffc25
 
 
 
 
 
 
 
65ed2e4
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65ed2e4
a8ffc25
 
 
 
 
 
 
 
 
65ed2e4
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65ed2e4
a8ffc25
 
 
 
 
65ed2e4
a8ffc25
 
 
 
 
65ed2e4
a8ffc25
 
65ed2e4
a8ffc25
 
 
 
6003912
a8ffc25
65ed2e4
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5da4d70
a8ffc25
 
5da4d70
a8ffc25
 
 
 
 
65ed2e4
 
 
 
a8ffc25
 
5da4d70
a8ffc25
 
5da4d70
a8ffc25
 
65ed2e4
 
a8ffc25
65ed2e4
a8ffc25
 
65ed2e4
 
a8ffc25
 
6003912
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6003912
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65ed2e4
a8ffc25
10622de
a8ffc25
 
 
10622de
a8ffc25
 
 
 
 
 
 
10622de
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
10622de
a8ffc25
 
10622de
a8ffc25
 
10622de
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10622de
a8ffc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ca44502
a8ffc25
ca44502
 
 
 
 
 
 
 
 
 
65ed2e4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
import streamlit as st
import os
import re
import subprocess
import tempfile
import json
from groq import Groq
from prompts import load_system_prompt, create_default_prompt_files
from streamlit_ace import st_ace

# デフォルトのプロンプトファイルを作成
create_default_prompt_files()

# Streamlitアプリのセットアップ
st.set_page_config(
    page_title="コーディングアシスタント & エディタ",
    page_icon="💻",
    layout="wide"
)

# セッション状態の初期化
if "files" not in st.session_state:
    st.session_state.files = {}  # ファイル名: コンテンツ

if "current_file" not in st.session_state:
    st.session_state.current_file = None

# APIキーの取得(Hugging Face Spaces環境変数から)
api_key = os.getenv("GROQ_API_KEY", "")

# 使用可能なモデルのリスト
MODELS = {
    "mistral_saba": "mistral-saba-24b",
    "qwen_coder": "qwen-2.5-coder-32b",
    "deepseek": "deepseek-r1-distill-llama-70b"
}

# 動作モードの定義
MODES = ["通常モード", "実装モード", "エラー修正モード"]

# サイドバー - モード選択とファイル管理
with st.sidebar:
    st.title("コーディングアシスタント & エディタ 💻")
    
    # モード選択タブ
    selected_mode = st.radio(
        "モード選択",
        MODES
    )
    
    # モード説明の表示
    mode_descriptions = {
        "通常モード": "Mistral Sabaがユーザー入力を分析し、必要に応じてDeepSeekの知識も活用した上で、Qwen Coderが回答を生成します。",
        "実装モード": "ユーザー入力の分析に基づき、Qwen Coderが設計書を作成した後、実装コードを提供します。",
        "エラー修正モード": "提示されたコードのエラーを分析し、Qwen Coderが修正案を提供します。"
    }
    
    st.markdown(f"**{selected_mode}**: {mode_descriptions[selected_mode]}")
    
    # ファイル管理セクション
    st.header("ファイル管理")
    
    # 新規ファイル作成
    new_file_name = st.text_input("新規ファイル名", value="", key="new_file")
    new_file_col1, new_file_col2 = st.columns([3, 1])
    
    with new_file_col1:
        file_extension = st.selectbox(
            "拡張子",
            options=[".py", ".js", ".html", ".css", ".json", ".txt"]
        )
    
    with new_file_col2:
        if st.button("作成", key="create_file_btn"):
            full_filename = new_file_name + file_extension
            if full_filename not in st.session_state.files and new_file_name:
                st.session_state.files[full_filename] = ""
                st.session_state.current_file = full_filename
                st.success(f"ファイル '{full_filename}' を作成しました")
                st.rerun()
            elif not new_file_name:
                st.error("ファイル名を入力してください")
            else:
                st.error(f"ファイル '{full_filename}' は既に存在します")
    
    # 既存ファイル一覧
    if st.session_state.files:
        st.subheader("既存ファイル")
        for filename in st.session_state.files.keys():
            col1, col2, col3 = st.columns([3, 1, 1])
            with col1:
                if st.button(filename, key=f"open_{filename}"):
                    st.session_state.current_file = filename
                    st.rerun()
            with col2:
                if st.button("削除", key=f"delete_{filename}"):
                    del st.session_state.files[filename]
                    if st.session_state.current_file == filename:
                        st.session_state.current_file = None
                    st.rerun()
            with col3:
                if st.button("コピー", key=f"copy_{filename}"):
                    name, ext = os.path.splitext(filename)
                    new_name = f"{name}_copy{ext}"
                    counter = 1
                    while new_name in st.session_state.files:
                        new_name = f"{name}_copy_{counter}{ext}"
                        counter += 1
                    st.session_state.files[new_name] = st.session_state.files[filename]
                    st.rerun()
    
    # チャット履歴のクリア機能
    if st.button("会話履歴をクリア"):
        st.session_state.messages = []
        st.session_state.thinking_process = []
        st.session_state.is_first_message = True
        st.session_state.conversation_state = {
            "topic": None,
            "last_question_type": None,
            "mentioned_entities": set(),
            "follow_up_count": 0
        }
        st.rerun()
    
    # 接続情報と使い方のガイド
    st.sidebar.markdown("---")
    st.subheader("使い方ガイド")
    with st.expander("アプリの機能", expanded=True):
        st.markdown("""
        ### 主な機能
        1. **コードエディタ** - 複数のプログラミング言語に対応したシンタックスハイライト付きエディタ
        2. **Pythonコード実行** - エディタ上のPythonコードをその場で実行
        3. **AIアシスタント** - コーディングに関する質問や実装の提案
        4. **ファイル管理** - 複数のファイルの作成・保存・編集
        5. **エラー修正** - コードのエラーを自動で診断・修正
        
        ### 特殊コマンド
        - `/mode 通常` - 通常の質問応答モードに切り替え
        - `/mode 実装` - コード実装モードに切り替え
        - `/mode エラー修正` - エラー修正モードに切り替え
        """)

    with st.expander("Hugging Face Spacesでの設定方法"):
        st.markdown("""
        ### Hugging Face Spacesでの設定手順
        
        1. **APIキーの設定**:
           - Spacesのダッシュボードに移動
           - Settings > Repository secrets
           - 新しいシークレットを追加: キー名 `GROQ_API_KEY`、値に自分のGroq APIキーを入力
        
        2. **依存関係の追加**:
           - `requirements.txt` に以下を追加:
             ```
             streamlit
             groq
             streamlit-ace
             ```
        
        3. **ファイル構造**:
           - `app.py`: メインアプリケーションファイル
           - `prompts.py`: プロンプト管理用ファイル
           - `prompts/`: プロンプトを保存するディレクトリ
        """)

# メインエリアを2つのカラムに分割
code_col, chat_col = st.columns([1, 1])

# コードエディタカラム
with code_col:
    st.header("コードエディタ")
    
    if st.session_state.current_file:
        # ファイルの拡張子に基づいて言語を設定
        file_ext = os.path.splitext(st.session_state.current_file)[1].lower()
        lang_map = {
            ".py": "python",
            ".js": "javascript",
            ".html": "html",
            ".css": "css",
            ".json": "json",
            ".txt": "text"
        }
        language = lang_map.get(file_ext, "python")
        
        # エディタの表示
        st.subheader(f"編集中: {st.session_state.current_file}")
        
        code = st_ace(
            value=st.session_state.files[st.session_state.current_file],
            language=language,
            theme="monokai",
            keybinding="vscode",
            font_size=14,
            min_lines=20,
            key=f"ace_editor_{st.session_state.current_file}"
        )
        
        # コードを保存
        st.session_state.files[st.session_state.current_file] = code
        
        # Python codeの場合、実行ボタンを表示
        if language == "python":
            if st.button("コードを実行", key="run_code_btn"):
                with st.spinner("コードを実行中..."):
                    # 一時ファイルを作成してコードを実行
                    with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as tmp:
                        tmp.write(code.encode())
                        tmp_name = tmp.name
                    
                    try:
                        # サブプロセスでPythonコードを実行
                        result = subprocess.run(
                            ["python", tmp_name],
                            capture_output=True,
                            text=True,
                            timeout=10  # 10秒のタイムアウト
                        )
                        
                        # 実行結果を表示
                        if result.stdout:
                            st.code(result.stdout, language="text")
                        
                        if result.stderr:
                            st.error(result.stderr)
                            
                            # エラーが発生した場合、自動的にエラー修正モードに切り替える
                            if st.button("エラー修正AIに相談", key="ask_error_fix"):
                                st.session_state.current_mode = "エラー修正モード"
                                error_query = f"次のPythonコードにエラーがあります。修正方法を教えてください。\n\n```python\n{code}\n```\n\nエラーメッセージ:\n```\n{result.stderr}\n```"
                                
                                # チャットにエラー修正用のメッセージを追加
                                if "messages" not in st.session_state:
                                    st.session_state.messages = []
                                
                                st.session_state.messages.append({"role": "user", "content": error_query})
                                st.rerun()
                    
                    except subprocess.TimeoutExpired:
                        st.error("実行がタイムアウトしました(10秒以上かかりました)")
                    
                    except Exception as e:
                        st.error(f"実行中にエラーが発生しました: {str(e)}")
                    
                    finally:
                        # 一時ファイルを削除
                        if os.path.exists(tmp_name):
                            os.unlink(tmp_name)
        
        # ファイルエクスポート機能
        st.download_button(
            label="ファイルをダウンロード",
            data=code,
            file_name=st.session_state.current_file,
            key="download_file"
        )
    
    else:
        st.info("左のサイドバーから既存のファイルを選択するか、新しいファイルを作成してください。")

# チャットカラム
with chat_col:
    st.header("AIアシスタント")
    
    # セッション初期化
    if "messages" not in st.session_state:
        st.session_state.messages = []
        
    if "thinking_process" not in st.session_state:
        st.session_state.thinking_process = []
        
    if "current_mode" not in st.session_state:
        st.session_state.current_mode = selected_mode
        
    if "is_first_message" not in st.session_state:
        st.session_state.is_first_message = True
    
    # 会話状態の初期化
    if "conversation_state" not in st.session_state:
        st.session_state.conversation_state = {
            "topic": None,  # 会話の主要トピック
            "last_question_type": None,  # 前回の質問のタイプ
            "mentioned_entities": set(),  # 会話で言及されたエンティティ
            "follow_up_count": 0  # フォローアップ質問の数
        }
    
    # モード変更の検出
    if st.session_state.current_mode != selected_mode:
        st.session_state.current_mode = selected_mode
        st.session_state.is_first_message = True
        
        # モード変更時にこれまでのチャットを要約
        if len(st.session_state.messages) > 0:
            with st.spinner("前回の会話内容を要約しています..."):
                try:
                    # チャット履歴を文字列にまとめる
                    chat_history = ""
                    for msg in st.session_state.messages:
                        role = "ユーザー" if msg["role"] == "user" else "アシスタント"
                        chat_history += f"{role}: {msg['content']}\n\n"
                    
                    # 要約を生成
                    client = Groq(api_key=api_key)
                    summarize_prompt = f"""
                    以下はユーザーとアシスタントの間の会話です。この会話を300単語以内で要約してください。
                    特に重要な内容、技術的な詳細、解決した問題に焦点を当ててください。
                    
                    {chat_history}
                    """
                    
                    # Mistral Sabaモデルのシステムプロンプトをロード
                    summary_system_prompt = load_system_prompt(MODELS["mistral_saba"])
                    
                    summary_response = client.chat.completions.create(
                        model=MODELS["mistral_saba"],
                        messages=[
                            {"role": "system", "content": summary_system_prompt},
                            {"role": "user", "content": summarize_prompt}
                        ],
                        temperature=0.3,
                        max_tokens=1000
                    )
                    
                    summary = summary_response.choices[0].message.content
                    st.session_state.chat_summary = summary
                    
                    # 要約をthinking_processに追加
                    st.session_state.thinking_process.append({
                        "title": "前回の会話要約",
                        "content": summary
                    })
                    
                    # ユーザーに要約を通知
                    st.info(f"モードが変更されました。前回の会話の要約:\n\n{summary}")
                except Exception as e:
                    st.error(f"会話要約の生成中にエラーが発生しました: {str(e)}")
    
    # 物理学関連のキーワード
    PHYSICS_KEYWORDS = [
        "物理", "力学", "電磁気", "熱力学", "量子", "相対性", "波動", "力", "運動", "エネルギー",
        "physics", "mechanics", "electromagnetism", "thermodynamics", "quantum", "relativity", 
        "wave", "force", "motion", "energy", "シミュレーション", "simulation", "重力", "gravity",
        "加速度", "acceleration", "速度", "velocity", "衝突", "collision", "反発", "rebound",
        "摩擦", "friction", "剛体", "rigid body", "粒子", "particle", "流体", "fluid",
        "弾性", "elastic", "軌道", "trajectory", "バネ", "spring", "圧力", "pressure"
    ]
    
    # テキストが物理学に関連しているかチェック
    def is_physics_related(text):
        lower_text = text.lower()
        
        # 明確な物理関連キーワード(より具体的で誤検出が少ないもの)
        explicit_physics_keywords = [
            "物理法則", "物理シミュレーション", "ニュートン力学", "電磁気学", "熱力学", "量子力学", "相対性理論", 
            "physics simulation", "newton's laws", "electromagnetism", "thermodynamics", 
            "quantum mechanics", "relativity theory"
        ]
        
        # 単語境界を考慮すべきキーワード(一般的な単語だが、単独で使われた場合は物理関連の可能性が高い)
        word_boundary_keywords = [
            r"\b力学\b", r"\b重力\b", r"\b摩擦\b", r"\b衝突\b", r"\b運動方程式\b", r"\b加速度\b", r"\b速度\b", r"\b質量\b",
            r"\bgravity\b", r"\bfriction\b", r"\bcollision\b", r"\bvelocity\b", r"\bacceleration\b", r"\bmass\b"
        ]
        
        # 明確な物理用語が含まれているか
        explicit_match = any(keyword.lower() in lower_text for keyword in explicit_physics_keywords)
        
        # 境界を考慮すべき単語が含まれているか(単語として独立して存在する場合のみ)
        boundary_match = any(re.search(pattern, lower_text) for pattern in word_boundary_keywords)
        
        # 物理シミュレーション関連の特定のフレーズパターン
        physics_phrases = [
            r"物理.*シミュレ(ーション|ート)",
            r"physics.*simulation",
            r"ボールの.*衝突",
            r"ball.*collision",
            r"重力.*計算",
            r"gravity.*calculation",
            r"運動方程式",
            r"equation.*motion",
            r"力学.*モデル",
            r"mechanics.*model"
        ]
        phrase_match = any(re.search(pattern, lower_text) for pattern in physics_phrases)
        
        # 誤検出しやすいコンテキストのチェック(これらが含まれる場合は物理関連ではない可能性が高い)
        non_physics_contexts = [
            "アプリの動作", "システムの動作", "プログラムの動作", "ソフトウェアの動作",
            "app behavior", "system behavior", "program behavior", "software behavior",
            "UIの動き", "インターフェースの動き", "ボタンの動き",
            "UI movement", "interface movement", "button movement"
        ]
        negative_context = any(context.lower() in lower_text for context in non_physics_contexts)
        
        # 物理関連であると判断するロジック:
        # 1. 明確な物理用語が含まれている、または
        # 2. 境界を考慮すべき単語が含まれていて、かつ誤検出コンテキストが含まれていない、または
        # 3. 物理シミュレーション関連の特定のフレーズが含まれている
        return explicit_match or (boundary_match and not negative_context) or phrase_match
    
    # テキスト内にコードブロックが含まれているかを確認
    def contains_code_block(text):
        # マークダウンのコードブロックパターン
        code_block_pattern = r"```[\w]*\n[\s\S]*?\n```"
        return bool(re.search(code_block_pattern, text))
    
    # テキストからすべてのコードブロックを抽出する
    def extract_all_code_blocks(text):
        """テキストから全てのコードブロックを抽出する"""
        code_block_pattern = r"```(\w+)?\n([\s\S]*?)\n```"
        matches = re.findall(code_block_pattern, text)
        
        if not matches:
            return []
        
        code_blocks = []
        for lang, code in matches:
            lang = lang or "python"  # 言語指定がない場合はPythonと仮定
            code_blocks.append(f"```{lang}\n{code}\n```")
        
        return code_blocks
    
    # 生成されたコードを新しいファイルに保存する関数
    def save_generated_code_to_new_file(code_text, suggested_name=None):
        # コードブロックのマークアップを削除
        code_pattern = r"```(?:\w+)?\n([\s\S]*?)\n```"
        match = re.search(code_pattern, code_text)
        clean_code = match.group(1) if match else code_text
        
        # 言語の推測
        language = "py"  # デフォルトはPython
        lang_pattern = r"```(\w+)\n"
        lang_match = re.search(lang_pattern, code_text)
        if lang_match:
            detected_lang = lang_match.group(1).lower()
            lang_mapping = {
                "python": "py",
                "javascript": "js",
                "html": "html",
                "css": "css",
                "json": "json",
                "java": "java",
                "cpp": "cpp",
                "c++": "cpp",
                "c": "c",
                "go": "go",
                "rust": "rs",
                "typescript": "ts"
            }
            language = lang_mapping.get(detected_lang, detected_lang)
        
        # ファイル名の生成
        if not suggested_name:
            import hashlib
            import time
            # タイムスタンプとコードの一部からハッシュを生成
            timestamp = str(int(time.time()))
            code_hash = hashlib.md5(clean_code[:100].encode()).hexdigest()[:6]
            suggested_name = f"generated_{timestamp}_{code_hash}"
        
        # 拡張子が含まれていなければ追加
        if "." not in suggested_name:
            suggested_name = f"{suggested_name}.{language}"
        
        # 同名ファイルが存在する場合は連番を付ける
        base_name, ext = os.path.splitext(suggested_name)
        counter = 1
        file_name = suggested_name
        while file_name in st.session_state.files:
            file_name = f"{base_name}_{counter}{ext}"
            counter += 1
        
        # ファイルを保存
        st.session_state.files[file_name] = clean_code
        st.session_state.current_file = file_name
        
        return file_name
    
    # 過去のメッセージを表示
    for message in st.session_state.messages:
        with st.chat_message(message["role"]):
            st.markdown(message["content"])
    
    # 思考プロセスの表示(開閉可能なエクスパンダー)
    if st.session_state.thinking_process:
        with st.expander("思考プロセスを表示"):
            for i, thought in enumerate(st.session_state.thinking_process):
                st.subheader(thought["title"])
                st.markdown(thought["content"])
                st.divider()
    
    # 会話状態を更新する関数
    def update_conversation_state(user_input, ai_response):
        # 主要トピックの更新(最初のメッセージから)
        if st.session_state.conversation_state["topic"] is None and user_input:
            # 簡易的なトピック抽出(最初の文か最初の30単語)
            first_sentence = user_input.split('.')[0]
            topic = first_sentence[:100] + "..." if len(first_sentence) > 100 else first_sentence
            st.session_state.conversation_state["topic"] = topic
        
        # フォローアップカウントの更新
        if "?" in user_input or "?" in user_input:
            st.session_state.conversation_state["follow_up_count"] += 1
        
        # その他の状態更新ロジックをここに追加
        pass
    
    # APIを使用した思考プロセスと回答の生成
    def generate_response(user_input):
        client = Groq(api_key=api_key)
        thinking_results = []
        
        # ユーザー入力が物理関連かどうかをチェック
        is_physics = is_physics_related(user_input)
        
        # Step 1: Mistral Sabaによる入力分析
        mistral_system_prompt = load_system_prompt(MODELS["mistral_saba"])
        mistral_prompt = f"""
        以下のユーザー入力を分析してください。ユーザーの質問や要求の本質、重要なキーワード、技術的課題を抽出し、
        どのようなアプローチで回答すべきかを詳細に検討してください。
        
        ユーザー入力: {user_input}
        """
        
        mistral_response = client.chat.completions.create(
            model=MODELS["mistral_saba"],
            messages=[
                {"role": "system", "content": mistral_system_prompt},
                {"role": "user", "content": mistral_prompt}
            ],
            temperature=0.3,
            max_tokens=2000
        )
        
        mistral_analysis = mistral_response.choices[0].message.content
        thinking_results.append({
            "title": "Mistral Sabaによる分析",
            "content": mistral_analysis
        })
        
        # 物理関連の場合のみDeepSeekの意見を取得
        deepseek_knowledge = ""
        if is_physics:
            deepseek_system_prompt = load_system_prompt(MODELS["deepseek"])
            deepseek_prompt = f"""
            以下のユーザー入力は物理学に関連しています。物理学の観点から、この質問や課題に対する
            科学的知識、法則、公式、アプローチを提供してください。特に関連する物理概念と実装方法の
            関連性があれば説明してください。
            
            ユーザー入力: {user_input}
            """
            
            deepseek_response = client.chat.completions.create(
                model=MODELS["deepseek"],
                messages=[
                    {"role": "system", "content": deepseek_system_prompt},
                    {"role": "user", "content": deepseek_prompt}
                ],
                temperature=0.3,
                max_tokens=2000
            )
            
            deepseek_knowledge = deepseek_response.choices[0].message.content
            thinking_results.append({
                "title": "DeepSeekによる物理学的知見",
                "content": deepseek_knowledge
            })
        
        # Step 2: モード別の応答生成
        if st.session_state.current_mode == "通常モード":
            # 物理関連の場合のみDeepSeekの情報を含める
            deepseek_section = ""
            physics_consideration = ""
            
            if is_physics:
                physics_consideration = " DeepSeekの物理学的知見を考慮して"
                deepseek_section = f"DeepSeekの物理学的知見:\n{deepseek_knowledge}"
            
            # 現在のエディタのコードを取得(コードエディタで何かが開かれている場合)
            editor_code = ""
            editor_reference = ""
            if st.session_state.current_file and st.session_state.current_file in st.session_state.files:
                editor_code = st.session_state.files[st.session_state.current_file]
                editor_reference = f"現在エディタで開かれているファイル '{st.session_state.current_file}':\n```\n{editor_code}\n```"
            
            # 通常モード用のシステムプロンプトをロード
            system_prompt = load_system_prompt(MODELS["qwen_coder"], "通常モード")
            
            qwen_prompt = f"""
            以下のユーザー入力に対してコーディングアシスタントとして回答してください。
            Mistral Sabaの分析結果{physics_consideration}を考慮して、
            最適なソリューションを提供してください。
            
            ユーザー入力: {user_input}
            
            Mistral Sabaの分析:
            {mistral_analysis}
            """
            
            # 物理関連の場合のみDeepSeekの情報を追加
            if is_physics:
                qwen_prompt += f"\n\n{deepseek_section}"
            
            # エディタのコードが存在する場合は参照として追加
            if editor_code:
                qwen_prompt += f"\n\n{editor_reference}\n\nエディタで開かれているコードに関して、ユーザーの質問に答えるか、必要に応じてコードの改善を提案してください。"
            
            qwen_response = client.chat.completions.create(
                model=MODELS["qwen_coder"],
                messages=[
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": qwen_prompt}
                ],
                temperature=0.5,
                max_tokens=4000
            )
            
            final_response = qwen_response.choices[0].message.content
        
        elif st.session_state.current_mode == "実装モード":
            # 物理関連の条件分岐を変数で処理
            deepseek_section = ""
            physics_note = ""
            
            if is_physics:
                deepseek_section = f"DeepSeekの物理学的知見:\n{deepseek_knowledge}"
                physics_note = "この実装には物理シミュレーションが含まれているため、物理法則の正確性と数値的安定性を特に重視してください。"
            
            # Step 2a: 設計フェーズ - 専用のプロンプトを使用
            design_system_prompt = load_system_prompt(MODELS["qwen_coder"], "実装モード_設計")
            
            design_prompt = f"""
            以下のユーザー入力と分析結果に基づいて、実装に必要な設計書を作成してください。
            
            ユーザー入力: {user_input}
            
            Mistral Sabaの分析:
            {mistral_analysis}
            """
            
            # 物理関連の場合のみDeepSeekの情報を追加
            if is_physics:
                design_prompt += f"\n\n{deepseek_section}"
            
            design_response = client.chat.completions.create(
                model=MODELS["qwen_coder"],
                messages=[
                    {"role": "system", "content": design_system_prompt},
                    {"role": "user", "content": design_prompt}
                ],
                temperature=0.4,
                max_tokens=3000
            )
            
            design_doc = design_response.choices[0].message.content
            thinking_results.append({
                "title": "Qwen Coderによる設計書",
                "content": design_doc
            })
            
            # Step 2b: 実装フェーズ - 専用のプロンプトを使用
            implementation_system_prompt = load_system_prompt(MODELS["qwen_coder"], "実装モード_実装")
            
            implementation_prompt = f"""
            以下の設計書に基づいて、完全な実装コードを提供してください。
            コードは実行可能で、エラー処理が適切に行われているものにしてください。
            各コンポーネントには十分なコメントを付け、使用方法の例も含めてください。
            {physics_note}
            
            設計書:
            {design_doc}
            
            ユーザーの元々の要求:
            {user_input}
            """
            
            implementation_response = client.chat.completions.create(
                model=MODELS["qwen_coder"],
                messages=[
                    {"role": "system", "content": implementation_system_prompt},
                    {"role": "user", "content": implementation_prompt}
                ],
                temperature=0.5,
                max_tokens=4000
            )
            
            implementation_result = implementation_response.choices[0].message.content
            
            # 設計書と実装を組み合わせた最終結果
            final_response = f"# 設計書\n\n{design_doc}\n\n# 実装コード\n\n{implementation_result}"
            
            # コードの自動抽出と保存機能
            code_blocks = extract_all_code_blocks(implementation_result)
            if code_blocks and len(code_blocks) > 0:
                # 抽出したコードブロックに対してボタンを作成するための識別子を付加
                final_response += "\n\n**実装されたコードをエディタに保存できます。チャット上の「コードを保存」ボタンをクリックしてください。**"
                # 最初のコードブロックを抽出してセッション状態に保存
                st.session_state.last_generated_code = code_blocks[0]
        
        elif st.session_state.current_mode == "エラー修正モード":
            # エディタのコードを取得(存在する場合)
            editor_code = ""
            editor_reference = ""
            if st.session_state.current_file and st.session_state.current_file in st.session_state.files:
                editor_code = st.session_state.files[st.session_state.current_file]
                editor_reference = f"現在エディタで開かれているファイル '{st.session_state.current_file}':\n```\n{editor_code}\n```\n\nこのコードにエラーがある可能性があります。ユーザーからの情報と合わせて分析してください。"
            
            # 物理関連の条件分岐を変数で処理
            deepseek_section = ""
            
            if is_physics:
                deepseek_section = f"DeepSeekの物理学的知見:\n{deepseek_knowledge}"
            
            # エラー修正モード用のシステムプロンプトをロード
            error_system_prompt = load_system_prompt(MODELS["qwen_coder"], "エラー修正モード")
            
            error_prompt = f"""
            以下のユーザー入力はコードのエラーに関するものです。
            エラーの原因を特定し、修正案を提供してください。
            説明では:
            1. エラーの根本原因
            2. 問題箇所の特定
            3. 修正方法の詳細な説明
            4. 修正されたコード
            5. 同様のエラーを防ぐためのベストプラクティス
            を含めてください。
            
            ユーザー入力: {user_input}
            
            Mistral Sabaの分析:
            {mistral_analysis}
            """
            
            # エディタのコードを追加
            if editor_code:
                error_prompt += f"\n\n{editor_reference}"
            
            # 物理関連の場合のみDeepSeekの情報を追加
            if is_physics:
                error_prompt += f"\n\n{deepseek_section}"
            
            error_response = client.chat.completions.create(
                model=MODELS["qwen_coder"],
                messages=[
                    {"role": "system", "content": error_system_prompt},
                    {"role": "user", "content": error_prompt}
                ],
                temperature=0.4,
                max_tokens=4000
            )
            
            final_response = error_response.choices[0].message.content
        
        return final_response, thinking_results
    
    # 最後に生成されたコードを保存するセッション変数の初期化
    if "last_generated_code" not in st.session_state:
        st.session_state.last_generated_code = None
    
    # 「コードを保存」ボタンが押された場合の処理
    if st.session_state.last_generated_code and st.button("AIが生成したコードをエディタに保存"):
        with st.spinner("コードを保存中..."):
            file_name = save_generated_code_to_new_file(st.session_state.last_generated_code)
            st.success(f"コードを '{file_name}' として保存しました")
            st.session_state.last_generated_code = None  # 保存したのでクリア
            st.rerun()
    
    # ユーザー入力
    if prompt := st.chat_input("質問を入力してください"):
        # ユーザーメッセージを表示
        st.chat_message("user").markdown(prompt)
        
        # ユーザーメッセージをセッション状態に追加
        st.session_state.messages.append({"role": "user", "content": prompt})
        
        # モード切替コマンドを検出
        if prompt.strip().startswith("/mode"):
            parts = prompt.strip().split()
            if len(parts) < 2:
                response = "モード指定が不完全です。使用方法: /mode [通常|実装|エラー修正]"
            else:
                mode = parts[1].lower()
                valid_modes = {"通常": "通常モード", "実装": "実装モード", "エラー修正": "エラー修正モード"}
                
                if mode not in valid_modes and mode not in valid_modes.values():
                    response = f"無効なモードです。使用可能なモード: {', '.join(valid_modes.keys())}"
                else:
                    old_mode = st.session_state.current_mode
                    if mode in valid_modes:
                        st.session_state.current_mode = valid_modes[mode]
                    else:
                        st.session_state.current_mode = mode
                    
                    st.session_state.is_first_message = True
                    
                    # モード変更時の説明メッセージを用意
                    mode_descriptions = {
                        "通常モード": "通常の質問応答モードに切り替えました。",
                        "実装モード": "アイデアから設計と実装を生成するモードに切り替えました。",
                        "エラー修正モード": "エラーメッセージから問題を診断・修正するモードに切り替えました。"
                    }
                    
                    instructions = {
                        "通常モード": "質問や議論したいトピックを入力してください。",
                        "実装モード": "実装したい機能やアイデアを入力してください。",
                        "エラー修正モード": "発生したエラーメッセージとコードを入力してください。"
                    }
                    
                    response = f"モードを「{old_mode}」から「{st.session_state.current_mode}」に変更しました。\n\n{mode_descriptions[st.session_state.current_mode]}\n\n{instructions[st.session_state.current_mode]}"
            
            # レスポンスを表示
            st.chat_message("assistant").markdown(response)
            
            # アシスタントメッセージをセッション状態に追加
            st.session_state.messages.append({"role": "assistant", "content": response})
        
        # 通常の質問処理
        else:
            # APIキーがある場合のみ実行
            if api_key:
                try:
                    # 最初のメッセージはチェーンを使用、それ以降はQwen Coderのみで応答
                    if st.session_state.is_first_message:
                        with st.spinner("回答を生成中...(複数のモデルによる分析を行っています)"):
                            response, thinking_results = generate_response(prompt)
                            st.session_state.thinking_process = thinking_results
                        st.session_state.is_first_message = False
                    else:
                        with st.spinner("回答を生成中..."):
                            # 直接Qwen Coderで応答
                            client = Groq(api_key=api_key)
                            
                            # 現在のモードに合わせたシステムプロンプトをロード
                            current_system_prompt = load_system_prompt(MODELS["qwen_coder"], st.session_state.current_mode)
                            
                            # 会話の連続性のための追加指示
                            current_system_prompt += """
                            
                            重要: これは継続中の会話です。前の質問や回答を踏まえて、会話の自然な流れを維持してください。
                            ユーザーの最新の質問だけに集中するのではなく、会話全体の文脈を考慮してください。
                            繰り返しは避け、新しい情報や洞察を提供してください。
                            """
                            
                            # 過去の会話履歴を完全に構築
                            context = ""
                            if len(st.session_state.messages) > 2:
                                # 最近の会話履歴(最大トークン制限を考慮して適切な数に制限)
                                recent_messages = st.session_state.messages[-10:]  # 最近の10メッセージまで
                                for msg in recent_messages:
                                    role = "ユーザー" if msg["role"] == "user" else "アシスタント"
                                    # 全文を含める
                                    context += f"{role}: {msg['content']}\n\n"
                            
                            # 現在のエディタのコードを取得(存在する場合)
                            editor_code = ""
                            editor_reference = ""
                            if st.session_state.current_file and st.session_state.current_file in st.session_state.files:
                                editor_code = st.session_state.files[st.session_state.current_file]
                                editor_reference = f"\n\n現在エディタで開かれているファイル '{st.session_state.current_file}':\n```\n{editor_code}\n```\n\nエディタで開かれているコードに関して、ユーザーの質問に答えるか、必要に応じてコードの改善を提案してください。"
                            
                            qwen_prompt = f"""
                            あなたはユーザーと継続的な会話を行っているアシスタントです。
                            これまでの会話の流れを踏まえて、最新のユーザー入力に適切に応答してください。
                            
                            これまでの会話:
                            {context}
                            
                            最新のユーザー入力: {prompt}
                            
                            前の会話の文脈を考慮して、この最新の質問に直接回答してください。
                            前回までの内容を繰り返すのではなく、会話を自然に進めてください。
                            """
                            
                            # エディタのコードが存在する場合は参照として追加
                            if editor_code:
                                qwen_prompt += editor_reference
                            
                            qwen_response = client.chat.completions.create(
                                model=MODELS["qwen_coder"],
                                messages=[
                                    {"role": "system", "content": current_system_prompt},
                                    {"role": "user", "content": qwen_prompt}
                                ],
                                temperature=0.5,
                                max_tokens=4000
                            )
                            
                            response = qwen_response.choices[0].message.content
                    
                    # レスポンスを表示
                    st.chat_message("assistant").markdown(response)
                    
                    # アシスタントメッセージをセッション状態に追加
                    st.session_state.messages.append({"role": "assistant", "content": response})
                    
                    # 会話状態を更新
                    update_conversation_state(prompt, response)
                    
                    # モード切替提案を追加(UIには表示しない)
                    mode_suggestions = {
                        "通常モード": "\n\n---\n*実装を生成するには、「/mode 実装」と入力して実装モードに切り替えてください。*",
                        "実装モード": "\n\n---\n*このコードを検証・修正するには、「/mode エラー修正」と入力して検証修正モードに切り替えてください。*",
                        "エラー修正モード": "\n\n---\n*新しい実装を生成するには、「/mode 実装」と入力して実装モードに切り替えてください。*"
                    }
                    
                    # 提案はセッションステートには保存するが、UIには表示しない(次回のcontext生成で使用)
                    suggestion = mode_suggestions.get(st.session_state.current_mode, "")
                    st.session_state.messages[-1]["content"] += suggestion
                    
                except Exception as e:
                    st.error(f"エラーが発生しました: {str(e)}")
            else:
                st.error("GROQ_API_KEYが設定されていません。Hugging Face Spacesの環境変数で設定してください。")

# APIキーがない場合の警告表示
if not api_key:
    st.warning("""
    ### ⚠️ GROQ_API_KEYが設定されていません
    
    このアプリを使用するには、Hugging Face Spacesの環境変数設定でGROQ_API_KEYを設定する必要があります:
    
    1. Spacesのダッシュボードに移動
    2. Settings > Repository secrets
    3. 新しいシークレットを追加: キー名 `GROQ_API_KEY`、値に自分のGroq APIキーを入力
    4. アプリを再起動
    """)