PhilipL commited on
Commit
203d188
·
verified ·
1 Parent(s): 2a79b78

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +106 -169
app.py CHANGED
@@ -4,54 +4,41 @@
4
 
5
  import os
6
  import subprocess
 
 
 
 
7
 
8
- # 強制設定 Playwright 瀏覽器存放於 user 目錄
 
9
  os.environ["PLAYWRIGHT_BROWSERS_PATH"] = "/home/user/.cache/ms-playwright"
10
 
11
  try:
12
- # 如果目錄不存在,才執行下載,節省啟動時間
13
  if not os.path.exists(os.environ["PLAYWRIGHT_BROWSERS_PATH"]):
14
- print("正在為 Debian Trixie 環境安裝 Chromium...")
15
- # 僅安裝 chromium 即可,依賴項由 packages.txt 處理
16
  subprocess.run(["python3", "-m", "playwright", "install", "chromium"], check=True)
17
  except Exception as e:
18
  print(f"Playwright 初始化提示: {e}")
19
 
20
- # --- 3. 匯入必要的模組 ---
21
  import gradio as gr
22
  from playwright.sync_api import sync_playwright
23
- from dotenv import load_dotenv
24
- import pandas as pd
25
- import urllib.parse # 用於處理網址編碼
26
- import time # ✅ 確保這行存在
27
  from langchain_huggingface import HuggingFaceEmbeddings
28
  from langchain_community.vectorstores import FAISS
29
- from PIL import Image
30
  from openai import OpenAI
31
 
32
- # ✅ 這裡就是漏掉的地方,必須加入這行:
33
- from dotenv import load_dotenv
34
-
35
- # --- 匯入組員的模組 ---
36
- import emotion
37
- # ---------------------------
38
-
39
- load_dotenv()
40
-
41
- # 自動安裝 Playwright 瀏覽器 (HF 環境必要步驟)
42
  try:
43
- import playwright
44
  except ImportError:
45
- subprocess.run(["pip", "install", "playwright"])
46
- subprocess.run(["playwright", "install", "chromium"])
47
-
48
- from playwright.sync_api import sync_playwright
49
 
50
  # ==========================================
51
- # 0. 環境變數 & 系統初始化
52
  # ==========================================
53
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
54
- HF_TOKEN = os.getenv("HF_TOKEN")
55
 
56
  global_df = None
57
  global_mood_df = None
@@ -60,96 +47,57 @@ rag_initialized = False
60
 
61
  def init_static_data():
62
  global global_mood_df
63
- try:
64
- global_mood_df = pd.read_csv('mood_food_guide.csv')
65
- except:
66
- global_mood_df = pd.DataFrame({
67
- '分數': [1, 2, 3, 4, 5],
68
- '推薦料理類別': ["療癒系", "暖胃", "隨意", "豐富", "大餐"],
69
- '原因': ["心情低落需要甜食", "給疲憊的自己一點溫暖", "隨意探索", "享受美食的快樂", "慶祝美好的時刻"]
70
- })
 
71
 
72
  # ==========================================
73
- # 1. 優化後的動態同步功能
74
  # ==========================================
75
  def sync_google_maps(url):
76
  global global_df, global_retriever, rag_initialized
77
  clean_url = url.strip() if url else ""
78
  if not clean_url or "http" not in clean_url.lower():
79
- yield "❌ 請輸入有效的網址。", None
80
  return
81
 
82
  try:
83
- yield "🚀 [1/4] 啟動瀏覽器...", None
84
  with sync_playwright() as p:
85
- # 關閉 headless 偵測,模擬真實使用者
86
  browser = p.chromium.launch(headless=True)
87
- context = browser.new_context(
88
- viewport={'width': 1280, 'height': 900},
89
- user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
90
- )
91
- page = context.new_page()
92
-
93
- yield "🌐 [2/4] 正在前往地圖頁面...", None
94
  page.goto(clean_url, wait_until="domcontentloaded", timeout=60000)
 
 
 
95
 
