Song commited on
Commit
9c46c68
·
1 Parent(s): 388e561
__pycache__/game_system.cpython-313.pyc ADDED
Binary file (26.6 kB). View file
 
__pycache__/game_ui_helper.cpython-313.pyc ADDED
Binary file (11.9 kB). View file
 
app.py CHANGED
@@ -20,310 +20,146 @@ client = OpenAI(
20
  base_url="https://openrouter.ai/api/v1",
21
  api_key=OPENROUTER_API_KEY
22
  )
23
- MODEL = "google/gemma-3-27b-it:free" # 免費且中文支援良好
24
 
25
  # 家長人格與情境
26
- PARENT_PERSONAS = {
27
- "😰 焦慮型家長": "你非常擔心孩子,容易反覆確認,對冷處理會更焦慮,對同理語言會稍緩和。",
28
- "🤔 質疑型家長": "你懷疑教師專業,重邏輯與說明,對模糊回應會更強烈質疑。",
29
- "😠 高衝突型家長": "你情緒起伏大,對語氣敏感,容易覺得被推卸責任。",
30
- "🛡️ 過度保護型家長": "你極度保護孩子,傾向把所有問題外部化,容易將責任歸咎他人,對批評孩子行為的反應非常強烈。",
31
- "⚖️ 理性協商型家長": "你重視事實與解決方案,講求邏輯,但若教師回應不夠具體會變得冷淡或失望。",
32
- "😴 疲憊工作型家長": "你工作很忙、壓力大,對學校期待很高,但時間有限,容易因小事爆發累積情緒。",
33
- "🌏 文化差異型家長": "你來自不同文化背景,對台灣教育方式有疑慮,容易因價值觀差異產生誤解。"
34
- }
35
- SCENARIOS = {
36
- "📉 成績退步質疑": "情境:你認為孩子最近成績退步,懷疑老師是否給予足夠關心。一開始就表達擔心。",
37
- "📚 作業量過多抱怨": "情境:你認為作業太多,造成孩子壓力過大。一開始就直接抱怨。",
38
- "😢 孩子情緒問題歸咎老師": "情境:你認為孩子情緒不穩,是因為學校不當對待。一開始就帶情緒質問。",
39
- "😞 孩子被同學霸凌卻未即時處理": "情境:你發現孩子最近情緒低落,懷疑在學校被同學霸凌,但老師似乎沒有積極處理。一開始就帶著失望與焦急開場。",
40
- "✏️ 孩子不寫作業、家長被指責教養問題": "情境:老師反映孩子經常不寫作業,你覺得老師不該直接把責任推給家長。一開始就有些防衛地回應。",
41
- "🧩 特殊生輔導方式爭議(例如ADHD)": "情境:你的孩子有注意力缺陷診斷,老師的管教方式讓你覺得不夠理解與包容。一開始就表達不滿。",
42
- "💻 線上學習或遠距教學設備問題投訴": "情境:遠距上課期間,孩子因為家裡網路或設備問題跟不上進度,你認為學校沒有提供足夠支援。一開始就抱怨學校安排。"
43
- }
44
 
45
- # 自適應選擇
46
- def select_adaptive_persona_scenario(level: int) -> tuple[str, str, str]:
47
- """根據玩家等級自適應選擇人格與情境"""
48
- if level <= 2:
49
- target_diff = 1.2 # 簡單
50
- elif level <= 4:
51
- target_diff = 1.7 # 中等
52
- else:
53
- target_diff = 2.2 # 困難
54
 
