PhilipL commited on
Commit
3ed9b1d
·
verified ·
1 Parent(s): 75789b3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +171 -48
app.py CHANGED
@@ -10,6 +10,10 @@ from langchain_huggingface import HuggingFaceEmbeddings
10
  from langchain_community.vectorstores import FAISS
11
  from PIL import Image
12
  from dotenv import load_dotenv
 
 
 
 
13
  load_dotenv() # 自動尋找並載入 .env 檔案中的變數
14
 
15
  # ==========================================
@@ -34,27 +38,33 @@ def init_rag_system():
34
 
35
  print("⏳ 正在初始化系統...")
36
 
37
- # --- A. 讀取 CSV (用於篩選與對照) ---
38
  try:
39
- global_df = pd.read_csv('restaurants.csv')
40
- global_df['RAG_Content'] = global_df['RAG_Content'].fillna("")
41
- global_df['Category'] = global_df['Category'].fillna("其他")
42
-
43
- global_mood_df = pd.read_csv('mood_food_guide.csv')
 
 
 
 
 
 
 
 
 
 
44
  print(f"✅ CSV 資料讀取成功 (餐廳: {len(global_df)} 筆)")
45
  except Exception as e:
46
  print(f"❌ CSV 讀取失敗: {e}")
47
  return
48
 
49
- # --- B. 讀取預先建立好的 FAISS 索引 ---
50
- # 這裡不再重新建立,而是直接讀取 "faiss_index" 資料夾
51
  if os.path.exists("faiss_index"):
52
  try:
53
  print("⏳ 正在載入 FAISS 向量資料庫...")
54
  embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