96
- # --- 關鍵:處理 Google 的 Cookie/隱私同意視窗 ---
97
- try:
98
- # 尋找「全部接受」或「我同意」按鈕
99
- consent_btn = page.locator('button:has-text("Accept all"), button:has-text("全部接受"), button:has-text("我同意")')
100
- if consent_btn.count() > 0:
101
- consent_btn.first.click()
102
- time.sleep(2)
103
- except: pass
104
-
105
- yield "⏳ [3/4] 等待列表內容載入...", None
106
- time.sleep(5) # 給予足夠時間執行 JS
107
 
108
- # 執行自動捲動以觸發圖片載入
109
- page.mouse.wheel(0, 3000)
110
- time.sleep(2)
111
-
112
- # 存下除錯截圖
113
- page.screenshot(path="debug_screenshot.png")
114
-
115
- yield "📄 [4/4] 解析餐廳資訊...", "debug_screenshot.png"
116
-
117
- # 使用更廣泛的選取器邏輯
118
- results = page.evaluate("""() => {
119
- const data = [];
120
- // 尋找所有可能是清單項目的 div
121
- const cards = document.querySelectorAll('div[role="article"], div.m67Hec, .U9S99c');
122
-
123
- cards.forEach(card => {
124
- const nameEl = card.querySelector('.fontHeadlineSmall, .q876ed, div[style*="font-size: 1.125rem"]');
125
- const addrEl = card.querySelector('.fontBodyMedium, .W4Efsd');
126
- const imgEl = card.querySelector('img[src^="http"]');
127
-
128
- if (nameEl && nameEl.innerText.trim()) {
129
- data.push({
130
- name: nameEl.innerText.trim(),
131
- addr: addrEl ? addrEl.innerText.trim() : "地址詳見地圖",
132
- img: imgEl ? imgEl.src : ""
133
- });
134
- }
135
- });
136
- return data;
137
- }""")
138
-
139
  restaurant_list = []
140
- for res in results:
141
- restaurant_list.append({
142
- "Name": res['name'],
143
- "Address": res['addr'],
144
- "Image_URL": res['img'] if res['img'] else "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500",
145
- "RAG_Content": f"餐廳:{res['name']},地址:{res['addr']}",
146
- "URL": f"https://www.google.com/search?q={urllib.parse.quote(res['name'])}"
147
- })
148
-
 
 
149
  browser.close()
150
 
151
  if not restaurant_list:
152
- yield "⚠️ 仍找不到餐廳。請查看下方的「除錯截圖」確認頁面內容。", "debug_screenshot.png"
153
  return
154
 
155
  global_df = pd.DataFrame(restaurant_list)
@@ -157,20 +105,18 @@ def sync_google_maps(url):
157
  vectorstore = FAISS.from_texts(global_df['RAG_Content'].tolist(), embeddings, metadatas=global_df.to_dict('records'))
158
  global_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
159
  rag_initialized = True
160
- yield f"✅ 同步成功!抓到 {len(restaurant_list)} 間餐廳。", "debug_screenshot.png"
161
-
162
  except Exception as e:
163
- yield f"❌ 系統錯誤:{str(e)}", None
164
 
165
  # ==========================================
166
- # 2. 核心推薦邏輯 (確保 fallback 機制)
167
  # ==========================================
168
  def get_restaurant_data(mood_score_str, food_choice):
169
  init_static_data()
170
  if global_df is None or global_df.empty:
171
  return None, True, "資料庫未載入", "尚未同步地圖資料", ""
172
 
173
- # 取得目前資料庫中所有餐廳的名稱清單,方便回報給使用者
174
  all_restaurant_names = "、".join(global_df['Name'].tolist())
175
 
176
  try: score = int(str(mood_score_str).split(' ')[0])
@@ -184,73 +130,73 @@ def get_restaurant_data(mood_score_str, food_choice):
184
 
185
  is_match = True
186
  if food_keyword:
187
- # 過濾包含關鍵字的餐廳
188
  filtered = candidates[candidates['Name'].str.contains(food_keyword) | candidates['RAG_Content'].str.contains(food_keyword)]
189
  if not filtered.empty:
190
  candidates = filtered
191
  else:
192
- # ❗ 關鍵修改:如果找不到符合的,標記為「未命中」並使用全部資料隨機抽樣
193
  is_match = False
194
 
195
- # 從候選(或全部)中隨機挑選一間
196
  result = candidates.sample(1).iloc[0]
197
  return result, is_match, "個人清單", mood_reason, all_restaurant_names
198
 
199
- # (其餘 generate_content_with_groq, mood_agent_logic 與橋接邏輯保持不變...)
200
- def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, mood_guide_reason, debug_mode=False):
201
  if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY"
202
  client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
203
- system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友..."
204
- user_msg = f"【狀態】心情分數:{mood_score}\n【日記】{user_diary}\n【建議原因】{mood_guide_reason}\n【餐廳】{restaurant_name}\n資料:{restaurant_detail}"
205
  try:
206
  response = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}])
207
  return response.choices[0].message.content
208
  except Exception as e: return f"Groq Error: {str(e)}"
209
 
210
- def generate_image_huggingface(prompt):
211
- if not HF_TOKEN: return None
212
- try:
213
- hf_client = InferenceClient(token=HF_TOKEN)
214
- return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
215
- except: return None
216
-
217
  def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
218
  if not rag_initialized:
219
- yield "⚠️ 請先同步地圖清單!", None, ""
220
  return
221
 
222
- # 取得推薦餐廳資料
223
- restaurant, is_match, rec_categories, mood_reason, all_names = get_restaurant_data(score_input, food_input)
224
 
225
  prefix = ""
226
- status_hint = ""
227
  if not is_match and food_input != "隨便":
228
- prefix = f"> 💡 **溫馨提示**:您的清單目前只有:**{all_names}**,沒找到符合『{food_input}』的店。我先幫您從現有名單挑一間!\n\n"
229
- status_hint = f"\n(注意:清單中無 '{food_input}',請幽默解釋為何先推薦這間。)"
230
 
231
  name = restaurant['Name']
 
232
  url = restaurant.get('URL', '#')
233
- # 直接抓取剛才爬到的圖片 URL
234
- img_url = restaurant.get('Image_URL')
235
- rag_info = str(restaurant.get('RAG_Content', '')) + status_hint
236
 
237
- # 生成 AI 文字評論
238
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, mood_reason)
 
239
 
240
- final_response = f"{prefix}### 🍽️ 推薦:{name}\n\n{ai_text}"
241
- map_html = f'<div style="text-align:center"><a href="{url}" target="_blank" style="background:#4CAF50;color:white;padding:8px 16px;border-radius:20px;text-decoration:none">🗺️ 導航</a></div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- # 第二個回傳值改為 img_url,Gradio 的 gr.Image 會自動處理網址
244
- yield final_response, img_url, map_html
245
 
246
  # ==========================================
247
- # 3. 橋接邏輯 (保留原本情緒辨識部分)
248
  # ==========================================
249
  def _score_to_radio_value(score):
250
  mapping = {1: "1 (心情差)", 2: "2 (不太好)", 3: "3 (普通)", 4: "4 (不錯)", 5: "5 (超棒)"}
251
- try: score = int(score)
252
- except: score = 3
253
- return mapping.get(score, "3 (普通)")
254
 
255
  def bridge_start_click(st):
256
  res = emotion.on_restart(st)
@@ -282,66 +228,57 @@ def bridge_predict_upload(img, st):
282
  return out_result, score_update, tabs_update, out_st
283
 
284
  # ==========================================
285
- # 4. Gradio 介面建構
286
  # ==========================================
287
- combined_css = emotion.css
288
-
289
- with gr.Blocks(title="AI 心情食堂", css=combined_css) as demo:
290
  st_state = gr.State(emotion.AppState())
291
 
292
  with gr.Tabs() as tabs:
293
  # Tab 1: 情緒辨識
294
- with gr.TabItem("😊 情緒辨識 (Step 1)", id=0):
295
- with gr.Column(elem_id="app_container"):
296
- gr.Markdown("### 第一步:測測你的心情能量")
297
  with gr.Row():
298
- btn_start = gr.Button("📸 開啟攝影機辨識", variant="primary")
299
- btn_stop = gr.Button("⏹️ 停止", variant="secondary", visible=False)
300
  cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機畫面", visible=False)
301
- result_markdown = gr.Markdown(emotion._hint_html("請按「開啟攝影機辨識」或下方上傳照片。"))
302
- gr.Markdown("---")
303
- upload_img = gr.Image(sources=["upload"], type="numpy", label="上傳照片")
304
 
305
  # Tab 2: 主功能區
306
- with gr.TabItem("🍽️ AI 心情食堂 (Step 2)", id=1):
307
  with gr.Column():
308
- gr.Markdown("## 🍱 同步口袋名單\n請貼上你的 Google Maps「已儲存地點」分享連結(需設為共用)。")
309
  with gr.Row():
310
- map_url = gr.Textbox(label="Google Maps 分享連結", placeholder="https://goo.gl/maps/...")
311
- sync_btn = gr.Button("🔄 同步我的清單", variant="secondary")
312
- with gr.Row():
313
- map_url = gr.Textbox(label="Google Maps 分享連結", placeholder="https://goo.gl/maps/...")
314
- sync_btn = gr.Button("🔄 同步我的清單", variant="secondary")
315
- debug_view = gr.Image(label="爬蟲即時畫面 (除錯用)", type="filepath", height=300)
316
-
317
- sync_msg = gr.Markdown("ℹ️ 尚未同步資料庫")
318
 
319
- gr.Markdown("---")
320
 
321
  with gr.Row():
322
  with gr.Column(scale=1):
323
  score_input = gr.Radio(
324
  ["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"],
325
- label="1. 心情分數",
326
  value="3 (普通)"
327
  )
328
- food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
329
- diary_input = gr.Textbox(lines=4, label="3. 心情日記", placeholder="寫下今天的心情...")
330
- debug_mode_btn = gr.Checkbox(label="🔧 開啟除錯模式", value=False)
331
- submit_btn = gr.Button("🍱 推薦今天的驚喜", variant="primary")
332
 
333
  with gr.Column(scale=1):
334
- agent_output = gr.Markdown(label="AI 回應")
335
- image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
336
- map_output = gr.HTML(label="地圖導航")
337
 
338
  # 事件綁定
339
- sync_btn.click(fn=sync_google_maps, inputs=[map_url], outputs=[sync_msg, debug_view])
340
  btn_start.click(fn=bridge_start_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state])
341
  btn_stop.click(fn=bridge_stop_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state])
342
  cam.stream(fn=bridge_predict_frame, inputs=[cam, st_state], outputs=[cam, result_markdown, st_state, btn_stop, btn_start, score_input, tabs])
343
  upload_img.change(fn=bridge_predict_upload, inputs=[upload_img, st_state], outputs=[result_markdown, score_input, tabs, st_state])
344
- submit_btn.click(fn=mood_agent_logic, inputs=[score_input, food_input, diary_input, debug_mode_btn], outputs=[agent_output, image_output, map_output])
345
 
346
  if __name__ == "__main__":
347
- demo.launch(ssr_mode=False)
 
4
 
5
  import os
6
  import subprocess
7
+ import urllib.parse
8
+ import time
9
+ import pandas as pd
10
+ from dotenv import load_dotenv
11
 
12
+ # --- 1. 環境變數 & Playwright 初始化 ---
13
+ load_dotenv()
14
  os.environ["PLAYWRIGHT_BROWSERS_PATH"] = "/home/user/.cache/ms-playwright"
15
 
16
  try:
 
17
  if not os.path.exists(os.environ["PLAYWRIGHT_BROWSERS_PATH"]):