55
- candidates = []
56
- for scenario, sd in SCENARIO_DIFFICULTY.items():
57
- for persona, pd in PERSONA_COMPLEXITY.items():
58
- diff = sd * pd
59
- if abs(diff - target_diff) < 0.5: # 容許範圍
60
- candidates.append((scenario, persona, diff))
61
-
62
- if not candidates:
63
- # 後備:隨機選擇
64
- scenario = random.choice(list(SCENARIOS.keys()))
65
- persona = random.choice(list(PARENT_PERSONAS.keys()))
66
- diff = SCENARIO_DIFFICULTY[scenario] * PERSONA_COMPLEXITY[persona]
67
- else:
68
- scenario, persona, diff = random.choice(candidates)
69
-
70
- if diff < 1.5:
71
- diff_level = "簡單"
72
- elif diff < 2.0:
73
- diff_level = "中等"
74
- else:
75
- diff_level = "困難"
76
-
77
- return scenario, persona, diff_level
78
-
79
- # Prompt
80
- PARENT_SYSTEM_PROMPT = """
81
- 你是一位家長,正在與孩子的老師溝通。你的任務:
82
-
83
- 以家長第一人稱回應,語氣自然真實
84
- 根據你的家長人格特質做出符合邏輯的情緒反應
85
- 嚴格禁止:給教學建議、評價教師對錯、下結論或說教
86
- 只表達你的擔心、不滿、或疑問
87
- """
88
-
89
- RISK_ANALYSIS_PROMPT = """
90
- 你是一位親師溝通風險分析專家。請分析教師的回應,評估對家長可能產生的溝通風險。
91
- 分析維度:
92
-
93
- 情緒衝擊 - 是否可能激怒或傷害家長情感?
94
- 責任歸屬 - 是否被視為推卸責任或指責家長?
95
- 同理表現 - 是否展現足夠的理解與關懷?
96
- 溝通風格 - 是否過於強硬、模糊或帶有攻擊性?
97
-
98
- 輸出必須是純粹且完整的 JSON 物件,不要有任何額外文字、markdown、或代碼塊。
99
- JSON 結構(issues 與 suggestions 各最多 4 項):
100
- {
101
- "risk_level": "低/中/高",
102
- "risk_score": 0-100 的整數,
103
- "issues": ["問題1", "問題2"],
104
- "parent_reaction": "預期家長情緒反應(一句話)",
105
- "suggestions": ["改進建議1", "改進建議2"]
106
- }
107
- """
108
-
109
- # LLM 呼叫
110
  def call_llm(messages: list, temperature: float = 0.7, max_tokens: int = 500) -> str:
111
- try:
112
- response = client.chat.completions.create(
113
- model=MODEL,
114
- messages=messages,
115
- temperature=temperature,
116
- max_tokens=max_tokens
117
- )
118
- return response.choices[0].message.content.strip()
119
- except Exception as e:
120
- raise gr.Error(f"LLM 呼叫失敗:{str(e)}")
121
 
122
  def generate_parent_response(persona: str, scenario: str, chat_history: list, teacher_msg: str | None = None) -> str:
123
- messages = [
124
- {"role": "system", "content": PARENT_SYSTEM_PROMPT},
125
- {"role": "system", "content": PARENT_PERSONAS[persona]},
126
- {"role": "system", "content": SCENARIOS[scenario]},
127
- ]
128
- for msg in chat_history:
129
- if msg[0] is not None: # 教師訊息
130
- messages.append({"role": "user", "content": f"老師說:{msg[0]}"})
131
- if msg[1] is not None: # 家長訊息
132
- messages.append({"role": "assistant", "content": msg[1]})
133
- if teacher_msg is None:
134
- messages.append({"role": "user", "content": "請以家長角色根據情境主動開場,表達你的擔心或不滿。"})
135
- temperature = 0.85
136
- else:
137
- messages.append({"role": "user", "content": f"老師說:{teacher_msg}"})
138
- temperature = 0.85
139
- return call_llm(messages, temperature=temperature)
140
 
141
  def analyze_risk(teacher_input: str, persona: str, scenario: str) -> tuple[str, int]:
142
- if not teacher_input.strip():
143
- return "💬 請輸入您的回應,系統將即時分析風險", 50
144
- messages = [
145
- {"role": "system", "content": RISK_ANALYSIS_PROMPT},
146
- {"role": "system", "content": f"家長人格:{PARENT_PERSONAS[persona]}"},
147
- {"role": "system", "content": f"情境:{SCENARIOS[scenario]}"},
148
- {"role": "user", "content": f"教師回應:{teacher_input}"}
149
- ]
150
- try:
151
- response = call_llm(messages, temperature=0.2, max_tokens=800)
152
- response = response.strip()
153
- if response.startswith("```json"):
154
- response = response[7:]
155
- if response.endswith("```"):
156
- response = response[:-3]
157
- start = response.find("{")
158
- end = response.rfind("}") + 1
159
- json_str = response[start:end]
160
- data = json.loads(json_str)
161
- risk_score = max(0, min(100, int(data.get("risk_score", 50))))
162
- emoji, color, level = ("🟢", "#10B981", "低") if risk_score <= 33 else ("🟡", "#F59E0B", "中") if risk_score <= 66 else ("🔴", "#EF4444", "高")
163
- issues = "\n- " + "\n- ".join(data.get("issues", ["無明顯問題"])) if data.get("issues") else "無明顯問題"
164
- suggestions = "\n- " + "\n- ".join(data.get("suggestions", ["繼續保持"])) if data.get("suggestions") else "繼續保持"
165
- html = f"""
166
- {emoji} 風險等級:{level}({risk_score})
167
- 風險指數
168
-
169
-
170
- {risk_score}%
171
-
172
-
173
- 📋 潛在問題
174
- {issues}
175
- 💡 改進建議
176
- {suggestions}
177
- 💭 家長可能反應
178
- {data.get("parent_reaction", "家長可能會冷靜接受")}
179
- """
180
- return html, risk_score
181
- except Exception as e:
182
- return f"⚠️ 分析失敗:{str(e)}", 50
183
 
184
- # 對話邏輯
185
  def start_conversation(persona: str, scenario: str, challenge_mode: str):
186
- # 如果選擇了自適應,則使用自適應選擇
187
  if persona == "🤖 自適應選擇" or scenario == "🎯 自適應挑戰":
188
  adaptive = game_state.get_adaptive_selection()
189
  persona = adaptive["persona"]
190
  scenario = adaptive["scenario"]
191
 
192
- # 設定挑戰模式
193
  game_state.current_challenge_mode = challenge_mode
194
- game_state.challenge_rounds = 0
 
 
195
  game_state.challenge_start_time = time.time()
196
 
197
- if challenge_mode == "困難模式":
198
- # 選擇高難度組合
199
- high_difficulty = []
200
- for s, sd in SCENARIO_DIFFICULTY.items():
201
- for p, pd in PERSONA_COMPLEXITY.items():
202
- if sd * pd >= 2.0:
203
- high_difficulty.append((s, p))
204
- if high_difficulty:
205
- scenario, persona = random.choice(high_difficulty)
206
 
207
- opening = generate_parent_response(persona, scenario, [], None)
208
- chat = [[None, opening]] # 第一條只有家長訊息
209
- game_state.reset_session() # 重置 session 狀態
210
  level_info = game_state.get_level_info()
211
  ach_summary = game_state.get_achievements_summary()
212
  stats_summary = game_state.get_statistics_summary()
213
  challenge_info = game_state.get_daily_challenge_info()
214
- level_html, score_html, ach_html, stats_html, progress_html, challenge_html = update_game_display(level_info, ach_summary, stats_summary, challenge_info)
215
- game_state.last_response_time = time.time() # 開始計時
 
 
 
 
 
 
216
  return (
217
- chat,
218
- "💬 家長已開場,請輸入您的回應",
219
- "",
220
  level_html, score_html, ach_html, stats_html, progress_html, challenge_html,
221
- "", False
222
  )
223
 
224
  def continue_conversation(persona: str, scenario: str, chat_history: list, teacher_input: str):
225
  if not teacher_input.strip():
226
- return chat_history, "請輸入內容", teacher_input, *[gr.update() for _ in range(9)]
227
- # 生成家長回應
228
- parent_response = generate_parent_response(persona, scenario, chat_history, teacher_input)
229
- new_chat = chat_history + [[teacher_input, parent_response]]
230
- # 風險分析
231
- risk_html, risk_score = analyze_risk(teacher_input, persona, scenario)
232
- # 計算回應時間
233
- response_time = time.time() - game_state.last_response_time
234
- game_state.last_response_time = time.time()
235
- # 處理遊戲化邏輯
236
- score = game_state.calculate_score(risk_score, scenario, persona, response_time)
237
- game_state.update_stats(risk_score, score, scenario, persona, response_time)
238
  new_achievements = game_state.check_achievements()
