Marco310 commited on
Commit
3b3daa9
·
1 Parent(s): b7d08cf

buildup gradio app

Browse files
app.py CHANGED
@@ -1,19 +1,21 @@
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
@@ -21,7 +23,7 @@ from ui.components.confirmation import create_confirmation_area, create_exit_but
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,
@@ -29,496 +31,1006 @@ from core.utils import (
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
 
@@ -526,7 +1038,8 @@ class LifeFlowAI:
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()
 
1
  """
2
+ LifeFlow AI - Multi-User Safe with Real Streaming
3
+ 支持多用戶並發 + 真正的串流輸出 + 安全的設定隔離
4
  """
5
 
6
  import sys
7
  from pathlib import Path
 
 
8
  import gradio as gr
9
  from datetime import datetime
10
  import time as time_module
11
+ import json
12
+ import uuid
13
+ from typing import Dict, Any, Optional
14
 
15
+ # ===== 導入配置 =====
16
+ from config import DEFAULT_SETTINGS, APP_TITLE
17
 
18
+ # ===== 導入 UI 組件 =====
19
  from ui.theme import get_enhanced_css
20
  from ui.components.header import create_header, create_top_controls
21
  from ui.components.input_form import create_input_form, toggle_location_inputs
 
23
  from ui.components.results import create_team_area, create_result_area, create_tabs
24
  from ui.components.modals import create_settings_modal, create_doc_modal
25
 
26
+ # ===== 導入核心工具 =====
27
  from core.utils import (
28
  create_agent_stream_output, create_agent_card_enhanced,
29
  create_task_card, create_summary_card, create_animated_map,
 
31
  create_result_visualization
32
  )
33
 
34
+ # ===== 導入 Agno Agent 組件 =====
35
+ from agno.models.google import Gemini
36
+ from agno.agent import RunEvent
37
+ from agno.run.team import TeamRunEvent
38
+
39
+ # ===== 導入 Agent 系統 =====
40
+ from src.agent.base import UserState, Location, get_context
41
+ from src.agent.planner import create_planner_agent
42
+ from src.agent.core_team import create_core_team
43
+ from src.infra.context import set_session_id, get_session_id
44
+ from src.infra.poi_repository import poi_repo
45
+ from src.tools import (
46
+ ScoutToolkit, OptimizationToolkit,
47
+ NavigationToolkit, WeatherToolkit, ReaderToolkit
48
+ )
49
+ from src.infra.config import get_settings
50
+ from src.infra.logger import get_logger
51
+
52
+ logger = get_logger(__name__)
53
+
54
+
55
+ class UserSession:
56
+ """
57
+ 單個用戶的會話數據
58
+ 每個用戶有獨立的實例,確保資料隔離
59
+ """
60
+
61
+ def __init__(self):
62
+ self.session_id: Optional[str] = None
63
+ self.planner_agent = None
64
+ self.core_team = None
65
+ self.user_state: Optional[UserState] = None
66
+ self.task_list: list = []
67
+ self.reasoning_messages: list = []
68
+ self.chat_history: list = []
69
+ self.planning_completed: bool = False
70
+
71
+ # 存儲位置信息(用於重新初始化 Agent)
72
+ self.lat: Optional[float] = None
73
+ self.lng: Optional[float] = None
74
+
75
+ # [Security Fix] 用戶個人的自定義設定 (API Keys, Model Choice)
76
+ self.custom_settings: Dict[str, Any] = {}
77
+
78
+ # 系統預設設定 (從環境變數讀取)
79
+ self.agno_settings = get_settings()
80
+
81
+ def to_dict(self) -> Dict[str, Any]:
82
+ """序列化為字典(用於 Gradio State)"""
83
+ return {
84
+ 'session_id': self.session_id,
85
+ 'task_list': self.task_list,
86
+ 'reasoning_messages': self.reasoning_messages,
87
+ 'chat_history': self.chat_history,
88
+ 'planning_completed': self.planning_completed,
89
+ 'lat': self.lat,
90
+ 'lng': self.lng,
91
+ # [Security Fix] 保存用戶自定義設定
92
+ 'custom_settings': self.custom_settings
93
+ }
94
+
95
+ @classmethod
96
+ def from_dict(cls, data: Dict[str, Any]) -> 'UserSession':
97
+ """從字典恢復(用於 Gradio State)"""
98
+ session = cls()
99
+ session.session_id = data.get('session_id')
100
+ session.task_list = data.get('task_list', [])
101
+ session.reasoning_messages = data.get('reasoning_messages', [])
102
+ session.chat_history = data.get('chat_history', [])
103
+ session.planning_completed = data.get('planning_completed', False)
104
+ session.lat = data.get('lat')
105
+ session.lng = data.get('lng')
106
+ # [Security Fix] 恢復用戶自定義設定
107
+ session.custom_settings = data.get('custom_settings', {})
108
+ return session
109
+
110
 
111
  class LifeFlowAI:
112
+ """LifeFlow AI - Multi-User Safe Version"""
113
 
114
  def __init__(self):
115
+ # [Security Fix] 移除全域 settings,避免用戶間資料汙染
116
+ pass
117
+
118
+ def _initialize_agents(self, session: UserSession, lat: float, lng: float):
119
+ """初始化 Agents(每個用戶獨立,並應用用戶設定)"""
120
+ session.lat = lat
121
+ session.lng = lng
122
+
123
+ if session.planner_agent is not None:
124
+ logger.debug(f"Agents already initialized for session {session.session_id}")
125
+ return
126
+
127
+ # 生成 Session ID
128
+ if session.session_id is None:
129
+ session.session_id = str(uuid.uuid4())
130
+ token = set_session_id(session.session_id)
131
+ logger.info(f"🆔 New Session: {session.session_id}")
132
+ else:
133
+ set_session_id(session.session_id)
134
+ logger.info(f"🔄 Restoring Session: {session.session_id}")
135
+
136
+ # 設定用戶狀態
137
+ session.user_state = UserState(location=Location(lat=lat, lng=lng))
138
+
139
+ # [Security Fix] 讀取用戶選擇的模型,如果沒有則使用預設
140
+ selected_model_id = session.custom_settings.get('model', 'gemini-2.5-flash')
141
+
142
+ # [Security Fix] 優先使用用戶提供的 API Key (這裡以 Gemini 為例,若支援其他模型需擴充邏輯)
143
+ # 注意:實際應用中需根據選擇的 Model ID (Claude/Gemini) 來決定使用哪個 Key
144
+ gemini_key = session.custom_settings.get('gemini_api_key') or session.agno_settings.gemini_api_key
145
+
146
+ # 初始化模型 (應用設定)
147
+ planner_model = Gemini(
148
+ id=selected_model_id,
149
+ thinking_budget=2048,
150
+ api_key=gemini_key
151
+ )
152
 
153
+ main_model = Gemini(
154
+ id=selected_model_id,
155
+ thinking_budget=1024,
156
+ api_key=gemini_key
157
+ )
 
 
158
 
159
+ lite_model = Gemini(
160
+ id="gemini-2.5-flash-lite", # 輕量級模型通常固定或由次要選項決定
161
+ api_key=gemini_key
 
 
 
 
 
 
 
 
162
  )
163
 
164
+ # 配置模型和工具
165
+ models_dict = {
166
+ "team": main_model,
167
+ "scout": main_model,
168
+ "optimizer": lite_model,
169
+ "navigator": lite_model,
170
+ "weatherman": lite_model,
171
+ "presenter": main_model,
172
+ }
173
+
174
+ # [Note] 如果 Toolkit 支援傳入 API Key,應在此處從 session.custom_settings 傳入
175
+ tools_dict = {
176
+ "scout": [ScoutToolkit()],
177
+ "optimizer": [OptimizationToolkit()],
178
+ "navigator": [NavigationToolkit()],
179
+ "weatherman": [WeatherToolkit()],
180
+ "presenter": [ReaderToolkit()],
181
+ }
182
+
183
+ planner_kwargs = {
184
+ "additional_context": get_context(session.user_state),
185
+ "timezone_identifier": session.user_state.utc_offset,
186
+ "debug_mode": False,
187
+ }
188
+
189
+ team_kwargs = {
190
+ "timezone_identifier": session.user_state.utc_offset,
191
+ }
192
+
193
+ # 創建 Agents
194
+ session.planner_agent = create_planner_agent(
195
+ planner_model,
196
+ planner_kwargs,
197
+ session_id=session.session_id
198
+ )
199
 
200
+ session.core_team = create_core_team(
201
+ models_dict,
202
+ team_kwargs,
203
+ tools_dict,
204
+ session_id=session.session_id
 
 
 
 
 
 
205
  )
206
 
207
+ logger.info(f"✅ Agents initialized for session {session.session_id} using model {selected_model_id}")
208
 
209
+ def step1_analyze_tasks(self, user_input: str, auto_location: bool,
210
+ lat: float, lon: float, session: UserSession):
211
+ """Step 1: 真正的串流分析"""
212
+ if not user_input.strip():
213
+ yield from self._empty_step1_outputs(session)
214
+ return
 
 
 
215
 
216
+ if auto_location:
217
+ lat, lon = 25.033, 121.565
 
218
 
219
+ try:
220
+ self._initialize_agents(session, lat, lon)
 
221
 
222
+ self._add_reasoning(session, "planner", "🚀 Starting analysis...")
223
+ yield self._create_step1_outputs(
224
+ stream_text="🤔 Analyzing your request with AI...",
225
+ session=session,
226
+ agent_status=("planner", "working", "Initializing...")
227
+ )
228
 
229
+ time_module.sleep(0.3)
 
230
 
231
+ self._add_reasoning(session, "planner", f"Processing: {user_input[:50]}...")
232
+ yield self._create_step1_outputs(
233
+ stream_text="🤔 Analyzing your request with AI...\n📋 AI is extracting tasks...",
234
+ session=session,
235
+ agent_status=("planner", "working", "Extracting tasks...")
236
+ )
 
 
 
 
 
 
237
 
238
+ planner_stream = session.planner_agent.run(
239
+ f"help user to update the task_list, user's message: {user_input}",
240
+ stream=True,
241
+ stream_events=True
242
+ )
243
+
244
+ accumulated_response = ""
245
+ displayed_text = "🤔 Analyzing your request with AI...\n📋 AI is extracting tasks...\n\n"
246
+ show_content = True
247
+
248
+ for chunk in planner_stream:
249
+ if chunk.event == RunEvent.run_content:
250
+ content = chunk.content
251
+ accumulated_response += content
252
+
253
+ if show_content:
254
+ if "@@@" in accumulated_response:
255
+ show_content = False
256
+ remaining = content.split("@@@")[0]
257
+ if remaining:
258
+ displayed_text += remaining
259
+ else:
260
+ displayed_text += content
261
+
262
+ yield self._create_step1_outputs(
263
+ stream_text=displayed_text,
264
+ session=session,
265
+ agent_status=("planner", "working", "Processing...")
266
+ )
267
+
268
+ json_data = "{" + accumulated_response.split("{", maxsplit=1)[-1]
269
+ json_data = json_data.replace("`", "").replace("@", "").replace("\\", " ").replace("\n", " ")
270
+
271
+ session.planner_agent.update_session_state(
272
+ session_id=session.session_id,
273
+ session_state_updates={"task_list": json_data}
274
+ )
275
+
276
+ try:
277
+ task_list_data = json.loads(json_data)
278
+ session.task_list = self._convert_task_list_to_ui_format(task_list_data)
279
+ except json.JSONDecodeError as e:
280
+ logger.error(f"Failed to parse task_list: {e}")
281
+ session.task_list = []
282
+
283
+ self._add_reasoning(session, "planner", f"✅ Extracted {len(session.task_list)} tasks")
284
+
285
+ high_priority = sum(1 for t in session.task_list if t.get("priority") == "HIGH")
286
+ total_time = sum(
287
+ int(t.get("duration", "0").split()[0])
288
+ for t in session.task_list
289
+ if t.get("duration")
290
+ )
291
+
292
+ final_text = displayed_text + f"\n✅ Analysis complete! Found {len(session.task_list)} tasks."
293
+
294
+ yield self._create_step1_complete_outputs(
295
+ stream_text=final_text,
296
+ session=session,
297
+ high_priority=high_priority,
298
+ total_time=total_time
299
+ )
300
+
301
+ except Exception as e:
302
+ logger.error(f"Error in step1: {e}", exc_info=True)
303
+ yield self._create_error_outputs(str(e), session)
304
+
305
+ def modify_task_chat(self, user_message: str, session: UserSession):
306
+ """修改任務(帶真正的串流)"""
307
  if not user_message.strip():
308
+ chat_html = self._generate_chat_history_html(session)
309
+ task_html = self._generate_task_list_html(session)
310
+ yield chat_html, task_html, session.to_dict()
311
+ return
312
 
313
+ session.chat_history.append({
 
314
  "role": "user",
315
  "message": user_message,
316
  "time": datetime.now().strftime("%H:%M:%S")
317
  })
318
 
319
+ yield (
320
+ self._generate_chat_history_html(session),
321
+ self._generate_task_list_html(session),
322
+ session.to_dict()
323
+ )
324
 
325
+ try:
326
+ if session.planner_agent is None:
327
+ if session.lat is not None and session.lng is not None:
328
+ session.chat_history.append({
329
+ "role": "assistant",
330
+ "message": "🔄 Restoring AI system...",
331
+ "time": datetime.now().strftime("%H:%M:%S")
332
+ })
333
+ yield (
334
+ self._generate_chat_history_html(session),
335
+ self._generate_task_list_html(session),
336
+ session.to_dict()
337
+ )
338
+ self._initialize_agents(session, session.lat, session.lng)
339
+ session.chat_history.pop()
340
+ else:
341
+ session.chat_history.append({
342
+ "role": "assistant",
343
+ "message": "❌ Error: Please restart the planning process.",
344
+ "time": datetime.now().strftime("%H:%M:%S")
345
+ })
346
+ yield (
347
+ self._generate_chat_history_html(session),
348
+ self._generate_task_list_html(session),
349
+ session.to_dict()
350
+ )
351
+ return
352
+
353
+ session.chat_history.append({
354
+ "role": "assistant",
355
+ "message": "🤔 AI is thinking...",
356
+ "time": datetime.now().strftime("%H:%M:%S")
357
+ })
358
+ yield (
359
+ self._generate_chat_history_html(session),
360
+ self._generate_task_list_html(session),
361
+ session.to_dict()
362
+ )
363
 
364
+ planner_stream = session.planner_agent.run(
365
+ f"help user to update the task_list, user's message: {user_message}",
366
+ stream=True,
367
+ stream_events=True
368
+ )
369
 
370
+ accumulated_response = ""
371
+ displayed_thinking = "🤔 AI is thinking...\n\n"
372
+ show_content = True
373
+
374
+ for chunk in planner_stream:
375
+ if chunk.event == RunEvent.run_content:
376
+ content = chunk.content
377
+ accumulated_response += content
378
+
379
+ if show_content:
380
+ if "@@@" in accumulated_response:
381
+ show_content = False
382
+ content = content.split("@@@")[0]
383
+
384
+ if content:
385
+ displayed_thinking += content
386
+ session.chat_history[-1] = {
387
+ "role": "assistant",
388
+ "message": displayed_thinking,
389
+ "time": datetime.now().strftime("%H:%M:%S")
390
+ }
391
+ yield (
392
+ self._generate_chat_history_html(session),
393
+ self._generate_task_list_html(session),
394
+ session.to_dict()
395
+ )
396
+
397
+ json_data = "{" + accumulated_response.split("{", maxsplit=1)[-1]
398
+ json_data = json_data.replace("`", "").replace("@", "").replace("\\", " ").replace("\n", " ")
399
+
400
+ session.planner_agent.update_session_state(
401
+ session_id=session.session_id,
402
+ session_state_updates={"task_list": json_data}
403
+ )
404
 
405
+ task_list_data = json.loads(json_data)
406
+ session.task_list = self._convert_task_list_to_ui_format(task_list_data)
407
+
408
+ session.chat_history[-1] = {
409
+ "role": "assistant",
410
+ "message": "✅ Tasks updated based on your request",
411
+ "time": datetime.now().strftime("%H:%M:%S")
412
+ }
413
+
414
+ self._add_reasoning(session, "planner", f"Updated: {user_message[:30]}...")
415
+
416
+ yield (
417
+ self._generate_chat_history_html(session),
418
+ self._generate_task_list_html(session),
419
+ session.to_dict()
420
+ )
421
+
422
+ except Exception as e:
423
+ logger.error(f"Error in modify_task_chat: {e}", exc_info=True)
424
+ if session.chat_history and "thinking" in session.chat_history[-1].get("message", "").lower():
425
+ session.chat_history.pop()
426
+ session.chat_history.append({
427
+ "role": "assistant",
428
+ "message": f"❌ Error: {str(e)}",
429
+ "time": datetime.now().strftime("%H:%M:%S")
430
+ })
431
+ yield (
432
+ self._generate_chat_history_html(session),
433
+ self._generate_task_list_html(session),
434
+ session.to_dict()
435
+ )
436
 
437
+ def step2_search_pois(self, session: UserSession):
438
+ """Step 2: Scout 開始工作"""
439
+ self._add_reasoning(session, "team", "🚀 Core Team activated")
440
+ self._add_reasoning(session, "scout", "Searching for POIs...")
441
 
442
  agent_updates = [
443
  create_agent_card_enhanced("planner", "complete", "Tasks ready"),
444
+ create_agent_card_enhanced("scout", "working", "Searching..."),
445
  *[create_agent_card_enhanced(k, "idle", "On standby")
446
  for k in ["optimizer", "validator", "weather", "traffic"]]
447
  ]
448
 
449
+ return (
450
+ get_reasoning_html_reversed(session.reasoning_messages),
451
+ "🗺️ Scout is searching...",
452
+ *agent_updates,
453
+ session.to_dict()
454
+ )
455
 
456
+ def step3_run_core_team(self, session: UserSession):
457
+ """Step 3: 運行 Core Team(帶串流)"""
458
+ try:
459
+ if session.core_team is None or session.planner_agent is None:
460
+ if session.lat is not None and session.lng is not None:
461
+ self._initialize_agents(session, session.lat, session.lng)
462
+ else:
463
+ raise ValueError("Cannot restore agents: location not available")
464
 
465
+ set_session_id(session.session_id)
466
+ task_list_input = session.planner_agent.get_session_state().get("task_list")
467
 
468
+ if not task_list_input:
469
+ raise ValueError("No task list available")
 
 
470
 
471
+ if isinstance(task_list_input, str):
472
+ task_list_str = task_list_input
473
+ else:
474
+ task_list_str = json.dumps(task_list_input, indent=2, ensure_ascii=False)
 
 
 
475
 
476
+ self._add_reasoning(session, "team", "🎯 Multi-agent collaboration started")
 
 
 
 
 
 
477
 
478
+ team_stream = session.core_team.run(
479
+ f"Plan this trip: {task_list_str}",
480
+ stream=True,
481
+ stream_events=True,
482
+ session_id=session.session_id
483
+ )
484
 
485
+ report_content = ""
486
+
487
+ for event in team_stream:
488
+ if event.event in [TeamRunEvent.run_content]:
489
+ report_content += event.content
490
+ elif event.event == "tool_call":
491
+ tool_name = event.tool_call.get('function', {}).get('name', 'unknown')
492
+ self._add_reasoning(session, "team", f"🔧 Tool: {tool_name}")
493
+ elif event.event == TeamRunEvent.run_completed:
494
+ self._add_reasoning(session, "team", "🎉 Completed")
495
+
496
+ report_html = f"## 🎯 Planning Complete\n\n{report_content}..."
497
+ return report_html, session.to_dict()
498
+
499
+ except Exception as e:
500
+ logger.error(f"Error in step3: {e}")
501
+ return f"## ❌ Error\n\n{str(e)}", session.to_dict()
502
+
503
+ def step4_finalize(self, session: UserSession):
504
+ """Step 4: 完成"""
505
+ try:
506
+ final_ref_id = poi_repo.get_last_id_by_session(session.session_id)
507
+ if not final_ref_id:
508
+ raise ValueError(f"No final result found for session {session.session_id}")
509
+
510
+ structured_data = poi_repo.load(final_ref_id)
511
+ timeline = structured_data.get("timeline", [])
512
+ metrics = structured_data.get("metrics", {})
513
+ traffic_summary = structured_data.get("traffic_summary", {})
514
+
515
+ timeline_html = self._generate_timeline_html(timeline)
516
+ metrics_html = self._generate_metrics_html(metrics, traffic_summary)
517
+
518
+ safe_task_list = session.task_list if session.task_list else []
519
+ result_viz = create_result_visualization(safe_task_list, structured_data)
520
+ map_fig = self._generate_map_from_data(structured_data)
521
+
522
+ agent_updates = [
523
+ create_agent_card_enhanced(k, "complete", "✓ Complete")
524
+ for k in ["planner", "scout", "optimizer", "validator", "weather", "traffic"]
525
+ ]
526
+
527
+ self._add_reasoning(session, "team", "🎉 All completed")
528
+ session.planning_completed = True
529
+
530
+ return (
531
+ timeline_html, metrics_html, result_viz, map_fig,
532
+ gr.update(visible=True), gr.update(visible=False),
533
+ "🎉 Planning completed!",
534
+ *agent_updates,
535
+ session.to_dict()
536
+ )
537
 
538
+ except Exception as e:
539
+ logger.error(f"Error in step4: {e}", exc_info=True)
540
+ error_html = f"<div style='color:red'>Error: {str(e)}</div>"
541
+ default_map = create_animated_map()
542
+ agent_updates = [
543
+ create_agent_card_enhanced(k, "idle", "Waiting...")
544
+ for k in ["planner", "scout", "optimizer", "validator", "weather", "traffic"]
545
+ ]
546
+ return (
547
+ error_html, error_html, error_html, default_map,
548
+ gr.update(visible=True), gr.update(visible=False),
549
+ f"Error: {str(e)}",
550
+ *agent_updates,
551
+ session.to_dict()
552
+ )
553
 
554
+ # ===== 輔助方法 =====
 
555
 
556
+ # [Security Fix] 新增:保存設定到 User Session
557
+ def save_settings(self, google_key, weather_key, anthropic_key, model, session_data):
558
+ """保存設定到用戶 Session"""
559
+ session = UserSession.from_dict(session_data)
560
 
561
+ session.custom_settings['google_maps_api_key'] = google_key
562
+ session.custom_settings['openweather_api_key'] = weather_key
563
+ session.custom_settings['anthropic_api_key'] = anthropic_key
564
+ session.custom_settings['model'] = model
565
 
566
+ return "✅ Settings saved locally!", session.to_dict()
567
 
568
+ def _add_reasoning(self, session: UserSession, agent: str, message: str):
569
+ session.reasoning_messages.append({
570
+ "agent": agent,
571
+ "message": message,
572
+ "time": datetime.now().strftime("%H:%M:%S")
573
+ })
574
 
575
+ def _create_step1_outputs(self, stream_text: str, session: UserSession, agent_status: tuple):
576
+ agent_key, status, message = agent_status
577
  return (
578
+ self._create_stream_html(stream_text), "", "",
579
+ get_reasoning_html_reversed(session.reasoning_messages),
580
+ gr.update(visible=False), gr.update(visible=False), "", message,
581
+ create_agent_card_enhanced(agent_key, status, message),
582
+ create_agent_card_enhanced("scout", "idle", "On standby"),
583
+ create_agent_card_enhanced("optimizer", "idle", "On standby"),
584
+ create_agent_card_enhanced("validator", "idle", "On standby"),
585
+ create_agent_card_enhanced("weather", "idle", "On standby"),
586
+ create_agent_card_enhanced("traffic", "idle", "On standby"),
587
+ session.to_dict()
588
  )
589
 
590
+ def _create_step1_complete_outputs(self, stream_text: str, session: UserSession,
591
+ high_priority: int, total_time: int):
592
+ summary_html = create_summary_card(len(session.task_list), high_priority, total_time)
593
+ task_html = self._generate_task_list_html(session)
594
+ return (
595
+ self._create_stream_html(stream_text), summary_html, task_html,
596
+ get_reasoning_html_reversed(session.reasoning_messages),
597
+ gr.update(visible=True), gr.update(visible=False),
598
+ self._generate_chat_welcome_html(), "✓ Tasks extracted",
599
+ create_agent_card_enhanced("planner", "complete", "Tasks ready"),
600
+ create_agent_card_enhanced("scout", "idle", "On standby"),
601
+ create_agent_card_enhanced("optimizer", "idle", "On standby"),
602
+ create_agent_card_enhanced("validator", "idle", "On standby"),
603
+ create_agent_card_enhanced("weather", "idle", "On standby"),
604
+ create_agent_card_enhanced("traffic", "idle", "On standby"),
605
+ session.to_dict()
606
+ )
607
 
608
+ def _empty_step1_outputs(self, session: UserSession):
609
+ yield (
610
+ create_agent_stream_output(), "", "",
611
+ get_reasoning_html_reversed(),
612
+ gr.update(visible=False), gr.update(visible=False), "",
613
+ "Waiting for input...",
614
+ create_agent_card_enhanced("planner", "idle", "On standby"),
615
+ create_agent_card_enhanced("scout", "idle", "On standby"),
616
+ create_agent_card_enhanced("optimizer", "idle", "On standby"),
617
+ create_agent_card_enhanced("validator", "idle", "On standby"),
618
+ create_agent_card_enhanced("weather", "idle", "On standby"),
619
+ create_agent_card_enhanced("traffic", "idle", "On standby"),
620
+ session.to_dict()
621
+ )
622
 
623
+ def _create_error_outputs(self, error: str, session: UserSession):
624
+ return (
625
+ self._create_stream_html(f"❌ Error: {error}"), "", "",
626
+ get_reasoning_html_reversed(session.reasoning_messages),
627
+ gr.update(visible=False), gr.update(visible=False), "",
628
+ f"Error: {error}",
629
+ create_agent_card_enhanced("planner", "idle", "Error occurred"),
630
+ create_agent_card_enhanced("scout", "idle", "On standby"),
631
+ create_agent_card_enhanced("optimizer", "idle", "On standby"),
632
+ create_agent_card_enhanced("validator", "idle", "On standby"),
633
+ create_agent_card_enhanced("weather", "idle", "On standby"),
634
+ create_agent_card_enhanced("traffic", "idle", "On standby"),
635
+ session.to_dict()
636
+ )
637
 
638
+ def _create_stream_html(self, text: str) -> str:
639
+ return f"""<div class="stream-container"><div class="stream-text">{text}<span class="stream-cursor"></span></div></div>"""
640
+
641
+ def _convert_task_list_to_ui_format(self, task_list_data):
642
+ ui_tasks = []
643
+ if isinstance(task_list_data, dict):
644
+ tasks = task_list_data.get("tasks", [])
645
+ elif isinstance(task_list_data, list):
646
+ tasks = task_list_data
647
+ else:
648
+ return []
649
+
650
+ for i, task in enumerate(tasks, 1):
651
+ ui_task = {
652
+ "id": i,
653
+ "title": task.get("description", "Task"),
654
+ "priority": task.get("priority", "MEDIUM"),
655
+ "time": task.get("time_window", "Anytime"),
656
+ "duration": f"{task.get('duration_minutes', 30)} minutes",
657
+ "location": task.get("location_hint", "To be determined"),
658
+ "icon": self._get_task_icon(task.get("category", "other"))
659
+ }
660
+ ui_tasks.append(ui_task)
661
+ return ui_tasks
662
+
663
+ def _get_task_icon(self, category: str) -> str:
664
+ icons = {
665
+ "medical": "🏥", "shopping": "🛒", "postal": "📮",
666
+ "food": "🍽️", "entertainment": "🎭", "transportation": "🚗",
667
+ "other": "📋"
668
+ }
669
+ return icons.get(category.lower(), "📋")
670
+
671
+ def _generate_task_list_html(self, session: UserSession) -> str:
672
+ if not session.task_list:
673
+ return "<p>No tasks available</p>"
674
  html = ""
675
+ for task in session.task_list:
676
  html += create_task_card(
677
  task["id"], task["title"], task["priority"],
678
+ task["time"], task["duration"], task["location"],
679
+ task.get("icon", "📋")
680
  )
681
  return html
682
 
683
+ def _generate_chat_history_html(self, session: UserSession) -> str:
684
+ if not session.chat_history:
685
+ return self._generate_chat_welcome_html()
686
+ html = '<div class="chat-history">'
687
+ for msg in session.chat_history:
688
+ role_class = "user" if msg["role"] == "user" else "assistant"
689
+ icon = "👤" if msg["role"] == "user" else "🤖"
690
+ html += f"""
691
+ <div class="chat-message {role_class}">
692
+ <span class="chat-icon">{icon}</span>
693
+ <div class="chat-content">
694
+ <div class="chat-text">{msg["message"]}</div>
695
+ <div class="chat-time">{msg["time"]}</div>
696
+ </div>
697
+ </div>
698
+ """
699
+ html += '</div>'
700
+ return html
701
+
702
+ def _generate_chat_welcome_html(self) -> str:
703
  return """
704
+ <div class="chat-welcome">
705
+ <h3>💬 Chat with LifeFlow AI</h3>
706
+ <p>Modify your tasks by chatting here.</p>
 
707
  </div>
708
  """
709
 
710
+ def _generate_timeline_html(self, timeline):
711
+ """
712
+ 生成右側 Timeline Tab 的 HTML
713
+ 使用 structured_data['timeline'] 中的豐富數據
714
+ """
715
+ if not timeline:
716
+ return "<div>No timeline data</div>"
717
+
718
+ html = '<div class="timeline-container">'
719
+ for i, stop in enumerate(timeline):
720
+ time = stop.get("time", "N/A")
721
+ location = stop.get("location", "Unknown Location")
722
+ weather = stop.get("weather", "")
723
+ aqi = stop.get("aqi", {}).get("label", "")
724
+ travel_time = stop.get("travel_time_from_prev", "")
725
+
726
+ # 決定標記樣式
727
+ marker_class = "start" if i == 0 else "stop"
728
+
729
+ html += f"""
730
+ <div class="timeline-item">
731
+ <div class="timeline-marker {marker_class}">{i}</div>
732
+ <div class="timeline-content">
733
+ <div class="timeline-header">
734
+ <span class="time-badge">{time}</span>
735
+ <h4>{location}</h4>
736
  </div>
737
+ <div class="timeline-details">
738
+ {f'<span class="weather-tag">🌤️ {weather}</span>' if weather else ''}
739
+ {f'<span class="aqi-tag">{aqi}</span>' if aqi else ''}
 
 
 
 
 
 
740
  </div>
741
+ {f'<p class="travel-info">🚗 Drive: {travel_time}</p>' if i > 0 else ''}
742
  </div>
743
+ </div>
744
+ """
745
  html += '</div>'
746
+
747
+ # 補充少許 CSS 以確保美觀
748
+ html += """
749
+ <style>
750
+ .timeline-header { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; }
751
+ .time-badge { background: #e3f2fd; color: #1976d2; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 12px; }
752
+ .timeline-details { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 5px; }
753
+ .weather-tag, .aqi-tag { font-size: 11px; padding: 2px 6px; background: #f5f5f5; border-radius: 4px; border: 1px solid #e0e0e0; }
754
+ .travel-info { font-size: 12px; color: #666; margin: 0; display: flex; align-items: center; gap: 5px; }
755
+ </style>
756
+ """
757
  return html
758
 
759
+ def _generate_metrics_html(self, metrics, traffic_summary):
760
+ """生成左側的簡單 Metrics"""
761
+ if traffic_summary is None: traffic_summary = {}
762
+ if metrics is None: metrics = {}
763
+
764
+ total_distance = traffic_summary.get("total_distance_km", 0)
765
+ total_duration = traffic_summary.get("total_duration_min", 0)
766
+ efficiency = metrics.get("route_efficiency_pct", 90)
767
+
768
+ # 根據效率改變顏色
769
+ eff_color = "#50C878" if efficiency >= 80 else "#F5A623"
770
+
771
+ return f"""
772
+ <div class="metrics-container">
773
+ <div class="metric-card">
774
+ <h3>📏 Distance</h3>
775
+ <p class="metric-value">{total_distance:.1f} km</p>
776
+ </div>
777
+ <div class="metric-card">
778
+ <h3>⏱️ Duration</h3>
779
+ <p class="metric-value">{total_duration:.0f} min</p>
780
+ </div>
781
+ <div class="metric-card" style="border-bottom: 3px solid {eff_color}">
782
+ <h3>⚡ Efficiency</h3>
783
+ <p class="metric-value" style="color: {eff_color}">{efficiency:.0f}%</p>
784
+ </div>
785
+ </div>
786
+ """
787
+
788
+ def _generate_map_from_data(self, structured_data):
789
+ """
790
+ 生成 Plotly 地圖,包含真實道路軌跡 (Polyline)
791
+ """
792
+ import plotly.graph_objects as go
793
+ from core.utils import decode_polyline # 確保導入了解碼函數
794
+
795
+ try:
796
+ timeline = structured_data.get("timeline", [])
797
+ precise_result = structured_data.get("precise_traffic_result", {})
798
+ legs = precise_result.get("legs", [])
799
+
800
+ if not timeline:
801
+ return create_animated_map()
802
+
803
+ fig = go.Figure()
804
+
805
+ # 1. 繪製路線 (Polyline) - 藍色路徑
806
+ # 從 legs 中提取 polyline 並解碼
807
+ route_lats = []
808
+ route_lons = []
809
+
810
+ for leg in legs:
811
+ poly_str = leg.get("polyline")
812
+ if poly_str:
813
+ decoded = decode_polyline(poly_str)
814
+ # 解碼後是 list of (lat, lng)
815
+ leg_lats = [coord[0] for coord in decoded]
816
+ leg_lons = [coord[1] for coord in decoded]
817
+
818
+ # 為了讓線段連續,我們可以��其串接 (plotly 支援 nan 分隔,或直接單一 trace)
819
+ # 這裡簡單起見,我們將每個 leg 分開加,或者合併。合併效能較好。
820
+ route_lats.extend(leg_lats)
821
+ route_lons.extend(leg_lons)
822
+
823
+ if route_lats:
824
+ fig.add_trace(go.Scattermapbox(
825
+ lat=route_lats,
826
+ lon=route_lons,
827
+ mode='lines',
828
+ line=dict(width=4, color='#4A90E2'),
829
+ name='Route',
830
+ hoverinfo='none' # 路徑本身不需 hover
831
+ ))
832
+
833
+ # 2. 繪製站點 (Markers)
834
+ lats, lons, hover_texts, colors, sizes = [], [], [], [], []
835
+
836
+ for i, stop in enumerate(timeline):
837
+ coords = stop.get("coordinates", {})
838
+ lat = coords.get("lat")
839
+ lng = coords.get("lng")
840
+
841
+ if lat and lng:
842
+ lats.append(lat)
843
+ lons.append(lng)
844
+
845
+ # 構建 Hover Text
846
+ name = stop.get("location", "Stop")
847
+ time = stop.get("time", "")
848
+ weather = stop.get("weather", "")
849
+ text = f"<b>{i}. {name}</b><br>🕒 {time}<br>🌤️ {weather}"
850
+ hover_texts.append(text)
851
+
852
+ # 樣式:起點綠色,終點紅色,中間藍色
853
+ if i == 0:
854
+ colors.append('#50C878') # Start
855
+ sizes.append(15)
856
+ elif i == len(timeline) - 1:
857
+ colors.append('#FF6B6B') # End
858
+ sizes.append(15)
859
+ else:
860
+ colors.append('#F5A623') # Middle
861
+ sizes.append(12)
862
+
863
+ fig.add_trace(go.Scattermapbox(
864
+ lat=lats, lon=lons,
865
+ mode='markers+text',
866
+ marker=dict(size=sizes, color=colors, allowoverlap=True),
867
+ text=[str(i) for i in range(len(lats))], # 顯示序號
868
+ textposition="top center",
869
+ textfont=dict(size=14, color='black', family="Arial Black"),
870
+ hovertext=hover_texts,
871
+ hoverinfo='text',
872
+ name='Stops'
873
+ ))
874
+
875
+ # 設定地圖中心與縮放
876
+ if lats:
877
+ center_lat = sum(lats) / len(lats)
878
+ center_lon = sum(lons) / len(lons)
879
+ else:
880
+ center_lat, center_lon = 25.033, 121.565
881
+
882
+ fig.update_layout(
883
+ mapbox=dict(
884
+ style='open-street-map', # 或 'carto-positron' 看起來更乾淨
885
+ center=dict(lat=center_lat, lon=center_lon),
886
+ zoom=12
887
+ ),
888
+ margin=dict(l=0, r=0, t=0, b=0),
889
+ height=500,
890
+ showlegend=False
891
+ )
892
+
893
+ return fig
894
+ except Exception as e:
895
+ logger.error(f"Error generating map: {e}", exc_info=True)
896
+ return create_animated_map()
897
 
898
  def build_interface(self):
 
899
  with gr.Blocks(title=APP_TITLE) as demo:
 
900
  gr.HTML(get_enhanced_css())
 
901
  create_header()
902
 
 
903
  theme_btn, settings_btn, doc_btn = create_top_controls()
904
 
905
+ # 🔥 每個 Browser Tab 都有獨立的 UserSession
906
+ session_state = gr.State(value=UserSession().to_dict())
907
+
908
  with gr.Row():
 
909
  with gr.Column(scale=2, min_width=400):
 
910
  (input_area, agent_stream_output, user_input, auto_location,
911
  location_inputs, lat_input, lon_input, analyze_btn) = create_input_form(
912
  create_agent_stream_output()
913
  )
914
 
 
915
  (task_confirm_area, task_summary_display,
916
  task_list_display, exit_btn_inline, ready_plan_btn) = create_confirmation_area()
917
 
 
918
  team_area, agent_displays = create_team_area(create_agent_card_enhanced)
919
+ (result_area, result_display, timeline_display, metrics_display) = create_result_area(
920
+ create_animated_map)
921
 
 
 
 
 
 
922
  with gr.Column(scale=3, min_width=500):
923
+ status_bar = gr.Textbox(label="📊 Status", value="Waiting for input...", interactive=False,
924
+ max_lines=1)
 
 
 
 
 
 
925
  (tabs, report_tab, map_tab, report_output, map_output, reasoning_output,
926
  chat_input_area, chat_history_output, chat_input, chat_send) = create_tabs(
927
+ create_animated_map, get_reasoning_html_reversed()
 
928
  )
929
 
 
930
  (settings_modal, google_maps_key, openweather_key, anthropic_key,
931
  model_choice, close_settings_btn, save_settings_btn,
932
  settings_status) = create_settings_modal()
933
 
934
  doc_modal, close_doc_btn = create_doc_modal()
935
 
936
+ # ===== Event Handlers =====
937
 
938
+ auto_location.change(fn=toggle_location_inputs, inputs=[auto_location], outputs=[location_inputs])
939
+
940
+ def analyze_wrapper(ui, al, lat, lon, s):
941
+ session = UserSession.from_dict(s)
942
+ yield from self.step1_analyze_tasks(ui, al, lat, lon, session)
 
943
 
 
944
  analyze_btn.click(
945
+ fn=analyze_wrapper,
946
+ inputs=[user_input, auto_location, lat_input, lon_input, session_state],
947
  outputs=[
948
  agent_stream_output, task_summary_display, task_list_display,
949
  reasoning_output, task_confirm_area, chat_input_area,
950
+ chat_history_output, status_bar, *agent_displays, session_state
951
  ]
952
  ).then(
953
+ fn=lambda: (gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)),
 
 
 
 
 
954
  outputs=[input_area, task_confirm_area, chat_input_area]
955
  )
