Marco310 commited on
Commit
ecbe9e4
·
1 Parent(s): 1518a95

feat: improve UI/UX

Browse files

feat: update README.md info

Files changed (7) hide show
  1. README.md +1 -1
  2. app.py +58 -16
  3. config.py +3 -1
  4. core/visualizers.py +76 -75
  5. services/planner_service.py +9 -1
  6. ui/renderers.py +64 -48
  7. ui/theme.py +629 -9
README.md CHANGED
@@ -25,7 +25,7 @@ tags:
25
 
26
  ### Team Information:
27
  - **Man-Ho Li** - [@Marco310](https://huggingface.co/Marco310) - Lead Developer & AI Architect
28
- - ** ** - [@]() - Technical Consultant & Marketing Director
29
 
30
 
31
  ## 📺 Demo & Submission
 
25
 
26
  ### Team Information:
27
  - **Man-Ho Li** - [@Marco310](https://huggingface.co/Marco310) - Lead Developer & AI Architect
28
+ - **Chen-Yang Yu** - [@LittleFish-Coder](https://huggingface.co/LittleFish-Coder) - Technical Consultant & Marketing Director
29
 
30
 
31
  ## 📺 Demo & Submission
app.py CHANGED
@@ -321,8 +321,14 @@ class LifeFlowAI:
321
  task_summary_box = gr.HTML()
322
  task_list_box = gr.HTML()
323
  with gr.Column(scale=1):
324
- with gr.Group(elem_classes="panel-container chat-panel-native"):
325
- chatbot = gr.Chatbot(label="AI Assistant", type="messages", height=540, elem_classes="native-chatbot", bubble_full_width=False)
 
 
 
 
 
 
326
  with gr.Row(elem_classes="chat-input-row"):
327
  chat_input = gr.Textbox(show_label=False, placeholder="Type to modify tasks...", container=False, scale=5, autofocus=True)
328
  chat_send = gr.Button("➤", variant="primary", scale=1, min_width=50)
@@ -339,7 +345,8 @@ class LifeFlowAI:
339
  with gr.Tabs():
340
  with gr.Tab("📝 Full Report"):
341
  with gr.Group(elem_classes="live-report-wrapper"):
342
- live_report_md = gr.Markdown()
 
343
  with gr.Tab("📋 Task List"):
344
  with gr.Group(elem_classes="panel-container"):
345
  with gr.Group(elem_classes="scrollable-content"):
@@ -351,18 +358,53 @@ class LifeFlowAI:
351
  planning_log = gr.HTML(value="Waiting...")
352
 
353
  with gr.Group(visible=False, elem_classes="step-container") as step4_container:
354
- with gr.Row():
355
- with gr.Column(scale=1, elem_classes="split-left-panel"):
356
- with gr.Tabs():
357
- with gr.Tab("📊 Summary"):
358
- summary_tab_output = gr.HTML()
359
- with gr.Tab("📝 Full Report"):
360
- with gr.Group(elem_classes="live-report-wrapper"):
361
- report_tab_output = gr.Markdown()
362
- with gr.Tab("📋 Task List"):
363
- task_list_tab_output = gr.HTML()
364
- with gr.Column(scale=2, elem_classes="split-right-panel"):
365
- map_view = gr.HTML(label="Route Map")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
  # --- Modals & Events ---
368
  (settings_modal, g_key, g_stat, w_key, w_stat, llm_provider, main_key, main_stat, model_sel,
@@ -434,7 +476,7 @@ def main():
434
  demo = app.build_interface()
435
  demo.launch(
436
  server_name="0.0.0.0",
437
- server_port=8100,
438
  share=True,
439
  show_error=True,
440
  prevent_thread_lock=True
 
321
  task_summary_box = gr.HTML()
322
  task_list_box = gr.HTML()
323
  with gr.Column(scale=1):
324
+ with gr.Group(elem_classes="chat-panel-native"):
325
+ chatbot = gr.Chatbot(
326
+ label="AI Assistant",
327
+ type="messages",
328
+ height=575,
329
+ elem_classes="native-chatbot",
330
+ bubble_full_width=False
331
+ )
332
  with gr.Row(elem_classes="chat-input-row"):
333
  chat_input = gr.Textbox(show_label=False, placeholder="Type to modify tasks...", container=False, scale=5, autofocus=True)
334
  chat_send = gr.Button("➤", variant="primary", scale=1, min_width=50)
 
345
  with gr.Tabs():
346
  with gr.Tab("📝 Full Report"):
347
  with gr.Group(elem_classes="live-report-wrapper"):
348
+ live_report_md = gr.Markdown("🧠 Analyzing your tasks...")
349
+
350
  with gr.Tab("📋 Task List"):
351
  with gr.Group(elem_classes="panel-container"):
352
  with gr.Group(elem_classes="scrollable-content"):
 
358
  planning_log = gr.HTML(value="Waiting...")
359
 
360
  with gr.Group(visible=False, elem_classes="step-container") as step4_container:
361
+ # 定義狀態:抽屜預設開啟
362
+ drawer_state = gr.State(True)
363
+
364
+ with gr.Tabs():
365
+ # === Tab 1: 地圖 ===
366
+ with gr.Tab("🗺️ Route Map", id="tab_map"):
367
+ with gr.Group(elem_classes="map-container-relative"):
368
+ # 1. 地圖 (HTML)
369
+ # 注意:確保這裡是 map_view
370
+ map_view = gr.HTML(label="Route Map")
371
+
372
+ # 2. 浮動抽屜 (Overlay)
373
+ with gr.Group(visible=True, elem_classes="map-overlay-drawer") as summary_drawer:
374
+ with gr.Row(elem_classes="drawer-header"):
375
+ gr.Markdown("### 📊 Trip Summary", elem_classes="drawer-title")
376
+ close_drawer_btn = gr.Button("✕", elem_classes="drawer-close-btn", size="sm")
377
+
378
+ with gr.Group(elem_classes="drawer-content"):
379
+ with gr.Tabs(elem_classes="drawer-tabs"):
380
+ with gr.Tab("Overview"):
381
+ summary_tab_output = gr.HTML()
382
+ with gr.Tab("Tasks"):
383
+ task_list_tab_output = gr.HTML()
384
+
385
+ # 3. 開啟按鈕 (浮動,預設隱藏)
386
+ # 加上 variant='primary' 確保它醒目
387
+ open_drawer_btn = gr.Button("📊 Show Summary", visible=False, variant="primary",
388
+ elem_classes="map-overlay-btn")
389
+
390
+ # === Tab 2: 完整報告 ===
391
+ with gr.Tab("📝 Full Report", id="tab_report"):
392
+ # 使用 live-report-wrapper 確保有白紙背景效果
393
+ with gr.Group(elem_classes="live-report-wrapper"):
394
+ report_tab_output = gr.Markdown()
395
+
396
+ # === 事件綁定 (控制抽屜) ===
397
+ # 關閉:抽屜消失,按鈕出現
398
+ close_drawer_btn.click(
399
+ fn=lambda: (gr.update(visible=False), gr.update(visible=True)),
400
+ outputs=[summary_drawer, open_drawer_btn]
401
+ )
402
+
403
+ # 開啟:抽屜出現,按鈕消失
404
+ open_drawer_btn.click(
405
+ fn=lambda: (gr.update(visible=True), gr.update(visible=False)),
406
+ outputs=[summary_drawer, open_drawer_btn]
407
+ )
408
 
409
  # --- Modals & Events ---
410
  (settings_modal, g_key, g_stat, w_key, w_stat, llm_provider, main_key, main_stat, model_sel,
 
476
  demo = app.build_interface()
477
  demo.launch(
478
  server_name="0.0.0.0",
479
+ server_port=8300,
480
  share=True,
481
  show_error=True,
482
  prevent_thread_lock=True
config.py CHANGED
@@ -5,6 +5,8 @@ LifeFlow AI - Configuration
5
 
6
  import os
7
  from pathlib import Path
 
 
8
 
9
  # ===== 系統預設值 =====
10
  BASE_DIR = Path(__file__).parent
@@ -346,7 +348,7 @@ def check_environment():
346
  """檢查環境配置是否完整"""
347
  missing_keys = []
348
 
349
- required_keys = ['GEMINI_API_KEY', 'GOOGLE_MAPS_API_KEY']
350
 
351
  for key in required_keys:
352
  if not os.getenv(key):
 
5
 
6
  import os
7
  from pathlib import Path
8
+ from dotenv import load_dotenv
9
+ load_dotenv() # 這行會自動尋找並讀取專案根目錄下的 .env 檔案
10
 
11
  # ===== 系統預設值 =====
12
  BASE_DIR = Path(__file__).parent
 
348
  """檢查環境配置是否完整"""
349
  missing_keys = []
350
 
351
+ required_keys = ['GOOGLE_API_KEY', 'GOOGLE_MAPS_API_KEY', "OPENWEATHER_API_KEY", "GROQ_API_KEY"]
352
 
353
  for key in required_keys:
354
  if not os.getenv(key):
core/visualizers.py CHANGED
@@ -1,13 +1,23 @@
1
  import folium
2
  from folium import plugins
3
- from folium.plugins import PolyLineTextPath # <--- 新增這個 import
4
  from core.helpers import decode_polyline
5
  from src.infra.logger import get_logger
6
 
7
  logger = get_logger(__name__)
8
 
 
9
  CSS_STYLE = """
10
  <style>
 
 
 
 
 
 
 
 
 
11
  .map-card {
12
  font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
13
  width: 300px;
@@ -94,7 +104,6 @@ def create_popup_html(title, subtitle, color, metrics, is_alternative=False):
94
  'route': '#4285F4'
95
  }.get(color, '#34495e')
96
 
97
- # 如果是 Route 但傳入了 hex color,直接使用
98
  if color.startswith('#'):
99
  bg_color = color
100
 
@@ -134,19 +143,18 @@ def create_popup_html(title, subtitle, color, metrics, is_alternative=False):
134
 
135
  def create_animated_map(structured_data=None):
136
  """
137
- LifeFlow AI - Interactive Map Generator with Directional Arrows
138
  """
139
- # 1. 初始化地圖
140
  center_lat, center_lon = 25.033, 121.565
141
 
142
- # 嘗試從數據中獲取中心點,否則預設台北
143
  if structured_data and structured_data.get("global_info", {}).get("start_location"):
144
  sl = structured_data["global_info"]["start_location"]
145
  if "lat" in sl and "lng" in sl:
146
  center_lat, center_lon = sl["lat"], sl["lng"]
147
 
 
148
  m = folium.Map(location=[center_lat, center_lon], zoom_start=13, tiles="OpenStreetMap",
149
- height=520,
150
  width="100%"
151
  )
152
 
@@ -156,7 +164,6 @@ def create_animated_map(structured_data=None):
156
  return m._repr_html_()
157
 
158
  try:
159
- # --- 資料提取 ---
160
  timeline = structured_data.get("timeline", [])
161
  precise_result = structured_data.get("precise_traffic_result", {})
162
  legs = precise_result.get("legs", [])
@@ -164,7 +171,6 @@ def create_animated_map(structured_data=None):
164
  raw_tasks = structured_data.get("tasks", [])
165
  route_info = structured_data.get("route", [])
166
 
167
- # 建立查找表
168
  index_to_name = {stop.get("stop_index"): stop.get("location") for stop in timeline}
169
 
170
  poi_id_to_name = {}
@@ -180,7 +186,6 @@ def create_animated_map(structured_data=None):
180
 
181
  bounds = []
182
 
183
- # 定義顏色主題 (Green -> Blue -> Red -> Orange -> Purple)
184
  THEMES = [
185
  ('#2ecc71', 'green'),
186
  ('#3498db', 'blue'),
@@ -189,7 +194,7 @@ def create_animated_map(structured_data=None):
189
  ('#9b59b6', 'purple')
190
  ]
191
 
192
- # --- Layer 1: 路線 (帶箭頭的彩色路段) ---
193
  route_group = folium.FeatureGroup(name="🚗 Route Path", show=True)
194
 
195
  for i, leg in enumerate(legs):
@@ -205,20 +210,16 @@ def create_animated_map(structured_data=None):
205
  from_n = index_to_name.get(from_idx, f"Point {from_idx}")
206
  to_n = index_to_name.get(to_idx, f"Point {to_idx}")
207
 
208
- # 🔥 顏色邏輯:這段路的顏色 = 目的地的 Marker 顏色
209
- # to_index 對應到 timeline 的 index
210
- # 我們用 to_index 來決定顏色循環
211
  theme_idx = to_idx % len(THEMES)
212
  color_hex, color_name = THEMES[theme_idx]
213
 
214
  popup_html = create_popup_html(
215
  title=f"LEG {i + 1}",
216
  subtitle=f"{from_n} ➔ {to_n}",
217
- color=color_hex, # 直接傳入 HEX
218
  metrics={"Duration": f"{dur} min", "Distance": f"{dist / 1000:.1f} km"}
219
  )
220
 
221
- # 1. 畫實線
222
  line = folium.PolyLine(
223
  locations=decoded,
224
  color=color_hex,
@@ -229,71 +230,83 @@ def create_animated_map(structured_data=None):
229
  )
230
  line.add_to(route_group)
231
 
232
- # 2. 🔥 加箭頭 (PolyLineTextPath)
233
- # 使用 ' ➤ ' 符號,並設定 repeat=True 讓它沿路出現
234
  PolyLineTextPath(
235
  line,
236
- " ➤ ", # 空格是為了調整箭頭密度
237
  repeat=True,
238
- offset=7, # 讓箭頭在線的中間
239
- attributes={
240
- 'fill': color_hex, # 箭頭顏色跟線一樣
241
- 'font-weight': 'bold',
242
- 'font-size': '18'
243
- }
244
  ).add_to(route_group)
245
 
246
  route_group.add_to(m)
247
 
248
- # --- Layer 2: 備用方案 (Alternatives) ---
249
- alt_group = folium.FeatureGroup(name="🔹 Alternatives", show=True) # 合併成一個 Layer 比較整潔
250
 
251
  for idx, task in enumerate(tasks_detail):
252
  tid = task.get("task_id")
253
  step_seq = task_id_to_seq.get(tid, 0)
254
 
 
255
  theme_idx = step_seq % len(THEMES)
256
  theme_color, theme_name = THEMES[theme_idx]
257
 
258
  chosen = task.get("chosen_poi", {})
259
- if chosen is None:
 
 
 
260
  continue
261
 
262
  center_lat, center_lng = chosen.get("lat"), chosen.get("lng")
 
 
263
 
264
- if center_lat and center_lng:
265
- for alt in task.get("alternative_pois", []):
266
- alat, alng = alt.get("lat"), alt.get("lng")
267
- if alat and alng:
268
- bounds.append([alat, alng])
269
- folium.PolyLine(
270
- locations=[[center_lat, center_lng], [alat, alng]],
271
- color=theme_color, weight=2, dash_array='5, 5', opacity=0.5
272
- ).add_to(alt_group)
273
-
274
- poi_name = poi_id_to_name.get(alt.get("poi_id"), "Alternative Option")
275
- extra_min = alt.get("delta_travel_time_min", 0)
276
-
277
- popup_html = create_popup_html(
278
- title="ALTERNATIVE",
279
- subtitle=poi_name,
280
- color="gray",
281
- metrics={
282
- "Add. Time": f"+{extra_min} min",
283
- },
284
- is_alternative=True
285
- )
286
-
287
- folium.CircleMarker(
288
- location=[alat, alng], radius=5,
289
- color=theme_color, fill=True, fill_color="white", fill_opacity=1,
290
- popup=folium.Popup(popup_html, max_width=320),
291
- tooltip=f"Alt: {poi_name}"
292
- ).add_to(alt_group)
293
-
294
- alt_group.add_to(m)
295
-
296
- # --- Layer 3: 主要站點 (Markers) ---
 
 
 
 
 
 
 
 
 
 
 
 
297
  stops_group = folium.FeatureGroup(name="📍 Stops", show=True)
298
 
299
  for i, stop in enumerate(timeline):
@@ -302,11 +315,8 @@ def create_animated_map(structured_data=None):
302
 
303
  if lat and lng:
304
  bounds.append([lat, lng])
305
-
306
- # 顏色邏輯:與 Leg 顏色保持一致 (index 決定)
307
  theme_idx = i % len(THEMES)
308
  color_code, theme_name = THEMES[theme_idx]
309
-
310
  loc_name = stop.get("location", "")
311
 
312
  popup_html = create_popup_html(
@@ -315,8 +325,8 @@ def create_animated_map(structured_data=None):
315
  color=theme_name,
316
  metrics={
317
  "Arrival": stop.get("time", ""),
318
- "Weather": stop.get("weather", "").split(',')[0], # 簡化顯示
319
- "AQI": stop.get("aqi", {}).get("label", "").split(' ')[-1] # 只顯示 Emoji
320
  }
321
  )
322
 
@@ -327,7 +337,6 @@ def create_animated_map(structured_data=None):
327
  else:
328
  icon_type = 'map-marker'
329
 
330
- # 使用 bevel 樣式讓 Marker 看起來更立體
331
  icon = folium.Icon(color=theme_name, icon=icon_type, prefix='fa')
332
 
333
  folium.Marker(
@@ -338,21 +347,13 @@ def create_animated_map(structured_data=None):
338
 
339
  stops_group.add_to(m)
340
 
341
- # --- 控制元件 ---
342
  folium.LayerControl(collapsed=True).add_to(m)
343
 
344
  if bounds:
345
  m.fit_bounds(bounds, padding=(50, 50))
346
 
347
-
348
  except Exception as e:
349
  logger.error(f"Folium map error: {e}", exc_info=True)
350
  return m._repr_html_()
351
 
352
- return m._repr_html_()
353
-
354
-
355
- if __name__ == "__main__":
356
- test_data = {'status': 'OK', 'total_travel_time_min': 19, 'total_travel_distance_m': 7567, 'metrics': {'total_tasks': 2, 'completed_tasks': 2, 'completion_rate_pct': 100.0, 'original_distance_m': 15478, 'optimized_distance_m': 7567, 'distance_saved_m': 7911, 'distance_improvement_pct': 51.1, 'original_duration_min': 249, 'optimized_duration_min': 229, 'time_saved_min': 20, 'time_improvement_pct': 8.4, 'route_efficiency_pct': 91.7}, 'route': [{'step': 0, 'node_index': 0, 'arrival_time': '2025-11-27T10:00:00+08:00', 'departure_time': '2025-11-27T10:00:00+08:00', 'type': 'depot', 'task_id': None, 'poi_id': None, 'service_duration_min': 0}, {'step': 1, 'node_index': 1, 'arrival_time': '2025-11-27T10:17:57+08:00', 'departure_time': '2025-11-27T12:17:57+08:00', 'type': 'task_poi', 'task_id': '1', 'poi_id': 'ChIJH56c2rarQjQRphD9gvC8BhI', 'service_duration_min': 120}, {'step': 2, 'node_index': 7, 'arrival_time': '2025-11-27T12:19:05+08:00', 'departure_time': '2025-11-27T13:49:05+08:00', 'type': 'task_poi', 'task_id': '2', 'poi_id': 'ChIJQXcl6LarQjQRGUMnQ18F0lE', 'service_duration_min': 90}], 'visited_tasks': ['1', '2'], 'skipped_tasks': [], 'tasks_detail': [{'task_id': '1', 'priority': 'HIGH', 'visited': True, 'chosen_poi': {'node_index': 1, 'poi_id': 'ChIJH56c2rarQjQRphD9gvC8BhI', 'lat': 25.033976, 'lng': 121.56453889999999, 'interval_idx': 0}, 'alternative_pois': []}, {'task_id': '2', 'priority': 'HIGH', 'visited': True, 'chosen_poi': {'node_index': 7, 'poi_id': 'ChIJQXcl6LarQjQRGUMnQ18F0lE', 'lat': 25.033337099999997, 'lng': 121.56465960000001, 'interval_idx': 0}, 'alternative_pois': [{'node_index': 10, 'poi_id': 'ChIJwQWwVe6rQjQRRGA4WzYdO2U', 'lat': 25.036873699999997, 'lng': 121.5679503, 'interval_idx': 0, 'delta_travel_time_min': 7, 'delta_travel_distance_m': 2003}, {'node_index': 6, 'poi_id': 'ChIJ01XRzrurQjQRnp5ZsHbAAuE', 'lat': 25.039739800000003, 'lng': 121.5665985, 'interval_idx': 0, 'delta_travel_time_min': 9, 'delta_travel_distance_m': 2145}, {'node_index': 5, 'poi_id': 'ChIJaeY0sNCrQjQRBmpF8-RmywQ', 'lat': 25.0409656, 'lng': 121.5429975, 'interval_idx': 0, 'delta_travel_time_min': 24, 'delta_travel_distance_m': 6549}]}], 'tasks': [{'task_id': '1', 'priority': 'HIGH', 'service_duration_min': 120, 'time_window': {'earliest_time': '2025-11-27T10:00:00+08:00', 'latest_time': '2025-11-27T22:00:00+08:00'}, 'candidates': [{'poi_id': 'ChIJH56c2rarQjQRphD9gvC8BhI', 'name': 'Taipei 101', 'lat': 25.033976, 'lng': 121.56453889999999, 'rating': None, 'time_window': None}]}, {'task_id': '2', 'priority': 'HIGH', 'service_duration_min': 90, 'time_window': {'earliest_time': '2025-11-27T11:30:00+08:00', 'latest_time': '2025-11-27T14:30:00+08:00'}, 'candidates': [{'poi_id': 'ChIJbbvUtW6pQjQRLvK71hSUXN8', 'name': 'Din Tai Fung Mitsukoshi Nanxi Restaurant', 'lat': 25.0523074, 'lng': 121.5211037, 'rating': 4.4, 'time_window': None}, {'poi_id': 'ChIJA-U6X-epQjQR-T9BLmEfUlc', 'name': 'Din Tai Fung Xinsheng Branch', 'lat': 25.033889, 'lng': 121.5321338, 'rating': 4.6, 'time_window': None}, {'poi_id': 'ChIJbTKSE4KpQjQRXDZZI57v-pM', 'name': 'Din Tai Fung Xinyi Branch', 'lat': 25.0335035, 'lng': 121.53011799999999, 'rating': 4.4, 'time_window': None}, {'poi_id': 'ChIJaeY0sNCrQjQRBmpF8-RmywQ', 'name': 'Din Tai Fung Fuxing Restaurant', 'lat': 25.0409656, 'lng': 121.5429975, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJ01XRzrurQjQRnp5ZsHbAAuE', 'name': 'Din Tai Fung A4 Branch', 'lat': 25.039739800000003, 'lng': 121.5665985, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJQXcl6LarQjQRGUMnQ18F0lE', 'name': 'Din Tai Fung 101', 'lat': 25.033337099999997, 'lng': 121.56465960000001, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJ7y2qTJeuQjQRbpDcQImxLO0', 'name': 'Din Tai Fung Tianmu Restaurant', 'lat': 25.105072000000003, 'lng': 121.52447500000001, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJxRJqSBmoQjQR3gtSHvJgkZk', 'name': 'Din Tai Fung Mega City Restaurant', 'lat': 25.0135467, 'lng': 121.46675080000001, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJwQWwVe6rQjQRRGA4WzYdO2U', 'name': 'Din Tai Fung A13 Branch', 'lat': 25.036873699999997, 'lng': 121.5679503, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJ34CbayyoQjQRDbCGQgDk_RY', 'name': 'Ding Tai Feng', 'lat': 25.0059716, 'lng': 121.48668440000002, 'rating': 3.6, 'time_window': None}]}], 'global_info': {'language': 'en-US', 'plan_type': 'TRIP', 'departure_time': '2025-11-27T10:00:00+08:00', 'start_location': {'name': 'Taipei Main Station', 'lat': 25.0474428, 'lng': 121.5170955}}, 'traffic_summary': {'total_distance_km': 7.567, 'total_duration_min': 15}, 'precise_traffic_result': {'total_distance_meters': 7567, 'total_duration_seconds': 925, 'total_residence_time_minutes': 210, 'total_time_seconds': 13525, 'start_time': '2025-11-27 10:00:00+08:00', 'end_time': '2025-11-27 05:45:25+00:00', 'stops': [{'lat': 25.0474428, 'lng': 121.5170955}, {'lat': 25.033976, 'lng': 121.56453889999999}, {'lat': 25.033337099999997, 'lng': 121.56465960000001}], 'legs': [{'from_index': 0, 'to_index': 1, 'travel_mode': 'DRIVE', 'distance_meters': 7366, 'duration_seconds': 857, 'departure_time': '2025-11-27T02:00:00+00:00', 'polyline': 'm_{wCqttdVQbAu@KwDe@ELMTKl@C\\@LC^@PLPRJdC\\LLe@bEIj@_@|@I\\?P}EGMERqAAyAf@iD\\sC`AqKTmDJYXoDn@oFH{A`@{PNoB\\wChAyFfCaLrAcHnAyH|BeMZcCRmCNoGVcJPoI@{CIwBc@mHw@aNGeCD}BpBm^Ak@LyDB_@KmD]qDu@iGNs@SuASsBdUPZ@FcNP{ODcItBfANBxClA^D`A@As@D{@HyGpFBtD?pADlKB?|@FD`@@lAE'}, {'from_index': 1, 'to_index': 2, 'travel_mode': 'DRIVE', 'distance_meters': 201, 'duration_seconds': 68, 'departure_time': '2025-11-27T04:14:17+00:00', 'polyline': 'mmxwC}d~dVfBAXGJON]l@?CrC'}]}, 'solved_waypoints': [{'lat': 25.0474428, 'lng': 121.5170955}, {'lat': 25.033976, 'lng': 121.56453889999999}, {'lat': 25.033337099999997, 'lng': 121.56465960000001}], 'timeline': [{'stop_index': 0, 'time': '10:00', 'location': 'start point', 'address': '', 'weather': 'Rain, 20.76°C', 'aqi': {'aqi': 2, 'label': 'AQI 2 🟡'}, 'travel_time_from_prev': '0 mins', 'coordinates': {'lat': 25.0474428, 'lng': 121.5170955}}, {'stop_index': 1, 'time': '10:14', 'location': 'Taipei 101', 'address': '', 'weather': 'Rain, 20.75°C', 'aqi': {'aqi': 2, 'label': 'AQI 2 🟡'}, 'travel_time_from_prev': '14 mins', 'coordinates': {'lat': 25.033976, 'lng': 121.56453889999999}}, {'stop_index': 2, 'time': '12:15', 'location': 'Din Tai Fung Mitsukoshi Nanxi Restaurant', 'address': '', 'weather': 'Rain, 20.75°C', 'aqi': {'aqi': 2, 'label': 'AQI 2 🟡'}, 'travel_time_from_prev': '1 mins', 'coordinates': {'lat': 25.033337099999997, 'lng': 121.56465960000001}}]}
357
-
358
- create_animated_map(structured_data=test_data)
 
1
  import folium
2
  from folium import plugins
3
+ from folium.plugins import PolyLineTextPath
4
  from core.helpers import decode_polyline
5
  from src.infra.logger import get_logger
6
 
7
  logger = get_logger(__name__)
8
 
9
+ # 🔥 CSS 修正:加入 body/html 的重置,確保地圖填滿 iframe,不會有白邊
10
  CSS_STYLE = """
11
  <style>
12
+ html, body {
13
+ height: 100%;
14
+ margin: 0;
15
+ padding: 0;
16
+ }
17
+ .folium-map {
18
+ height: 100%;
19
+ width: 100%;
20
+ }
21
  .map-card {
22
  font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
23
  width: 300px;
 
104
  'route': '#4285F4'
105
  }.get(color, '#34495e')
106
 
 
107
  if color.startswith('#'):
108
  bg_color = color
109
 
 
143
 
144
  def create_animated_map(structured_data=None):
145
  """
146
+ LifeFlow AI - Interactive Map Generator
147
  """
 
148
  center_lat, center_lon = 25.033, 121.565
149
 
 
150
  if structured_data and structured_data.get("global_info", {}).get("start_location"):
151
  sl = structured_data["global_info"]["start_location"]
152
  if "lat" in sl and "lng" in sl:
153
  center_lat, center_lon = sl["lat"], sl["lng"]
154
 
155
+ # 🔥 Map修正 1: height="100%" 讓它自動填滿父容器 (ui/theme.py 定義的 650px)
156
  m = folium.Map(location=[center_lat, center_lon], zoom_start=13, tiles="OpenStreetMap",
157
+ height="100%",
158
  width="100%"
159
  )
160
 
 
164
  return m._repr_html_()
165
 
166
  try:
 
167
  timeline = structured_data.get("timeline", [])
168
  precise_result = structured_data.get("precise_traffic_result", {})
169
  legs = precise_result.get("legs", [])
 
171
  raw_tasks = structured_data.get("tasks", [])
172
  route_info = structured_data.get("route", [])
173
 
 
174
  index_to_name = {stop.get("stop_index"): stop.get("location") for stop in timeline}
175
 
176
  poi_id_to_name = {}
 
186
 
187
  bounds = []
188
 
 
189
  THEMES = [
190
  ('#2ecc71', 'green'),
191
  ('#3498db', 'blue'),
 
194
  ('#9b59b6', 'purple')
195
  ]
196
 
197
+ # --- Layer 1: 路線 ---
198
  route_group = folium.FeatureGroup(name="🚗 Route Path", show=True)
199
 
200
  for i, leg in enumerate(legs):
 
210
  from_n = index_to_name.get(from_idx, f"Point {from_idx}")
211
  to_n = index_to_name.get(to_idx, f"Point {to_idx}")
212
 
 
 
 
213
  theme_idx = to_idx % len(THEMES)
214
  color_hex, color_name = THEMES[theme_idx]
215
 
216
  popup_html = create_popup_html(
217
  title=f"LEG {i + 1}",
218
  subtitle=f"{from_n} ➔ {to_n}",
219
+ color=color_hex,
220
  metrics={"Duration": f"{dur} min", "Distance": f"{dist / 1000:.1f} km"}
221
  )
222
 
 
223
  line = folium.PolyLine(
224
  locations=decoded,
225
  color=color_hex,
 
230
  )
231
  line.add_to(route_group)
232
 
 
 
233
  PolyLineTextPath(
234
  line,
235
+ " ➤ ",
236
  repeat=True,
237
+ offset=7,
238
+ attributes={'fill': color_hex, 'font-weight': 'bold', 'font-size': '18'}
 
 
 
 
239
  ).add_to(route_group)
240
 
241
  route_group.add_to(m)
242
 
243
+ # --- Layer 2: 備用方案 (分組修正) ---
244
+ # 🔥 Map修正 2: 不再使用單一的 alt_group,而是為每個任務建立獨立的 FeatureGroup
245
 
246
  for idx, task in enumerate(tasks_detail):
247
  tid = task.get("task_id")
248
  step_seq = task_id_to_seq.get(tid, 0)
249
 
250
+ # 取得該任務的主題色
251
  theme_idx = step_seq % len(THEMES)
252
  theme_color, theme_name = THEMES[theme_idx]
253
 
254
  chosen = task.get("chosen_poi", {})
255
+ alternatives = task.get("alternative_pois", [])
256
+
257
+ # 如果沒有備選點,就跳過
258
+ if not chosen or not alternatives:
259
  continue
260
 
261
  center_lat, center_lng = chosen.get("lat"), chosen.get("lng")
262
+ if not center_lat or not center_lng:
263
+ continue
264
 
265
+ # 取得主地點名稱 (用於圖層標籤)
266
+ chosen_name = poi_id_to_name.get(chosen.get("poi_id"), f"Task {idx + 1}")
267
+
268
+ # 🔥 建立該任務專屬的 Group,並預設顯示 (show=True)
269
+ # 圖層名稱範例: "↳ Alt: Taipei 101"
270
+ specific_alt_group = folium.FeatureGroup(
271
+ name=f"↳ Alt: {chosen_name}",
272
+ show=True
273
+ )
274
+
275
+ for alt in alternatives:
276
+ alat, alng = alt.get("lat"), alt.get("lng")
277
+ if alat and alng:
278
+ bounds.append([alat, alng])
279
+
280
+ # 虛線連接
281
+ folium.PolyLine(
282
+ locations=[[center_lat, center_lng], [alat, alng]],
283
+ color=theme_color, weight=2, dash_array='5, 5', opacity=0.5
284
+ ).add_to(specific_alt_group)
285
+
286
+ poi_name = poi_id_to_name.get(alt.get("poi_id"), "Alternative Option")
287
+ extra_min = alt.get("delta_travel_time_min", 0)
288
+
289
+ popup_html = create_popup_html(
290
+ title="ALTERNATIVE",
291
+ subtitle=poi_name,
292
+ color="gray",
293
+ metrics={
294
+ "Add. Time": f"+{extra_min} min",
295
+ },
296
+ is_alternative=True
297
+ )
298
+
299
+ folium.CircleMarker(
300
+ location=[alat, alng], radius=5,
301
+ color=theme_color, fill=True, fill_color="white", fill_opacity=1,
302
+ popup=folium.Popup(popup_html, max_width=320),
303
+ tooltip=f"Alt: {poi_name}"
304
+ ).add_to(specific_alt_group)
305
+
306
+ # 將這個任務的專屬 Group 加入地圖
307
+ specific_alt_group.add_to(m)
308
+
309
+ # --- Layer 3: 主要站點 ---
310
  stops_group = folium.FeatureGroup(name="📍 Stops", show=True)
311
 
312
  for i, stop in enumerate(timeline):
 
315
 
316
  if lat and lng:
317
  bounds.append([lat, lng])
 
 
318
  theme_idx = i % len(THEMES)
319
  color_code, theme_name = THEMES[theme_idx]
 
320
  loc_name = stop.get("location", "")
321
 
322
  popup_html = create_popup_html(
 
325
  color=theme_name,
326
  metrics={
327
  "Arrival": stop.get("time", ""),
328
+ "Weather": stop.get("weather", "").split(',')[0],
329
+ "AQI": stop.get("aqi", {}).get("label", "").split(' ')[-1]
330
  }
331
  )
332
 
 
337
  else:
338
  icon_type = 'map-marker'
339
 
 
340
  icon = folium.Icon(color=theme_name, icon=icon_type, prefix='fa')
341
 
342
  folium.Marker(
 
347
 
348
  stops_group.add_to(m)
349
 
 
350
  folium.LayerControl(collapsed=True).add_to(m)
351
 
352
  if bounds:
353
  m.fit_bounds(bounds, padding=(50, 50))
354
 
 
355
  except Exception as e:
356
  logger.error(f"Folium map error: {e}", exc_info=True)
357
  return m._repr_html_()
358
 
359
+ return m._repr_html_()
 
 
 
 
 
 
services/planner_service.py CHANGED
@@ -6,6 +6,7 @@ LifeFlow AI - Planner Service (Refactored for MCP Architecture)
6
  ✅ 保持業務邏輯不變
7
  """
8
  import json
 
9
  import time
10
  import uuid
11
  from datetime import datetime
@@ -145,7 +146,14 @@ class PlannerService:
145
  selected_model_id = settings.get("model", "gemini-2.5-flash")
146
  helper_model_id = settings.get("groq_fast_model", "openai/gpt-oss-20b")
147
  enable_fast_mode = settings.get("enable_fast_mode", False)
148
- groq_api_key = settings.get("groq_api_key", "")
 
 
 
 
 
 
 
149
 
150
  # 初始化 Main Brain
151
  if provider.lower() == "gemini":
 
6
  ✅ 保持業務邏輯不變
7
  """
8
  import json
9
+ import os
10
  import time
11
  import uuid
12
  from datetime import datetime
 
146
  selected_model_id = settings.get("model", "gemini-2.5-flash")
147
  helper_model_id = settings.get("groq_fast_model", "openai/gpt-oss-20b")
148
  enable_fast_mode = settings.get("enable_fast_mode", False)
149
+
150
+ if main_api_key is None:
151
+ provider = "Gemini"
152
+ main_api_key = os.environ.get("GEMINI_API_KEY")
153
+ selected_model_id = "gemini-2.5-flash"
154
+ logger.warning("⚠️ Main API key not provided, defaulting to Gemini-2.5-flash with env var.")
155
+
156
+ groq_api_key = settings["groq_api_key"] if settings.get("groq_api_key") else os.environ.get("GROQ_API_KEY")
157
 
158
  # 初始化 Main Brain
159
  if provider.lower() == "gemini":
ui/renderers.py CHANGED
@@ -2,7 +2,20 @@
2
  from typing import Union
3
  from datetime import datetime
4
  from config import AGENTS_INFO
5
-
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  def _format_iso_time(iso_str: str) -> str:
8
  if not iso_str or iso_str == "N/A": return ""
@@ -19,31 +32,34 @@ def create_agent_stream_output(text: str = None) -> str:
19
 
20
 
21
  def create_agent_dashboard(status_dict: dict) -> str:
22
- leader_key = 'team'
23
- member_keys = ['scout', 'weatherman', 'optimizer', 'navigator', 'presenter']
24
 
25
- def _render_card(key):
 
26
  info = AGENTS_INFO.get(key, {})
27
  state = status_dict.get(key, {})
28
  status = state.get('status', 'idle')
29
  msg = state.get('message', 'Standby')
30
- active_class = "working" if status == "working" else ""
 
 
31
  icon = info.get('icon', '🤖')
32
  name = info.get('name', key.title())
33
- role = info.get('role', 'Agent')
34
- color = info.get('color', '#6366f1')
35
 
36
- return f"""
37
- <div class="agent-card-wrap {active_class}">
38
- <div class="agent-card-inner" style="border-top: 3px solid {color}">
39
- <div class="agent-avatar">{icon}</div>
 
40
  <div class="agent-name">{name}</div>
41
- <div class="agent-role">{role}</div>
42
- <div class="status-badge">{msg}</div>
43
  </div>
44
  </div>
45
  """
46
 
 
 
47
  html = f"""
48
  <div class="agent-war-room">
49
  <div class="org-chart">
@@ -61,40 +77,39 @@ def create_agent_dashboard(status_dict: dict) -> str:
61
  return html
62
 
63
 
64
- def create_summary_card(total_tasks: Union[str, int], high_priority: int, total_time: int, location: str = "Taipei City",
65
- date: str = "Today") -> str:
 
 
66
  return f"""
67
- <div style="background: #f8fafc; border-radius: 12px; padding: 16px; margin-bottom: 20px; border: 1px solid #e2e8f0;">
68
- <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
69
- <div>
70
- <div style="font-size: 0.8rem; color: #64748b; font-weight: 600;">TRIP SUMMARY</div>
71
- <div style="font-size: 1.1rem; font-weight: 700; color: #1e293b;">{location}</div>
72
- <div style="font-size: 0.9rem; color: #6366f1;">📅 {date}</div>
73
- </div>
74
- <div style="text-align: right;">
75
- <div style="font-size: 2rem; font-weight: 800; color: #6366f1; line-height: 1;">{total_tasks}</div>
76
- <div style="font-size: 0.8rem; color: #64748b;">Tasks</div>
77
- </div>
78
  </div>
79
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
80
- <div style="background: white; padding: 8px; border-radius: 8px; text-align: center; border: 1px solid #e2e8f0;">
81
- <div style="font-size: 0.8rem; color: #64748b;">Duration</div>
82
- <div style="font-weight: 600; color: #334155;">{total_time} min</div>
 
 
 
 
83
  </div>
84
- <div style="background: white; padding: 8px; border-radius: 8px; text-align: center; border: 1px solid #e2e8f0;">
85
- <div style="font-size: 0.8rem; color: #64748b;">High Prio</div>
86
- <div style="font-weight: 600; color: #ef4444;">{high_priority}</div>
87
  </div>
88
  </div>
89
  </div>
90
  """
91
 
92
 
93
- def create_task_card(task_num: int, task_title: str, priority: str, time_window: dict, duration: str, location: str,
94
- icon: str = "📋") -> str:
95
- p_color = {"HIGH": "#ef4444", "MEDIUM": "#f59e0b", "LOW": "#10b981"}.get(priority.upper(), "#94a3b8")
96
 
97
- #print(f"{task_title}: {time_window}")
98
  display_time = "Anytime"
99
  if isinstance(time_window, dict):
100
  s_clean = _format_iso_time(time_window.get('earliest_time', ''))
@@ -105,22 +120,23 @@ def create_task_card(task_num: int, task_title: str, priority: str, time_window:
105
  display_time = f"After {s_clean}"
106
  elif e_clean:
107
  display_time = f"Before {e_clean}"
108
-
109
  elif time_window:
110
  display_time = str(time_window)
 
111
 
112
  return f"""
113
- <div class="task-card-item" style="border-left: 4px solid {p_color};">
114
- <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
115
- <div style="font-weight: 700; color: #334155; display: flex; align-items: center; gap: 8px;">
116
- <span style="background:#f1f5f9; padding:4px; border-radius:6px;">{icon}</span>
117
- {task_title}
118
- </div>
119
- <span style="font-size: 0.7rem; font-weight: 700; color: {p_color}; background: {p_color}15; padding: 2px 8px; border-radius: 12px; height: fit-content;">{priority}</span>
120
  </div>
121
- <div style="font-size: 0.85rem; color: #64748b; display: flex; flex-direction: column; gap: 4px;">
122
- <div style="display: flex; align-items: center; gap: 6px;"><span>📍</span> {location}</div>
123
- <div style="display: flex; align-items: center; gap: 6px;"><span>🕒</span> {display_time} <span style="color:#cbd5e1">|</span> ⏳ {duration}</div>
 
 
 
 
124
  </div>
125
  </div>
126
  """
 
2
  from typing import Union
3
  from datetime import datetime
4
  from config import AGENTS_INFO
5
+ import json
6
+
7
+ def _format_location(loc_data) -> str:
8
+ if isinstance(loc_data, dict):
9
+ return f"{loc_data.get('lat', 0):.4f}, {loc_data.get('lng', 0):.4f}"
10
+ if isinstance(loc_data, str) and "lat" in loc_data:
11
+ try:
12
+ # 嘗試把單引號換雙引號解析,解析失敗就回傳原字串
13
+ clean = loc_data.replace("'", '"')
14
+ data = json.loads(clean)
15
+ return f"{data.get('lat', 0):.2f}, {data.get('lng', 0):.2f}"
16
+ except:
17
+ pass
18
+ return str(loc_data)
19
 
20
  def _format_iso_time(iso_str: str) -> str:
21
  if not iso_str or iso_str == "N/A": return ""
 
32
 
33
 
34
  def create_agent_dashboard(status_dict: dict) -> str:
35
+ # 定義順序
36
+ order = ['team', 'scout', 'weatherman', 'optimizer', 'navigator', 'presenter']
37
 
38
+ cards_html = ""
39
+ for key in order:
40
  info = AGENTS_INFO.get(key, {})
41
  state = status_dict.get(key, {})
42
  status = state.get('status', 'idle')
43
  msg = state.get('message', 'Standby')
44
+
45
+ is_working = status == 'working'
46
+ css_class = "working" if is_working else ""
47
  icon = info.get('icon', '🤖')
48
  name = info.get('name', key.title())
 
 
49
 
50
+ # 簡單的卡片結構
51
+ cards_html += f"""
52
+ <div class="agent-status-card {css_class}">
53
+ <div class="agent-icon">{icon}</div>
54
+ <div class="agent-info">
55
  <div class="agent-name">{name}</div>
56
+ <div class="agent-msg">{msg}</div>
 
57
  </div>
58
  </div>
59
  """
60
 
61
+ return f'<div class="agent-status-row">{cards_html}</div>'
62
+
63
  html = f"""
64
  <div class="agent-war-room">
65
  <div class="org-chart">
 
77
  return html
78
 
79
 
80
+ def create_summary_card(total_tasks, high_priority, total_time, location="Taipei", date="Today") -> str:
81
+ clean_loc = _format_location(location)
82
+
83
+ # 這裡的 HTML 結構很單純,不會破壞佈局
84
  return f"""
85
+ <div class="summary-card-modern">
86
+ <div class="summary-main">
87
+ <div class="summary-label">TRIP SUMMARY</div>
88
+ <div class="summary-loc">📍 {clean_loc}</div>
89
+ <div class="summary-date">📅 {date}</div>
 
 
 
 
 
 
90
  </div>
91
+ <div class="summary-metrics">
92
+ <div class="metric-box">
93
+ <span class="m-val">{total_tasks}</span>
94
+ <span class="m-label">Tasks</span>
95
+ </div>
96
+ <div class="metric-box">
97
+ <span class="m-val">{total_time}<small>mins</small></span>
98
+ <span class="m-label">Duration</span>
99
  </div>
100
+ <div class="metric-box alert">
101
+ <span class="m-val">{high_priority}</span>
102
+ <span class="m-label">High Prio</span>
103
  </div>
104
  </div>
105
  </div>
106
  """
107
 
108
 
109
+ def create_task_card(task_num, task_title, priority, time_window, duration, location, icon="📋") -> str:
110
+ p_cls = priority.lower() # high, medium, low
 
111
 
112
+ # --- 🔥 恢復 Time Window 邏輯 ---
113
  display_time = "Anytime"
114
  if isinstance(time_window, dict):
115
  s_clean = _format_iso_time(time_window.get('earliest_time', ''))
 
120
  display_time = f"After {s_clean}"
121
  elif e_clean:
122
  display_time = f"Before {e_clean}"
 
123
  elif time_window:
124
  display_time = str(time_window)
125
+ # -------------------------------
126
 
127
  return f"""
128
+ <div class="task-card-modern border-{p_cls}">
129
+ <div class="tc-header">
130
+ <span class="tc-title">{icon} {task_title}</span>
131
+ <span class="tc-badge badge-{p_cls}">{priority}</span>
 
 
 
132
  </div>
133
+ <div class="tc-body">
134
+ <div class="tc-row">📍 {location}</div>
135
+ <div class="tc-row">
136
+ <span>🕒 {display_time}</span>
137
+ <span style="color: #cbd5e1; margin: 0 6px;">|</span>
138
+ <span>⏳ {duration}</span>
139
+ </div>
140
  </div>
141
  </div>
142
  """
ui/theme.py CHANGED
@@ -232,22 +232,23 @@ def get_enhanced_css() -> str:
232
  z-index: 2;
233
  }
234
 
235
- /* 右側卡片 */
236
  .timeline-card {
237
- flex: 1;
238
- background: white !important;
239
  border: 1px solid #e2e8f0 !important;
240
  border-radius: 12px !important;
241
  padding: 16px !important;
242
- box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important;
243
  transition: transform 0.2s, box-shadow 0.2s;
244
  display: block !important;
245
  }
246
 
 
247
  .timeline-card:hover {
 
248
  transform: translateX(4px);
249
- box-shadow: 0 4px 6px rgba(0,0,0,0.05) !important;
250
- border-color: var(--primary-color) !important;
251
  }
252
 
253
  .timeline-header {
@@ -266,8 +267,9 @@ def get_enhanced_css() -> str:
266
  }
267
 
268
  .timeline-meta {
269
- display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: #64748b;
270
- background: #f8fafc; padding: 8px; border-radius: 8px; margin-top: 8px;
 
271
  }
272
 
273
  .timeline-item:first-child .timeline-marker { border-color: #10b981 !important; }
@@ -331,7 +333,9 @@ def get_enhanced_css() -> str:
331
  .chat-input-row { padding: 12px !important; border-top: 1px solid #e2e8f0; background: white; align-items: center !important; }
332
 
333
  .split-view-container { display: flex; gap: 24px; height: calc(100vh - 140px); min-height: 600px; }
334
- .split-left-panel { flex: 1; position: sticky; overflow-y: auto; }
 
 
335
  .split-right-panel { flex: 2; position: sticky; top: 100px; height: 100%; }
336
 
337
  /* Animations */
@@ -344,6 +348,12 @@ def get_enhanced_css() -> str:
344
  ::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; }
345
  ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
346
  ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
 
 
 
 
 
 
347
 
348
  /* Dark Mode Support */
349
  .theme-dark body, .theme-dark .gradio-container { background: #0f172a !important; }
@@ -672,5 +682,615 @@ def get_enhanced_css() -> str:
672
  min-height: 20px !important; /* 預留高度防止跳動 */
673
  }
674
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
  </style>
676
  """
 
232
  z-index: 2;
233
  }
234
 
235
+ /* Timeline 卡片:改為淺灰色背景,製造層次感 */
236
  .timeline-card {
237
+ background: #f8fafc !important; /* 淺灰底 */
 
238
  border: 1px solid #e2e8f0 !important;
239
  border-radius: 12px !important;
240
  padding: 16px !important;
241
+ box-shadow: 0 1px 2px rgba(0,0,0,0.03) !important;
242
  transition: transform 0.2s, box-shadow 0.2s;
243
  display: block !important;
244
  }
245
 
246
+ /* Hover 效果:卡片變白並浮起 */
247
  .timeline-card:hover {
248
+ background: #ffffff !important;
249
  transform: translateX(4px);
250
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08) !important;
251
+ border-color: var(--primary) !important;
252
  }
253
 
254
  .timeline-header {
 
267
  }
268
 
269
  .timeline-meta {
270
+ background: #f1f5f9;
271
+ border: 1px solid #e2e8f0;
272
+ padding: 8px; border-radius: 8px; margin-top: 8px; color: #64748b;
273
  }
274
 
275
  .timeline-item:first-child .timeline-marker { border-color: #10b981 !important; }
 
333
  .chat-input-row { padding: 12px !important; border-top: 1px solid #e2e8f0; background: white; align-items: center !important; }
334
 
335
  .split-view-container { display: flex; gap: 24px; height: calc(100vh - 140px); min-height: 600px; }
336
+ .split-left-panel .panel-container, .split-left-panel .gradio-group {
337
+ background: white !important;
338
+ }
339
  .split-right-panel { flex: 2; position: sticky; top: 100px; height: 100%; }
340
 
341
  /* Animations */
 
348
  ::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; }
349
  ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
350
  ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
351
+
352
+ /* 修正 Scrollbar 樣式,讓它明顯一點但好看 */
353
+ .drawer-content::-webkit-scrollbar { width: 6px; }
354
+ .drawer-content::-webkit-scrollbar-track { background: transparent; }
355
+ .drawer-content::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
356
+ .drawer-content::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
357
 
358
  /* Dark Mode Support */
359
  .theme-dark body, .theme-dark .gradio-container { background: #0f172a !important; }
 
682
  min-height: 20px !important; /* 預留高度防止跳動 */
683
  }
684
 
685
+ /* === 1. Summary Card (儀表板) === */
686
+ .summary-card-modern {
687
+ /* 改為藍白漸層 */
688
+ background: linear-gradient(135deg, #ffffff 0%, #eff6ff 100%) !important;
689
+ border: 1px solid #dbeafe !important;
690
+
691
+ /* 🔥 修正:縮小內距與下邊距,防止撐出滾動條 */
692
+ border-radius: 16px;
693
+ padding: 14px 18px !important; /* 原本是 24px */
694
+ margin-bottom: 12px !important; /* 原本是 20px */
695
+
696
+ display: flex;
697
+ justify-content: space-between;
698
+ align-items: center;
699
+ flex-wrap: wrap;
700
+ gap: 15px;
701
+
702
+ /* 消除 Gradio 預設可能帶入的額外高度 */
703
+ min-height: 0 !important;
704
+ }
705
+
706
+ /* 讓 Summary 內的地點文字更跳 */
707
+ .summary-loc {
708
+ font-size: 1.4rem;
709
+ font-weight: 800;
710
+ color: #312e81; /* 深靛藍 */
711
+ margin: 4px 0;
712
+ text-shadow: 0 1px 0 rgba(255,255,255,0.8);
713
+ }
714
+
715
+ .summary-label { font-size: 0.75rem; color: #94a3b8; font-weight: 700; letter-spacing: 0.5px; }
716
+
717
+ .summary-date { font-size: 0.9rem; color: #6366f1; }
718
+
719
+ .summary-metrics { display: flex; gap: 12px; }
720
+ .metric-box {
721
+ background: rgba(255, 255, 255, 0.7);
722
+ border: 1px solid #e2e8f0;
723
+ padding: 8px 16px;
724
+ border-radius: 12px;
725
+ min-width: 80px;
726
+ backdrop-filter: blur(4px);
727
+ box-shadow: 0 2px 4px rgba(0,0,0,0.02);
728
+ }
729
+ .m-val { display: block; font-size: 1.2rem; font-weight: 800; color: #334155; line-height: 1; }
730
+ .m-val small { font-size: 0.7rem; margin-left: 2px; }
731
+ .m-label { font-size: 0.7rem; color: #64748b; }
732
+ .metric-box.alert .m-val { color: #ef4444; }
733
+
734
+ /* === 2. Agent Status Row (橫向狀態列) === */
735
+ .agent-status-row {
736
+ display: flex;
737
+ gap: 10px;
738
+ overflow-x: auto; /* 如果螢幕太窄,允許橫向捲動 */
739
+ padding-bottom: 5px;
740
+ margin-bottom: 15px;
741
+ }
742
+ .agent-status-card {
743
+ flex: 1;
744
+ min-width: 120px;
745
+ background: white;
746
+ border: 1px solid #e2e8f0;
747
+ border-radius: 10px;
748
+ padding: 10px;
749
+ display: flex;
750
+ align-items: center;
751
+ gap: 10px;
752
+ transition: all 0.3s;
753
+ }
754
+ .agent-status-card.working {
755
+ border-color: #6366f1;
756
+ background: #eff6ff;
757
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
758
+ }
759
+ .agent-name { font-size: 0.8rem; font-weight: 700; color: #1e293b; }
760
+ .agent-msg { font-size: 0.7rem; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
761
+ .agent-status-card.working .agent-msg { color: #6366f1; font-weight: 600; }
762
+
763
+ /* === 3. Task Card Modern (任務卡片) === */
764
+ .task-card-modern {
765
+ background: white;
766
+ border: 1px solid #e2e8f0;
767
+ border-radius: 10px;
768
+ padding: 14px;
769
+ margin-bottom: 10px;
770
+ border-left-width: 4px;
771
+ }
772
+ .border-high { border-left-color: #ef4444; }
773
+ .border-medium { border-left-color: #f59e0b; }
774
+ .border-low { border-left-color: #10b981; }
775
+
776
+ .tc-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
777
+ .tc-title { font-weight: 600; color: #334155; font-size: 0.95rem; }
778
+ .tc-badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 12px; font-weight: 700; text-transform: uppercase; }
779
+
780
+ .badge-high { background: #fef2f2; color: #ef4444; }
781
+ .badge-medium { background: #fffbeb; color: #f59e0b; }
782
+ .badge-low { background: #ecfdf5; color: #10b981; }
783
+
784
+ .tc-body { font-size: 0.85rem; color: #64748b; display: flex; flex-direction: column; gap: 4px; }
785
+
786
+ /* 1. 報告與任務列表的通用容器 */
787
+ .live-report-wrapper, .panel-container {
788
+ background: white !important;
789
+ border: 1px solid #e2e8f0 !important;
790
+ border-radius: 16px !important;
791
+ padding: 30px !important;
792
+
793
+ /* 🔥 關鍵:給它一個最小高度,讓它看起來像一張完整的紙,不會因為內容少而縮成一團 */
794
+ min-height: 600px !important;
795
+
796
+ /* 增加立體感 */
797
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03) !important;
798
+
799
+ /* 讓內部的 Markdown 排版更舒適 */
800
+ line-height: 1.6 !important;
801
+ font-size: 1rem !important;
802
+ color: #334155 !important;
803
+ }
804
+
805
+ /* 2. 針對 Tab 內容的微調 */
806
+ /* 讓 Tab 下方的內容區塊與 Tab 標籤無縫銜接 (可選,視 Gradio 版本而定) */
807
+ .tabitem {
808
+ border: none !important;
809
+ background: transparent !important;
810
+ }
811
+
812
+ /* 3. 優化 "Analyzing..." 載入文字 */
813
+ /* 讓它不要只是孤單地浮在左上角,而是置中並帶點呼吸感 */
814
+ .live-report-wrapper p:only-child, .panel-container p:only-child {
815
+ text-align: center !important;
816
+ margin-top: 100px !important;
817
+ color: #94a3b8 !important;
818
+ font-weight: 500 !important;
819
+ animation: pulse-text 2s infinite !important;
820
+ }
821
+
822
+ @keyframes pulse-text {
823
+ 0% { opacity: 0.6; }
824
+ 50% { opacity: 1; }
825
+ 100% { opacity: 0.6; }
826
+ }
827
+
828
+
829
+ /* 讓氣泡容器不要拉伸 (預設靠左) */
830
+ .message-wrap {
831
+ display: flex !important;
832
+ flex-direction: column !important;
833
+ align-items: flex-start !important;
834
+ gap: 6px !important;
835
+ }
836
+
837
+
838
+ /* User 的氣泡改為靠右 */
839
+ .message-wrap.user-row {
840
+ align-items: flex-end !important;
841
+ }
842
+
843
+ /* 氣泡本體:寬度適應內容,不要繼承 100% */
844
+ .message-bubble {
845
+ width: fit-content !important;
846
+ max-width: 80% !important;
847
+ height: auto !important;
848
+ display: block !important;
849
+ white-space: pre-wrap !important;
850
+ padding: 10px 14px !important;
851
+ }
852
+
853
+ /* ============= 3. 強化 Page 3 Agent 特效 (呼吸燈) ============= */
854
+ /* 定義強烈的脈衝動畫 */
855
+ @keyframes strong-pulse {
856
+ 0% {
857
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4);
858
+ border-color: #6366f1;
859
+ transform: scale(1);
860
+ }
861
+ 50% {
862
+ box-shadow: 0 0 0 6px rgba(99, 102, 241, 0); /* 擴散光圈 */
863
+ border-color: #818cf8;
864
+ transform: scale(1.02); /* 微微放大 */
865
+ }
866
+ 100% {
867
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
868
+ border-color: #6366f1;
869
+ transform: scale(1);
870
+ }
871
+ }
872
+
873
+ /* Agent 容器基礎樣式 */
874
+ .agent-status-row {
875
+ display: flex; gap: 10px; overflow-x: auto; padding: 10px 5px; margin-bottom: 15px;
876
+ }
877
+ .agent-status-card {
878
+ flex: 1; min-width: 140px; background: white;
879
+ border: 1px solid #e2e8f0; border-radius: 12px;
880
+ padding: 12px; display: flex; align-items: center; gap: 10px;
881
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
882
+ position: relative;
883
+ }
884
+
885
+ /* 🔥 關鍵:套用動畫到 Working 狀態 */
886
+ .agent-status-card.working {
887
+ animation: strong-pulse 2s infinite !important; /* 強制執行動畫 */
888
+ background: linear-gradient(135deg, #ffffff 0%, #e0e7ff 100%) !important; /* 微藍背景 */
889
+ z-index: 10;
890
+ }
891
+
892
+ /* 文字顏色也跟著變亮 */
893
+ .agent-status-card.working .agent-msg {
894
+ color: #4f46e5 !important;
895
+ font-weight: 700 !important;
896
+ }
897
+
898
+ /* 靜態樣式補充 */
899
+ .agent-icon { font-size: 1.5rem; }
900
+ .agent-info { overflow: hidden; }
901
+ .agent-name { font-size: 0.85rem; font-weight: 700; color: #1e293b; }
902
+ .agent-msg { font-size: 0.75rem; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
903
+
904
+ /* 強制修正氣泡變形與留白問題 */
905
+
906
+ .map-container-relative {
907
+ position: relative !important;
908
+ height: 650px !important; /* 🔥 強制高度 */
909
+ width: 100% !important;
910
+ border-radius: 16px;
911
+ overflow: hidden !important;
912
+ border: 1px solid #e2e8f0;
913
+ background: white;
914
+ }
915
+
916
+ /* 強制 iframe 填滿容器 */
917
+ .map-container-relative iframe {
918
+ width: 100% !important;
919
+ height: 100% !important;
920
+ border: none !important;
921
+ }
922
+
923
+ /* 2. 浮動抽屜本體 */
924
+ .map-overlay-drawer {
925
+ position: absolute !important;
926
+ top: 20px !important;
927
+ left: 20px !important;
928
+ bottom: 20px !important;
929
+ width: 380px !important; /* 固定寬度,確保不會擋住太多地圖 */
930
+ height: 85% !important;
931
+
932
+ background: rgba(255, 255, 255, 0.95) !important;
933
+ backdrop-filter: blur(12px) !important; /* 毛玻璃效果 */
934
+ border-radius: 16px !important;
935
+ border: 1px solid rgba(255, 255, 255, 0.6) !important;
936
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15), 0 4px 10px -2px rgba(0, 0, 0, 0.1) !important;
937
+
938
+ z-index: 500 !important; /* 確保在地圖之上 (Leaflet 通常是 z-index 400) */
939
+ display: flex !important;
940
+ flex-direction: column !important;
941
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
942
+ /* 🔥 關鍵:讓內部元素可以用 Flex 排列 */
943
+
944
+ display: flex !important;
945
+ flex-direction: column !important;
946
+ overflow: hidden !important; /* 防止圓角被內容切掉 */
947
+ }
948
+
949
+ /* 3. 抽屜標頭 */
950
+ .drawer-header {
951
+ padding: 16px 20px !important;
952
+ border-bottom: 1px solid #e2e8f0 !important;
953
+ background: #f8fafc !important;
954
+ border-radius: 16px 16px 0 0 !important;
955
+ display: flex !important;
956
+ justify-content: space-between !important;
957
+ align-items: center !important;
958
+ flex-shrink: 0 !important;
959
+ height: 60px !important;
960
+ }
961
+
962
+ .drawer-title p {
963
+ margin: 0 !important;
964
+ font-size: 1rem !important;
965
+ font-weight: 700 !important;
966
+ color: #334155 !important;
967
+ }
968
+
969
+ .drawer-tabs .tab-nav {
970
+ border-bottom: 1px solid #f1f5f9 !important;
971
+ margin: 0 !important;
972
+ }
973
+
974
+ /* 5. 關閉按鈕 (X) */
975
+ .drawer-close-btn {
976
+ background: transparent !important;
977
+ color: #94a3b8 !important;
978
+ border: none !important;
979
+ box-shadow: none !important;
980
+ font-size: 1.2rem !important;
981
+ padding: 4px !important;
982
+ width: 32px !important;
983
+ min-width: 32px !important;
984
+ }
985
+ .drawer-close-btn:hover {
986
+ color: #ef4444 !important;
987
+ background: #fef2f2 !important;
988
+ border-radius: 50% !important;
989
+ }
990
+
991
+ /* === 修復抽屜開關按鈕 === */
992
+ .map-overlay-btn {
993
+ position: absolute !important;
994
+ top: 10px !important;
995
+ left:60px !important;
996
+
997
+ /* 🔥 關鍵:必須比 Leaflet 地圖 (400) 和 Drawer (500) 高 */
998
+ z-index: 1000 !important;
999
+
1000
+ background: white !important;
1001
+ color: #6366f1 !important;
1002
+ border: 1px solid #cbd5e1 !important;
1003
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important;
1004
+ font-weight: 600 !important;
1005
+ padding: 8px 12px !important;
1006
+ }
1007
+
1008
+ .map-overlay-btn:hover {
1009
+ transform: translateY(-2px);
1010
+ box-shadow: 0 6px 10px rgba(0,0,0,0.15) !important;
1011
+ }
1012
+
1013
+ /* 隱藏 Map 預設的 Label */
1014
+ .map-container-relative .label-wrap {
1015
+ display: none !important;
1016
+ }
1017
+
1018
+ /* 顏色修正 (確保不被舊樣式覆蓋) */
1019
+
1020
+
1021
+ /* --- 2. Page 4: Map Overlay Drawer (捲軸與排版修復) --- */
1022
+
1023
+ /* 抽屜容器:Flex Column 佈局 */
1024
+ .map-overlay-drawer {
1025
+ display: flex !important;
1026
+ flex-direction: column !important;
1027
+ max-height: calc(100% - 40px) !important; /* 防止超出地圖邊界 */
1028
+ overflow: hidden !important; /* 外層隱藏溢出 */
1029
+ background: rgba(255, 255, 255, 0.98) !important;
1030
+ border: 1px solid rgba(226, 232, 240, 0.8) !important;
1031
+ z-index: 5000 !important;
1032
+ height: 85% !important;
1033
+ max-height: 600px !important;
1034
+ }
1035
+
1036
+ /* Header:固定高度 */
1037
+ .drawer-header {
1038
+ flex-shrink: 0 !important;
1039
+ height: auto !important;
1040
+ border-bottom: 1px solid #e2e8f0 !important;
1041
+ background: #f8fafc !important;
1042
+ }
1043
+
1044
+ /* 🔥 修正 Tab 跑版:移除負邊距,讓它自然排列 */
1045
+ .drawer-tabs {
1046
+ margin-top: 0 !important; /* 還原 margin */
1047
+ display: flex !important;
1048
+ flex-direction: column !important;
1049
+ height: 100% !important;
1050
+ }
1051
+
1052
+ /* 讓 Tab 的內容區塊也能捲動 */
1053
+ .drawer-tabs .tabitem {
1054
+ overflow-y: visible !important;
1055
+ height: auto !important;
1056
+ }
1057
+
1058
+ /* 美化捲軸 (Chrome/Safari) */
1059
+ .drawer-content::-webkit-scrollbar {
1060
+ width: 6px;
1061
+ }
1062
+ .drawer-content::-webkit-scrollbar-track {
1063
+ background: transparent;
1064
+ }
1065
+ .drawer-content::-webkit-scrollbar-thumb {
1066
+ background: #cbd5e1;
1067
+ border-radius: 3px;
1068
+ }
1069
+
1070
+ /* --- 3. Summary Card Modern (漸層色補丁) --- */
1071
+ .summary-card-modern {
1072
+ background: linear-gradient(120deg, #ffffff 0%, #eff6ff 100%) !important;
1073
+ border: 1px solid #dbeafe !important;
1074
+ box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.1) !important;
1075
+ }
1076
+
1077
+
1078
+ .header-controls {
1079
+ position: fixed !important;
1080
+ top: 24px !important;
1081
+ right: 24px !important;
1082
+ z-index: 99999 !important;
1083
+
1084
+ display: flex !important;
1085
+ flex-direction: column !important; /* 垂直排列 */
1086
+ gap: 8px !important;
1087
+
1088
+ /* 強制縮小寬度,防止佔滿橫條 */
1089
+ width: auto !important;
1090
+ min-width: 0 !important;
1091
+ height: auto !important;
1092
+
1093
+ /* 膠囊視覺還原 */
1094
+ background: rgba(255, 255, 255, 0.9) !important;
1095
+ backdrop-filter: blur(8px) !important;
1096
+ padding: 8px !important;
1097
+ border-radius: 50px !important;
1098
+ border: 1px solid rgba(255, 255, 255, 0.6) !important;
1099
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
1100
+ }
1101
+
1102
+
1103
+ /* 3. Page 4 Map 滾動條修復:強制高度與捲軸 */
1104
+ .map-overlay-drawer {
1105
+ display: flex !important;
1106
+ flex-direction: column !important;
1107
+ height: 85% !important; /* 🔥 必須給定高度,捲軸才會出現 */
1108
+ overflow: hidden !important;
1109
+ }
1110
+
1111
+
1112
+ /* 4. Page 4 Map 按鈕位移:把 Zoom 按鈕移到右邊 */
1113
+ .leaflet-left {
1114
+ left: auto !important;
1115
+ right: 20px !important;
1116
+ }
1117
+ .header-controls {
1118
+ position: fixed !important; top: 24px !important; right: 24px !important; z-index: 99999 !important;
1119
+ display: flex !important; flex-direction: column !important; gap: 8px !important;
1120
+ width: auto !important; height: auto !important;
1121
+ background: rgba(255, 255, 255, 0.9) !important; backdrop-filter: blur(8px) !important;
1122
+ padding: 8px !important; border-radius: 50px !important;
1123
+ border: 1px solid rgba(255, 255, 255, 0.6) !important;
1124
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
1125
+ }
1126
+
1127
+ /* 2. Page 4 Map Drawer: 強制內容區塊顯示 */
1128
+ /* 這裡只做最基本的顯示,不搞複雜的捲軸邏輯,讓瀏覽器自己處理 */
1129
+ .map-overlay-drawer {
1130
+ display: flex !important;
1131
+ flex-direction: column !important;
1132
+ height: 80% !important;
1133
+ overflow: hidden !important;
1134
+ }
1135
+
1136
+ .drawer-content {
1137
+ flex: 1 !important;
1138
+ overflow-y: auto !important;
1139
+ background: white !important; /* 確保內容區是白色的 */
1140
+ padding: 0 !important;
1141
+ display: block !important;
1142
+ }
1143
+
1144
+ /* 3. Page 4 Map 按鈕: 不要動它! */
1145
+ /* 刪除任何 .leaflet-left 的設定,讓它自己回到左邊 */
1146
+
1147
+ /* 4. 修改 Show Summary 按鈕位置 (避開左邊的原生按鈕) */
1148
+ .map-overlay-btn {
1149
+ position: absolute !important;
1150
+ top: 10px !important;
1151
+ left: 60px !important; /* 強制往右移 60px */
1152
+ z-index: 5000 !important;
1153
+ }
1154
+
1155
+ 這確實讓人崩潰,那個 "321 / 31" 的斷行顯示 Gradio 內部的 Flex 容器寬度被壓縮到了極限,而藍色方塊依然巨大則是因為 垂直拉伸 (Stretch) 屬性 仍然存在。
1156
+
1157
+ 這表示即使我們拿掉了 height,Gradio 內部的 CSS 權重還是比我們的高,或者是某層 div 被我們之前的 CSS 誤傷導致寬度歸零。
1158
+
1159
+ 請執行這個 「最終純淨版」 修正。這一次,我們不再對抗 Gradio 的 Flex 機制,而是順著它的毛摸,但強制加上「最小寬度」保護,並殺死「垂直拉伸」。
1160
+
1161
+ 1. 確保 app.py 的 height 真的刪掉了
1162
+ (如果你剛剛已經刪了,這步請再次確認就好)
1163
+
1164
+ Python
1165
+
1166
+ chatbot = gr.Chatbot(
1167
+ label="AI Assistant",
1168
+ type="messages",
1169
+ # height=540, <-- 這行絕對不能有
1170
+ elem_classes="native-chatbot",
1171
+ bubble_full_width=False
1172
+ )
1173
+ 2. 替換 ui/theme.py (純淨修復版)
1174
+ 請全選刪除 ui/theme.py 裡 Chatbot 相關的 CSS,換成這段。這段代碼移除了所有復雜的絕對定位,回歸最單純的排版,但加強了對 寬度 (Width) 的保護。
1175
+
1176
+ CSS
1177
+
1178
+ /* ============= 🛡️ LifeFlow Chatbot: 純淨修復版 🛡️ ============= */
1179
+
1180
+ /* 1. 外層容器:給定高度,讓它有捲軸 */
1181
+ .chat-panel-native {
1182
+ background: white !important;
1183
+ border: 1px solid #e2e8f0 !important;
1184
+ border-radius: 16px !important;
1185
+ height: 600px !important;
1186
+ overflow: hidden !important;
1187
+ display: flex !important;
1188
+ flex-direction: column !important;
1189
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
1190
+ }
1191
+
1192
+ /* 2. Chatbot 本體:自然填滿剩餘空間 */
1193
+ .native-chatbot {
1194
+ background: white !important;
1195
+ flex-grow: 1 !important;
1196
+ overflow-y: auto !important;
1197
+ padding: 20px !important;
1198
+ border: none !important;
1199
+ }
1200
+
1201
+ /* 3. 訊息行 (Row) */
1202
+ .native-chatbot .message-row {
1203
+ display: flex !important;
1204
+ flex-direction: row !important;
1205
+ width: 100% !important;
1206
+ margin-bottom: 16px !important;
1207
+
1208
+ /* 🔥 關鍵一:殺死垂直拉伸 (解決巨大方塊) */
1209
+ align-items: flex-start !important;
1210
+ height: auto !important;
1211
+
1212
+ /* 避免被壓縮 */
1213
+ flex-shrink: 0 !important;
1214
+ }
1215
+
1216
+ /* 4. 氣泡外殼 (Wrapper) - 這是解決 "321/31" 的關鍵 */
1217
+ /* 之前的版本可能漏了這個,導致外殼寬度為 0 */
1218
+ .native-chatbot .message-wrap {
1219
+ display: flex !important;
1220
+ flex-direction: column !important;
1221
+
1222
+ /* 🔥 關鍵二:寬度自動,但不能小於內容 */
1223
+ width: auto !important;
1224
+ min-width: 0 !important;
1225
+ max-width: 85% !important;
1226
+
1227
+ flex-shrink: 1 !important; /* 允許在極端狀況下縮小 */
1228
+ flex-grow: 0 !important; /* 禁止無故變寬 */
1229
+ }
1230
+
1231
+ /* 5. 氣泡本體 (Bubble) */
1232
+ .native-chatbot .message {
1233
+ width: fit-content !important;
1234
+ height: auto !important;
1235
+ padding: 8px 12px !important;
1236
+ font-size: 0.95rem !important;
1237
+ line-height: 1.5 !important;
1238
+
1239
+ /* 🔥 關鍵三:正確的斷字設定 */
1240
+ white-space: pre-wrap !important;
1241
+ word-break: break-word !important;
1242
+ overflow-wrap: break-word !important;
1243
+ }
1244
+
1245
+ /* 6. Bot (靠左) */
1246
+ .native-chatbot .message-row:not(.user-row) {
1247
+ justify-content: flex-start !important;
1248
+ }
1249
+
1250
+ .native-chatbot .message.bot,
1251
+ .native-chatbot .message.model,
1252
+ .native-chatbot .message.assistant,
1253
+ .native-chatbot .message-row:not(.user-row) .message {
1254
+ background-color: #f1f5f9 !important;
1255
+ color: #1e293b !important;
1256
+ border: 1px solid #e2e8f0 !important;
1257
+ border-radius: 4px 16px 16px 16px !important;
1258
+ }
1259
+
1260
+ .native-chatbot .message.bot *, .native-chatbot .message-row:not(.user-row) .message * {
1261
+ color: #1e293b !important;
1262
+ }
1263
+
1264
+ /* 7. User (靠右) */
1265
+ .native-chatbot .message-row.user-row {
1266
+ justify-content: flex-end !important;
1267
+ }
1268
+
1269
+ .native-chatbot .message.user,
1270
+ .native-chatbot .message-row.user-row .message {
1271
+ background-color: #6366f1 !important;
1272
+ color: white !important;
1273
+ border: none !important;
1274
+ border-radius: 16px 4px 16px 16px !important;
1275
+ text-align: left !important;
1276
+ }
1277
+
1278
+ .native-chatbot .message.user * {
1279
+ color: white !important;
1280
+ }
1281
+
1282
+ /* 8. 輸入框 */
1283
+ .chat-input-row {
1284
+ background: white !important;
1285
+ border-top: 1px solid #e2e8f0 !important;
1286
+ padding: 16px !important;
1287
+ flex-shrink: 0 !important; /* 防止被壓扁 */
1288
+ }
1289
+
1290
+ /* 9. 隱藏垃圾 */
1291
+ .native-chatbot button,
1292
+ .native-chatbot .avatar-container,
1293
+ .native-chatbot .message-icon { display: none !important; }
1294
+
1295
  </style>
1296
  """