18
+ print("正在為環境安裝 Chromium...")
 
19
  subprocess.run(["python3", "-m", "playwright", "install", "chromium"], check=True)
20
  except Exception as e:
21
  print(f"Playwright 初始化提示: {e}")
22
 
23
+ # --- 2. 匯入核心模組 ---
24
  import gradio as gr
25
  from playwright.sync_api import sync_playwright
 
 
 
 
26
  from langchain_huggingface import HuggingFaceEmbeddings
27
  from langchain_community.vectorstores import FAISS
 
28
  from openai import OpenAI
29
 
30
+ # 假設這是您組員開發的模組
 
 
 
 
 
 
 
 
 
31
  try:
32
+ import emotion
33
  except ImportError:
34
+ # 防止因缺少 emotion 模組而無法執行
35
+ print("警告:找不到 emotion 模組,請確保該檔案存在。")
 
 
36
 
37
  # ==========================================
38
+ # 0. 全域設定
39
  # ==========================================
40
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
41
+ GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY") # 請確保 .env 中有此項
42
 
43
  global_df = None
44
  global_mood_df = None
 
47
 
48
  def init_static_data():
49
  global global_mood_df
50
+ if global_mood_df is None:
51
+ try:
52
+ global_mood_df = pd.read_csv('mood_food_guide.csv')
53
+ except:
54
+ global_mood_df = pd.DataFrame({
55
+ '分數': [1, 2, 3, 4, 5],
56
+ '推薦料理類別': ["療癒系", "暖胃", "隨意", "豐富", "大餐"],
57
+ '原因': ["心情低落需要甜食", "給疲憊的自己一點溫暖", "隨意探索", "享受美食的快樂", "慶祝美好的時刻"]
58
+ })
59
 
60
  # ==========================================
61
+ # 1. Google Maps 爬蟲同步功能 (僅爬取基本資料)
62
  # ==========================================
63
  def sync_google_maps(url):
64
  global global_df, global_retriever, rag_initialized
65
  clean_url = url.strip() if url else ""
66
  if not clean_url or "http" not in clean_url.lower():
67
+ yield "❌ 請輸入有效的網址。"
68
  return
69
 
70
  try:
71
+ yield "🚀 [1/4] 啟動瀏覽器..."
72
  with sync_playwright() as p:
 
73
  browser = p.chromium.launch(headless=True)
74
+ page = browser.new_page()
75
+ yield "🌐 [2/4] 連線中..."
 
 
 
 
 
76
  page.goto(clean_url, wait_until="domcontentloaded", timeout=60000)
77
+ yield "⏳ [3/4] 等待加載資料..."
78
+ time.sleep(10)
79
+ yield "📄 [4/4] 解析餐廳資訊..."
80
 
81
+ # 獲取標題與詳情
82
+ titles = page.locator('div.fontHeadlineSmall').all_inner_texts()
83
+ details = page.locator('div.fontBodyMedium').all_inner_texts()
 
 
 
 
 
 
 
 
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  restaurant_list = []
86
+ for i, name in enumerate(titles):
87
+ name = name.strip()
88
+ if name:
89
+ # 擷取地址或評價資訊
90
+ addr = details[i].strip() if i < len(details) else ""
91
+ restaurant_list.append({
92
+ "Name": name,
93
+ "Address": addr,
94
+ "RAG_Content": f"餐廳:{name},資訊:{addr}",
95
+ "URL": f"https://www.google.com/search?q={urllib.parse.quote(name)}"
96
+ })
97
  browser.close()
98
 
99
  if not restaurant_list:
100
+ yield "⚠️ 找不到餐廳。請確認連結已設為「共用」且為列表格式。"
101
  return
102
 
103
  global_df = pd.DataFrame(restaurant_list)
 
105
  vectorstore = FAISS.from_texts(global_df['RAG_Content'].tolist(), embeddings, metadatas=global_df.to_dict('records'))
106
  global_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
107
  rag_initialized = True