239
 
240
- # 檢查挑戰模式
241
- challenge_mode = getattr(game_state, 'current_challenge_mode', '普通模式')
242
- challenge_complete = False
243
- if challenge_mode == "時間挑戰 (30秒)":
244
- elapsed = time.time() - game_state.challenge_start_time
245
- if elapsed >= 30:
246
- challenge_complete = True
247
- risk_html += f"\n\n⏰ 時間挑戰結束!總用時: {elapsed:.1f}秒"
248
- elif challenge_mode == "連續挑戰 (3回合)":
249
- game_state.challenge_rounds += 1
250
- if game_state.challenge_rounds >= 3:
251
- challenge_complete = True
252
- risk_html += f"\n\n🏁 連續挑戰完成!共3回合"
253
 
254
  # 更新 UI
255
  level_info = game_state.get_level_info()
256
  ach_summary = game_state.get_achievements_summary()
257
  stats_summary = game_state.get_statistics_summary()
258
  challenge_info = game_state.get_daily_challenge_info()
259
- level_html, score_html, ach_html, stats_html, progress_html, challenge_html = update_game_display(level_info, ach_summary, stats_summary, challenge_info)
 
 
260
  popup_html = format_achievement_popup(new_achievements)
261
  popup_visible = len(new_achievements) > 0
262
-
 
 
 
 
263
  if challenge_complete:
264
  risk_html += "\n\n🎉 挑戰完成!請重新選擇情境開始新對話。"
265
- # 重置挑戰狀態
266
- game_state.current_challenge_mode = None
267
-
268
  return (
269
  new_chat,
270
  risk_html,
271
- "" if not challenge_complete else gr.update(interactive=False),
272
  level_html, score_html, ach_html, stats_html, progress_html, challenge_html,
273
- popup_html, popup_visible
274
  )
275
 
276
  def reset_conversation():
 
277
  level_info = game_state.get_level_info()
278
  ach_summary = game_state.get_achievements_summary()
279
  stats_summary = game_state.get_statistics_summary()
280
  challenge_info = game_state.get_daily_challenge_info()
281
- level_html, score_html, ach_html, stats_html, progress_html, challenge_html = update_game_display(level_info, ach_summary, stats_summary, challenge_info)
 
 
282
  return (
283
  [],
284
  "💬 請先選擇人格與情境,然後點「開始對話」",
285
- "",
286
  level_html, score_html, ach_html, stats_html, progress_html, challenge_html,
287
- "", False
288
  )
289
 
290
- # Gradio UI
291
  CUSTOM_CSS = """
292
- /* 原有 CSS 保留並加強成就彈窗 */
293
- .achievement-popup {
294
- position: fixed;
295
- top: 50%;
296
- left: 50%;
297
- transform: translate(-50%, -50%);
298
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
299
- color: white;
300
- padding: 32px 40px;
301
- border-radius: 20px;
302
- box-shadow: 0 20px 60px rgba(0,0,0,0.4);
303
- z-index: 10000;
304
- text-align: center;
305
- animation: popIn 0.8s ease;
306
- min-width: 300px;
307
- }
308
- @keyframes popIn {
309
- 0% { transform: translate(-50%, -50%) scale(0.3); opacity: 0; }
310
- 60% { transform: translate(-50%, -50%) scale(1.1); }
311
- 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
312
- }
313
- .game-panel { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 16px; color: white; margin-bottom: 16px; }
314
- .progress-bar { width: 100%; height: 8px; background: rgba(255,255,255,0.3); border-radius: 4px; overflow: hidden; }
315
- .progress-fill { height: 100%; background: #4ade80; border-radius: 4px; transition: width 0.5s ease; }
316
- .stat-item { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.1); }
317
- .stat-item:last-child { border-bottom: none; }
318
  """
319
 