956
 
957
+ def chat_wrapper(msg, s):
958
+ session = UserSession.from_dict(s)
959
+ yield from self.modify_task_chat(msg, session)
960
+
961
  chat_send.click(
962
+ fn=chat_wrapper,
963
+ inputs=[chat_input, session_state],
964
+ outputs=[chat_history_output, task_list_display, session_state]
965
+ ).then(fn=lambda: "", outputs=[chat_input])
 
 
 
966
 
 
967
  exit_btn_inline.click(
968
  fn=lambda: (
969
+ gr.update(visible=True), gr.update(visible=False),
970
+ gr.update(visible=False), gr.update(visible=False),
971
+ gr.update(visible=False), gr.update(visible=False),
972
+ gr.update(visible=False), "",
973
+ create_agent_stream_output(),
974
+ self._generate_chat_welcome_html(),
975
+ "Ready...",
976
+ UserSession().to_dict()
 
 
 
977
  ),
978
  outputs=[
979
  input_area, task_confirm_area, chat_input_area, result_area,
980
  team_area, report_tab, map_tab, user_input,
981
+ agent_stream_output, chat_history_output, status_bar, session_state
982
  ]
983
  )
984
 
 
985
  ready_plan_btn.click(
986
+ fn=lambda: (gr.update(visible=False), gr.update(visible=False),
987
+ gr.update(visible=True), gr.update(selected="ai_conversation_tab")),
 
 
 
 
 
988
  outputs=[task_confirm_area, chat_input_area, team_area, tabs]
989
  ).then(
990
+ fn=lambda s: self.step2_search_pois(UserSession.from_dict(s)),
991
+ inputs=[session_state],
992
+ outputs=[reasoning_output, status_bar, *agent_displays, session_state]
993
  ).then(
994
+ fn=lambda s: self.step3_run_core_team(UserSession.from_dict(s)),
995
+ inputs=[session_state],
996
+ outputs=[report_output, session_state]
997
  ).then(
998
+ fn=lambda: (gr.update(visible=True), gr.update(visible=True), gr.update(selected="report_tab")),
 
 
 
 
 
999
  outputs=[report_tab, map_tab, tabs]
1000
  ).then(
1001
+ fn=lambda s: self.step4_finalize(UserSession.from_dict(s)),
1002
+ inputs=[session_state],
1003
  outputs=[
1004
  timeline_display, metrics_display, result_display,
1005
+ map_output, map_tab, team_area, status_bar, *agent_displays, session_state
1006
  ]
1007
+ ).then(fn=lambda: gr.update(visible=True), outputs=[result_area])
 
 
 
