File size: 9,087 Bytes
45b631d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85c0ea4
45b631d
 
 
 
 
 
 
 
 
 
 
 
 
85c0ea4
45b631d
85c0ea4
45b631d
85c0ea4
45b631d
85c0ea4
 
45b631d
85c0ea4
45b631d
146c49f
45b631d
 
 
146c49f
85c0ea4
 
 
146c49f
85c0ea4
 
 
45b631d
85c0ea4
45b631d
 
 
 
85c0ea4
45b631d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85c0ea4
45b631d
146c49f
45b631d
 
 
146c49f
 
85c0ea4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45b631d
 
 
 
85c0ea4
45b631d
85c0ea4
 
45b631d
 
85c0ea4
 
 
 
 
 
 
 
 
45b631d
85c0ea4
 
45b631d
 
85c0ea4
 
45b631d
85c0ea4
 
 
 
 
45b631d
85c0ea4
 
45b631d
85c0ea4
 
 
 
 
 
 
 
 
 
 
b00b12d
 
 
 
 
 
 
 
 
 
 
85c0ea4
 
 
 
 
 
 
 
 
 
b00b12d
85c0ea4
 
 
 
 
 
 
 
45b631d
 
85c0ea4
 
 
 
 
 
 
 
 
 
 
45b631d
85c0ea4
 
 
45b631d
 
85c0ea4
45b631d
85c0ea4
 
45b631d
 
 
 
 
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
import gradio as gr
import uuid
import os
import csv
import datetime
from datetime import timezone, timedelta
import re
from openai import OpenAI
import sys

# --- JST(日本時間)の定義 ---
JST = timezone(timedelta(hours=9), 'JST')

# --- API Key and Client Initialization ---
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")

if not OPENROUTER_API_KEY:
    print("Warning: OpenRouter API Key not found.", file=sys.stderr)

try:
    client = OpenAI(
        base_url="https://openrouter.ai/api/v1",
        api_key=OPENROUTER_API_KEY,
    )
    print("OpenAI client initialized.")
except Exception as e:
    print(f"Failed to initialize OpenAI client: {e}", file=sys.stderr)

# --- Helper Functions ---
def generate_user_id():
    return str(uuid.uuid4())

def load_prompt(level):
    try:
        filename = f"{level.lower()}_prompt.txt"
        if os.path.exists(filename):
            with open(filename, "r", encoding="utf-8") as f:
                return f.read()
        return "You are a helpful AI assistant."
    except Exception:
        return "You are a helpful AI assistant."

# --- Core Logic Functions ---

def chat_process(message, display_history, full_logs, prompt_level):
    """
    チャット処理を行う関数
    """
    if display_history is None: display_history = []
    if full_logs is None: full_logs = []

    # 1. ユーザーメッセージの作成
    current_time_user = datetime.datetime.now(JST).strftime("%Y-%m-%d %H:%M:%S")
    user_msg_data = {
        "role": "user", 
        "content": message, 
        "timestamp": current_time_user
    }
    
    temp_display = display_history + [user_msg_data]
    temp_logs = full_logs + [user_msg_data]

    # APIリクエスト用のメッセージ作成
    system_prompt = load_prompt(prompt_level)
    messages_for_api = [{"role": "system", "content": system_prompt}]
    
    for msg in temp_display:
        messages_for_api.append({"role": msg["role"], "content": msg["content"]})

    ai_response = ""
    try:
        model_name = "google/gemini-2.5-flash"

        response = client.chat.completions.create(
            model=model_name,
            messages=messages_for_api,
            temperature=0.7,
            max_tokens=500,
        )
        content = response.choices[0].message.content
        ai_response = content if content is not None else ""
        if not ai_response:
            ai_response = "(応答が空でした)"

    except Exception as e:
        error_msg = f"API Error: {e}"
        print(error_msg, file=sys.stderr)
        ai_response = f"システムエラーが発生しました: {e}"

    # 2. AIメッセージの作成
    current_time_ai = datetime.datetime.now(JST).strftime("%Y-%m-%d %H:%M:%S")
    ai_msg_data = {
        "role": "assistant", 
        "content": ai_response, 
        "timestamp": current_time_ai
    }

    new_display = temp_display + [ai_msg_data]
    new_logs = temp_logs + [ai_msg_data]

    return "", new_display, new_display, new_logs

def change_difficulty_logic(new_level, full_logs):
    """
    難易度変更時の処理
    """
    if full_logs is None: full_logs = []

    notification_msg = (
        f"**[システム通知]**\n"
        f"難易度を **{new_level}** に変更しました。\n"
        f"解答開始と入力すれば指導が始まります。"
    )
    current_time = datetime.datetime.now(JST).strftime("%Y-%m-%d %H:%M:%S")
    
    msg_data = {
        "role": "assistant",
        "content": notification_msg,
        "timestamp": current_time
    }

    new_display_history = [msg_data]
    new_full_logs = full_logs + [msg_data]
    
    return new_display_history, new_display_history, new_full_logs, new_level