108
+ yield f"✅ 同步成功!共有 {len(restaurant_list)} 間餐廳。"
 
109
  except Exception as e:
110
+ yield f"❌ 系統錯誤:{str(e)}"
111
 
112
  # ==========================================
113
+ # 2. 推薦邏輯與 Embed API 生成
114
  # ==========================================
115
  def get_restaurant_data(mood_score_str, food_choice):
116
  init_static_data()
117
  if global_df is None or global_df.empty:
118
  return None, True, "資料庫未載入", "尚未同步地圖資料", ""
119
 
 
120
  all_restaurant_names = "、".join(global_df['Name'].tolist())
121
 
122
  try: score = int(str(mood_score_str).split(' ')[0])
 
130
 
131
  is_match = True
132
  if food_keyword:
 
133
  filtered = candidates[candidates['Name'].str.contains(food_keyword) | candidates['RAG_Content'].str.contains(food_keyword)]
134
  if not filtered.empty:
135
  candidates = filtered
136
  else:
 
137
  is_match = False
138
 
 
139
  result = candidates.sample(1).iloc[0]
140
  return result, is_match, "個人清單", mood_reason, all_restaurant_names
141
 
142
+ def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, mood_guide_reason):
 
143
  if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY"
144
  client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
145
+ system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友,會根據對方的心情推薦合適的餐廳。請用溫暖且具備食記風格的口吻來描述。"
146
+ user_msg = f"【狀態】心情分數:{mood_score}\n【日記】{user_diary}\n【建議原因】{mood_guide_reason}\n【推薦餐廳】{restaurant_name}\n餐廳資料:{restaurant_detail}"
147
  try:
148
  response = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}])
149
  return response.choices[0].message.content
150
  except Exception as e: return f"Groq Error: {str(e)}"
151
 
 
 
 
 
 
 
 
152
  def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
153
  if not rag_initialized:
154
+ yield "⚠️ 請先點擊上方按鈕同步地圖清單!", "", ""
155
  return
156
 
157
+ restaurant, is_match, _, mood_reason, all_names = get_restaurant_data(score_input, food_input)
 
158
 
159
  prefix = ""
 
160
  if not is_match and food_input != "隨便":
161
+ prefix = f"> 💡 **溫馨提示**:清單中暫無『{food_input}』,已從名單「{all_names}」中挑選最適合您今日心情的一間!\n\n"
 
162
 
163
  name = restaurant['Name']
164
+ addr = restaurant.get('Address', '')
165
  url = restaurant.get('URL', '#')
166
+ rag_info = str(restaurant.get('RAG_Content', ''))
 
 
167
 
168
+ # 產生 AI 文案
169
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, mood_reason)
170
+ final_markdown = f"{prefix}### 🍽️ 今日推薦:{name}\n\n{ai_text}"
171
 
172
+ # --- 關鍵:使用 Google Maps Embed API 替代圖片 ---
173
+ # query 參數包含 名稱 與 地址 以提高準確度
174
+ query_str = urllib.parse.quote(f"{name} {addr}")
175
+ if GOOGLE_MAPS_API_KEY:
176
+ embed_html = f"""
177
+ <div style="border-radius:15px; overflow:hidden; border:1px solid #ddd;">
178
+ <iframe
179
+ width="100%"
180
+ height="350"
181
+ frameborder="0" style="border:0"
182
+ src="https://www.google.com/maps/embed/v1/place?key={GOOGLE_MAPS_API_KEY}&q={query_str}"
183
+ allowfullscreen>
184
+ </iframe>
185
+ </div>
186
+ """
187
+ else:
188
+ embed_html = "<p style='color:red;'>⚠️ 尚未設定 GOOGLE_MAPS_API_KEY,無法顯示地圖預覽。</p>"
189
+
190
+ nav_button = f'<div style="text-align:center; margin-top:10px;"><a href="{url}" target="_blank" style="background:#4CAF50;color:white;padding:10px 20px;border-radius:25px;text-decoration:none;font-weight:bold;">📍 前往 Google Maps 導航</a></div>'
191
 