320
- with gr.Blocks(css=CUSTOM_CSS, title="一句話的距離|親師溝通 AI 演練場") as demo:
321
- gr.Markdown("""
322
- 一句話的距離
323
- AI 驅動親師溝通虛擬演練場(遊戲化版本)
324
- 練習高張力親師對話,解鎖成就、升級、累積分數!
325
- """)
326
- # 遊戲面板
327
  with gr.Column():
328
  with gr.Row(elem_classes="game-panel"):
329
  level_display = gr.HTML()
@@ -336,29 +172,18 @@ AI 驅動親師溝通虛擬演練場(遊戲化版本)
336
  challenge_display = gr.HTML()
337
  achievement_popup = gr.HTML("", visible=False)
338
 
 
339
  with gr.Row():
340
  with gr.Column(scale=1, min_width=300):
341
  persona_choices = ["🤖 自適應選擇"] + list(PARENT_PERSONAS.keys())
342
  scenario_choices = ["🎯 自適應挑戰"] + list(SCENARIOS.keys())
343
- challenge_modes = {
344
- "normal": "普通模式",
345
- "time_challenge": "時間挑戰 (30秒)",
346
- "continuous": "連續挑戰 (3回合)",
347
- "hard_mode": "困難模式",
348
- "daily_challenge": "每日挑戰"
349
- }
350
  persona_selector = gr.Radio(choices=persona_choices, label="👤 選擇家長人格", value="🤖 自適應選擇")
351
  scenario_selector = gr.Radio(choices=scenario_choices, label="📋 選擇溝通情境", value="🎯 自適應挑戰")
352
- challenge_selector = gr.Radio(choices=list(challenge_modes.values()), label="⚔️ 挑戰模式", value="普通模式")
353
  start_btn = gr.Button("🆕 開始對話", variant="primary", size="lg")
354
- with gr.Accordion("💡 使用說明", open=False):
355
- gr.Markdown("""
356
- 選擇家長人格與情境
357
- 點「開始對話」讓家長開場
358
- 輸入您的回應 → 系統立即分析風險並結算得分
359
- 連續低風險可觸發連擊加分,解鎖成就會彈出動畫!
360
- """)
361
-
362
  with gr.Column(scale=3):
363
  chatbot = gr.Chatbot(label="💬 對話紀錄", height=500)
364
  teacher_input = gr.Textbox(label="📝 您的回應(教師)", placeholder="請輸入您想說的話...", lines=4)
@@ -367,28 +192,29 @@ AI 驅動親師溝通虛擬演練場(遊戲化版本)
367
  clear_btn = gr.Button("🔄 重新選擇情境", variant="secondary")
368
  risk_output = gr.Markdown("💬 請先開始對話")
369
 
370
- # 事件
 
 
 
 
371
  start_btn.click(
372
  start_conversation,
373
  inputs=[persona_selector, scenario_selector, challenge_selector],
374
- outputs=[chatbot, risk_output, teacher_input, level_display, score_display, achievements_display, stats_display, progress_bar, challenge_display, achievement_popup, achievement_popup.visible]
375
  )
376
  submit_btn.click(
377
  continue_conversation,
378
  inputs=[persona_selector, scenario_selector, chatbot, teacher_input],
379
- outputs=[chatbot, risk_output, teacher_input, level_display, score_display, achievements_display, stats_display, progress_bar, challenge_display, achievement_popup, achievement_popup.visible]
380
  )
381
  clear_btn.click(
382
  reset_conversation,
383
- outputs=[chatbot, risk_output, teacher_input, level_display, score_display, achievements_display, stats_display, progress_bar, challenge_display, achievement_popup, achievement_popup.visible]
384
  )
385
- # 改變選擇時重置
386
- persona_selector.change(reset_conversation, outputs=[chatbot, risk_output, teacher_input, level_display, score_display, achievements_display, stats_display, progress_bar, challenge_display, achievement_popup, achievement_popup.visible])
387
- scenario_selector.change(reset_conversation, outputs=[chatbot, risk_output, teacher_input, level_display, score_display, achievements_display, stats_display, progress_bar, challenge_display, achievement_popup, achievement_popup.visible])
388
- challenge_selector.change(reset_conversation, outputs=[chatbot, risk_output, teacher_input, level_display, score_display, achievements_display, stats_display, progress_bar, challenge_display, achievement_popup, achievement_popup.visible])
389
 
