Spaces:
Running
Running
Add outdated demo
Browse files- prototypes/app_demo_v2.py +532 -0
prototypes/app_demo_v2.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LifeFlow AI - Main Application v3.1
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
import time as time_module
|
| 12 |
+
|
| 13 |
+
# 導入配置
|
| 14 |
+
from config import DEFAULT_SETTINGS, APP_TITLE
|
| 15 |
+
|
| 16 |
+
# 導入 UI 組件
|
| 17 |
+
from ui.theme import get_enhanced_css
|
| 18 |
+
from ui.components.header import create_header, create_top_controls
|
| 19 |
+
from ui.components.input_form import create_input_form, toggle_location_inputs
|
| 20 |
+
from ui.components.confirmation import create_confirmation_area, create_exit_button
|
| 21 |
+
from ui.components.results import create_team_area, create_result_area, create_tabs
|
| 22 |
+
from ui.components.modals import create_settings_modal, create_doc_modal
|
| 23 |
+
|
| 24 |
+
# 導入核心工具
|
| 25 |
+
from core.utils import (
|
| 26 |
+
create_agent_stream_output, create_agent_card_enhanced,
|
| 27 |
+
create_task_card, create_summary_card, create_animated_map,
|
| 28 |
+
get_reasoning_html_reversed, create_celebration_animation,
|
| 29 |
+
create_result_visualization
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class LifeFlowAI:
|
| 34 |
+
"""LifeFlow AI - v3.1 主類"""
|
| 35 |
+
|
| 36 |
+
def __init__(self):
|
| 37 |
+
self.settings = DEFAULT_SETTINGS.copy()
|
| 38 |
+
self.task_list = []
|
| 39 |
+
self.reasoning_messages = []
|
| 40 |
+
self.planning_completed = False
|
| 41 |
+
self.chat_history = []
|
| 42 |
+
|
| 43 |
+
def step1_analyze_tasks(self, user_input, auto_location, lat, lon):
|
| 44 |
+
"""
|
| 45 |
+
Step 1: 分析任務 - 帶串流輸出
|
| 46 |
+
完成後進入 Step 2 (Confirmation)
|
| 47 |
+
"""
|
| 48 |
+
if not user_input.strip():
|
| 49 |
+
return self._empty_step1_outputs()
|
| 50 |
+
|
| 51 |
+
# 🔧 串流輸出 1: 開始分析
|
| 52 |
+
stream_str = "🤔 Analyzing your request..."
|
| 53 |
+
stream_html = self._create_stream_html(stream_str)
|
| 54 |
+
yield (
|
| 55 |
+
stream_html, "", "", get_reasoning_html_reversed(),
|
| 56 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 57 |
+
"", # chat_history_output
|
| 58 |
+
"Starting analysis...",
|
| 59 |
+
*[create_agent_card_enhanced("planner", "working", "Analyzing..."),
|
| 60 |
+
*[create_agent_card_enhanced(k, "idle", "On standby")
|
| 61 |
+
for k in ["scout", "optimizer", "validator", "weather", "traffic"]]]
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
time_module.sleep(0.5)
|
| 65 |
+
|
| 66 |
+
stream_str += "\n📋 Extracting tasks from your input..."
|
| 67 |
+
# 🔧 串流輸出 2: 提取任務
|
| 68 |
+
stream_html = self._create_stream_html(stream_str)
|
| 69 |
+
yield (
|
| 70 |
+
stream_html, "", "", get_reasoning_html_reversed(),
|
| 71 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 72 |
+
"", # chat_history_output
|
| 73 |
+
"Extracting tasks...",
|
| 74 |
+
*[create_agent_card_enhanced("planner", "working", "Extracting tasks..."),
|
| 75 |
+
*[create_agent_card_enhanced(k, "idle", "On standby")
|
| 76 |
+
for k in ["scout", "optimizer", "validator", "weather", "traffic"]]]
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
time_module.sleep(0.5)
|
| 80 |
+
|
| 81 |
+
# 模擬提取的任務
|
| 82 |
+
self.task_list = [
|
| 83 |
+
{"id": 1, "title": "Visit hospital", "priority": "HIGH",
|
| 84 |
+
"time": "08:00-12:00", "duration": "45 minutes", "location": "Hospital nearby", "icon": "🏥"},
|
| 85 |
+
{"id": 2, "title": "Buy groceries", "priority": "MEDIUM",
|
| 86 |
+
"time": "Anytime", "duration": "30 minutes", "location": "Supermarket", "icon": "🛒"},
|
| 87 |
+
{"id": 3, "title": "Mail package", "priority": "HIGH",
|
| 88 |
+
"time": "Before 15:00", "duration": "20 minutes", "location": "Post office", "icon": "📮"}
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
# 添加推理訊息
|
| 92 |
+
self._add_reasoning("planner", "Analyzing input tasks...")
|
| 93 |
+
self._add_reasoning("planner", f"Extracted {len(self.task_list)} tasks")
|
| 94 |
+
|
| 95 |
+
# 生成摘要和任務卡片
|
| 96 |
+
high_priority = sum(1 for t in self.task_list if t["priority"] == "HIGH")
|
| 97 |
+
total_time = sum(int(t["duration"].split()[0]) for t in self.task_list)
|
| 98 |
+
|
| 99 |
+
summary_html = create_summary_card(len(self.task_list), high_priority, total_time)
|
| 100 |
+
task_list_html = self._generate_task_list_html()
|
| 101 |
+
|
| 102 |
+
stream_str += "\n✅ Analysis complete! Please review your tasks."
|
| 103 |
+
stream_html = self._create_stream_html(stream_str)
|
| 104 |
+
|
| 105 |
+
# Step 1 完成,準備進入 Step 2
|
| 106 |
+
yield (
|
| 107 |
+
stream_html, summary_html, task_list_html,
|
| 108 |
+
get_reasoning_html_reversed(self.reasoning_messages),
|
| 109 |
+
gr.update(visible=True), # task_confirm_area
|
| 110 |
+
gr.update(visible=False), # chat_input_area (先隱藏)
|
| 111 |
+
self._generate_chat_welcome_html(), # chat_history_output
|
| 112 |
+
"Tasks extracted successfully ✓",
|
| 113 |
+
*[create_agent_card_enhanced("planner", "complete", "Tasks extracted"),
|
| 114 |
+
*[create_agent_card_enhanced(k, "idle", "On standby")
|
| 115 |
+
for k in ["scout", "optimizer", "validator", "weather", "traffic"]]]
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
def modify_task_chat(self, user_message):
|
| 119 |
+
"""
|
| 120 |
+
處理用戶在 Chat with LifeFlow Tab 中的修改請求
|
| 121 |
+
這在 Step 2 時可用
|
| 122 |
+
"""
|
| 123 |
+
if not user_message.strip():
|
| 124 |
+
return self._generate_chat_history_html(), self._generate_task_list_html()
|
| 125 |
+
|
| 126 |
+
# 添加用戶消息
|
| 127 |
+
self.chat_history.append({
|
| 128 |
+
"role": "user",
|
| 129 |
+
"message": user_message,
|
| 130 |
+
"time": datetime.now().strftime("%H:%M:%S")
|
| 131 |
+
})
|
| 132 |
+
|
| 133 |
+
# 模擬 AI 回應
|
| 134 |
+
time_module.sleep(0.3)
|
| 135 |
+
ai_response = f"✅ I've updated your tasks based on your request: '{user_message}'"
|
| 136 |
+
|
| 137 |
+
self.chat_history.append({
|
| 138 |
+
"role": "assistant",
|
| 139 |
+
"message": ai_response,
|
| 140 |
+
"time": datetime.now().strftime("%H:%M:%S")
|
| 141 |
+
})
|
| 142 |
+
|
| 143 |
+
# 更新 reasoning
|
| 144 |
+
self._add_reasoning("planner", f"User requested: {user_message}")
|
| 145 |
+
|
| 146 |
+
# 返回更新的聊天歷史和任務列表
|
| 147 |
+
return self._generate_chat_history_html(), self._generate_task_list_html()
|
| 148 |
+
|
| 149 |
+
def step2_search_pois(self):
|
| 150 |
+
"""
|
| 151 |
+
Step 2.5: 搜索 POI - 專家團隊開始工作
|
| 152 |
+
Scout Agent 開始工作
|
| 153 |
+
"""
|
| 154 |
+
time_module.sleep(1)
|
| 155 |
+
self._add_reasoning("scout", "Searching for POIs...")
|
| 156 |
+
self._add_reasoning("scout", "Found hospital: NTU Hospital (800m, 4.8★)")
|
| 157 |
+
self._add_reasoning("scout", "Found supermarket: PX Mart (1.2km)")
|
| 158 |
+
|
| 159 |
+
reasoning_html = get_reasoning_html_reversed(self.reasoning_messages)
|
| 160 |
+
|
| 161 |
+
agent_updates = [
|
| 162 |
+
create_agent_card_enhanced("planner", "complete", "Tasks ready"),
|
| 163 |
+
create_agent_card_enhanced("scout", "working", "Searching POIs..."),
|
| 164 |
+
*[create_agent_card_enhanced(k, "idle", "On standby")
|
| 165 |
+
for k in ["optimizer", "validator", "weather", "traffic"]]
|
| 166 |
+
]
|
| 167 |
+
|
| 168 |
+
return reasoning_html, "🗺️ Searching for locations...", *agent_updates
|
| 169 |
+
|
| 170 |
+
def step3_optimize_route(self):
|
| 171 |
+
"""Step 3: 優化路線 - Optimizer 開始工作"""
|
| 172 |
+
time_module.sleep(1)
|
| 173 |
+
self._add_reasoning("optimizer", "Running TSPTW solver...")
|
| 174 |
+
self._add_reasoning("optimizer", "Optimized route: Hospital → Supermarket → Post Office")
|
| 175 |
+
|
| 176 |
+
report = """
|
| 177 |
+
## 🎯 Optimization Complete
|
| 178 |
+
|
| 179 |
+
### Route Details:
|
| 180 |
+
1. **🏥 Hospital** (09:00 - 10:00)
|
| 181 |
+
2. **🛒 Supermarket** (10:15 - 10:45)
|
| 182 |
+
3. **📮 Post Office** (11:05 - 11:25)
|
| 183 |
+
|
| 184 |
+
### Metrics:
|
| 185 |
+
- ✅ Total distance: 2.8 km
|
| 186 |
+
- ✅ Total time: 95 minutes
|
| 187 |
+
- ✅ All deadlines met
|
| 188 |
+
- ✅ Minimal travel distance
|
| 189 |
+
- ✅ Weather conditions favorable
|
| 190 |
+
"""
|
| 191 |
+
|
| 192 |
+
agent_updates = [
|
| 193 |
+
create_agent_card_enhanced("planner", "complete", "Analysis done"),
|
| 194 |
+
create_agent_card_enhanced("scout", "complete", "POI search done"),
|
| 195 |
+
create_agent_card_enhanced("optimizer", "working", "Optimizing route..."),
|
| 196 |
+
*[create_agent_card_enhanced(k, "idle", "On standby")
|
| 197 |
+
for k in ["validator", "weather", "traffic"]]
|
| 198 |
+
]
|
| 199 |
+
|
| 200 |
+
reasoning_html = get_reasoning_html_reversed(self.reasoning_messages)
|
| 201 |
+
return reasoning_html, report, "🎯 Optimizing route...", *agent_updates
|
| 202 |
+
|
| 203 |
+
def step4_finalize(self):
|
| 204 |
+
"""Step 4: 完成並生成報告(含慶祝動畫和增強視覺化)"""
|
| 205 |
+
time_module.sleep(1)
|
| 206 |
+
self._add_reasoning("validator", "Validating route quality...")
|
| 207 |
+
self._add_reasoning("weather", "Checking weather conditions...")
|
| 208 |
+
self._add_reasoning("traffic", "Analyzing traffic patterns...")
|
| 209 |
+
|
| 210 |
+
self.planning_completed = True
|
| 211 |
+
|
| 212 |
+
# 生成慶祝動畫
|
| 213 |
+
celebration_html = create_celebration_animation()
|
| 214 |
+
|
| 215 |
+
# 生成詳細的結果視覺化
|
| 216 |
+
result_html = create_result_visualization(self.task_list) + celebration_html
|
| 217 |
+
|
| 218 |
+
# 簡化的 timeline 和 metrics(保留兼容性,但現在主要信息在 result_html 中)
|
| 219 |
+
timeline_html = "<h3>🗓️ Detailed Timeline</h3><p>See the complete timeline in the result panel</p>"
|
| 220 |
+
metrics_html = "<h3>📈 Performance Metrics</h3><p>All optimization metrics are displayed above</p>"
|
| 221 |
+
|
| 222 |
+
map_fig = create_animated_map()
|
| 223 |
+
|
| 224 |
+
agent_updates = [
|
| 225 |
+
create_agent_card_enhanced(k, "complete", "Task complete")
|
| 226 |
+
for k in ["planner", "scout", "optimizer", "validator", "weather", "traffic"]
|
| 227 |
+
]
|
| 228 |
+
|
| 229 |
+
return (
|
| 230 |
+
timeline_html, metrics_html, result_html, map_fig,
|
| 231 |
+
gr.update(visible=True), # map_tab
|
| 232 |
+
gr.update(visible=False), # team_area (隱藏,任務完成)
|
| 233 |
+
"🎉 Planning complete! Review your optimized route.",
|
| 234 |
+
*agent_updates
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
def save_settings(self, google_key, weather_key, anthropic_key, model):
|
| 238 |
+
"""保存設定"""
|
| 239 |
+
self.settings['google_maps_api_key'] = google_key
|
| 240 |
+
self.settings['openweather_api_key'] = weather_key
|
| 241 |
+
self.settings['anthropic_api_key'] = anthropic_key
|
| 242 |
+
self.settings['model'] = model
|
| 243 |
+
return "✅ Settings saved successfully!"
|
| 244 |
+
|
| 245 |
+
def _create_stream_html(self, message):
|
| 246 |
+
"""創建串流輸出 HTML"""
|
| 247 |
+
return f"""
|
| 248 |
+
<div class="stream-container">
|
| 249 |
+
<div class="stream-text">{message}<span class="stream-cursor"></span></div>
|
| 250 |
+
</div>
|
| 251 |
+
"""
|
| 252 |
+
|
| 253 |
+
def _add_reasoning(self, agent, message):
|
| 254 |
+
"""添加推理訊息"""
|
| 255 |
+
self.reasoning_messages.append({
|
| 256 |
+
'agent': agent,
|
| 257 |
+
'message': message,
|
| 258 |
+
'time': datetime.now().strftime("%H:%M:%S")
|
| 259 |
+
})
|
| 260 |
+
|
| 261 |
+
def _generate_task_list_html(self):
|
| 262 |
+
"""生成任務列表 HTML"""
|
| 263 |
+
html = ""
|
| 264 |
+
for task in self.task_list:
|
| 265 |
+
html += create_task_card(
|
| 266 |
+
task["id"], task["title"], task["priority"],
|
| 267 |
+
task["time"], task["duration"], task["location"], task["icon"]
|
| 268 |
+
)
|
| 269 |
+
return html
|
| 270 |
+
|
| 271 |
+
def _generate_chat_welcome_html(self):
|
| 272 |
+
"""生成 Chat 歡迎訊息"""
|
| 273 |
+
return """
|
| 274 |
+
<div style="padding: 20px; text-align: center;">
|
| 275 |
+
<h3 style="color: #4A90E2; margin-bottom: 10px;">💬 Chat with LifeFlow</h3>
|
| 276 |
+
<p style="opacity: 0.8;">You can now modify your tasks by chatting with me!</p>
|
| 277 |
+
<p style="opacity: 0.6; font-size: 0.9em;">Try: "Change task 2 to high priority" or "Add a new task"</p>
|
| 278 |
+
</div>
|
| 279 |
+
"""
|
| 280 |
+
|
| 281 |
+
def _generate_chat_history_html(self):
|
| 282 |
+
"""生成聊天歷史 HTML"""
|
| 283 |
+
if not self.chat_history:
|
| 284 |
+
return self._generate_chat_welcome_html()
|
| 285 |
+
|
| 286 |
+
html = '<div class="chat-history-container" style="max-height: 400px; overflow-y: auto;">'
|
| 287 |
+
|
| 288 |
+
for msg in self.chat_history:
|
| 289 |
+
role = msg["role"]
|
| 290 |
+
message = msg["message"]
|
| 291 |
+
time = msg["time"]
|
| 292 |
+
|
| 293 |
+
if role == "user":
|
| 294 |
+
html += f'''
|
| 295 |
+
<div style="margin-bottom: 15px; text-align: right;">
|
| 296 |
+
<div style="display: inline-block; max-width: 70%; background: #E3F2FD; padding: 10px 15px; border-radius: 15px 15px 0 15px;">
|
| 297 |
+
<div style="font-size: 0.9em; font-weight: 500;">{message}</div>
|
| 298 |
+
<div style="font-size: 0.75em; opacity: 0.6; margin-top: 5px;">{time}</div>
|
| 299 |
+
</div>
|
| 300 |
+
</div>
|
| 301 |
+
'''
|
| 302 |
+
else:
|
| 303 |
+
html += f'''
|
| 304 |
+
<div style="margin-bottom: 15px; text-align: left;">
|
| 305 |
+
<div style="display: inline-block; max-width: 70%; background: #F5F5F5; padding: 10px 15px; border-radius: 15px 15px 15px 0;">
|
| 306 |
+
<div style="font-size: 0.85em; color: #4A90E2; font-weight: 600; margin-bottom: 5px;">🤖 LifeFlow AI</div>
|
| 307 |
+
<div style="font-size: 0.9em;">{message}</div>
|
| 308 |
+
<div style="font-size: 0.75em; opacity: 0.6; margin-top: 5px;">{time}</div>
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
'''
|
| 312 |
+
|
| 313 |
+
html += '</div>'
|
| 314 |
+
return html
|
| 315 |
+
|
| 316 |
+
def _empty_step1_outputs(self):
|
| 317 |
+
"""返回空的 Step 1 輸出"""
|
| 318 |
+
return (
|
| 319 |
+
create_agent_stream_output(),
|
| 320 |
+
"", "", get_reasoning_html_reversed(),
|
| 321 |
+
gr.update(visible=False),
|
| 322 |
+
gr.update(visible=False),
|
| 323 |
+
self._generate_chat_welcome_html(),
|
| 324 |
+
"Please enter your tasks",
|
| 325 |
+
*[create_agent_card_enhanced(k, "idle", "On standby")
|
| 326 |
+
for k in ["planner", "scout", "optimizer", "validator", "weather", "traffic"]]
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
def build_interface(self):
|
| 330 |
+
"""構建 Gradio 界面"""
|
| 331 |
+
with gr.Blocks(title=APP_TITLE) as demo:
|
| 332 |
+
# 注入 CSS 樣式
|
| 333 |
+
gr.HTML(get_enhanced_css())
|
| 334 |
+
# Header
|
| 335 |
+
create_header()
|
| 336 |
+
|
| 337 |
+
# Top Controls (Theme, Settings & Doc)
|
| 338 |
+
theme_btn, settings_btn, doc_btn = create_top_controls()
|
| 339 |
+
|
| 340 |
+
# Main Layout
|
| 341 |
+
with gr.Row():
|
| 342 |
+
# ========== Left Column (主操作區) ==========
|
| 343 |
+
with gr.Column(scale=2, min_width=400):
|
| 344 |
+
# Step 1: Input Form
|
| 345 |
+
(input_area, agent_stream_output, user_input, auto_location,
|
| 346 |
+
location_inputs, lat_input, lon_input, analyze_btn) = create_input_form(
|
| 347 |
+
create_agent_stream_output()
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
# Step 2: Confirmation Area (包含 Exit 和 Ready to plan 按鈕)
|
| 351 |
+
(task_confirm_area, task_summary_display,
|
| 352 |
+
task_list_display, exit_btn_inline, ready_plan_btn) = create_confirmation_area()
|
| 353 |
+
|
| 354 |
+
# Step 2.5/3: Team Area (取代 Confirmation Area)
|
| 355 |
+
team_area, agent_displays = create_team_area(create_agent_card_enhanced)
|
| 356 |
+
|
| 357 |
+
# Step 3: Result Area (最終結果展示)
|
| 358 |
+
(result_area, result_display,
|
| 359 |
+
timeline_display, metrics_display) = create_result_area(create_animated_map)
|
| 360 |
+
|
| 361 |
+
# ========== Right Column (狀態 + Tabs) ==========
|
| 362 |
+
with gr.Column(scale=3, min_width=500):
|
| 363 |
+
status_bar = gr.Textbox(
|
| 364 |
+
label="📊 Status",
|
| 365 |
+
value="Waiting for input...",
|
| 366 |
+
interactive=False,
|
| 367 |
+
max_lines=1
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
# Tabs (包含新的 Chat with LifeFlow Tab)
|
| 371 |
+
(tabs, report_tab, map_tab, report_output, map_output, reasoning_output,
|
| 372 |
+
chat_input_area, chat_history_output, chat_input, chat_send) = create_tabs(
|
| 373 |
+
create_animated_map,
|
| 374 |
+
get_reasoning_html_reversed()
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
# Modals
|
| 378 |
+
(settings_modal, google_maps_key, openweather_key, anthropic_key,
|
| 379 |
+
model_choice, close_settings_btn, save_settings_btn,
|
| 380 |
+
settings_status) = create_settings_modal()
|
| 381 |
+
|
| 382 |
+
doc_modal, close_doc_btn = create_doc_modal()
|
| 383 |
+
|
| 384 |
+
# ============= Event Handlers =============
|
| 385 |
+
|
| 386 |
+
# Auto location toggle
|
| 387 |
+
auto_location.change(
|
| 388 |
+
fn=toggle_location_inputs,
|
| 389 |
+
inputs=[auto_location],
|
| 390 |
+
outputs=[location_inputs]
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
# ====== Step 1: Analyze button ======
|
| 394 |
+
analyze_btn.click(
|
| 395 |
+
fn=self.step1_analyze_tasks,
|
| 396 |
+
inputs=[user_input, auto_location, lat_input, lon_input],
|
| 397 |
+
outputs=[
|
| 398 |
+
agent_stream_output, task_summary_display, task_list_display,
|
| 399 |
+
reasoning_output, task_confirm_area, chat_input_area,
|
| 400 |
+
chat_history_output, status_bar, *agent_displays
|
| 401 |
+
]
|
| 402 |
+
).then(
|
| 403 |
+
# Step 1 → Step 2: 隱藏輸入區,顯示確認區 + 開啟 Chat 輸入框
|
| 404 |
+
fn=lambda: (
|
| 405 |
+
gr.update(visible=False), # input_area
|
| 406 |
+
gr.update(visible=True), # task_confirm_area
|
| 407 |
+
gr.update(visible=True) # chat_input_area (開啟聊天功能)
|
| 408 |
+
),
|
| 409 |
+
outputs=[input_area, task_confirm_area, chat_input_area]
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
# ====== Step 2: Chat with LifeFlow (任務修改) ======
|
| 413 |
+
chat_send.click(
|
| 414 |
+
fn=self.modify_task_chat,
|
| 415 |
+
inputs=[chat_input],
|
| 416 |
+
outputs=[chat_history_output, task_list_display]
|
| 417 |
+
).then(
|
| 418 |
+
fn=lambda: "", # 清空輸入框
|
| 419 |
+
outputs=[chat_input]
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
# ====== Step 2: Exit button ======
|
| 423 |
+
exit_btn_inline.click(
|
| 424 |
+
fn=lambda: (
|
| 425 |
+
gr.update(visible=True), # input_area
|
| 426 |
+
gr.update(visible=False), # task_confirm_area
|
| 427 |
+
gr.update(visible=False), # chat_input_area (關閉聊天)
|
| 428 |
+
gr.update(visible=False), # result_area
|
| 429 |
+
gr.update(visible=False), # team_area
|
| 430 |
+
gr.update(visible=False), # report_tab
|
| 431 |
+
gr.update(visible=False), # map_tab
|
| 432 |
+
"", # user_input
|
| 433 |
+
create_agent_stream_output(), # agent_stream_output
|
| 434 |
+
self._generate_chat_welcome_html(), # chat_history_output (重置)
|
| 435 |
+
"Ready to start planning..." # status_bar
|
| 436 |
+
),
|
| 437 |
+
outputs=[
|
| 438 |
+
input_area, task_confirm_area, chat_input_area, result_area,
|
| 439 |
+
team_area, report_tab, map_tab, user_input,
|
| 440 |
+
agent_stream_output, chat_history_output, status_bar
|
| 441 |
+
]
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
# ====== Step 2 → Step 2.5: Ready to Plan button ======
|
| 445 |
+
ready_plan_btn.click(
|
| 446 |
+
# 隱藏確認區和聊天輸入,顯示專家團隊,切換到 AI Conversation Tab
|
| 447 |
+
fn=lambda: (
|
| 448 |
+
gr.update(visible=False), # task_confirm_area
|
| 449 |
+
gr.update(visible=False), # chat_input_area (關閉聊天輸入)
|
| 450 |
+
gr.update(visible=True), # team_area (顯示專家團隊)
|
| 451 |
+
gr.update(selected="ai_conversation_tab") # 切換到 AI Conversation Tab
|
| 452 |
+
),
|
| 453 |
+
outputs=[task_confirm_area, chat_input_area, team_area, tabs]
|
| 454 |
+
).then(
|
| 455 |
+
# Step 2.5: Scout 開始工作
|
| 456 |
+
fn=self.step2_search_pois,
|
| 457 |
+
outputs=[reasoning_output, status_bar, *agent_displays]
|
| 458 |
+
).then(
|
| 459 |
+
# Step 3: Optimizer 開始工作,切換到 Full Report Tab
|
| 460 |
+
fn=self.step3_optimize_route,
|
| 461 |
+
outputs=[reasoning_output, report_output, status_bar, *agent_displays]
|
| 462 |
+
).then(
|
| 463 |
+
# 顯示 Report 和 Map Tabs,並切換到 Full Report
|
| 464 |
+
fn=lambda: (
|
| 465 |
+
gr.update(visible=True), # report_tab
|
| 466 |
+
gr.update(visible=True), # map_tab
|
| 467 |
+
gr.update(selected="report_tab") # 切換到 Full Report Tab
|
| 468 |
+
),
|
| 469 |
+
outputs=[report_tab, map_tab, tabs]
|
| 470 |
+
).then(
|
| 471 |
+
# Step 4: 完成規劃
|
| 472 |
+
fn=self.step4_finalize,
|
| 473 |
+
outputs=[
|
| 474 |
+
timeline_display, metrics_display, result_display,
|
| 475 |
+
map_output, map_tab, team_area, status_bar, *agent_displays
|
| 476 |
+
]
|
| 477 |
+
).then(
|
| 478 |
+
fn=lambda: gr.update(visible=True),
|
| 479 |
+
outputs=[result_area]
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
+
# ====== Settings ======
|
| 483 |
+
settings_btn.click(
|
| 484 |
+
fn=lambda: gr.update(visible=True),
|
| 485 |
+
outputs=[settings_modal]
|
| 486 |
+
)
|
| 487 |
+
close_settings_btn.click(
|
| 488 |
+
fn=lambda: gr.update(visible=False),
|
| 489 |
+
outputs=[settings_modal]
|
| 490 |
+
)
|
| 491 |
+
save_settings_btn.click(
|
| 492 |
+
fn=self.save_settings,
|
| 493 |
+
inputs=[google_maps_key, openweather_key, anthropic_key, model_choice],
|
| 494 |
+
outputs=[settings_status]
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
# ====== Theme Toggle ======
|
| 498 |
+
theme_btn.click(
|
| 499 |
+
fn=None,
|
| 500 |
+
js="""
|
| 501 |
+
() => {
|
| 502 |
+
const container = document.querySelector('.gradio-container');
|
| 503 |
+
if (container) {
|
| 504 |
+
container.classList.toggle('theme-dark');
|
| 505 |
+
const isDark = container.classList.contains('theme-dark');
|
| 506 |
+
localStorage.setItem('lifeflow-theme', isDark ? 'dark' : 'light');
|
| 507 |
+
console.log('Theme toggled:', isDark ? 'dark' : 'light');
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
"""
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
# ====== Documentation ======
|
| 514 |
+
doc_btn.click(
|
| 515 |
+
fn=lambda: gr.update(visible=True),
|
| 516 |
+
outputs=[doc_modal]
|
| 517 |
+
)
|
| 518 |
+
close_doc_btn.click(
|
| 519 |
+
fn=lambda: gr.update(visible=False),
|
| 520 |
+
outputs=[doc_modal]
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
return demo
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
def main():
|
| 527 |
+
app = LifeFlowAI()
|
| 528 |
+
demo = app.build_interface()
|
| 529 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, share=False, show_error=True)
|
| 530 |
+
|
| 531 |
+
if __name__ == "__main__":
|
| 532 |
+
main()
|