192
+ yield final_markdown, embed_html, nav_button
 
193
 
194
  # ==========================================
195
+ # 3. 橋接情緒辨識邏輯 (保持原有功能)
196
  # ==========================================
197
  def _score_to_radio_value(score):
198
  mapping = {1: "1 (心情差)", 2: "2 (不太好)", 3: "3 (普通)", 4: "4 (不錯)", 5: "5 (超棒)"}
199
+ return mapping.get(int(score), "3 (普通)")
 
 
200
 
201
  def bridge_start_click(st):
202
  res = emotion.on_restart(st)
 
228
  return out_result, score_update, tabs_update, out_st
229
 
230
  # ==========================================
231
+ # 4. Gradio 介面
232
  # ==========================================
233
+ with gr.Blocks(title="AI 心情食堂", css=getattr(emotion, 'css', '')) as demo:
 
 
234
  st_state = gr.State(emotion.AppState())
235
 
236
  with gr.Tabs() as tabs:
237
  # Tab 1: 情緒辨識
238
+ with gr.TabItem("😊 第一步:測測心情", id=0):
239
+ with gr.Column():
240
+ gr.Markdown("### 📸 透過鏡頭或照片辨識您的當下情緒")
241
  with gr.Row():
242
+ btn_start = gr.Button("開啟攝影機", variant="primary")
243
+ btn_stop = gr.Button("停止", variant="secondary", visible=False)
244
  cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機畫面", visible=False)
245
+ result_markdown = gr.Markdown("請按上方按鈕開始辨識")
246
+ upload_img = gr.Image(sources=["upload"], type="numpy", label="或是上傳照片辨識")
 
247
 
248
  # Tab 2: 主功能區
249
+ with gr.TabItem("🍽️ 第二步:心情食堂推薦", id=1):
250
  with gr.Column():
251
+ gr.Markdown("### 🔗 同步 Google Maps 分享清單")
252
  with gr.Row():
253
+ map_url = gr.Textbox(label="清單連結", placeholder="https://maps.app.goo.gl/...")
254
+ sync_btn = gr.Button("🔄 同步我的口袋名單", variant="secondary")
255
+ sync_msg = gr.Markdown("ℹ️ 尚未載入資料庫")
 
 
 
 
 
256
 
257
+ gr.Divider()
258
 
259
  with gr.Row():
260
  with gr.Column(scale=1):
261
  score_input = gr.Radio(
262
  ["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"],
263
+ label="心情分數 (辨識後自動填寫)",
264
  value="3 (普通)"
265
  )
266
+ food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="想吃什麼?", value="隨便")
267
+ diary_input = gr.Textbox(lines=3, label="心情日記 (選填)", placeholder="寫下現在的想法...")
268
+ submit_btn = gr.Button("🍱 推薦今日驚喜", variant="primary")
 
269
 
270
  with gr.Column(scale=1):
271
+ agent_output = gr.Markdown(label="AI 推薦語")
272
+ map_embed_output = gr.HTML(label="地圖實景預覽")
273
+ map_nav_output = gr.HTML()
274
 
275
  # 事件綁定
276
+ sync_btn.click(fn=sync_google_maps, inputs=[map_url], outputs=[sync_msg])
277
  btn_start.click(fn=bridge_start_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state])
278
  btn_stop.click(fn=bridge_stop_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state])
279
  cam.stream(fn=bridge_predict_frame, inputs=[cam, st_state], outputs=[cam, result_markdown, st_state, btn_stop, btn_start, score_input, tabs])
280
  upload_img.change(fn=bridge_predict_upload, inputs=[upload_img, st_state], outputs=[result_markdown, score_input, tabs, st_state])
281
+ submit_btn.click(fn=mood_agent_logic, inputs=[score_input, food_input, diary_input, gr.Checkbox(value=False, visible=False)], outputs=[agent_output, map_embed_output, map_nav_output])
282
 
283
  if __name__ == "__main__":
284
+ demo.launch()