390
- gr.Markdown("""
391
- 本系統僅供練習與反思使用|風險分析由 AI 生成,僅供參考
392
- """)
393
 
394
  demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
20
  base_url="https://openrouter.ai/api/v1",
21
  api_key=OPENROUTER_API_KEY
22
  )
23
+ MODEL = "google/gemma-3-27b-it:free"
24
 
25
  # 家長人格與情境
26
+ PARENT_PERSONAS = PERSONA_COMPLEXITY
27
+ SCENARIOS = SCENARIO_DIFFICULTY
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ # Prompt(保持不變)
30
+ PARENT_SYSTEM_PROMPT = """..."""
31
+ RISK_ANALYSIS_PROMPT = """..."""
 
 
 
 
 
 
32
 
33
+ # LLM 呼叫(保持不變)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  def call_llm(messages: list, temperature: float = 0.7, max_tokens: int = 500) -> str:
35
+ # 原函數不變
36
+ ...
 
 
 
 
 
 
 
 
37
 
38
  def generate_parent_response(persona: str, scenario: str, chat_history: list, teacher_msg: str | None = None) -> str:
39
+ # 原函數不變
40
+ ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  def analyze_risk(teacher_input: str, persona: str, scenario: str) -> tuple[str, int]:
43
+ # 原函數不變
44
+ ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ # =============== 補全與修復的對話邏輯 ===============
47
  def start_conversation(persona: str, scenario: str, challenge_mode: str):
48
+ # 自適應選擇
49
  if persona == "🤖 自適應選擇" or scenario == "🎯 自適應挑戰":
50
  adaptive = game_state.get_adaptive_selection()
51
  persona = adaptive["persona"]
52
  scenario = adaptive["scenario"]
53
 
 
54
  game_state.current_challenge_mode = challenge_mode
55
+ game_state.current_persona = persona
56
+ game_state.current_scenario = scenario
57
+ game_state.current_round = 0
58
  game_state.challenge_start_time = time.time()
59
 
60
+ # 家長開場
61
+ parent_opening = generate_parent_response(persona, scenario, [], None)
62
+ new_chat = [(None, parent_opening)]
63
+
64
+ risk_html = "💬 請輸入您的回應,系統將即時分析風險"
65
+
66
+ # 更新遊戲狀態(新對話開始)
67
+ new_achievements = game_state.check_achievements()
 
68
 
 
 
 
69
  level_info = game_state.get_level_info()
70
  ach_summary = game_state.get_achievements_summary()
71
  stats_summary = game_state.get_statistics_summary()
72
  challenge_info = game_state.get_daily_challenge_info()
73
+ level_html, score_html, ach_html, stats_html, progress_html, challenge_html = update_game_display(
74
+ level_info, ach_summary, stats_summary, challenge_info)
75
+
76
+ popup_html = format_achievement_popup(new_achievements)
77
+ popup_visible = len(new_achievements) > 0
78
+
79
+ challenge_complete = False # 開始時不會完成
80
+
81
  return (
82
+ new_chat,
83
+ risk_html,
84
+ gr.update(value=""), # 清空輸入框
85
  level_html, score_html, ach_html, stats_html, progress_html, challenge_html,
86
+ gr.update(value=popup_html if popup_visible else "")
87
  )
88
 
89
  def continue_conversation(persona: str, scenario: str, chat_history: list, teacher_input: str):
90
  if not teacher_input.strip():
91
+ return chat_history, "⚠️ 請輸入回應內容", gr.update(), *[gr.update() for _ in range(6)], gr.update()
92
+
93
+ teacher_msg = teacher_input.strip()
94
+ risk_html, risk_score = analyze_risk(teacher_msg, game_state.current_persona, game_state.current_scenario)
95
+
96
+ # 計算分數與更新狀態
97
+ time_taken = time.time() - game_state.last_conversation_time if game_state.last_conversation_time else 0
98
+ score = game_state.calculate_score(risk_score, game_state.current_persona, game_state.current_scenario, time_taken)
99
+ game_state.add_conversation(score, risk_score, game_state.current_persona, game_state.current_scenario, time_taken)
100
+
 
 
101
  new_achievements = game_state.check_achievements()
