Spaces:
Running
Running
feat: improve UI/UX
Browse filesfeat: update README.md info
- README.md +1 -1
- app.py +58 -16
- config.py +3 -1
- core/visualizers.py +76 -75
- services/planner_service.py +9 -1
- ui/renderers.py +64 -48
- 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="
|
| 325 |
-
chatbot = gr.Chatbot(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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 = ['
|
| 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
|
| 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
|
| 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=
|
| 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,
|
| 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: 備用方案 (
|
| 249 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 260 |
continue
|
| 261 |
|
| 262 |
center_lat, center_lng = chosen.get("lat"), chosen.get("lng")
|
|
|
|
|
|
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
| 40 |
<div class="agent-name">{name}</div>
|
| 41 |
-
<div class="agent-
|
| 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
|
| 65 |
-
|
|
|
|
|
|
|
| 66 |
return f"""
|
| 67 |
-
<div
|
| 68 |
-
<div
|
| 69 |
-
<div>
|
| 70 |
-
|
| 71 |
-
|
| 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
|
| 80 |
-
<div
|
| 81 |
-
<
|
| 82 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
</div>
|
| 84 |
-
<div
|
| 85 |
-
<
|
| 86 |
-
<
|
| 87 |
</div>
|
| 88 |
</div>
|
| 89 |
</div>
|
| 90 |
"""
|
| 91 |
|
| 92 |
|
| 93 |
-
def create_task_card(task_num
|
| 94 |
-
|
| 95 |
-
p_color = {"HIGH": "#ef4444", "MEDIUM": "#f59e0b", "LOW": "#10b981"}.get(priority.upper(), "#94a3b8")
|
| 96 |
|
| 97 |
-
#
|
| 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-
|
| 114 |
-
<div
|
| 115 |
-
<
|
| 116 |
-
|
| 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
|
| 122 |
-
<div
|
| 123 |
-
<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 |
-
|
| 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.
|
| 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
|
| 250 |
-
border-color: var(--primary
|
| 251 |
}
|
| 252 |
|
| 253 |
.timeline-header {
|
|
@@ -266,8 +267,9 @@ def get_enhanced_css() -> str:
|
|
| 266 |
}
|
| 267 |
|
| 268 |
.timeline-meta {
|
| 269 |
-
|
| 270 |
-
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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 |
"""
|