1008
 
1009
+ # Settings
1010
+ settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal])
1011
+ close_settings_btn.click(fn=lambda: gr.update(visible=False), outputs=[settings_modal])
1012
+
1013
+ # [Security Fix] 儲存設定到 session_state
 
 
 
 
1014
  save_settings_btn.click(
1015
  fn=self.save_settings,
1016
+ inputs=[google_maps_key, openweather_key, anthropic_key, model_choice, session_state],
1017
+ outputs=[settings_status, session_state]
1018
  )
1019
 
1020
+ # Theme
1021
+ theme_btn.click(fn=None, js="""
 
 
1022
  () => {
1023
  const container = document.querySelector('.gradio-container');
1024
  if (container) {
1025
  container.classList.toggle('theme-dark');
1026
  const isDark = container.classList.contains('theme-dark');
1027
  localStorage.setItem('lifeflow-theme', isDark ? 'dark' : 'light');
 
1028
  }
1029
  }
1030
+ """)
 
1031
 
1032
+ doc_btn.click(fn=lambda: gr.update(visible=True), outputs=[doc_modal])
1033
+ close_doc_btn.click(fn=lambda: gr.update(visible=False), outputs=[doc_modal])
 
 
 
 
 
 
 
1034
 
1035
  return demo
1036
 
 
1038
  def main():