102
 
103
+ # 家長回應
104
+ parent_response = generate_parent_response(
105
+ game_state.current_persona, game_state.current_scenario, chat_history, teacher_msg)
106
+ new_chat = chat_history + [(teacher_msg, parent_response)]
 
 
 
 
 
 
 
 
 
107
 
108
  # 更新 UI
109
  level_info = game_state.get_level_info()
110
  ach_summary = game_state.get_achievements_summary()
111
  stats_summary = game_state.get_statistics_summary()
112
  challenge_info = game_state.get_daily_challenge_info()
113
+ level_html, score_html, ach_html, stats_html, progress_html, challenge_html = update_game_display(
114
+ level_info, ach_summary, stats_summary, challenge_info)
115
+
116
  popup_html = format_achievement_popup(new_achievements)
117
  popup_visible = len(new_achievements) > 0
118
+
119
+ # 挑戰完成檢查(簡化示例)
120
+ challenge_complete = game_state.check_challenge_complete() if game_state.current_challenge_mode else False
121
+
122
+ teacher_update = gr.update(value="", interactive=not challenge_complete)
123
  if challenge_complete:
124
  risk_html += "\n\n🎉 挑戰完成!請重新選擇情境開始新對話。"
125
+
 
 
126
  return (
127
  new_chat,
128
  risk_html,
129
+ teacher_update,
130
  level_html, score_html, ach_html, stats_html, progress_html, challenge_html,
131
+ gr.update(value=popup_html if popup_visible else "")
132
  )
133
 
134
  def reset_conversation():
135
+ game_state.reset_session()
136
  level_info = game_state.get_level_info()
137
  ach_summary = game_state.get_achievements_summary()
138
  stats_summary = game_state.get_statistics_summary()
139
  challenge_info = game_state.get_daily_challenge_info()
140
+ level_html, score_html, ach_html, stats_html, progress_html, challenge_html = update_game_display(
141
+ level_info, ach_summary, stats_summary, challenge_info)
142
+
143
  return (
144
  [],
145
  "💬 請先選擇人格與情境,然後點「開始對話」",
146
+ gr.update(value=""),
147
  level_html, score_html, ach_html, stats_html, progress_html, challenge_html,
148
+ gr.update(value="")
149
  )
150
 
151
+ # =============== CSS(保持不變)===============
152
  CUSTOM_CSS = """
153
+ /* 原有 CSS */
154
+ ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  """
156
 
157
+ # =============== Gradio UI ===============
158
+ with gr.Blocks(css=CUSTOM_CSS) as demo:
159
+ gr.Markdown("# 一句話的距離|親師溝通 AI 演練場")
160
+ gr.Markdown("AI 驅動親師溝通虛擬演練場(遊戲化版本)\n練習高張力親師溝通,解鎖成就、升級、累積分數!")
161
+
162
+ # 遊戲面板(保持不變)
 
163
  with gr.Column():
164
  with gr.Row(elem_classes="game-panel"):
165
  level_display = gr.HTML()
 
172
  challenge_display = gr.HTML()
173
  achievement_popup = gr.HTML("", visible=False)
174
 
175
+ # 選擇區(保持不變)
176
  with gr.Row():
177
  with gr.Column(scale=1, min_width=300):
178
  persona_choices = ["🤖 自適應選擇"] + list(PARENT_PERSONAS.keys())
179
  scenario_choices = ["🎯 自適應挑戰"] + list(SCENARIOS.keys())
180
+ challenge_modes = ["普通模式", "時間挑戰 (30秒)", "連續挑戰 (3回合)", "困難模式", "每日挑戰"]
 
 
 
 
 
 
181
  persona_selector = gr.Radio(choices=persona_choices, label="👤 選擇家長人格", value="🤖 自適應選擇")