def export_csv_logic(user_id, user_name, full_logs):
    print(f"Exporting CSV for UserID: {user_id}, Name: '{user_name}'")

    if not user_name or not user_name.strip():
        gr.Warning("名前が正しく取得できませんでした")
        return None

    if not full_logs:
        gr.Warning("保存する履歴がまだありません。")
        return None

    timestamp_str = datetime.datetime.now(JST).strftime("%Y%m%d_%H%M%S")
    
    safe_name = re.sub(r'[\\/*?:"<>|]', "", user_name)
    safe_name = safe_name.strip().replace(" ", "_")
    filename = f"chat_history_{safe_name}_{timestamp_str}.csv"
    
    try:
        with open(filename, "w", newline="", encoding="utf-8-sig") as f:
            writer = csv.writer(f)
            writer.writerow(["Timestamp", "Role", "Content"])
            
            for msg in full_logs:
                ts = msg.get("timestamp", "")
                role = msg.get("role", "")
                content = msg.get("content", "")
                writer.writerow([ts, role, content])
        
        print(f"Successfully saved to {filename}")
        return filename
    except Exception as e:
        print(f"CSV Export Error: {e}", file=sys.stderr)
        gr.Warning(f"CSV保存エラー: {e}")
        return None

def start_app_logic(user_name):
    """
    名前が入力されているか確認し、画面表示を切り替える
    """
    if not user_name or not user_name.strip():
        raise gr.Error("お名前を入力してください。")
    
    # Start画面を隠し(False)、Main画面を表示(True)
    return gr.update(visible=False), gr.update(visible=True)


# --- UI Definition ---
with gr.Blocks() as demo:
    # ステート定義
    user_id_state = gr.State(generate_user_id)
    display_history_state = gr.State([]) # 画面用
    full_logs_state = gr.State([])       # 保存用
    prompt_level_state = gr.State("Beginner")
    
    start_msg_constant = gr.State("解答開始")

    # =========================================
    # 1. スタート画面
    # =========================================
    with gr.Column(visible=True) as start_screen:
        gr.Markdown("# AI Chatbot System")
        gr.Markdown("学習を始めるには、お名前を入力して「解答開始」を押してください。")
        
        user_name_input = gr.Textbox(
            label="お名前", 
            placeholder="山田 太郎", 
            scale=2
        )
        
        start_btn = gr.Button("解答開始", variant="primary", size="lg")

    # =========================================
    # 2. メインチャット画面
    # =========================================
    with gr.Column(visible=False) as main_screen:
        gr.Markdown("# Multi-Level AI Chatbot")
        
        with gr.Row():
            export_btn = gr.DownloadButton("📥 会話をCSVで保存", scale=1)

        with gr.Row():
            gr.Markdown("## 問題の難易度を選択してください(初級から順番に取り組んでください)")
            
        with gr.Row():
            level_radio = gr.Radio(
                ["Beginner", "Intermediate", "Advanced"],
                label="Difficulty Level",
                value="Beginner",
                interactive=True
            )

        # 【修正箇所】Chatbotの定義にlatex_delimitersを追加
        chatbot = gr.Chatbot(
            type="messages", 
            height=500,
            latex_delimiters=[
                {"left": "$$", "right": "$$", "display": True},   # 行全体(ディスプレイ数式)
                {"left": "$", "right": "$", "display": False},    # インライン数式
                {"left": "\\(", "right": "\\)", "display": False},
                {"left": "\\[", "right": "\\]", "display": True},
            ]
        )
        
        msg_input = gr.Textbox(
            placeholder="メッセージを入力してください... (Enterで送信)", 
            label="Chat Input",
            lines=1
        )
        send_btn = gr.Button("送信", variant="primary")

    # --- Event Handling ---

    # A. スタートボタンの処理
    start_btn.click(
        fn=start_app_logic,
        inputs=[user_name_input],
        outputs=[start_screen, main_screen]
    ).then(
        fn=chat_process,
        inputs=[start_msg_constant, display_history_state, full_logs_state, prompt_level_state],
        outputs=[msg_input, chatbot, display_history_state, full_logs_state]
    )

    # B. メッセージ送信時の処理
    chat_event_args = {
        "fn": chat_process,
        "inputs": [msg_input, display_history_state, full_logs_state, prompt_level_state],
        "outputs": [msg_input, chatbot, display_history_state, full_logs_state]
    }

    msg_input.submit(**chat_event_args)
    send_btn.click(**chat_event_args)

    # C. 難易度変更時の処理
    level_radio.change(
        fn=change_difficulty_logic,
        inputs=[level_radio, full_logs_state],
        outputs=[chatbot, display_history_state, full_logs_state, prompt_level_state]
    )

    # D. CSV保存処理
    export_btn.click(
        fn=export_csv_logic,
        inputs=[user_id_state, user_name_input, full_logs_state],
        outputs=[export_btn]
    )

if __name__ == "__main__":
    demo.launch(debug=True, share=False)