55
-
56
- # allow_dangerous_deserialization=True 是必要的,因為我們讀取 pickle 檔
57
- # 由於這是我們自己建立的檔案,所以是安全的
58
  vectorstore = FAISS.load_local(
59
  "faiss_index",
60
  embeddings,
@@ -64,9 +74,10 @@ def init_rag_system():
64
  print("✅ FAISS 資料庫載入完成!")
65
  except Exception as e:
66
  print(f"❌ FAISS 載入失敗: {e}")
 
67
  else:
68
- print("⚠️ 警告:找不到 'faiss_index' 資料夾,RAG 功能將無法使用。")
69
- print("請先執行 build_index.py 並上傳資料夾。")
70
 
71
  rag_initialized = True
72
 
@@ -80,13 +91,22 @@ custom_css = """
80
  .vertical-radio label { width: 100% !important; margin: 0 !important; display: flex !important; }
81
  """
82
 
83
- img_path_1 = "images/image_0.png"
84
- img_path_2 = "images/image_1.png"
 
 
 
 
 
 
85
 
86
  if not os.path.exists("images"): os.makedirs("images")
87
- if not os.path.exists(img_path_1): Image.new('RGB', (400, 300), color='gray').save(img_path_1)
88
- if not os.path.exists(img_path_2): Image.new('RGB', (400, 300), color='lightblue').save(img_path_2)
 
 
89
 
 
90
  q1_options_map = {
91
  "不​要​再​問白癡​問題​了​我​只​想​吃飯​": "現實主義者 (只想吃飯)",
92
  "去​過隱​居​生活": "隱士 (嚮往平靜)",
@@ -101,6 +121,30 @@ q2_options_map = {
101
  "領域​展開:​無量​空處​": "領域展開 (思緒混亂或想放空)"
102
  }
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  # ==========================================
105
  # 3. 核心功能
106
  # ==========================================
@@ -114,17 +158,20 @@ def get_restaurant_data(mood_score_str, food_choice):
114
  except:
115
  score = 3
116
 
117
- mood_info = global_mood_df[global_mood_df['分數'] == score]
118
- if not mood_info.empty:
119
- rec_categories = mood_info.iloc[0]['推薦料理類別']
120
- mood_reason = mood_info.iloc[0]['原因']
121
- else:
122
- rec_categories = ""
123
- mood_reason = "隨意探索"
 
 
124
 
125
  candidates = global_df.copy()
126
 
127
  if rec_categories:
 
128
  candidates = candidates[candidates['Category'].apply(lambda x: str(x) in str(rec_categories) or str(rec_categories) in str(x))]
129
 
130
  food_keyword = "飯" if food_choice == "吃飯" else "麵" if food_choice == "吃麵" else ""
@@ -148,10 +195,21 @@ def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, m
148
 
149
  client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
150
 
151
- system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友。請根據使用者的日記、心情以及「心情美食指南」來推薦餐廳。"
 
 
 
 
 
 
 
 
 
 
152
  user_msg = f"""
153
  【狀態】心情分數:{mood_score},日記:{user_diary}
154
- 【測驗結果】人設:{quiz_result.get('q1', '未知')},魔法:{quiz_result.get('q2', '未知')}
 
155
 
156
  【心情美食指南建議】
157
  因為分數是 {mood_score},建議吃這類食物的原因是:「{mood_guide_reason}」。
@@ -162,9 +220,9 @@ def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, m
162
 
163
  任務:
164
  請用繁體中文寫一段溫暖有趣的回覆:
165
- 1. 先回應他的日記與測驗人設。
166
- 2. 引用「心情美食指南」的原因,告訴他為什麼現在適合吃這家餐廳(例如:「就像指南說的,現在你需要一點多巴胺...」)。
167
- 3. 介紹這家餐廳的特色。
168
  (只需要回覆文字內容)
169
  """
170
  try:
@@ -193,18 +251,20 @@ def mood_agent_logic(score_input, food_input, diary_input, quiz_state):
193
  yield "資料庫讀取錯誤", None, ""
194
  return
195
 
196
- name = restaurant['Name']
197
- address = restaurant['Address']
198
- url = restaurant['URL']
199
  img_prompt = restaurant.get('Visual_prompt')
200
  note = "(隨機推薦)" if is_random else ""
201
 
202
- # RAG 檢索 (直接用 global_retriever)
203
  rag_info = str(restaurant.get('RAG_Content', ''))
204
  if global_retriever:
205
- docs = global_retriever.invoke(name)
206
- if docs:
207
- rag_info = "\n".join([d.page_content for d in docs])
 
 
208
 
209
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, quiz_state, mood_reason)
210
 
@@ -218,15 +278,32 @@ def mood_agent_logic(score_input, food_input, diary_input, quiz_state):
218
  # ==========================================
219
  # 4. 介面互動邏輯
220
  # ==========================================
221
- # (這部分與之前相同,略)
222
  def handle_q1_change(selected_label, current_state):
223
  if not selected_label: return current_state, gr.Button(interactive=False)
224
- current_state["q1"] = q1_options_map[selected_label]
225
  return current_state, gr.Button(interactive=True, variant="primary")
226
 
227
  def handle_q2_change(selected_label, current_state):
228
  if not selected_label: return current_state, gr.Button(interactive=False)
229
- current_state["q2"] = q2_options_map[selected_label]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  return current_state, gr.Button(interactive=True, variant="primary", value="完成測驗 (前往點餐) ➔")
231
 
232
  # ==========================================
@@ -236,12 +313,13 @@ with gr.Blocks(title="AI 心情食堂") as demo:
236
  gr.HTML(f"<style>{custom_css}</style>")
237
  quiz_state = gr.State(value={})
238
 
 
239
  with gr.Tabs(elem_id="hidden_tabs") as tabs:
240
  # Tab 0: Q1
241
  with gr.TabItem("Q1", id=0):
242
  with gr.Column():
243
- gr.Markdown("### 🔮 第一題:直覺測試")
244
- gr.Image(value=img_path_1, type="filepath", label="請觀察圖片", height=300)
245
  gr.Markdown("**問題:請觀察上方圖片,如果是你,你會怎麼做?**")
246
  radio_q1 = gr.Radio(choices=list(q1_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
247
  btn_q1_next = gr.Button("下一頁 ➔", interactive=False)
@@ -249,16 +327,44 @@ with gr.Blocks(title="AI 心情食堂") as demo:
249
  # Tab 1: Q2
250
  with gr.TabItem("Q2", id=1):
251
  with gr.Column():
252
- gr.Markdown("### 🔮 第二題:魔法適性")
253
- gr.Image(value=img_path_2, type="filepath", label="請觀察圖片", height=300)
254
  gr.Markdown("**問題:身為魔導士的你,會選擇哪一個法術?**")
255
  radio_q2 = gr.Radio(choices=list(q2_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
256
- btn_q2_finish = gr.Button("完成測驗 ➔", interactive=False)
257
 
258
- # Tab 2: Main
259
- with gr.TabItem("Main", id=2):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  with gr.Column():
261
  gr.Markdown(f"## 🍱 呷飽沒??!")
 
262
  with gr.Row():
263
  with gr.Column(scale=1):
264
  score_input = gr.Radio(["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"], label="1. 心情分數", value="3 (普通)")
@@ -270,11 +376,28 @@ with gr.Blocks(title="AI 心情食堂") as demo:
270
  image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
271
  map_output = gr.HTML(label="地圖導航")
272
 
 
 
273
  radio_q1.change(fn=handle_q1_change, inputs=[radio_q1, quiz_state], outputs=[quiz_state, btn_q1_next])
274
  btn_q1_next.click(fn=lambda: gr.Tabs(selected=1), outputs=tabs)
275
- radio_q2.change(fn=handle_q2_change, inputs=[radio_q2, quiz_state], outputs=[quiz_state, btn_q2_finish])
276
- btn_q2_finish.click(fn=lambda: gr.Tabs(selected=2), outputs=tabs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
 
 
278
  submit_btn.click(
279
  fn=mood_agent_logic,
280
  inputs=[score_input, food_input, diary_input, quiz_state],
 
10
  from langchain_community.vectorstores import FAISS
11
  from PIL import Image
12
  from dotenv import load_dotenv
13
+ import warnings
14
+
15
+ # 忽略警告
16
+ warnings.filterwarnings("ignore")
17
  load_dotenv() # 自動尋找並載入 .env 檔案中的變數
18
 
19
  # ==========================================
 
38
 
39
  print("⏳ 正在初始化系統...")
40
 
41
+ # --- A. 讀取 CSV ---
42
  try:
43
+ # 嘗試讀取 restaurants.csv (請確保檔案存在)
44
+ if os.path.exists('restaurants.csv'):
45
+ global_df = pd.read_csv('restaurants.csv')
46
+ global_df['RAG_Content'] = global_df['RAG_Content'].fillna("")
47
+ global_df['Category'] = global_df['Category'].fillna("其他")
48
+ else:
49
+ print("⚠️ 找不到 restaurants.csv,將使用空的 DataFrame")
50
+ global_df = pd.DataFrame(columns=['Name', 'Category', 'Address', 'URL', 'Visual_prompt', 'RAG_Content'])
51
+
52
+ # 嘗試讀取 mood_food_guide.csv (若無則建立預設)
53
+ if os.path.exists('mood_food_guide.csv'):
54
+ global_mood_df = pd.read_csv('mood_food_guide.csv')
55
+ else:
56
+ global_mood_df = pd.DataFrame(columns=['分數', '推薦料理類別', '原因'])
57
+
58
  print(f"✅ CSV 資料讀取成功 (餐廳: {len(global_df)} 筆)")
59
  except Exception as e:
60
  print(f"❌ CSV 讀取失敗: {e}")
61
  return
62
 
63
+ # --- B. 讀取 FAISS 索引 ---
 
64
  if os.path.exists("faiss_index"):
65
  try:
66
  print("⏳ 正在載入 FAISS 向量資料庫...")
67
  embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
 
 
 
68
  vectorstore = FAISS.load_local(
69
  "faiss_index",
70
  embeddings,
 
74
  print("✅ FAISS 資料庫載入完成!")
75
  except Exception as e:
76
  print(f"❌ FAISS 載入失敗: {e}")
77
+ global_retriever = None
78
  else:
79
+ print("⚠️ 警告:找不到 'faiss_index' 資料夾,RAG 功能將受限。")
80
+ global_retriever = None
81
 
82
  rag_initialized = True
83
 
 
91
  .vertical-radio label { width: 100% !important; margin: 0 !important; display: flex !important; }
92
  """
93
 
94
+ # 設定圖片路徑
95
+ img_paths = {
96
+ "q1": "images/image_0.png",
97
+ "q2": "images/image_1.png",
98
+ "q3": "images/image_3.png",
99
+ "q4": "images/image_4.png",
100
+ "q5": "images/image_5.png"
101
+ }
102
 
103
  if not os.path.exists("images"): os.makedirs("images")
104
+ # 自動產生缺失的圖片(僅作為佔位符)
105
+ for key, path in img_paths.items():
106
+ if not os.path.exists(path):
107
+ Image.new('RGB', (400, 300), color='#E0E0E0').save(path)
108
 
109
+ # --- 選項設定 (您可以修改這裡的選項內容) ---
110
  q1_options_map = {
111
  "不​要​再​問白癡​問題​了​我​只​想​吃飯​": "現實主義者 (只想吃飯)",
112
  "去​過隱​居​生活": "隱士 (嚮往平靜)",
 
121
  "領域​展開:​無量​空處​": "領域展開 (思緒混亂或想放空)"
122
  }
123
 
124
+ # [新增] Q3 選項 - 旅遊
125
+ q3_options_map = {
126
+ "日本 (秋葉原/古都巡禮)": "哈日族 (喜歡精緻美食)",
127
+ "歐洲 (充滿歷史與浪漫)": "浪漫派 (喜歡氣氛佳)",
128
+ "熱帶海島 (放空度假)": "享樂主義 (喜歡海鮮或冷飲)",
129
+ "南極 (挑戰極限)": "冒險家 (喜歡新奇食物)"
130
+ }
131
+
132
+ # [新增] Q4 選項 - 社交
133
+ q4_options_map = {
134
+ "那個看起來最受歡迎的核心人物": "外向者 (喜歡熱鬧聚餐)",
135
+ "角落安靜滑手機的人": "內向者 (喜歡安靜獨食)",
136
+ "正在專心吃東西的人": "吃貨 (食物比人重要)",
137
+ "看起來跟我一樣尷尬的人": "隨和者 (好相處)"
138
+ }
139
+
140
+ # [新增] Q5 選項 - 追劇
141
+ q5_options_map = {
142
+ "絕命毒師 (Breaking Bad)": "重口味/劇情控",
143
+ "六人行 (Friends)": "輕鬆美式/早午餐",
144
+ "鬼滅之刃": "熱血動漫迷",
145
+ "甄嬛傳": "宮廷/精緻料理"
146
+ }
147
+
148
  # ==========================================
149
  # 3. 核心功能
150
  # ==========================================
 
158
  except:
159
  score = 3
160
 
161
+ # 嘗試從 mood_food_guide 獲取建議,若無則預設
162
+ rec_categories = ""
163
+ mood_reason = "隨意探索"
164
+
165
+ if global_mood_df is not None and not global_mood_df.empty:
166
+ mood_info = global_mood_df[global_mood_df['分數'] == score]
167
+ if not mood_info.empty:
168
+ rec_categories = mood_info.iloc[0]['推薦料理類別']
169
+ mood_reason = mood_info.iloc[0]['原因']
170
 
171
  candidates = global_df.copy()
172
 
173
  if rec_categories:
174
+ # 簡單篩選類別
175
  candidates = candidates[candidates['Category'].apply(lambda x: str(x) in str(rec_categories) or str(rec_categories) in str(x))]
176
 
177
  food_keyword = "飯" if food_choice == "吃飯" else "麵" if food_choice == "吃麵" else ""
 
195
 
196
  client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
197
 
198
+ system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友。請根據使用者的日記、心情以及「五題心理測驗結果」來推薦餐廳。"
199
+
200
+ # 組合測驗結果字串
201
+ quiz_summary = f"""
202
+ 1. 直覺/人設: {quiz_result.get('q1', '未知')}
203
+ 2. 魔法/口味: {quiz_result.get('q2', '未知')}
204
+ 3. 旅遊/嚮往: {quiz_result.get('q3', '未知')}
205
+ 4. 社交/個性: {quiz_result.get('q4', '未知')}
206
+ 5. 追劇/喜好: {quiz_result.get('q5', '未知')}
207
+ """
208
+
209
  user_msg = f"""
210
  【狀態】心情分數:{mood_score},日記:{user_diary}
211
+ 【心理測驗全結果】
212
+ {quiz_summary}
213
 
214
  【心情美食指南建議】
215
  因為分數是 {mood_score},建議吃這類食物的原因是:「{mood_guide_reason}」。
 
220
 
221
  任務:
222
  請用繁體中文寫一段溫暖有趣的回覆:
223
+ 1. 綜合分析他的測驗結果(例如:提到他想去哪裡玩、或是想看什麼劇的關聯性)。
224
+ 2. 結合日記內容與心情給予安慰或鼓勵。
225
+ 3. 推薦上述餐廳,說明為什麼這家店適合「現在的他」。
226
  (只需要回覆文字內容)
227
  """
228
  try:
 
251
  yield "資料庫讀取錯誤", None, ""
252
  return
253
 
254
+ name = str(restaurant['Name'])
255
+ address = str(restaurant.get('Address', '未知地址'))
256
+ url = str(restaurant.get('URL', f'https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(name)}'))
257
  img_prompt = restaurant.get('Visual_prompt')
258
  note = "(隨機推薦)" if is_random else ""
259
 
260
+ # RAG 檢索
261
  rag_info = str(restaurant.get('RAG_Content', ''))
262
  if global_retriever:
263
+ try:
264
+ docs = global_retriever.invoke(name)
265
+ if docs:
266
+ rag_info = "\n".join([d.page_content for d in docs])
267
+ except: pass
268
 
269
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, quiz_state, mood_reason)
270
 
 
278
  # ==========================================
279
  # 4. 介面互動邏輯
280
  # ==========================================
281
+
282
  def handle_q1_change(selected_label, current_state):
283
  if not selected_label: return current_state, gr.Button(interactive=False)
284
+ current_state["q1"] = q1_options_map.get(selected_label, selected_label)
285
  return current_state, gr.Button(interactive=True, variant="primary")
286
 
287
  def handle_q2_change(selected_label, current_state):
288
  if not selected_label: return current_state, gr.Button(interactive=False)
289
+ current_state["q2"] = q2_options_map.get(selected_label, selected_label)
290
+ return current_state, gr.Button(interactive=True, variant="primary")
291
+
292
+ # [新增] Q3, Q4, Q5 的處理函式
293
+ def handle_q3_change(selected_label, current_state):
294
+ if not selected_label: return current_state, gr.Button(interactive=False)
295
+ current_state["q3"] = q3_options_map.get(selected_label, selected_label)
296
+ return current_state, gr.Button(interactive=True, variant="primary")
297
+
298
+ def handle_q4_change(selected_label, current_state):
299
+ if not selected_label: return current_state, gr.Button(interactive=False)
300
+ current_state["q4"] = q4_options_map.get(selected_label, selected_label)
301
+ return current_state, gr.Button(interactive=True, variant="primary")
302
+
303
+ def handle_q5_change(selected_label, current_state):
304
+ if not selected_label: return current_state, gr.Button(interactive=False)
305
+ current_state["q5"] = q5_options_map.get(selected_label, selected_label)
306
+ # 最後一題,按鈕文字改為完成
307
  return current_state, gr.Button(interactive=True, variant="primary", value="完成測驗 (前往點餐) ➔")
308
 
309
  # ==========================================
 
313
  gr.HTML(f"<style>{custom_css}</style>")
314
  quiz_state = gr.State(value={})
315
 
316
+ # 使用 Tabs 隱藏導覽列,透過按鈕切換
317
  with gr.Tabs(elem_id="hidden_tabs") as tabs:
318
  # Tab 0: Q1
319
  with gr.TabItem("Q1", id=0):
320
  with gr.Column():
321
+ gr.Markdown("### 🔮 Q1:直覺測試")
322
+ gr.Image(value=img_paths["q1"], type="filepath", label="請觀察圖片", height=300)
323
  gr.Markdown("**問題:請觀察上方圖片,如果是你,你會怎麼做?**")
324
  radio_q1 = gr.Radio(choices=list(q1_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
325
  btn_q1_next = gr.Button("下一頁 ➔", interactive=False)
 
327
  # Tab 1: Q2
328
  with gr.TabItem("Q2", id=1):
329
  with gr.Column():
330
+ gr.Markdown("### 🔮 Q2:魔法適性")
331
+ gr.Image(value=img_paths["q2"], type="filepath", label="請觀察圖片", height=300)
332
  gr.Markdown("**問題:身為魔導士的你,會選擇哪一個法術?**")
333
  radio_q2 = gr.Radio(choices=list(q2_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
334
+ btn_q2_next = gr.Button("下一頁 ➔", interactive=False)
335
 
336
+ # Tab 2: Q3 (新增)
337
+ with gr.TabItem("Q3", id=2):
338
+ with gr.Column():
339
+ gr.Markdown("### ✈️ Q3:旅遊嚮往")
340
+ gr.Image(value=img_paths["q3"], type="filepath", label="機場圖片", height=300)
341
+ gr.Markdown("**問題:不考慮其他因素,假你在桃機你最想去哪裡玩?**")
342
+ radio_q3 = gr.Radio(choices=list(q3_options_map.keys()), label="請選擇目的地", elem_classes="vertical-radio")
343
+ btn_q3_next = gr.Button("下一頁 ➔", interactive=False)
344
+
345
+ # Tab 3: Q4 (新增)
346
+ with gr.TabItem("Q4", id=3):
347
+ with gr.Column():
348
+ gr.Markdown("### 🤝 Q4:社交直覺")
349
+ gr.Image(value=img_paths["q4"], type="filepath", label="聚會圖片", height=300)
350
+ gr.Markdown("**問題:你是小大一,班上還不熟,現在你正參加你們班上的認識彼此的活動。下列哪一個人是你會選擇搭話的人?**")
351
+ radio_q4 = gr.Radio(choices=list(q4_options_map.keys()), label="請選擇對象", elem_classes="vertical-radio")
352
+ btn_q4_next = gr.Button("下一頁 ➔", interactive=False)
353
+
354
+ # Tab 4: Q5 (新增)
355
+ with gr.TabItem("Q5", id=4):
356
+ with gr.Column():
357
+ gr.Markdown("### 📺 Q5:追劇時光")
358
+ gr.Image(value=img_paths["q5"], type="filepath", label="劇照/電視圖片", height=300)
359
+ gr.Markdown("**問題:假設失憶了忘記以下所列的劇的劇情,而你現在閒了下來剛好想看劇,你會想看哪一部?**")
360
+ radio_q5 = gr.Radio(choices=list(q5_options_map.keys()), label="請選擇劇集", elem_classes="vertical-radio")
361
+ btn_q5_finish = gr.Button("完成測驗 ➔", interactive=False)
362
+
363
+ # Tab 5: Main (ID 改為 5)
364
+ with gr.TabItem("Main", id=5):
365
  with gr.Column():
366
  gr.Markdown(f"## 🍱 呷飽沒??!")
367
+ gr.Markdown("測驗完成!AI 已根據你的選擇分析出最適合你的用餐氛圍。")
368
  with gr.Row():
369
  with gr.Column(scale=1):
370
  score_input = gr.Radio(["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"], label="1. 心情分數", value="3 (普通)")
 
376
  image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
377
  map_output = gr.HTML(label="地圖導航")
378
 
379
+ # --- Event Binding ---
380
+ # Q1 -> Q2
381
  radio_q1.change(fn=handle_q1_change, inputs=[radio_q1, quiz_state], outputs=[quiz_state, btn_q1_next])
382
  btn_q1_next.click(fn=lambda: gr.Tabs(selected=1), outputs=tabs)
383
+
384
+ # Q2 -> Q3
385
+ radio_q2.change(fn=handle_q2_change, inputs=[radio_q2, quiz_state], outputs=[quiz_state, btn_q2_next])
386
+ btn_q2_next.click(fn=lambda: gr.Tabs(selected=2), outputs=tabs)
387
+
388
+ # Q3 -> Q4
389
+ radio_q3.change(fn=handle_q3_change, inputs=[radio_q3, quiz_state], outputs=[quiz_state, btn_q3_next])
390
+ btn_q3_next.click(fn=lambda: gr.Tabs(selected=3), outputs=tabs)
391
+
392
+ # Q4 -> Q5
393
+ radio_q4.change(fn=handle_q4_change, inputs=[radio_q4, quiz_state], outputs=[quiz_state, btn_q4_next])
394
+ btn_q4_next.click(fn=lambda: gr.Tabs(selected=4), outputs=tabs)
395
+
396
+ # Q5 -> Main
397
+ radio_q5.change(fn=handle_q5_change, inputs=[radio_q5, quiz_state], outputs=[quiz_state, btn_q5_finish])
398
+ btn_q5_finish.click(fn=lambda: gr.Tabs(selected=5), outputs=tabs)
399
 
400
+ # Main Agent Submit
401
  submit_btn.click(
402
  fn=mood_agent_logic,
403
  inputs=[score_input, food_input, diary_input, quiz_state],