182
  scenario_selector = gr.Radio(choices=scenario_choices, label="📋 選擇溝通情境", value="🎯 自適應挑戰")
183
+ challenge_selector = gr.Radio(choices=challenge_modes, label="⚔️ 挑戰模式", value="普通模式")
184
  start_btn = gr.Button("🆕 開始對話", variant="primary", size="lg")
185
+ # 使用說明(保持不變)
186
+
 
 
 
 
 
 
187
  with gr.Column(scale=3):
188
  chatbot = gr.Chatbot(label="💬 對話紀錄", height=500)
189
  teacher_input = gr.Textbox(label="📝 您的回應(教師)", placeholder="請輸入您想說的話...", lines=4)
 
192
  clear_btn = gr.Button("🔄 重新選擇情境", variant="secondary")
193
  risk_output = gr.Markdown("💬 請先開始對話")
194
 
195
+ # =============== 事件綁定(關鍵修復)===============
196
+ common_outputs = [chatbot, risk_output, teacher_input, level_display, score_display,
197
+ achievements_display, stats_display, progress_bar, challenge_display,
198
+ achievement_popup]
199
+
200
  start_btn.click(
201
  start_conversation,
202
  inputs=[persona_selector, scenario_selector, challenge_selector],
203
+ outputs=common_outputs
204
  )
205
  submit_btn.click(
206
  continue_conversation,
207
  inputs=[persona_selector, scenario_selector, chatbot, teacher_input],
208
+ outputs=common_outputs
209
  )
210
  clear_btn.click(
211
  reset_conversation,
212
+ outputs=common_outputs
213
  )
214
+ persona_selector.change(reset_conversation, outputs=common_outputs)
215
+ scenario_selector.change(reset_conversation, outputs=common_outputs)
216
+ challenge_selector.change(reset_conversation, outputs=common_outputs)
 
217
 
218
+ gr.Markdown("本系統僅供練習與反思使用|風險分析由 AI 生成,僅供參考")
 
 
219
 
220
  demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
game_system.py CHANGED
@@ -267,6 +267,11 @@ class GameState:
267
  self.last_conversation_time = None
268
  self.current_streak = 0
269
  self.high_score_streak = 0
 
 
 
 
 
270
 
271
  def load_data(self) -> Dict:
272
  """載入玩家數據"""
@@ -687,6 +692,26 @@ class GameState:
687
  })
688
  return achievements
689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
 
691
  # ============================================================================
692
  # 遊戲狀態實例
 
267
  self.last_conversation_time = None
268
  self.current_streak = 0
269
  self.high_score_streak = 0
270
+ self.current_challenge_mode = None
271
+ self.current_persona = None
272
+ self.current_scenario = None
273
+ self.current_round = 0
274
+ self.challenge_start_time = None
275
 
276
  def load_data(self) -> Dict:
277
  """載入玩家數據"""
 
692
  })
693
  return achievements
694
 
695
+ def check_challenge_complete(self) -> bool:
696
+ """檢查挑戰是否完成"""
697
+ if not self.current_challenge_mode:
698
+ return False
699
+ if self.current_challenge_mode == "時間挑戰 (30秒)":
700
+ if self.challenge_start_time:
701
+ elapsed = time.time() - self.challenge_start_time
702
+ return elapsed >= 30
703
+ return False
704
+ elif self.current_challenge_mode == "連續挑戰 (3回合)":
705
+ return self.current_round >= 3
706
+ else:
707
+ return False
708
+
709
+ def add_conversation(self, score: int, risk_score: int, persona: str, scenario: str, time_taken: float):
710
+ """添加對話並更新統計"""
711
+ self.update_stats(risk_score, score, scenario, persona, time_taken)
712
+ if self.current_challenge_mode == "連續挑戰 (3回合)":
713
+ self.current_round += 1
714
+
715
 
716
  # ============================================================================
717
  # 遊戲狀態實例
requirements.txt CHANGED
@@ -1,3 +1,2 @@
1
- gradio
2
- openai
3
- litellm
 
1
+ gradio>=4.0
2
+ openai