1039
  app = LifeFlowAI()
1040
  demo = app.build_interface()
1041
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True, show_error=True)
1042
+
1043
 
1044
  if __name__ == "__main__":
1045
  main()
config.py CHANGED
@@ -1,58 +1,335 @@
1
  """
2
  LifeFlow AI - Configuration
 
3
  """
4
 
 
 
 
 
5
  APP_TITLE = "LifeFlow AI - Intelligent Daily Trip Planner"
6
 
 
7
  AGENTS_INFO = {
8
  'planner': {
9
  'name': 'Planner',
10
  'role': 'Task Analyzer',
11
  'icon': '📋',
12
- 'color': '#f093fb'
 
 
13
  },
14
  'scout': {
15
  'name': 'Scout',
16
  'role': 'POI Searcher',
17
  'icon': '🗺️',
18
- 'color': '#4facfe'
 
 
19
  },
20
  'optimizer': {
21
  'name': 'Optimizer',
22
  'role': 'Route Optimizer',
23
  'icon': '⚡',
24
- 'color': '#43e97b'
 
 
25
  },
26
  'validator': {
27
  'name': 'Validator',
28
  'role': 'Feasibility Checker',
29
  'icon': '✅',
30
- 'color': '#fa709a'
 
 
31
  },
32
  'weather': {
33
  'name': 'Weather',
34
  'role': 'Weather Advisor',
35
  'icon': '🌤️',
36
- 'color': '#feca57'
 
 
37
  },
38
  'traffic': {
39
  'name': 'Traffic',
40
  'role': 'Traffic Analyzer',
41
  'icon': '🚗',
42
- 'color': '#ff6348'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
  }
45
 
 
46
  DEFAULT_SETTINGS = {
47
- 'google_maps_api_key': '',
48
- 'openweather_api_key': '',
49
- 'anthropic_api_key': '',
50
- 'model': 'claude-sonnet-4-20250514'
 
51
  }
52
 
 
53
  MODEL_CHOICES = [
 
 
54
  'claude-sonnet-4-20250514',
55
  'claude-sonnet-3-5-20241022',
56
  'gpt-4-turbo',
57
- 'gemini-pro'
58
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  LifeFlow AI - Configuration
3
+ 整合 Agno Agent 系統配置
4
  """
5
 
6
+ import os
7
+ from pathlib import Path
8
+
9
+ # ===== 應用配置 =====
10
  APP_TITLE = "LifeFlow AI - Intelligent Daily Trip Planner"
11
 
12
+ # ===== Agent 資訊配置 (前端顯示用) =====
13
  AGENTS_INFO = {
14
  'planner': {
15
  'name': 'Planner',
16
  'role': 'Task Analyzer',
17
  'icon': '📋',
18
+ 'avatar': '📋',
19
+ 'color': '#f093fb',
20
+ 'glow': 'rgba(240, 147, 251, 0.3)'
21
  },
22
  'scout': {
23
  'name': 'Scout',
24
  'role': 'POI Searcher',
25
  'icon': '🗺️',
26
+ 'avatar': '🗺️',
27
+ 'color': '#4facfe',
28
+ 'glow': 'rgba(79, 172, 254, 0.3)'
29
  },
30
  'optimizer': {
31
  'name': 'Optimizer',
32
  'role': 'Route Optimizer',
33
  'icon': '⚡',
34
+ 'avatar': '',
35
+ 'color': '#43e97b',
36
+ 'glow': 'rgba(67, 233, 123, 0.3)'
37
  },
38
  'validator': {
39
  'name': 'Validator',
40
  'role': 'Feasibility Checker',
41
  'icon': '✅',
42
+ 'avatar': '',
43
+ 'color': '#fa709a',
44
+ 'glow': 'rgba(250, 112, 154, 0.3)'
45
  },
46
  'weather': {
47
  'name': 'Weather',
48
  'role': 'Weather Advisor',
49
  'icon': '🌤️',
50
+ 'avatar': '🌤️',
51
+ 'color': '#feca57',
52
+ 'glow': 'rgba(254, 202, 87, 0.3)'
53
  },
54
  'traffic': {
55
  'name': 'Traffic',
56
  'role': 'Traffic Analyzer',
57
  'icon': '🚗',
58
+ 'avatar': '🚗',
59
+ 'color': '#ff6348',
60
+ 'glow': 'rgba(255, 99, 72, 0.3)'
61
+ },
62
+ 'navigator': {
63
+ 'name': 'Navigator',
64
+ 'role': 'Route Navigator',
65
+ 'icon': '🧭',
66
+ 'avatar': '🧭',
67
+ 'color': '#5f27cd',
68
+ 'glow': 'rgba(95, 39, 205, 0.3)'
69
+ },
70
+ 'weatherman': {
71
+ 'name': 'Weatherman',
72
+ 'role': 'Weather Forecaster',
73
+ 'icon': '⛅',
74
+ 'avatar': '⛅',
75
+ 'color': '#00d2d3',
76
+ 'glow': 'rgba(0, 210, 211, 0.3)'
77
+ },
78
+ 'presenter': {
79
+ 'name': 'Presenter',
80
+ 'role': 'Report Generator',
81
+ 'icon': '📊',
82
+ 'avatar': '📊',
83
+ 'color': '#ee5a6f',
84
+ 'glow': 'rgba(238, 90, 111, 0.3)'
85
+ },
86
+ 'team': {
87
+ 'name': 'Team Leader',
88
+ 'role': 'Orchestrator',
89
+ 'icon': '👨‍✈️',
90
+ 'avatar': '👨‍✈️',
91
+ 'color': '#0abde3',
92
+ 'glow': 'rgba(10, 189, 227, 0.3)'
93
  }
94
  }
95
 
96
+ # ===== API Keys (從環境變數讀取) =====
97
  DEFAULT_SETTINGS = {
98
+ 'google_maps_api_key': os.getenv('GOOGLE_MAPS_API_KEY', ''),
99
+ 'openweather_api_key': os.getenv('OPENWEATHER_API_KEY', ''),
100
+ 'anthropic_api_key': os.getenv('ANTHROPIC_API_KEY', ''),
101
+ 'gemini_api_key': os.getenv('GEMINI_API_KEY', ''),
102
+ 'model': 'gemini-2.5-flash'
103
  }
104
 
105
+ # ===== LLM 模型選擇 =====
106
  MODEL_CHOICES = [
107
+ 'gemini-2.5-flash',
108
+ 'gemini-2.5-flash-lite',
109
  'claude-sonnet-4-20250514',
110
  'claude-sonnet-3-5-20241022',
111
  'gpt-4-turbo',
112
+ 'gpt-4o'
113
+ ]
114
+
115
+ # ===== Agno Agent 配置 =====
116
+ AGENT_CONFIG = {
117
+ # Planner Agent 配置
118
+ 'planner': {
119
+ 'model': 'gemini-2.5-flash',
120
+ 'thinking_budget': 2048,
121
+ 'temperature': 0.7,
122
+ 'debug_mode': False
123
+ },
124
+
125
+ # Core Team 配置
126
+ 'team': {
127
+ 'model': 'gemini-2.5-flash',
128
+ 'thinking_budget': 1024,
129
+ 'temperature': 0.7,
130
+ 'debug_mode': False,
131
+ 'debug_level': 1
132
+ },
133
+
134
+ # Member Agents 配置
135
+ 'scout': {
136
+ 'model': 'gemini-2.5-flash',
137
+ 'temperature': 0.5
138
+ },
139
+
140
+ 'optimizer': {
141
+ 'model': 'gemini-2.5-flash-lite',
142
+ 'temperature': 0.3
143
+ },
144
+
145
+ 'navigator': {
146
+ 'model': 'gemini-2.5-flash-lite',
147
+ 'temperature': 0.3
148
+ },
149
+
150
+ 'weatherman': {
151
+ 'model': 'gemini-2.5-flash-lite',
152
+ 'temperature': 0.5
153
+ },
154
+
155
+ 'presenter': {
156
+ 'model': 'gemini-2.5-flash',
157
+ 'temperature': 0.7
158
+ }
159
+ }
160
+
161
+ # ===== 資料庫配置 =====
162
+ DATABASE_CONFIG = {
163
+ 'planner_db': 'tmp/planner.db',
164
+ 'team_db': 'tmp/team.db',
165
+ 'poi_db': 'storage/lifeflow_payloads.db',
166
+ 'offload_db': 'storage/offloaded_data.db'
167
+ }
168
+
169
+ # ===== 路徑配置 =====
170
+ BASE_DIR = Path(__file__).parent
171
+ STORAGE_DIR = BASE_DIR / 'storage'
172
+ TMP_DIR = BASE_DIR / 'tmp'
173
+ LOGS_DIR = BASE_DIR / 'logs'
174
+
175
+ # 確保目錄存在
176
+ for dir_path in [STORAGE_DIR, TMP_DIR, LOGS_DIR]:
177
+ dir_path.mkdir(parents=True, exist_ok=True)
178
+
179
+ # ===== 日誌配置 =====
180
+ LOGGING_CONFIG = {
181
+ 'version': 1,
182
+ 'disable_existing_loggers': False,
183
+ 'formatters': {
184
+ 'default': {
185
+ 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
186
+ },
187
+ 'detailed': {
188
+ 'format': '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
189
+ }
190
+ },
191
+ 'handlers': {
192
+ 'console': {
193
+ 'class': 'logging.StreamHandler',
194
+ 'level': 'INFO',
195
+ 'formatter': 'default',
196
+ 'stream': 'ext://sys.stdout'
197
+ },
198
+ 'file': {
199
+ 'class': 'logging.handlers.RotatingFileHandler',
200
+ 'level': 'DEBUG',
201
+ 'formatter': 'detailed',
202
+ 'filename': str(LOGS_DIR / 'lifeflow.log'),
203
+ 'maxBytes': 10485760, # 10MB
204
+ 'backupCount': 5
205
+ }
206
+ },
207
+ 'root': {
208
+ 'level': 'INFO',
209
+ 'handlers': ['console', 'file']
210
+ }
211
+ }
212
+
213
+ # ===== Gradio UI 配置 =====
214
+ GRADIO_CONFIG = {
215
+ 'server_name': '0.0.0.0',
216
+ 'server_port': 7860,
217
+ 'share': False,
218
+ 'show_error': True,
219
+ 'quiet': False
220
+ }
221
+
222
+ # ===== 預設位置 (台北101) =====
223
+ DEFAULT_LOCATION = {
224
+ 'lat': 25.033,
225
+ 'lng': 121.565,
226
+ 'name': 'Taipei 101'
227
+ }
228
+
229
+ # ===== Tool 配置 =====
230
+ TOOL_CONFIG = {
231
+ 'scout': {
232
+ 'search_radius': 5000, # 米
233
+ 'max_results': 10,
234
+ 'min_rating': 3.5
235
+ },
236
+ 'optimizer': {
237
+ 'algorithm': 'ortools_tsptw',
238
+ 'max_iterations': 1000,
239
+ 'time_limit_seconds': 30
240
+ },
241
+ 'navigator': {
242
+ 'travel_mode': 'driving',
243
+ 'avoid_highways': False,
244
+ 'avoid_tolls': False
245
+ },
246
+ 'weather': {
247
+ 'forecast_hours': 24,
248
+ 'update_interval_minutes': 60
249
+ }
250
+ }
251
+
252
+ # ===== 功能開關 =====
253
+ FEATURE_FLAGS = {
254
+ 'enable_weather': True,
255
+ 'enable_traffic': True,
256
+ 'enable_chat': True,
257
+ 'enable_map_visualization': True,
258
+ 'enable_reasoning_display': True,
259
+ 'enable_celebration_animation': True,
260
+ 'enable_stream_output': True
261
+ }
262
+
263
+ # ===== 性能配置 =====
264
+ PERFORMANCE_CONFIG = {
265
+ 'max_concurrent_requests': 10,
266
+ 'request_timeout_seconds': 120,
267
+ 'cache_ttl_seconds': 3600,
268
+ 'enable_caching': True
269
+ }
270
+
271
+ # ===== 安全配置 =====
272
+ SECURITY_CONFIG = {
273
+ 'max_input_length': 1000,
274
+ 'max_tasks_per_request': 20,
275
+ 'rate_limit_per_minute': 60,
276
+ 'enable_input_validation': True
277
+ }
278
+
279
+
280
+ # ===== 導出配置函數 =====
281
+ def get_agent_config(agent_name: str) -> dict:
282
+ """獲取指定 Agent 的配置"""
283
+ return AGENT_CONFIG.get(agent_name, {})
284
+
285
+
286
+ def get_tool_config(tool_name: str) -> dict:
287
+ """獲取指定 Tool 的配置"""
288
+ return TOOL_CONFIG.get(tool_name, {})
289
+
290
+
291
+ def is_feature_enabled(feature_name: str) -> bool:
292
+ """檢查功能是否啟用"""
293
+ return FEATURE_FLAGS.get(feature_name, False)
294
+
295
+
296
+ def get_database_path(db_name: str) -> str:
297
+ """獲取資料庫路徑"""
298
+ return DATABASE_CONFIG.get(db_name, '')
299
+
300
+
301
+ # ===== 環境檢查 =====
302
+ def check_environment():
303
+ """檢查環境配置是否完整"""
304
+ missing_keys = []
305
+
306
+ required_keys = ['GEMINI_API_KEY', 'GOOGLE_MAPS_API_KEY']
307
+
308
+ for key in required_keys:
309
+ if not os.getenv(key):
310
+ missing_keys.append(key)
311
+
312
+ if missing_keys:
313
+ print(f"⚠️ Warning: Missing environment variables: {', '.join(missing_keys)}")
314
+ print("Please set them in your .env file or environment")
315
+ return False
316
+
317
+ return True
318
+
319
+
320
+ # ===== 初始化配置 =====
321
+ def initialize_config():
322
+ """初始化配置"""
323
+ # 檢查環境
324
+ check_environment()
325
+
326
+ # 創建必要的目錄
327
+ for dir_path in [STORAGE_DIR, TMP_DIR, LOGS_DIR]:
328
+ dir_path.mkdir(parents=True, exist_ok=True)
329
+
330
+ print("✅ Configuration initialized successfully")
331
+
332
+
333
+ # 自動初始化
334
+ if __name__ != "__main__":
335
+ initialize_config()
core/utils.py CHANGED
@@ -4,6 +4,7 @@ LifeFlow AI - Core Utilities
4
 
5
  import plotly.graph_objects as go
6
  from config import AGENTS_INFO
 
7
 
8
 
9
  def create_agent_stream_output() -> str:
@@ -58,13 +59,30 @@ def create_agent_card_enhanced(agent_key: str, status: str = "idle", message: st
58
 
59
 
60
  def create_task_card(task_num: int, task_title: str, priority: str,
61
- time_window: str, duration: str, location: str, icon: str = "📋") -> str:
62
  """創建任務卡片"""
63
  priority_color_map = {
64
  "HIGH": "#FF6B6B",
65
  "MEDIUM": "#F5A623",
66
  "LOW": "#7ED321"
67
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  priority_class = f"priority-{priority.lower()}"
70
  task_color = priority_color_map.get(priority, "#999")
@@ -97,7 +115,7 @@ def create_task_card(task_num: int, task_title: str, priority: str,
97
 
98
 
99
  def create_summary_card(total_tasks: int, high_priority: int, total_time: int) -> str:
100
- """創建摘要卡片"""
101
  return f"""
102
  <div class="summary-card">
103
  <h3 style="margin: 0 0 15px 0; font-size: 20px;">📋 Task Summary</h3>
@@ -107,7 +125,7 @@ def create_summary_card(total_tasks: int, high_priority: int, total_time: int) -
107
  <span class="stat-label">Tasks</span>
108
  </div>
109
  <div class="stat-item">
110
- <span class="stat-value" style="color: #FF6B6B;">{high_priority}</span>
111
  <span class="stat-label">High Priority</span>
112
  </div>
113
  <div class="stat-item">
@@ -248,373 +266,200 @@ def create_celebration_animation() -> str:
248
  """
249
 
250
 
251
- def create_result_visualization(task_list: list) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  """
253
- 創建詳細的結果視覺化卡片
254
- 包含:快速摘要、時間線預覽、優化指標、關鍵亮點
255
  """
256
- # 計算統計數據
257
- total_tasks = len(task_list)
258
- total_time_min = sum(int(t.get('duration', '0').split()[0]) for t in task_list)
259
- total_time_hours = total_time_min // 60
260
- total_time_remain = total_time_min % 60
261
- time_str = f"{total_time_hours}h {total_time_remain}min" if total_time_hours > 0 else f"{total_time_min}min"
262
-
263
- # 模擬數據(實際應該從優化結果獲取)
264
- total_distance = 2.8
265
- efficiency = 95
266
- time_saved = 25
267
- distance_reduced = 12
268
-
269
- # 構建 HTML
 
 
 
 
 
 
 
 
 
 
 
270
  html = f"""
271
  <div class="result-visualization">
272
- <!-- 標題 -->
273
  <div class="result-header">
274
  <h2 style="color: #50C878; margin: 0; font-size: 28px; display: flex; align-items: center; gap: 10px;">
275
  <span class="celebration-icon" style="font-size: 36px; animation: bounce 1s infinite;">🎉</span>
276
  <span>Planning Complete!</span>
277
  </h2>
278
- <p style="margin: 10px 0 0 0; opacity: 0.8; font-size: 14px;">Your optimized route is ready</p>
279
  </div>
280
 
281
- <!-- 快速摘要 -->
282
  <div class="quick-summary" style="margin-top: 25px;">
283
  <h3 style="font-size: 18px; margin: 0 0 15px 0; color: #333;">📊 Quick Summary</h3>
284
  <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
285
  <div class="summary-metric">
286
  <div class="metric-icon">🎯</div>
287
  <div class="metric-content">
288
- <div class="metric-value">{total_tasks}</div>
289
- <div class="metric-label">Total Tasks</div>
290
  </div>
291
  </div>
292
  <div class="summary-metric">
293
  <div class="metric-icon">⏱️</div>
294
  <div class="metric-content">
295
  <div class="metric-value">{time_str}</div>
296
- <div class="metric-label">Total Time</div>
297
  </div>
298
  </div>
299
  <div class="summary-metric">
300
  <div class="metric-icon">🚗</div>
301
  <div class="metric-content">
302
- <div class="metric-value">{total_distance}km</div>
303
  <div class="metric-label">Total Distance</div>
304
  </div>
305
  </div>
306
  <div class="summary-metric success">
307
- <div class="metric-icon">✅</div>
308
  <div class="metric-content">
309
- <div class="metric-value">All Met</div>
310
- <div class="metric-label">Deadlines</div>
311
  </div>
312
  </div>
313
  </div>
314
  </div>
315
 
316
- <!-- 時間線預覽 -->
317
  <div class="timeline-preview" style="margin-top: 25px;">
318
  <h3 style="font-size: 18px; margin: 0 0 15px 0; color: #333;">🗓️ Timeline Preview</h3>
319
  <div class="timeline-container">
320
- """
321
 
322
- # 添加時間線項目
323
- for i, task in enumerate(task_list):
324
- task_time = task.get('time', 'Anytime')
325
- task_title = task.get('title', 'Task')
326
- task_icon = task.get('icon', '📋')
327
-
328
- # 解析時間(簡化示例)
329
- if '-' in task_time:
330
- times = task_time.split('-')
331
- start = times[0].strip()
332
- end = times[1].strip()
333
- else:
334
- start = '10:00'
335
- end = '10:30'
336
 
337
  html += f"""
338
  <div class="timeline-item">
339
- <div class="timeline-time">{start}</div>
340
- <div class="timeline-connector">━━━</div>
341
  <div class="timeline-task">
342
- <span class="timeline-icon">{task_icon}</span>
343
- <span class="timeline-title">{task_title}</span>
 
 
 
344
  </div>
345
- <div class="timeline-connector">���━━</div>
346
- <div class="timeline-time">{end}</div>
347
  </div>
348
- """
349
 
350
- # 添加旅行時間(除了最後一個任務)
351
- if i < len(task_list) - 1:
352
- travel_time = ['15min', '20min', '10min'][i] if i < 3 else '15min'
353
  html += f"""
354
  <div class="timeline-travel">
355
  <div class="travel-arrow">↓</div>
356
- <div class="travel-time">{travel_time}</div>
357
  </div>
358
- """
359
 
360
  html += """
361
  </div>
362
  </div>
363
 
364
- <!-- 優化指標 -->
365
  <div class="optimization-metrics" style="margin-top: 25px;">
366
  <h3 style="font-size: 18px; margin: 0 0 15px 0; color: #333;">📈 Optimization Metrics</h3>
367
  <div class="metrics-grid">
368
- """
369
 
370
- # 添加進度條指標
371
- metrics = [
372
  ("Route Efficiency", efficiency, "#50C878"),
373
- ("Time Optimization", 100 - (time_saved / 60 * 100), "#4A90E2"),
374
- ("Distance Reduction", distance_reduced, "#F5A623")
375
  ]
376
 
377
- for label, value, color in metrics:
 
 
378
  html += f"""
379
  <div class="metric-bar">
380
  <div class="metric-bar-header">
381
  <span class="metric-bar-label">{label}</span>
382
- <span class="metric-bar-value">{int(value)}%</span>
383
  </div>
384
  <div class="metric-bar-track">
385
- <div class="metric-bar-fill" style="width: {value}%; background: {color};"></div>
386
  </div>
387
  </div>
388
- """
389
 
390
- html += """
391
  </div>
392
  <div class="optimization-stats" style="margin-top: 15px; display: flex; gap: 20px; flex-wrap: wrap;">
393
  <div class="stat-badge">
394
- <span class="stat-badge-icon">⚡</span>
395
- <span class="stat-badge-text">Time Saved: <strong>25 min</strong></span>
396
  </div>
397
  <div class="stat-badge">
398
  <span class="stat-badge-icon">📏</span>
399
- <span class="stat-badge-text">Distance Reduced: <strong>12%</strong></span>
400
- </div>
401
- </div>
402
- </div>
403
-
404
- <!-- 關鍵亮點 -->
405
- <div class="key-highlights" style="margin-top: 25px;">
406
- <h3 style="font-size: 18px; margin: 0 0 15px 0; color: #333;">⚡ Key Highlights</h3>
407
- <div class="highlights-list">
408
- <div class="highlight-item success">
409
- <span class="highlight-icon">✓</span>
410
- <span class="highlight-text">No traffic delays expected</span>
411
- </div>
412
- <div class="highlight-item success">
413
- <span class="highlight-icon">✓</span>
414
- <span class="highlight-text">All locations open during visit</span>
415
- </div>
416
- <div class="highlight-item success">
417
- <span class="highlight-icon">✓</span>
418
- <span class="highlight-text">Weather conditions: Favorable</span>
419
- </div>
420
- <div class="highlight-item info">
421
- <span class="highlight-icon">ℹ</span>
422
- <span class="highlight-text">Recommended departure: 08:45 AM</span>
423
  </div>
424
  </div>
425
  </div>
426
  </div>
427
 
428
  <style>
429
- @keyframes bounce {
430
- 0%, 100% { transform: translateY(0); }
431
- 50% { transform: translateY(-10px); }
432
- }
433
-
434
- .result-visualization {
435
- padding: 25px;
436
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
437
- border-radius: 15px;
438
- box-shadow: 0 10px 30px rgba(0,0,0,0.1);
439
- }
440
-
441
- .quick-summary .summary-metric {
442
- background: white;
443
- padding: 15px;
444
- border-radius: 12px;
445
- display: flex;
446
- align-items: center;
447
- gap: 12px;
448
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
449
- transition: transform 0.2s;
450
- }
451
-
452
- .quick-summary .summary-metric:hover {
453
- transform: translateY(-3px);
454
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
455
- }
456
-
457
- .quick-summary .summary-metric.success {
458
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
459
- color: white;
460
- }
461
-
462
- .metric-icon {
463
- font-size: 28px;
464
- }
465
-
466
- .metric-value {
467
- font-size: 20px;
468
- font-weight: bold;
469
- line-height: 1.2;
470
- }
471
-
472
- .metric-label {
473
- font-size: 12px;
474
- opacity: 0.8;
475
- }
476
-
477
- .timeline-preview {
478
- background: white;
479
- padding: 20px;
480
- border-radius: 12px;
481
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
482
- }
483
-
484
- .timeline-item {
485
- display: flex;
486
- align-items: center;
487
- gap: 8px;
488
- margin: 8px 0;
489
- font-size: 14px;
490
- }
491
-
492
- .timeline-time {
493
- font-weight: 600;
494
- color: #4A90E2;
495
- min-width: 50px;
496
- }
497
-
498
- .timeline-connector {
499
- color: #ddd;
500
- font-weight: bold;
501
- }
502
-
503
- .timeline-task {
504
- background: #f0f4f8;
505
- padding: 8px 12px;
506
- border-radius: 8px;
507
- display: flex;
508
- align-items: center;
509
- gap: 8px;
510
- flex: 1;
511
- }
512
-
513
- .timeline-travel {
514
- display: flex;
515
- align-items: center;
516
- gap: 8px;
517
- margin-left: 60px;
518
- font-size: 13px;
519
- color: #666;
520
- }
521
-
522
- .travel-arrow {
523
- color: #4A90E2;
524
- font-weight: bold;
525
- }
526
-
527
- .optimization-metrics {
528
- background: white;
529
- padding: 20px;
530
- border-radius: 12px;
531
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
532
- }
533
-
534
- .metric-bar {
535
- margin-bottom: 15px;
536
- }
537
-
538
- .metric-bar-header {
539
- display: flex;
540
- justify-content: space-between;
541
- margin-bottom: 6px;
542
- font-size: 13px;
543
- }
544
-
545
- .metric-bar-label {
546
- font-weight: 500;
547
- }
548
-
549
- .metric-bar-value {
550
- font-weight: 600;
551
- color: #50C878;
552
- }
553
-
554
- .metric-bar-track {
555
- height: 8px;
556
- background: #e0e0e0;
557
- border-radius: 4px;
558
- overflow: hidden;
559
- }
560
-
561
- .metric-bar-fill {
562
- height: 100%;
563
- border-radius: 4px;
564
- transition: width 1s ease-out;
565
- }
566
-
567
- .stat-badge {
568
- background: #f0f4f8;
569
- padding: 8px 15px;
570
- border-radius: 20px;
571
- display: inline-flex;
572
- align-items: center;
573
- gap: 8px;
574
- font-size: 13px;
575
- }
576
-
577
- .stat-badge-icon {
578
- font-size: 16px;
579
- }
580
-
581
- .key-highlights {
582
- background: white;
583
- padding: 20px;
584
- border-radius: 12px;
585
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
586
- }
587
-
588
- .highlight-item {
589
- display: flex;
590
- align-items: center;
591
- gap: 12px;
592
- padding: 10px;
593
- margin: 6px 0;
594
- border-radius: 8px;
595
- font-size: 14px;
596
- }
597
-
598
- .highlight-item.success {
599
- background: #e8f5e9;
600
- }
601
-
602
- .highlight-item.info {
603
- background: #e3f2fd;
604
- }
605
-
606
- .highlight-icon {
607
- font-size: 18px;
608
- font-weight: bold;
609
- }
610
-
611
- .highlight-item.success .highlight-icon {
612
- color: #4CAF50;
613
- }
614
-
615
- .highlight-item.info .highlight-icon {
616
- color: #2196F3;
617
- }
618
  </style>
619
  """
620
  return html
 
4
 
5
  import plotly.graph_objects as go
6
  from config import AGENTS_INFO
7
+ from datetime import datetime
8
 
9
 
10
  def create_agent_stream_output() -> str:
 
59
 
60
 
61
  def create_task_card(task_num: int, task_title: str, priority: str,
62
+ time_window: dict, duration: str, location: str, icon: str = "📋") -> str:
63
  """創建任務卡片"""
64
  priority_color_map = {
65
  "HIGH": "#FF6B6B",
66
  "MEDIUM": "#F5A623",
67
  "LOW": "#7ED321"
68
  }
69
+ if time_window is None:
70
+ time_window = "Anytime"
71
+
72
+ elif isinstance(time_window, dict):
73
+ start = time_window.get('earliest_time', None)
74
+ end = time_window.get('latest_time', None)
75
+ start = datetime.fromisoformat(start).strftime("%H:%M") if start else "Before"
76
+ end = datetime.fromisoformat(end).strftime("%H:%M") if end else "After"
77
+
78
+ if start == "Before" and end == "After":
79
+ time_window = "Anytime"
80
+ elif start == "After":
81
+ time_window = f"After {end}"
82
+ elif end == "Before":
83
+ time_window = f"Before {start}"
84
+ else:
85
+ time_window = f"{start} - {end}"
86
 
87
  priority_class = f"priority-{priority.lower()}"
88
  task_color = priority_color_map.get(priority, "#999")
 
115
 
116
 
117
  def create_summary_card(total_tasks: int, high_priority: int, total_time: int) -> str:
118
+ """創建摘要卡片""" #style="color: #FF6B6B;
119
  return f"""
120
  <div class="summary-card">
121
  <h3 style="margin: 0 0 15px 0; font-size: 20px;">📋 Task Summary</h3>
 
125
  <span class="stat-label">Tasks</span>
126
  </div>
127
  <div class="stat-item">
128
+ <span class="stat-value">{high_priority}</span>
129
  <span class="stat-label">High Priority</span>
130
  </div>
131
  <div class="stat-item">
 
266
  """
267
 
268
 
269
+ def decode_polyline(polyline_str):
270
+ """
271
+ 解碼 Google Maps Polyline 字符串為 (lat, lng) 列表
272
+ 純 Python 實現,無需額外依賴
273
+ """
274
+ index, lat, lng = 0, 0, 0
275
+ coordinates = []
276
+ changes = {'latitude': 0, 'longitude': 0}
277
+
278
+ while index < len(polyline_str):
279
+ for unit in ['latitude', 'longitude']:
280
+ shift, result = 0, 0
281
+ while True:
282
+ byte = ord(polyline_str[index]) - 63
283
+ index += 1
284
+ result |= (byte & 0x1f) << shift
285
+ shift += 5
286
+ if not byte >= 0x20:
287
+ break
288
+ if (result & 1):
289
+ changes[unit] = ~(result >> 1)
290
+ else:
291
+ changes[unit] = (result >> 1)
292
+
293
+ lat += changes['latitude']
294
+ lng += changes['longitude']
295
+ coordinates.append((lat / 100000.0, lng / 100000.0))
296
+
297
+ return coordinates
298
+
299
+
300
+ def create_result_visualization(task_list: list, structured_data: dict) -> str:
301
  """
302
+ 創建詳細的結果視覺化卡片 (綁定真實數據)
 
303
  """
304
+ # 提取數據
305
+ metrics = structured_data.get("metrics", {})
306
+ traffic = structured_data.get("traffic_summary", {})
307
+ timeline = structured_data.get("timeline", [])
308
+
309
+ # 1. 計算摘要數據
310
+ completed_tasks = metrics.get("completed_tasks", 0)
311
+ total_tasks = metrics.get("total_tasks", 0)
312
+
313
+ total_dist_km = traffic.get("total_distance_km", 0)
314
+ total_dur_min = traffic.get("total_duration_min", 0)
315
+
316
+ # 格式化時間 (例如 85 min -> 1h 25min)
317
+ hours = int(total_dur_min // 60)
318
+ mins = int(total_dur_min % 60)
319
+ time_str = f"{hours}h {mins}m" if hours > 0 else f"{mins}min"
320
+
321
+ # 2. 優化指標
322
+ efficiency = metrics.get("route_efficiency_pct", 90)
323
+ time_saved_pct = metrics.get("time_improvement_pct", 0)
324
+ dist_saved_pct = metrics.get("distance_improvement_pct", 0)
325
+
326
+ time_saved_val = metrics.get("time_saved_min", 0)
327
+ dist_saved_val_km = metrics.get("distance_saved_m", 0) / 1000
328
+
329
  html = f"""
330
  <div class="result-visualization">
 
331
  <div class="result-header">
332
  <h2 style="color: #50C878; margin: 0; font-size: 28px; display: flex; align-items: center; gap: 10px;">
333
  <span class="celebration-icon" style="font-size: 36px; animation: bounce 1s infinite;">🎉</span>
334
  <span>Planning Complete!</span>
335
  </h2>
336
+ <p style="margin: 10px 0 0 0; opacity: 0.8; font-size: 14px;">Optimized based on real-time traffic & weather</p>
337
  </div>
338
 
 
339
  <div class="quick-summary" style="margin-top: 25px;">
340
  <h3 style="font-size: 18px; margin: 0 0 15px 0; color: #333;">📊 Quick Summary</h3>
341
  <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
342
  <div class="summary-metric">
343
  <div class="metric-icon">🎯</div>
344
  <div class="metric-content">
345
+ <div class="metric-value">{completed_tasks} / {total_tasks}</div>
346
+ <div class="metric-label">Tasks Completed</div>
347
  </div>
348
  </div>
349
  <div class="summary-metric">
350
  <div class="metric-icon">⏱️</div>
351
  <div class="metric-content">
352
  <div class="metric-value">{time_str}</div>
353
+ <div class="metric-label">Total Duration</div>
354
  </div>
355
  </div>
356
  <div class="summary-metric">
357
  <div class="metric-icon">🚗</div>
358
  <div class="metric-content">
359
+ <div class="metric-value">{total_dist_km:.1f} km</div>
360
  <div class="metric-label">Total Distance</div>
361
  </div>
362
  </div>
363
  <div class="summary-metric success">
364
+ <div class="metric-icon">⚡</div>
365
  <div class="metric-content">
366
+ <div class="metric-value">{efficiency}%</div>
367
+ <div class="metric-label">Efficiency Score</div>
368
  </div>
369
  </div>
370
  </div>
371
  </div>
372
 
 
373
  <div class="timeline-preview" style="margin-top: 25px;">
374
  <h3 style="font-size: 18px; margin: 0 0 15px 0; color: #333;">🗓️ Timeline Preview</h3>
375
  <div class="timeline-container">
376
+ """
377
 
378
+ # 綁定時間線數據
379
+ # 跳過 start point (index 0) 如果不需要顯示,或者全部顯示
380
+ for i, stop in enumerate(timeline):
381
+ time = stop.get("time", "")
382
+ loc_name = stop.get("location", "")
383
+ weather = stop.get("weather", "")
384
+ travel_time = stop.get("travel_time_from_prev", "")
385
+
386
+ # 簡單的圖標邏輯
387
+ icon = "🏁" if i == 0 else ("🔚" if i == len(timeline) - 1 else "📍")
 
 
 
 
388
 
389
  html += f"""
390
  <div class="timeline-item">
391
+ <div class="timeline-time">{time}</div>
392
+ <div class="timeline-connector">━━</div>
393
  <div class="timeline-task">
394
+ <span class="timeline-icon">{icon}</span>
395
+ <div class="timeline-info">
396
+ <span class="timeline-title">{loc_name}</span>
397
+ <span class="timeline-meta">{weather}</span>
398
+ </div>
399
  </div>
 
 
400
  </div>
401
+ """
402
 
403
+ # 添加交通時間 (如果不是最後一個)
404
+ if i < len(timeline) - 1:
405
+ next_travel = timeline[i + 1].get("travel_time_from_prev", "0 mins")
406
  html += f"""
407
  <div class="timeline-travel">
408
  <div class="travel-arrow">↓</div>
409
+ <div class="travel-time">Drive: {next_travel}</div>
410
  </div>
411
+ """
412
 
413
  html += """
414
  </div>
415
  </div>
416
 
 
417
  <div class="optimization-metrics" style="margin-top: 25px;">
418
  <h3 style="font-size: 18px; margin: 0 0 15px 0; color: #333;">📈 Optimization Metrics</h3>
419
  <div class="metrics-grid">
420
+ """
421
 
422
+ # 綁定指標數據
423
+ viz_metrics = [
424
  ("Route Efficiency", efficiency, "#50C878"),
425
+ ("Time Saved", time_saved_pct, "#4A90E2"),
426
+ ("Distance Reduced", dist_saved_pct, "#F5A623")
427
  ]
428
 
429
+ for label, value, color in viz_metrics:
430
+ # 限制 value 在 0-100 之間顯示
431
+ display_val = min(max(value, 0), 100)
432
  html += f"""
433
  <div class="metric-bar">
434
  <div class="metric-bar-header">
435
  <span class="metric-bar-label">{label}</span>
436
+ <span class="metric-bar-value">{value:.1f}%</span>
437
  </div>
438
  <div class="metric-bar-track">
439
+ <div class="metric-bar-fill" style="width: {display_val}%; background: {color};"></div>
440
  </div>
441
  </div>
442
+ """
443
 
444
+ html += f"""
445
  </div>
446
  <div class="optimization-stats" style="margin-top: 15px; display: flex; gap: 20px; flex-wrap: wrap;">
447
  <div class="stat-badge">
448
+ <span class="stat-badge-icon">⏱️</span>
449
+ <span class="stat-badge-text">Saved: <strong>{time_saved_val} min</strong></span>
450
  </div>
451
  <div class="stat-badge">
452
  <span class="stat-badge-icon">📏</span>
453
+ <span class="stat-badge-text">Reduced: <strong>{dist_saved_val_km:.1f} km</strong></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  </div>
455
  </div>
456
  </div>
457
  </div>
458
 
459
  <style>
460
+ /* ... 保留原本的 CSS ... */
461
+ .timeline-info {{display: flex; flex-direction: column; }}
462
+ .timeline-meta {{font - size: 11px; color: #666; margin-top: 2px; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  </style>
464
  """
465
  return html
ui/components/header.py CHANGED
@@ -1,26 +1,23 @@
1
  """
2
  LifeFlow AI - Header Component
3
- 應用標題和頂部控制按鈕
4
  """
5
  import gradio as gr
6
 
7
-
8
  def create_header():
9
- """創建應用 Header - 更大的標題"""
10
- header_html = """
11
  <div class="app-header">
12
  <h1>✨ LifeFlow AI</h1>
13
  <p>Your Intelligent Daily Trip Planning Assistant</p>
14
  </div>
15
- """
16
- return gr.HTML(header_html)
17
-
18
 
19
  def create_top_controls():
20
- """創建頂部控制按鈕(Theme Toggle, Settings & Doc)- 使用絕對定位"""
21
- # 不使用 gr.Row,直接創建獨立按鈕
22
- theme_btn = gr.Button("🌓", elem_id="theme-toggle", size="sm")
23
- settings_btn = gr.Button("⚙️", elem_id="settings-btn", size="sm")
24
- doc_btn = gr.Button("📖", elem_id="doc-btn", size="sm")
 
 
25
 
26
  return theme_btn, settings_btn, doc_btn
 
1
  """
2
  LifeFlow AI - Header Component
 
3
  """
4
  import gradio as gr
5
 
 
6
  def create_header():
7
+ return gr.HTML("""
 
8
  <div class="app-header">
9
  <h1>✨ LifeFlow AI</h1>
10
  <p>Your Intelligent Daily Trip Planning Assistant</p>
11
  </div>
12
+ """)
 
 
13
 
14
  def create_top_controls():
15
+ """
16
+ 創建右上角懸浮按鈕
17
+ """
18
+ with gr.Group(elem_id="top-controls-container"):
19
+ theme_btn = gr.Button("🌓", elem_classes="control-btn", size="sm")
20
+ settings_btn = gr.Button("⚙️", elem_classes="control-btn", size="sm")
21
+ doc_btn = gr.Button("📖", elem_classes="control-btn", size="sm")
22
 
23
  return theme_btn, settings_btn, doc_btn
ui/components/input_form.py CHANGED
@@ -1,10 +1,8 @@
1
  """
2
  LifeFlow AI - Input Form Component
3
- Step 1: 用戶輸入表單
4
  """
5
  import gradio as gr
6
 
7
-
8
  def create_input_form(agent_stream_html):
9
  """創建輸入表單"""
10
  with gr.Group(visible=True) as input_area:
@@ -16,8 +14,11 @@ def create_input_form(agent_stream_html):
16
  lines=3
17
  )
18
  gr.Markdown("---")
19
- auto_location = gr.Checkbox(label="📍 Auto-detect my location", value=False)
20
 
 
 
 
 
21
  with gr.Group(visible=True) as location_inputs:
22
  with gr.Row():
23
  lat_input = gr.Number(label="Latitude", value=25.033, precision=6)
@@ -28,7 +29,10 @@ def create_input_form(agent_stream_html):
28
  return (input_area, agent_stream_output, user_input, auto_location,
29
  location_inputs, lat_input, lon_input, analyze_btn)
30
 
31
-
32
- def toggle_location_inputs(auto_location):
33
- """切換位置輸入顯示"""
34
- return gr.update(visible=not auto_location)
 
 
 
 
1
  """
2
  LifeFlow AI - Input Form Component
 
3
  """
4
  import gradio as gr
5
 
 
6
  def create_input_form(agent_stream_html):
7
  """創建輸入表單"""
8
  with gr.Group(visible=True) as input_area:
 
14
  lines=3
15
  )
16
  gr.Markdown("---")
 
17
 
18
+ # Auto-location toggle
19
+ auto_location = gr.Checkbox(label="📍 Auto-detect my location (Simulated)", value=False)
20
+
21
+ # Manual Location Inputs (Group)
22
  with gr.Group(visible=True) as location_inputs:
23
  with gr.Row():
24
  lat_input = gr.Number(label="Latitude", value=25.033, precision=6)
 
29
  return (input_area, agent_stream_output, user_input, auto_location,
30
  location_inputs, lat_input, lon_input, analyze_btn)
31
 
32
+ def toggle_location_inputs(auto_loc):
33
+ """
34
+ Visibility Toggle Logic:
35
+ If Auto-detect is ON (True) -> Hide Manual Inputs (False)
36
+ If Auto-detect is OFF (False) -> Show Manual Inputs (True)
37
+ """
38
+ return gr.update(visible=not auto_loc)
ui/components/modals.py CHANGED
@@ -1,12 +1,6 @@
1
  """
2
  LifeFlow AI - Settings & Documentation Modals
3
- 設定和文檔模態框
4
  """
5
- import sys
6
- from pathlib import Path
7
-
8
- current_dir = Path(__file__).parent.parent.parent
9
- sys.path.insert(0, str(current_dir))
10
 
11
  import gradio as gr
12
  from config import MODEL_CHOICES
 
1
  """
2
  LifeFlow AI - Settings & Documentation Modals
 
3
  """
 
 
 
 
 
4
 
5
  import gradio as gr
6
  from config import MODEL_CHOICES
ui/components/results.py CHANGED
@@ -1,18 +1,9 @@
1
  """
2
  LifeFlow AI - Results Component
3
- Step 3: 結果展示
4
  ✅ 新增 Chat with LifeFlow Tab
5
  ✅ 支援任務修改對話
6
  """
7
- import sys
8
- from pathlib import Path
9
-
10
- current_dir = Path(__file__).parent.parent.parent
11
- sys.path.insert(0, str(current_dir))
12
-
13
  import gradio as gr
14
- from config import AGENTS_INFO
15
-
16
 
17
  def create_team_area(create_agent_card_func):
18
  """創建 AI Team 展示區域"""
 
1
  """
2
  LifeFlow AI - Results Component
 
3
  ✅ 新增 Chat with LifeFlow Tab
4
  ✅ 支援任務修改對話
5
  """
 
 
 
 
 
 
6
  import gradio as gr
 
 
7
 
8
  def create_team_area(create_agent_card_func):
9
  """創建 AI Team 展示區域"""