Marco310 commited on
Commit
6b71d3d
·
1 Parent(s): 50a7f50

feat: Complete UI/UX redesign and fix critical workflow bugs

Browse files
app.py CHANGED
@@ -1,475 +1,355 @@
1
  """
2
- LifeFlow AI - Main Application (Refactored)
3
- Controller Layer: 負責 UI 佈局與 Service 連接
 
4
  """
5
 
6
  import gradio as gr
7
- from typing import List
8
-
9
- # ===== Core & Data Models =====
10
- from core.session import UserSession
11
- from services.planner_service import PlannerService
12
-
13
- # ===== UI & Config =====
14
- from config import APP_TITLE, DEFAULT_SETTINGS
15
  from ui.theme import get_enhanced_css
 
 
 
 
16
  from ui.renderers import (
17
- create_agent_stream_output,
18
  create_summary_card,
19
- create_agent_card_enhanced,
20
- get_reasoning_html_reversed,
21
- generate_chat_history_html_bubble
22
  )
 
 
23
  from core.visualizers import create_animated_map
24
 
25
- # ===== UI Components =====
26
- from ui.components.header import create_header
27
- from ui.components.input_form import create_input_form, toggle_location_inputs
28
- from ui.components.confirmation import create_confirmation_area
29
- from ui.components.results import create_team_area, create_result_area, create_tabs
30
- from ui.components.modals import create_settings_modal, create_doc_modal
31
-
32
-
33
  class LifeFlowAI:
34
  def __init__(self):
35
  self.service = PlannerService()
36
 
37
- def _check_api_status(self, session: UserSession) -> str:
38
- """檢查 API Key 狀態"""
 
 
 
 
 
 
 
 
 
39
  has_gemini = bool(session.custom_settings.get("gemini_api_key"))
40
  has_google = bool(session.custom_settings.get("google_maps_api_key"))
 
 
41
 
42
- if has_gemini and has_google:
43
- return "✅ System Ready - Waiting for input..."
44
- else:
45
- missing = []
46
- if not has_gemini: missing.append("Gemini Key")
47
- if not has_google: missing.append("Google Maps Key")
48
- return f"⚠️ Missing Keys: {', '.join(missing)} - Please configure Settings ↗"
49
 
50
- def _get_agent_outputs(self, active_agent: str = None, status: str = "idle", message: str = "Waiting") -> List[str]:
51
- """輔助函數:生成 Agent 卡片 HTML"""
52
 
53
- # 🔥 [關鍵修正] 更新為你的新 Agent 名單 (共 5 個)
54
- # 必須與 create_team_area 中的順序完全一致!
55
- agents = ['team', 'scout', 'optimizer', 'navigator', 'weatherman', 'presenter']
56
-
57
- outputs = []
58
- for agent in agents:
59
- if agent == active_agent:
60
- outputs.append(create_agent_card_enhanced(agent, status, message))
61
- else:
62
- outputs.append(create_agent_card_enhanced(agent, "idle", "On standby"))
63
-
64
- return outputs
65
-
66
- def _update_task_summary(self, session: UserSession) -> str:
67
- """統一計算並回傳 Summary Card HTML"""
68
- tasks = session.task_list
69
- if not tasks:
70
- return create_summary_card(0, 0, 0)
71
-
72
- high_priority = sum(1 for t in tasks if t.get("priority", "").upper() == "HIGH")
73
-
74
- total_time = 0
75
- for t in tasks:
76
- dur_str = str(t.get("duration", "0"))
77
- try:
78
- val = int(dur_str.split()[0])
79
- total_time += val
80
- except (ValueError, IndexError):
81
- pass
82
-
83
- return create_summary_card(len(tasks), high_priority, total_time)
84
-
85
- # -------------------------------------------------------------------------
86
- # Step 1: Analyze (分析任務)
87
- # -------------------------------------------------------------------------
88
  def analyze_wrapper(self, user_input, auto_loc, lat, lon, session_data):
89
  session = UserSession.from_dict(session_data)
90
-
91
- # 1. 輸入驗證
92
- if not user_input or not user_input.strip():
93
- agent_outputs = self._get_agent_outputs("team", "idle", "Waiting")
94
- yield (
95
- "<div style='color: #ef4444; font-weight: bold; padding: 5px;'>⚠️ Please describe your plans first!</div>",
96
- gr.HTML(), gr.HTML(),
97
- get_reasoning_html_reversed(session.reasoning_messages),
98
- gr.update(visible=False), gr.update(visible=False), gr.HTML(),
99
- "⚠️ Input Required", gr.update(visible=True), *agent_outputs, session.to_dict()
100
- )
101
- return
102
-
103
- # 2. 執行分析
104
  iterator = self.service.run_step1_analysis(user_input, auto_loc, lat, lon, session)
105
-
106
  for event in iterator:
107
  evt_type = event.get("type")
108
- agent_status = event.get("agent_status", ("team", "idle", "Waiting"))
109
- agent_outputs = self._get_agent_outputs(*agent_status)
110
  current_session = event.get("session", session)
111
- reasoning_html = get_reasoning_html_reversed(current_session.reasoning_messages)
112
-
113
- if evt_type == "stream":
114
- html_output = create_agent_stream_output(event.get("stream_text", ""))
115
  yield (
116
- html_output,
117
- gr.HTML(), gr.HTML(), reasoning_html,
118
- gr.update(visible=False), gr.update(visible=False), gr.HTML(),
119
- f"Processing: {agent_status[2]}", gr.update(visible=True), *agent_outputs, current_session.to_dict()
 
 
 
 
120
  )
121
- elif evt_type == "complete":
122
- summary_html = self._update_task_summary(current_session)
123
-
124
- task_html = self.service.generate_task_list_html(current_session)
125
-
126
- final_text = event.get("stream_text", "Analysis complete!")
127
- html_output = create_agent_stream_output(final_text)
128
-
129
- final_agents = self._get_agent_outputs("team", "complete", "Tasks ready")
130
 
 
131
  yield (
132
- html_output,
133
- gr.HTML(value=summary_html), # ✅ 更新 Summary Component
134
- gr.HTML(value=task_html),
135
- reasoning_html,
136
- gr.update(visible=True), gr.update(visible=True),
137
- generate_chat_history_html_bubble(current_session),
138
- "✓ Tasks extracted", gr.update(visible=False), *final_agents, current_session.to_dict()
 
139
  )
140
 
141
- elif evt_type == "error":
142
- err_msg = event.get("message", "Unknown error")
143
- error_agents = self._get_agent_outputs("team", "idle", "Error")
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  yield (
146
- f"<div style='color:red'>Error: {err_msg}</div>", gr.HTML(), gr.HTML(), reasoning_html,
147
- gr.update(visible=False), gr.update(visible=False), gr.HTML(),
148
- f"Error: {err_msg}", gr.update(visible=True), *error_agents, current_session.to_dict()
 
 
 
 
 
149
  )
150
 
151
-
152
- # -------------------------------------------------------------------------
153
- # Chat Modification (修改任務)
154
- # -------------------------------------------------------------------------
155
  def chat_wrapper(self, msg, session_data):
156
  session = UserSession.from_dict(session_data)
157
  iterator = self.service.modify_task_chat(msg, session)
158
-
159
  for event in iterator:
160
- current_session = event.get("session", session)
161
-
162
- # 🔥 [Update 2] Step 2 Modify: 每次對話更新後,重新計算 Summary
163
- new_summary_html = self._update_task_summary(current_session)
164
-
165
- yield (
166
- generate_chat_history_html_bubble(current_session), # Output 1: Chat History
167
- self.service.generate_task_list_html(current_session), # Output 2: Task List
168
- new_summary_html, # Output 3: ✅ Summary Card (新增這個)
169
- current_session.to_dict() # Output 4: Session State
170
- )
171
 
172
- # -------------------------------------------------------------------------
173
- # Step 2 -> 3 Transition (驗證與過渡)
174
- # -------------------------------------------------------------------------
175
- def transition_to_planning(self, session_data):
176
  session = UserSession.from_dict(session_data)
177
 
178
- # 1. 🛑 防呆檢查
179
- if not session.task_list or len(session.task_list) == 0:
180
- agent_outputs = self._get_agent_outputs("team", "idle", "No tasks")
181
- reasoning = get_reasoning_html_reversed(session.reasoning_messages)
182
 
183
- return (
184
- gr.update(visible=True), # task_confirm_area
185
- gr.update(visible=True), # chat_input_area
186
- gr.update(visible=False), # team_area
187
- gr.update(), # tabs
188
- gr.update(), # 🔥 [新增] report_tab (維持現狀)
189
- gr.update(), # 🔥 [新增] map_tab (維持現狀)
190
- reasoning,
191
- "⚠️ Error: No tasks to plan! Please add tasks.",
192
- *agent_outputs,
193
- session.to_dict()
194
- )
195
 
196
- # 2. 執行 Step 2 (Scout Search)
197
- result = self.service.run_step2_search(session)
198
- current_session = result.get("session", session)
199
-
200
- reasoning_html = get_reasoning_html_reversed(current_session.reasoning_messages)
201
- agent_outputs = self._get_agent_outputs("scout", "working", "Searching POIs...")
202
-
203
- return (
204
- gr.update(visible=False), # task_confirm_area
205
- gr.update(visible=False), # chat_input_area
206
- gr.update(visible=True), # team_area
207
- gr.update(selected="report_tab"), # tabs (預設看 Log)
208
- gr.update(visible=True), # 🔥 [關鍵修改] report_tab 設為可見!
209
- gr.update(visible=False), # 🔥 [關鍵修改] map_tab 設為可見!
210
- reasoning_html,
211
- "🗺️ Scout is searching locations...",
212
- *agent_outputs,
213
- current_session.to_dict()
214
- )
215
-
216
- # -------------------------------------------------------------------------
217
- # Step 3 Wrapper (Team Analysis)
218
- # -------------------------------------------------------------------------
219
- def step3_wrapper(self, session_data):
220
- session = UserSession.from_dict(session_data)
221
 
222
- # 1. 防呆回傳 (9個值)
223
- if not session.task_list:
224
- agent_outputs = self._get_agent_outputs("team", "idle", "No tasks")
225
- # yield 順序: Report, Reasoning, Agents(6個), Session
226
- yield ("", "", *agent_outputs, session.to_dict())
227
- return
228
 
229
  iterator = self.service.run_step3_team(session)
230
- current_report = "⏳ Generating plan..."
231
 
232
  for event in iterator:
 
233
  evt_type = event.get("type")
234
- current_session = event.get("session", session)
235
-
236
- # 準備 UI Data
237
- reasoning_html = get_reasoning_html_reversed(current_session.reasoning_messages)
238
- agent_status = event.get("agent_status", ("team", "working", "Processing..."))
239
- agent_outputs = self._get_agent_outputs(*agent_status)
240
 
241
- # 更新 Report 暫存
242
  if evt_type == "report_stream":
243
- current_report = event.get("content", current_report)
244
-
245
- # 🔥 統一回傳 (確保任何事件都更新所有 UI)
246
- # 必須是 9 個值:Report, Reasoning, Agent1...6, Session
247
- yield (
248
- current_report,
249
- reasoning_html,
250
- *agent_outputs,
251
- current_session.to_dict()
252
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
- # -------------------------------------------------------------------------
255
- # Step 4 Wrapper (Optimization & Results)
256
- # -------------------------------------------------------------------------
257
  def step4_wrapper(self, session_data):
258
  session = UserSession.from_dict(session_data)
259
- if not session.task_list: # 二次防呆
260
- default_map = create_animated_map()
261
- agent_outputs = self._get_agent_outputs("team", "idle", "No tasks")
262
- return (
263
- "", "", "", default_map,
264
- gr.update(), gr.update(), "⚠️ Planning aborted",
265
- *agent_outputs, session.to_dict()
266
- )
267
-
268
  result = self.service.run_step4_finalize(session)
269
- current_session = result.get("session", session)
270
 
271
- if result["type"] == "success":
272
- agent_outputs = self._get_agent_outputs("team", "complete", "Done")
273
  return (
274
- result["timeline_html"], result["metrics_html"], result["result_html"], result["map_fig"],
275
- gr.update(visible=True), gr.update(visible=False), "🎉 Planning completed!",
276
- *agent_outputs, current_session.to_dict()
 
 
 
 
277
  )
278
  else:
279
- default_map = create_animated_map()
280
- agent_outputs = self._get_agent_outputs("team", "idle", "Error")
281
- err = result.get("message", "Error")
282
- return (
283
- f"Error: {err}", "", "", default_map,
284
- gr.update(), gr.update(), f"Error: {err}",
285
- *agent_outputs, current_session.to_dict()
286
- )
287
-
288
- def save_settings(self, google_key, weather_key, gemini_key, model, session_data):
289
- session = UserSession.from_dict(session_data)
290
- session.custom_settings['google_maps_api_key'] = google_key
291
- session.custom_settings['openweather_api_key'] = weather_key
292
- session.custom_settings['gemini_api_key'] = gemini_key
293
- session.custom_settings['model'] = model
294
- new_status = self._check_api_status(session)
295
- return "✅ Settings saved locally!", session.to_dict(), new_status
296
-
297
- # -------------------------------------------------------------------------
298
- # UI Builder
299
- # -------------------------------------------------------------------------
300
- def build_interface(self):
301
- def reset_app(old_session_data):
302
- old_session = UserSession.from_dict(old_session_data)
303
- new_session = UserSession()
304
- new_session.custom_settings = old_session.custom_settings
305
- status_msg = self._check_api_status(new_session)
306
  return (
307
  gr.update(visible=True), gr.update(visible=False),
308
- gr.update(visible=False), gr.update(visible=False),
309
- gr.update(visible=False), gr.update(visible=False),
310
- gr.update(visible=False), "",
311
- create_agent_stream_output(),
312
- status_msg,
313
- "", "", "",
314
- new_session.to_dict()
315
  )
316
 
317
- def initial_check(session_data):
318
- s = UserSession.from_dict(session_data)
319
- return self._check_api_status(s)
320
 
321
- with gr.Blocks(title=APP_TITLE) as demo:
 
322
  gr.HTML(get_enhanced_css())
323
- home_btn, theme_btn, settings_btn, doc_btn = create_header()
324
- session_state = gr.State(value=UserSession().to_dict())
325
-
326
- with gr.Row():
327
- with gr.Column(scale=2, min_width=400):
328
- (input_area, agent_stream_output, user_input, auto_location,
329
- location_inputs, lat_input, lon_input, analyze_btn) = create_input_form(
330
- create_agent_stream_output()
331
- )
332
- (task_confirm_area, task_summary_display,
333
- task_list_display, exit_btn_inline, ready_plan_btn) = create_confirmation_area()
334
- team_area, agent_displays = create_team_area(create_agent_card_enhanced)
335
- (result_area, result_display, timeline_display, metrics_display) = create_result_area(
336
- create_animated_map)
337
-
338
- with gr.Column(scale=3, min_width=500):
339
- status_bar = gr.Textbox(label="📊 Status", value="⚠️ Checking System Status...", interactive=False,
340
- max_lines=1)
341
- (tabs, report_tab, map_tab, report_output, map_output, reasoning_output,
342
- chat_input_area, chat_history_output, chat_input, chat_send) = create_tabs(
343
- create_animated_map, get_reasoning_html_reversed()
344
- )
345
-
346
- # Modals
347
- (settings_modal, google_maps_key, openweather_key, gemini_api_key,
348
- model_choice, close_settings_btn, save_settings_btn, settings_status) = create_settings_modal()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  doc_modal, close_doc_btn = create_doc_modal()
350
 
351
- auto_location.change(fn=toggle_location_inputs, inputs=[auto_location], outputs=[location_inputs])
 
 
 
 
352
 
353
- # ---------------------------------------------------------------------
354
- # Event 1: Analyze (Step 1)
355
- # ---------------------------------------------------------------------
356
  analyze_event = analyze_btn.click(
357
  fn=self.analyze_wrapper,
358
- inputs=[user_input, auto_location, lat_input, lon_input, session_state],
359
- outputs=[
360
- agent_stream_output, task_summary_display, task_list_display,
361
- reasoning_output, task_confirm_area, chat_input_area,
362
- chat_history_output, status_bar, input_area,
363
- *agent_displays, session_state
364
- ]
365
- )
366
 
367
- # ---------------------------------------------------------------------
368
- # Event 2: Chat
369
- # ---------------------------------------------------------------------
370
- chat_event = chat_send.click(
371
- fn=self.chat_wrapper,
372
- inputs=[chat_input, session_state],
373
- outputs=[
374
- chat_history_output, # 1. 對話框
375
- task_list_display, # 2. 任務列表
376
- task_summary_display, # 🔥 [Critical Fix] 3. 摘要卡片 (必須加這行!)
377
- session_state # 4. Session
378
- ]
379
- ).then(fn=lambda: "", outputs=[chat_input])
380
-
381
- # ---------------------------------------------------------------------
382
- # Event 3: Planning Chain (Step 2 -> 3 -> 4)
383
- # 使用 transition_to_planning 進行驗證,如果沒任務會自動停止
384
- # ---------------------------------------------------------------------
385
- step2_event = ready_plan_btn.click(
386
- fn=self.transition_to_planning,
387
- inputs=[session_state],
388
- outputs=[
389
- task_confirm_area,
390
- chat_input_area,
391
- team_area,
392
- tabs,
393
- report_tab, # 🔥 [新增] 這裡要接 report_tab
394
- map_tab, # 🔥 [新增] 這裡要接 map_tab
395
- reasoning_output,
396
- status_bar,
397
- *agent_displays,
398
- session_state
399
- ]
400
- )
401
 
402
- step3_event = step2_event.then(
 
 
 
403
  fn=self.step3_wrapper,
404
  inputs=[session_state],
405
- outputs=[
406
- report_output, # 1. 報告 Tab
407
- reasoning_output, # 2. AI Conversation Log (這裡漏掉了!)
408
- *agent_displays, # 3-8. Agent Grid (這裡也漏掉了!)
409
- session_state # 9. Session
410
- ]
411
- )
412
-
413
- step3_ui_update = step3_event.then(
414
- fn=lambda: (gr.update(visible=True), gr.update(visible=True), gr.update(selected="report_tab")),
415
- outputs=[report_tab, map_tab, tabs]
416
  )
417
 
418
- step4_event = step3_ui_update.then(
419
  fn=self.step4_wrapper,
420
  inputs=[session_state],
 
421
  outputs=[
422
- timeline_display, metrics_display, result_display,
423
- map_output, map_tab, team_area, status_bar, *agent_displays, session_state
 
 
 
 
 
424
  ]
425
- ).then(fn=lambda: gr.update(visible=True), outputs=[result_area])
426
-
427
- # ---------------------------------------------------------------------
428
- # Reset & Cancels
429
- # ---------------------------------------------------------------------
430
- reset_outputs = [
431
- input_area, task_confirm_area, chat_input_area, result_area,
432
- team_area, report_tab, map_tab, user_input,
433
- agent_stream_output, status_bar,
434
- task_summary_display, task_list_display, chat_history_output,
435
- session_state
436
- ]
437
-
438
- home_btn.click(
439
- fn=reset_app,
440
- inputs=[session_state],
441
- outputs=reset_outputs,
442
- cancels=[analyze_event, step4_event, chat_event]
443
- )
444
 
445
- exit_btn_inline.click(
446
- fn=reset_app,
447
- inputs=[session_state],
448
- outputs=reset_outputs,
449
- cancels=[analyze_event, step4_event, chat_event]
450
- )
 
 
 
 
 
 
451
 
452
- # Others
453
- settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal])
454
- close_settings_btn.click(fn=lambda: gr.update(visible=False), outputs=[settings_modal])
455
- save_settings_btn.click(
456
- fn=self.save_settings,
457
- inputs=[google_maps_key, openweather_key, gemini_api_key, model_choice, session_state],
458
- outputs=[settings_status, session_state, status_bar]
459
- )
460
- theme_btn.click(fn=None,
461
- js="() => { document.querySelector('.gradio-container').classList.toggle('theme-dark'); }")
462
- doc_btn.click(fn=lambda: gr.update(visible=True), outputs=[doc_modal])
463
- close_doc_btn.click(fn=lambda: gr.update(visible=False), outputs=[doc_modal])
464
- demo.load(fn=initial_check, inputs=[session_state], outputs=[status_bar])
465
  return demo
466
 
 
 
 
 
467
 
468
  def main():
469
  app = LifeFlowAI()
470
  demo = app.build_interface()
471
- demo.launch(server_name="0.0.0.0", server_port=7860, share=True, show_error=True)
472
-
473
 
474
  if __name__ == "__main__":
475
  main()
 
1
  """
2
+ LifeFlow AI - Main Application (Fixed Output Mismatch)
3
+ 修復 Step 4 回傳值數量不匹配導致的 AttributeError
4
+ ✅ 確保 outputs 清單包含所有 7 個元件
5
  """
6
 
7
  import gradio as gr
8
+ import inspect
9
+ from datetime import datetime
 
 
 
 
 
 
10
  from ui.theme import get_enhanced_css
11
+ from ui.components.header import create_header
12
+ from ui.components.progress_stepper import create_progress_stepper, update_stepper
13
+ from ui.components.input_form import create_input_form, toggle_location_inputs
14
+ from ui.components.modals import create_settings_modal, create_doc_modal
15
  from ui.renderers import (
16
+ create_agent_dashboard,
17
  create_summary_card,
18
+ create_task_card
 
 
19
  )
20
+ from core.session import UserSession
21
+ from services.planner_service import PlannerService
22
  from core.visualizers import create_animated_map
23
 
 
 
 
 
 
 
 
 
24
  class LifeFlowAI:
25
  def __init__(self):
26
  self.service = PlannerService()
27
 
28
+ def _get_dashboard_html(self, active_agent: str = None, status: str = "idle", message: str = "Waiting") -> str:
29
+ agents = ['team', 'scout', 'optimizer', 'navigator', 'weatherman', 'presenter']
30
+ status_dict = {a: {'status': 'idle', 'message': 'Standby'} for a in agents}
31
+ if active_agent in status_dict:
32
+ status_dict[active_agent] = {'status': status, 'message': message}
33
+ if active_agent != 'team' and status == 'working':
34
+ status_dict['team'] = {'status': 'working', 'message': f'Monitoring {active_agent}...'}
35
+ return create_agent_dashboard(status_dict)
36
+
37
+ def _check_api_status(self, session_data):
38
+ session = UserSession.from_dict(session_data)
39
  has_gemini = bool(session.custom_settings.get("gemini_api_key"))
40
  has_google = bool(session.custom_settings.get("google_maps_api_key"))
41
+ msg = "✅ System Ready" if (has_gemini and has_google) else "⚠️ Missing API Keys"
42
+ return msg
43
 
44
+ def _get_gradio_chat_history(self, session):
45
+ history = []
46
+ for msg in session.chat_history:
47
+ history.append({"role": msg["role"], "content": msg["message"]})
48
+ return history
 
 
49
 
50
+ # ================= Event Wrappers =================
 
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  def analyze_wrapper(self, user_input, auto_loc, lat, lon, session_data):
53
  session = UserSession.from_dict(session_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  iterator = self.service.run_step1_analysis(user_input, auto_loc, lat, lon, session)
 
55
  for event in iterator:
56
  evt_type = event.get("type")
 
 
57
  current_session = event.get("session", session)
58
+ if evt_type == "error":
 
 
 
59
  yield (
60
+ gr.update(), # step1
61
+ gr.update(), # step2
62
+ gr.update(), # step3
63
+ gr.HTML(f"<div style='color:red'>{event.get('message')}</div>"), # s1_stream
64
+ gr.update(), # task_list
65
+ gr.update(), # task_summary
66
+ gr.update(), # chatbot
67
+ current_session.to_dict() # state
68
  )
69
+ return
 
 
 
 
 
 
 
 
70
 
71
+ if evt_type == "stream":
72
  yield (
73
+ gr.update(visible=True),
74
+ gr.update(visible=False),
75
+ gr.update(visible=False),
76
+ event.get("stream_text", ""),
77
+ gr.update(),
78
+ gr.update(),
79
+ gr.update(),
80
+ current_session.to_dict()
81
  )
82
 
83
+ elif evt_type == "complete":
84
+ tasks_html = self.service.generate_task_list_html(current_session)
85
+ date_str = datetime.now().strftime("%Y-%m-%d")
86
+ loc_str = "Selected Location"
87
+ if current_session.lat and current_session.lng:
88
+ loc_str = f"{current_session.lat:.2f}, {current_session.lng:.2f}"
89
+
90
+ summary_html = create_summary_card(len(current_session.task_list), event.get("high_priority", 0), event.get("total_time", 0), location=loc_str, date=date_str)
91
+ chat_history = self._get_gradio_chat_history(current_session)
92
+ if not chat_history:
93
+ chat_history = [
94
+ {"role": "assistant", "content": event.get('stream_text', "")},
95
+ {"role": "assistant", "content": "Hi! I'm LifeFlow. Tell me if you want to change priorities, add stops, or adjust times."}]
96
+ current_session.chat_history.append({"role": "assistant", "message": "Hi! I'm LifeFlow...", "time": ""})
97
 
98
  yield (
99
+ gr.update(visible=False), # Hide S1
100
+ gr.update(visible=True), # Show S2
101
+ gr.update(visible=False), # Hide S3
102
+ "", # Clear Stream
103
+ gr.HTML(tasks_html),
104
+ gr.HTML(summary_html),
105
+ chat_history,
106
+ current_session.to_dict()
107
  )
108
 
 
 
 
 
109
  def chat_wrapper(self, msg, session_data):
110
  session = UserSession.from_dict(session_data)
111
  iterator = self.service.modify_task_chat(msg, session)
 
112
  for event in iterator:
113
+ sess = event.get("session", session)
114
+ tasks_html = self.service.generate_task_list_html(sess)
115
+ summary_html = event.get("summary_html", gr.HTML())
116
+ gradio_history = self._get_gradio_chat_history(sess)
117
+ yield (gradio_history, tasks_html, summary_html, sess.to_dict())
 
 
 
 
 
 
118
 
119
+ def step3_wrapper(self, session_data):
 
 
 
120
  session = UserSession.from_dict(session_data)
121
 
122
+ # Init variables
123
+ log_content = ""
124
+ report_content = ""
 
125
 
126
+ tasks_html = self.service.generate_task_list_html(session)
127
+ init_dashboard = self._get_dashboard_html('team', 'working', 'Initializing...')
 
 
 
 
 
 
 
 
 
 
128
 
129
+ # HTML Content (No Indentation)
130
+ loading_html = inspect.cleandoc("""
131
+ <div style="text-align: center; padding: 60px 20px; color: #64748b;">
132
+ <div style="font-size: 48px; margin-bottom: 20px; animation: pulse 1.5s infinite;">🧠</div>
133
+ <div style="font-size: 1.2rem; font-weight: 600; color: #334155;">AI Team is analyzing your request...</div>
134
+ <div style="font-size: 0.9rem; margin-top: 8px;">Checking routes, weather, and optimizing schedule.</div>
135
+ </div>
136
+ <style>@keyframes pulse { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.7; } 100% { transform: scale(1); opacity: 1; } }</style>
137
+ """)
138
+
139
+ init_log = '<div style="padding: 10px; color: #94a3b8; font-style: italic;">Waiting for agents...</div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
+ yield (init_dashboard, init_log, loading_html, tasks_html, session.to_dict())
 
 
 
 
 
142
 
143
  iterator = self.service.run_step3_team(session)
 
144
 
145
  for event in iterator:
146
+ sess = event.get("session", session)
147
  evt_type = event.get("type")
 
 
 
 
 
 
148
 
 
149
  if evt_type == "report_stream":
150
+ report_content = event.get("content", "")
151
+
152
+ if evt_type == "reasoning_update":
153
+ agent, status, msg = event.get("agent_status")
154
+ time_str = datetime.now().strftime('%H:%M:%S')
155
+ log_entry = f"""
156
+ <div style="margin-bottom: 8px; border-left: 3px solid #6366f1; padding-left: 10px;">
157
+ <div style="font-size: 0.75rem; color: #94a3b8;">{time_str} • {agent.upper()}</div>
158
+ <div style="color: #334155; font-size: 0.9rem;">{msg}</div>
159
+ </div>
160
+ """
161
+ log_content = log_entry + log_content
162
+ dashboard_html = self._get_dashboard_html(agent, status, msg)
163
+ log_html = f'<div style="height: 500px; overflow-y: auto; padding: 10px; background: #fff;">{log_content}</div>'
164
+ current_report = report_content + "\n\n" if report_content else loading_html
165
+
166
+ yield (dashboard_html, log_html, current_report, tasks_html, sess.to_dict())
167
+
168
+ if evt_type == "complete":
169
+ final_report = event.get("report_html", report_content)
170
+ final_log = f"""
171
+ <div style="margin-bottom: 8px; border-left: 3px solid #10b981; padding-left: 10px; background: #ecfdf5; padding: 10px;">
172
+ <div style="font-weight: bold; color: #059669;">✅ Planning Completed</div>
173
+ </div>
174
+ {log_content}
175
+ """
176
+ final_log_html = f'<div style="height: 500px; overflow-y: auto; padding: 10px; background: #fff;">{final_log}</div>'
177
+
178
+ yield (
179
+ self._get_dashboard_html('team', 'complete', 'Planning Finished'),
180
+ final_log_html,
181
+ final_report,
182
+ tasks_html,
183
+ sess.to_dict()
184
+ )
185
 
 
 
 
186
  def step4_wrapper(self, session_data):
187
  session = UserSession.from_dict(session_data)
 
 
 
 
 
 
 
 
 
188
  result = self.service.run_step4_finalize(session)
 
189
 
190
+ # 🔥 Wrapper 回傳 7 個值
191
+ if result['type'] == 'success':
192
  return (
193
+ gr.update(visible=False), # 1. Step 3 Hide
194
+ gr.update(visible=True), # 2. Step 4 Show
195
+ result['summary_tab_html'], # 3. Summary Tab HTML
196
+ result['report_md'], # 4. Report Tab Markdown
197
+ result['task_list_html'], # 5. Task List HTML
198
+ result['map_fig'], # 6. Map Plot
199
+ session.to_dict() # 7. Session State
200
  )
201
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  return (
203
  gr.update(visible=True), gr.update(visible=False),
204
+ "", "", "", None,
205
+ session.to_dict()
 
 
 
 
 
206
  )
207
 
208
+ # ================= Main UI Builder =================
 
 
209
 
210
+ def build_interface(self):
211
+ with gr.Blocks(title="LifeFlow AI", css=".gradio-container {max-width: 100% !important; padding: 0;}") as demo:
212
  gr.HTML(get_enhanced_css())
213
+
214
+ session_state = gr.State(UserSession().to_dict())
215
+
216
+ with gr.Column(elem_classes="step-container"):
217
+ home_btn, theme_btn, settings_btn, doc_btn = create_header()
218
+ stepper = create_progress_stepper(1)
219
+ status_bar = gr.Markdown("Ready", visible=False)
220
+
221
+ # STEP 1
222
+ with gr.Group(visible=True, elem_classes="step-container centered-input-container") as step1_container:
223
+ (input_area, s1_stream_output, user_input, auto_loc, loc_group, lat_in, lon_in, analyze_btn) = create_input_form("")
224
+
225
+ # STEP 2
226
+ with gr.Group(visible=False, elem_classes="step-container") as step2_container:
227
+ gr.Markdown("### ✅ Review & Refine Tasks")
228
+ with gr.Row(elem_classes="step2-split-view"):
229
+ with gr.Column(scale=1):
230
+ with gr.Group(elem_classes="panel-container"):
231
+ gr.HTML('<div class="panel-header">📋 Your Tasks</div>')
232
+ with gr.Group(elem_classes="scrollable-content"):
233
+ task_summary_box = gr.HTML()
234
+ task_list_box = gr.HTML()
235
+ with gr.Column(scale=1):
236
+ with gr.Group(elem_classes="panel-container chat-panel-native"):
237
+ chatbot = gr.Chatbot(label="AI Assistant", type="messages", height=540, elem_classes="native-chatbot", bubble_full_width=False)
238
+ with gr.Row(elem_classes="chat-input-row"):
239
+ chat_input = gr.Textbox(show_label=False, placeholder="Type to modify tasks...", container=False, scale=5, autofocus=True)
240
+ chat_send = gr.Button("➤", variant="primary", scale=1, min_width=50)
241
+ with gr.Row(elem_classes="action-footer"):
242
+ back_btn = gr.Button("← Back", variant="secondary", scale=1)
243
+ plan_btn = gr.Button("🚀 Start Planning", variant="primary", scale=2)
244
+
245
+ # STEP 3
246
+ with gr.Group(visible=False, elem_classes="step-container") as step3_container:
247
+ gr.Markdown("### 🤖 AI Team Operations")
248
+ with gr.Group(elem_classes="agent-dashboard-container"):
249
+ agent_dashboard = gr.HTML(value=self._get_dashboard_html())
250
+
251
+ with gr.Row():
252
+ with gr.Column(scale=2):
253
+ with gr.Tabs():
254
+ with gr.Tab("📝 Full Report"):
255
+ with gr.Group(elem_classes="live-report-wrapper"):
256
+ live_report_md = gr.Markdown()
257
+ with gr.Tab("📋 Task List"):
258
+ with gr.Group(elem_classes="panel-container"):
259
+ with gr.Group(elem_classes="scrollable-content"):
260
+ task_list_s3 = gr.HTML()
261
+ with gr.Column(scale=1):
262
+ gr.Markdown("### ⚡ Activity Log")
263
+ with gr.Group(elem_classes="panel-container"):
264
+ planning_log = gr.HTML(value="Waiting...")
265
+
266
+ # STEP 4
267
+ with gr.Group(visible=False, elem_classes="step-container") as step4_container:
268
+ with gr.Row():
269
+ # Left: Tabs (Summary / Report / Tasks)
270
+ with gr.Column(scale=1, elem_classes="split-left-panel"):
271
+ with gr.Tabs():
272
+ with gr.Tab("📊 Summary"):
273
+ summary_tab_output = gr.HTML()
274
+
275
+ with gr.Tab("📝 Full Report"):
276
+ report_tab_output = gr.Markdown()
277
+
278
+ with gr.Tab("📋 Task List"):
279
+ task_list_tab_output = gr.HTML()
280
+
281
+ # Right: Hero Map
282
+ with gr.Column(scale=2, elem_classes="split-right-panel"):
283
+ map_view = gr.Plot(label="Route Map", show_label=False)
284
+
285
+ # Modals & Events
286
+ (settings_modal, g_key, w_key, gem_key, model_sel, close_set, save_set, set_stat) = create_settings_modal()
287
  doc_modal, close_doc_btn = create_doc_modal()
288
 
289
+ settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal])
290
+ close_set.click(fn=lambda: gr.update(visible=False), outputs=[settings_modal])
291
+ doc_btn.click(fn=lambda: gr.update(visible=True), outputs=[doc_modal])
292
+ close_doc_btn.click(fn=lambda: gr.update(visible=False), outputs=[doc_modal])
293
+ theme_btn.click(fn=None, js="() => { document.querySelector('.gradio-container').classList.toggle('theme-dark'); }")
294
 
 
 
 
295
  analyze_event = analyze_btn.click(
296
  fn=self.analyze_wrapper,
297
+ inputs=[user_input, auto_loc, lat_in, lon_in, session_state],
298
+ outputs=[step1_container, step2_container, step3_container, s1_stream_output, task_list_box, task_summary_box, chatbot, session_state]
299
+ ).then(fn=lambda: update_stepper(2), outputs=[stepper])
 
 
 
 
 
300
 
301
+ chat_send.click(fn=self.chat_wrapper, inputs=[chat_input, session_state], outputs=[chatbot, task_list_box, task_summary_box, session_state]).then(fn=lambda: "", outputs=[chat_input])
302
+ chat_input.submit(fn=self.chat_wrapper, inputs=[chat_input, session_state], outputs=[chatbot, task_list_box, task_summary_box, session_state]).then(fn=lambda: "", outputs=[chat_input])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
+ step3_start = plan_btn.click(
305
+ fn=lambda: (gr.update(visible=False), gr.update(visible=True), update_stepper(3)),
306
+ outputs=[step2_container, step3_container, stepper]
307
+ ).then(
308
  fn=self.step3_wrapper,
309
  inputs=[session_state],
310
+ outputs=[agent_dashboard, planning_log, live_report_md, task_list_s3, session_state],
311
+ show_progress="hidden"
 
 
 
 
 
 
 
 
 
312
  )
313
 
314
+ step3_start.then(
315
  fn=self.step4_wrapper,
316
  inputs=[session_state],
317
+ # 🔥🔥🔥 關鍵修正:這裡列出了 7 個 Outputs,必須對應 Wrapper 回傳的 7 個值
318
  outputs=[
319
+ step3_container, # 1. Hide Step 3
320
+ step4_container, # 2. Show Step 4
321
+ summary_tab_output, # 3. Summary Tab
322
+ report_tab_output, # 4. Report Tab
323
+ task_list_tab_output, # 5. Task List Tab
324
+ map_view, # 6. Map
325
+ session_state # 7. State
326
  ]
327
+ ).then(fn=lambda: update_stepper(4), outputs=[stepper])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
+ def reset_all(session_data):
330
+ old_session = UserSession.from_dict(session_data)
331
+ new_session = UserSession()
332
+ new_session.custom_settings = old_session.custom_settings
333
+ return (gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), update_stepper(1), new_session.to_dict(), "")
334
+
335
+ home_btn.click(fn=reset_all, inputs=[session_state], outputs=[step1_container, step2_container, step3_container, step4_container, stepper, session_state, user_input])
336
+ back_btn.click(fn=reset_all, inputs=[session_state], outputs=[step1_container, step2_container, step3_container, step4_container, stepper, session_state, user_input])
337
+
338
+ save_set.click(fn=self.save_settings, inputs=[g_key, w_key, gem_key, model_sel, session_state], outputs=[set_stat, session_state, status_bar])
339
+ auto_loc.change(fn=toggle_location_inputs, inputs=auto_loc, outputs=loc_group)
340
+ demo.load(fn=self._check_api_status, inputs=[session_state], outputs=[status_bar])
341
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  return demo
343
 
344
+ def save_settings(self, g, w, gem, m, s_data):
345
+ sess = UserSession.from_dict(s_data)
346
+ sess.custom_settings.update({'google_maps_api_key': g, 'openweather_api_key': w, 'gemini_api_key': gem, 'model': m})
347
+ return "✅ Saved", sess.to_dict(), "Settings Updated"
348
 
349
  def main():
350
  app = LifeFlowAI()
351
  demo = app.build_interface()
352
+ demo.launch(server_name="0.0.0.0", server_port=8080, share=True, show_error=True)
 
353
 
354
  if __name__ == "__main__":
355
  main()
core/session.py CHANGED
@@ -1,22 +1,13 @@
1
  """
2
  LifeFlow AI - Session Management
3
- 負責定義用戶會話的數據結構 (Model)
4
  """
5
  import uuid
6
  from typing import Dict, Any, Optional
7
- from src.agent.base import UserState, Location
8
- from src.infra.context import set_session_id
9
  from src.infra.config import get_settings
10
- from src.infra.logger import get_logger
11
-
12
- logger = get_logger(__name__)
13
 
14
  class UserSession:
15
- """
16
- 單個用戶的會話數據
17
- 每個用戶有獨立的實例,確保資料隔離
18
- """
19
-
20
  def __init__(self):
21
  self.session_id: Optional[str] = None
22
  self.planner_agent = None
@@ -27,24 +18,22 @@ class UserSession:
27
  self.chat_history: list = []
28
  self.planning_completed: bool = False
29
 
30
- # 存儲位置信息(用於重新初始化 Agent)
 
 
31
  self.lat: Optional[float] = None
32
  self.lng: Optional[float] = None
33
-
34
- # [Security Fix] 用戶個人的自定義設定 (API Keys, Model Choice)
35
  self.custom_settings: Dict[str, Any] = {}
36
-
37
- # 系統預設設定
38
  self.agno_settings = get_settings()
39
 
40
  def to_dict(self) -> Dict[str, Any]:
41
- """序列化為字典(用於 Gradio State)"""
42
  return {
43
  'session_id': self.session_id,
44
  'task_list': self.task_list,
45
  'reasoning_messages': self.reasoning_messages,
46
  'chat_history': self.chat_history,
47
  'planning_completed': self.planning_completed,
 
48
  'lat': self.lat,
49
  'lng': self.lng,
50
  'custom_settings': self.custom_settings
@@ -52,13 +41,13 @@ class UserSession:
52
 
53
  @classmethod
54
  def from_dict(cls, data: Dict[str, Any]) -> 'UserSession':
55
- """從字典恢復(用於 Gradio State)"""
56
  session = cls()
57
  session.session_id = data.get('session_id')
58
  session.task_list = data.get('task_list', [])
59
  session.reasoning_messages = data.get('reasoning_messages', [])
60
  session.chat_history = data.get('chat_history', [])
61
  session.planning_completed = data.get('planning_completed', False)
 
62
  session.lat = data.get('lat')
63
  session.lng = data.get('lng')
64
  session.custom_settings = data.get('custom_settings', {})
 
1
  """
2
  LifeFlow AI - Session Management
3
+ 新增 final_report 欄位,用於在 Step 3 和 Step 4 之間傳遞報告內容
4
  """
5
  import uuid
6
  from typing import Dict, Any, Optional
7
+ from src.agent.base import UserState
 
8
  from src.infra.config import get_settings
 
 
 
9
 
10
  class UserSession:
 
 
 
 
 
11
  def __init__(self):
12
  self.session_id: Optional[str] = None
13
  self.planner_agent = None
 
18
  self.chat_history: list = []
19
  self.planning_completed: bool = False
20
 
21
+ # 🔥 新增:儲存 Step 3 生成的完整報告
22
+ self.final_report: str = ""
23
+
24
  self.lat: Optional[float] = None
25
  self.lng: Optional[float] = None
 
 
26
  self.custom_settings: Dict[str, Any] = {}
 
 
27
  self.agno_settings = get_settings()
28
 
29
  def to_dict(self) -> Dict[str, Any]:
 
30
  return {
31
  'session_id': self.session_id,
32
  'task_list': self.task_list,
33
  'reasoning_messages': self.reasoning_messages,
34
  'chat_history': self.chat_history,
35
  'planning_completed': self.planning_completed,
36
+ 'final_report': self.final_report, # 🔥 序列化
37
  'lat': self.lat,
38
  'lng': self.lng,
39
  'custom_settings': self.custom_settings
 
41
 
42
  @classmethod
43
  def from_dict(cls, data: Dict[str, Any]) -> 'UserSession':
 
44
  session = cls()
45
  session.session_id = data.get('session_id')
46
  session.task_list = data.get('task_list', [])
47
  session.reasoning_messages = data.get('reasoning_messages', [])
48
  session.chat_history = data.get('chat_history', [])
49
  session.planning_completed = data.get('planning_completed', False)
50
+ session.final_report = data.get('final_report', "") # 🔥 反序列化
51
  session.lat = data.get('lat')
52
  session.lng = data.get('lng')
53
  session.custom_settings = data.get('custom_settings', {})
core/visualizers.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
- LifeFlow AI - Visualizers
3
- 負責生成 Plotly 圖表物件 (完整版)
 
4
  """
5
  import plotly.graph_objects as go
6
  from core.helpers import decode_polyline
@@ -10,57 +11,49 @@ logger = get_logger(__name__)
10
 
11
  def create_animated_map(structured_data=None):
12
  """
13
- 生成 Plotly 地圖,包含真實道路軌跡 (Polyline)
14
- :param structured_data: 從資料庫載入的完整行程資料
15
  """
 
16
  fig = go.Figure()
17
 
18
- # 如果沒有數據,回傳預設地圖 (台北101周邊)
 
 
 
 
19
  if not structured_data:
20
- default_lats = [25.033, 25.038]
21
- default_lons = [121.565, 121.560]
22
  fig.add_trace(go.Scattermapbox(
23
- lat=default_lats, lon=default_lons,
24
- mode='markers', marker=dict(size=10, color='#999'),
25
- text=['No Data'], hoverinfo='text'
26
  ))
27
- center_lat, center_lon = 25.033, 121.565
28
  else:
29
  try:
30
  timeline = structured_data.get("timeline", [])
31
  precise_result = structured_data.get("precise_traffic_result", {})
32
  legs = precise_result.get("legs", [])
33
 
34
- # 1. 繪製路線 (Polyline) - 藍色路徑
35
- route_lats = []
36
- route_lons = []
37
-
38
  for leg in legs:
39
  poly_str = leg.get("polyline")
40
  if poly_str:
41
  decoded = decode_polyline(poly_str)
42
- # 解碼後是 list of (lat, lng)
43
- leg_lats = [coord[0] for coord in decoded]
44
- leg_lons = [coord[1] for coord in decoded]
45
-
46
- # 插入 None 來斷開不同路段 (如果你希望路段分開)
47
- # 或者直接串接 (視覺上較連續)
48
- route_lats.extend(leg_lats)
49
- route_lons.extend(leg_lons)
50
- route_lats.append(None) # 斷開不同路段,避免直線飛越
51
  route_lons.append(None)
52
 
53
  if route_lats:
54
  fig.add_trace(go.Scattermapbox(
55
- lat=route_lats,
56
- lon=route_lons,
57
- mode='lines',
58
- line=dict(width=4, color='#4A90E2'),
59
- name='Route',
60
- hoverinfo='none'
61
  ))
62
 
63
- # 2. 繪製站點 (Markers)
64
  lats, lons, hover_texts, colors, sizes = [], [], [], [], []
65
 
66
  for i, stop in enumerate(timeline):
@@ -72,56 +65,48 @@ def create_animated_map(structured_data=None):
72
  lats.append(lat)
73
  lons.append(lng)
74
 
75
- # 構建 Hover Text
76
- name = stop.get("location", "Stop")
77
- time_str = stop.get("time", "")
78
  weather = stop.get("weather", "")
79
- text = f"<b>{i}. {name}</b><br>🕒 {time_str}<br>🌤️ {weather}"
 
80
  hover_texts.append(text)
81
 
82
- # 樣式:起點綠色,終點紅色,中間黃色
83
  if i == 0:
84
- colors.append('#50C878') # Start
85
- sizes.append(15)
86
- elif i == len(timeline) - 1:
87
- colors.append('#FF6B6B') # End
88
- sizes.append(15)
89
  else:
90
- colors.append('#F5A623') # Middle
91
- sizes.append(12)
92
-
93
- fig.add_trace(go.Scattermapbox(
94
- lat=lats, lon=lons,
95
- mode='markers+text',
96
- marker=dict(size=sizes, color=colors, allowoverlap=True),
97
- text=[str(i) for i in range(len(lats))],
98
- textposition="top center",
99
- textfont=dict(size=14, color='black', family="Arial Black"),
100
- hovertext=hover_texts,
101
- hoverinfo='text',
102
- name='Stops'
103
- ))
104
-
105
- # 計算中心點
106
  if lats:
 
 
 
 
 
 
 
 
 
 
107
  center_lat = sum(lats) / len(lats)
108
  center_lon = sum(lons) / len(lons)
109
- else:
110
- center_lat, center_lon = 25.033, 121.565
111
 
112
  except Exception as e:
113
- logger.error(f"Error generating map data: {e}", exc_info=True)
114
- center_lat, center_lon = 25.033, 121.565
115
 
116
- # 設定地圖樣式
117
  fig.update_layout(
118
  mapbox=dict(
119
  style='open-street-map',
120
  center=dict(lat=center_lat, lon=center_lon),
121
- zoom=12
122
  ),
123
  margin=dict(l=0, r=0, t=0, b=0),
124
- height=500,
125
  showlegend=False
126
  )
127
 
 
1
  """
2
+ LifeFlow AI - Visualizers (Fixed)
3
+ 確保 create_animated_map 永遠回傳 go.Figure 物件
4
+ ✅ 防止回傳字串導致 Gradio 崩潰
5
  """
6
  import plotly.graph_objects as go
7
  from core.helpers import decode_polyline
 
11
 
12
  def create_animated_map(structured_data=None):
13
  """
14
+ 生成地圖物件。
15
+ 注意:必須回傳 go.Figure(),絕對不能回傳字串!
16
  """
17
+ # 1. 初始化一個空地圖 (預設值)
18
  fig = go.Figure()
19
 
20
+ # 設定預設中心點 (台北101)
21
+ center_lat, center_lon = 25.033, 121.565
22
+ zoom_level = 13
23
+
24
+ # 2. 如果沒有數據,直接回傳空地圖
25
  if not structured_data:
26
+ # 加一個透明點定住中心
 
27
  fig.add_trace(go.Scattermapbox(
28
+ lat=[center_lat], lon=[center_lon],
29
+ mode='markers', marker=dict(size=0, opacity=0)
 
30
  ))
 
31
  else:
32
  try:
33
  timeline = structured_data.get("timeline", [])
34
  precise_result = structured_data.get("precise_traffic_result", {})
35
  legs = precise_result.get("legs", [])
36
 
37
+ # --- 繪製路線 (Polyline) ---
38
+ route_lats, route_lons = [], []
 
 
39
  for leg in legs:
40
  poly_str = leg.get("polyline")
41
  if poly_str:
42
  decoded = decode_polyline(poly_str)
43
+ route_lats.extend([c[0] for c in decoded])
44
+ route_lons.extend([c[1] for c in decoded])
45
+ # 斷開不同路段
46
+ route_lats.append(None)
 
 
 
 
 
47
  route_lons.append(None)
48
 
49
  if route_lats:
50
  fig.add_trace(go.Scattermapbox(
51
+ lat=route_lats, lon=route_lons, mode='lines',
52
+ line=dict(width=5, color='#6366f1'), # Primary Purple
53
+ name='Route', hoverinfo='none'
 
 
 
54
  ))
55
 
56
+ # --- 繪製站點 (Markers) ---
57
  lats, lons, hover_texts, colors, sizes = [], [], [], [], []
58
 
59
  for i, stop in enumerate(timeline):
 
65
  lats.append(lat)
66
  lons.append(lng)
67
 
68
+ name = stop.get("location", f"Stop {i}")
69
+ time = stop.get("time", "")
 
70
  weather = stop.get("weather", "")
71
+
72
+ text = f"<b>{i+1}. {name}</b><br>🕒 {time}<br>🌤️ {weather}"
73
  hover_texts.append(text)
74
 
75
+ # 顏色邏輯:起點綠、終點紅、中間黃
76
  if i == 0:
77
+ colors.append('#10b981'); sizes.append(20)
78
+ elif i == len(timeline)-1:
79
+ colors.append('#ef4444'); sizes.append(20)
 
 
80
  else:
81
+ colors.append('#f59e0b'); sizes.append(15)
82
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  if lats:
84
+ fig.add_trace(go.Scattermapbox(
85
+ lat=lats, lon=lons, mode='markers+text',
86
+ marker=dict(size=sizes, color=colors, allowoverlap=True),
87
+ text=[str(i+1) for i in range(len(lats))],
88
+ textposition="top center",
89
+ textfont=dict(size=14, color='black', family="Arial Black"),
90
+ hovertext=hover_texts, hoverinfo='text', name='Stops'
91
+ ))
92
+
93
+ # 更新中心點為路徑中心
94
  center_lat = sum(lats) / len(lats)
95
  center_lon = sum(lons) / len(lons)
 
 
96
 
97
  except Exception as e:
98
+ logger.error(f"Map generation error: {e}", exc_info=True)
99
+ # 發生錯誤時,保持回傳預設的 fig,不要讓程式崩潰
100
 
101
+ # 3. 設定地圖樣式
102
  fig.update_layout(
103
  mapbox=dict(
104
  style='open-street-map',
105
  center=dict(lat=center_lat, lon=center_lon),
106
+ zoom=zoom_level
107
  ),
108
  margin=dict(l=0, r=0, t=0, b=0),
109
+ height=700, # 地圖高度
110
  showlegend=False
111
  )
112
 
services/planner_service.py CHANGED
@@ -12,15 +12,10 @@ from contextlib import contextmanager
12
  # 導入 Core & UI 模組
13
  from core.session import UserSession
14
  from ui.renderers import (
15
- create_agent_stream_output,
16
- create_agent_card_enhanced,
17
  create_task_card,
18
  create_summary_card,
19
  create_timeline_html_enhanced,
20
- generate_chat_history_html_bubble,
21
- get_reasoning_html_reversed,
22
  create_result_visualization,
23
- create_metrics_cards
24
  )
25
  from core.visualizers import create_animated_map
26
 
@@ -422,10 +417,15 @@ class PlannerService:
422
  high_priority = sum(1 for t in session.task_list if t.get("priority") == "HIGH")
423
  total_time = sum(int(t.get("duration", "0").split()[0]) for t in session.task_list if t.get("duration"))
424
  summary_html = create_summary_card(len(session.task_list), high_priority, total_time)
 
 
 
 
 
425
 
426
  session.chat_history[-1] = {
427
  "role": "assistant",
428
- "message": "✅ Tasks updated based on your request",
429
  "time": datetime.now().strftime("%H:%M:%S")
430
  }
431
  self._add_reasoning(session, "planner", f"Updated: {user_message[:30]}...")
@@ -554,7 +554,8 @@ class PlannerService:
554
  logger.info(f"Run time (s): {time.perf_counter() - start_time}")
555
 
556
  # 迴圈結束
557
- report_html = f"## 🎯 Planning Complete\n\n{report_content}"
 
558
  yield {
559
  "type": "complete",
560
  "report_html": report_html,
@@ -571,36 +572,105 @@ class PlannerService:
571
  def run_step4_finalize(self, session: UserSession) -> Dict[str, Any]:
572
  try:
573
  session = self._get_live_session(session)
574
-
575
  final_ref_id = poi_repo.get_last_id_by_session(session.session_id)
576
- if not final_ref_id:
577
- raise ValueError(f"No final result found , {session.session_id}")
578
 
579
  structured_data = poi_repo.load(final_ref_id)
580
 
581
- # 1. Timeline (右側 Tab)
582
  timeline_html = create_timeline_html_enhanced(structured_data.get("timeline", []))
583
 
584
- # 2. Metrics Cards (左側簡單指標) -> 使用新恢復的函數
585
  metrics = structured_data.get("metrics", {})
586
  traffic = structured_data.get("traffic_summary", {})
587
- metrics_html = create_metrics_cards(metrics, traffic)
588
 
589
- # 3. Detailed Visualization (中間結果區) -> 使用完整版函數
590
- result_viz = create_result_visualization(session.task_list, structured_data)
591
-
592
- # 4. Real Map (地圖 Tab) -> 傳入 structured_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  map_fig = create_animated_map(structured_data)
 
 
 
 
 
 
 
594
 
595
- self._add_reasoning(session, "team", "🎉 All completed")
596
  session.planning_completed = True
597
 
598
  return {
599
  "type": "success",
600
- "timeline_html": timeline_html,
601
- "metrics_html": metrics_html,
602
- "result_html": result_viz,
603
- "map_fig": map_fig,
604
  "session": session
605
  }
606
 
 
12
  # 導入 Core & UI 模組
13
  from core.session import UserSession
14
  from ui.renderers import (
 
 
15
  create_task_card,
16
  create_summary_card,
17
  create_timeline_html_enhanced,
 
 
18
  create_result_visualization,
 
19
  )
20
  from core.visualizers import create_animated_map
21
 
 
417
  high_priority = sum(1 for t in session.task_list if t.get("priority") == "HIGH")
418
  total_time = sum(int(t.get("duration", "0").split()[0]) for t in session.task_list if t.get("duration"))
419
  summary_html = create_summary_card(len(session.task_list), high_priority, total_time)
420
+ #
421
+
422
+ done_message = session.chat_history[-1]["message"].replace(
423
+ "🤔 AI is thinking...",
424
+ "✅ Tasks updated based on your request")
425
 
426
  session.chat_history[-1] = {
427
  "role": "assistant",
428
+ "message": done_message,
429
  "time": datetime.now().strftime("%H:%M:%S")
430
  }
431
  self._add_reasoning(session, "planner", f"Updated: {user_message[:30]}...")
 
554
  logger.info(f"Run time (s): {time.perf_counter() - start_time}")
555
 
556
  # 迴圈結束
557
+ session.final_report = report_html = f"## 🎯 Planning Complete\n\n{report_content}"
558
+
559
  yield {
560
  "type": "complete",
561
  "report_html": report_html,
 
572
  def run_step4_finalize(self, session: UserSession) -> Dict[str, Any]:
573
  try:
574
  session = self._get_live_session(session)
 
575
  final_ref_id = poi_repo.get_last_id_by_session(session.session_id)
576
+ if not final_ref_id: raise ValueError(f"No results found")
 
577
 
578
  structured_data = poi_repo.load(final_ref_id)
579
 
580
+ # 1. Timeline
581
  timeline_html = create_timeline_html_enhanced(structured_data.get("timeline", []))
582
 
583
+ # 2. Summary Card & Metrics (快樂表)
584
  metrics = structured_data.get("metrics", {})
585
  traffic = structured_data.get("traffic_summary", {})
 
586
 
587
+ print("metrics\n", metrics)
588
+
589
+ # 🔥 組合 Summary Tab 的內容
590
+ # 包含: 總結卡片 + 演算法快樂表 + 時間軸
591
+ task_count = len(session.task_list)
592
+ high_prio = sum(1 for t in session.task_list if t.get("priority") == "HIGH")
593
+ total_time = traffic.get("total_duration_min", 0)
594
+
595
+ summary_card = create_summary_card(task_count, high_prio, int(total_time))
596
+ metrics_viz = create_result_visualization(session.task_list, structured_data)
597
+
598
+ # 將三個元件合併為一個 HTML (用於 Summary Tab)
599
+ # 順序: Card -> Viz -> Timeline
600
+ full_summary_html = f"{summary_card}<br/>{metrics_viz}<br/><h3>📍 Timeline</h3>{timeline_html}"
601
+
602
+ task_count = len(session.task_list)
603
+ high_prio = sum(1 for t in session.task_list if t.get("priority") == "HIGH")
604
+
605
+ # 時間 (優先使用優化後的時間,如果沒有則用交通總時間)
606
+ total_time = metrics.get("optimized_duration_min", traffic.get("total_duration_min", 0))
607
+
608
+ # 距離 (公尺轉公里)
609
+ dist_m = metrics.get("optimized_distance_m", 0)
610
+ total_dist_km = dist_m / 1000.0
611
+
612
+ # 效率與節省 (用於快樂表)
613
+ efficiency = metrics.get("route_efficiency_pct", 0)
614
+ saved_dist_m = metrics.get("distance_saved_m", 0)
615
+ saved_time_min = metrics.get("time_saved_min", 0)
616
+
617
+ summary_card = create_summary_card(task_count, high_prio, int(total_time))
618
+
619
+ # B. 進階數據快樂表 (AI Efficiency & Distance)
620
+ # 根據效率分數改變顏色 (高於 80% 綠色,否則橘色)
621
+ eff_color = "#047857" if efficiency >= 80 else "#d97706"
622
+ eff_bg = "#ecfdf5" if efficiency >= 80 else "#fffbeb"
623
+ eff_border = "#a7f3d0" if efficiency >= 80 else "#fde68a"
624
+
625
+ ai_stats_html = f"""
626
+ <div style="display: flex; gap: 12px; margin-bottom: 20px;">
627
+ <div style="flex: 1; background: {eff_bg}; padding: 16px; border-radius: 12px; border: 1px solid {eff_border};">
628
+ <div style="font-size: 0.8rem; color: {eff_color}; font-weight: 600; display: flex; align-items: center; gap: 4px;">
629
+ <span>🚀 AI EFFICIENCY</span>
630
+ </div>
631
+ <div style="font-size: 1.8rem; font-weight: 800; color: {eff_color}; line-height: 1.2;">
632
+ {efficiency:.1f}%
633
+ </div>
634
+ <div style="font-size: 0.75rem; color: {eff_color}; opacity: 0.9; margin-top: 4px;">
635
+ ⚡ Saved {saved_time_min:.0f} mins
636
+ </div>
637
+ </div>
638
+
639
+ <div style="flex: 1; background: #eff6ff; padding: 16px; border-radius: 12px; border: 1px solid #bfdbfe;">
640
+ <div style="font-size: 0.8rem; color: #2563eb; font-weight: 600;">🚗 TOTAL DISTANCE</div>
641
+ <div style="font-size: 1.8rem; font-weight: 800; color: #1d4ed8; line-height: 1.2;">
642
+ {total_dist_km:.2f} <span style="font-size: 1rem;">km</span>
643
+ </div>
644
+ <div style="font-size: 0.75rem; color: #2563eb; opacity: 0.9; margin-top: 4px;">
645
+ 📉 Reduced {saved_dist_m} m
646
+ </div>
647
+ </div>
648
+ </div>
649
+ """
650
+
651
+ full_summary_html = f"{summary_card}{ai_stats_html}<h3>📍 Itinerary Timeline</h3>{timeline_html}"
652
+
653
+ # C. 其他 Tab 內容
654
+ task_list_html = self.generate_task_list_html(session)
655
+
656
+ # 3. Map
657
  map_fig = create_animated_map(structured_data)
658
+ if isinstance(map_fig, str):
659
+ logger.error(f"CRITICAL: map_fig is a string ('{map_fig}'). Creating default map.")
660
+ from core.visualizers import create_animated_map as default_map_gen
661
+ map_fig = default_map_gen(None)
662
+
663
+ # 4. Task List
664
+ task_list_html = self.generate_task_list_html(session)
665
 
 
666
  session.planning_completed = True
667
 
668
  return {
669
  "type": "success",
670
+ "summary_tab_html": full_summary_html, # 快樂表
671
+ "report_md": session.final_report, # Full Report
672
+ "task_list_html": task_list_html, # Task List
673
+ "map_fig": map_fig, # Map
674
  "session": session
675
  }
676
 
ui/components/header.py CHANGED
@@ -1,30 +1,40 @@
1
  """
2
- LifeFlow AI - Header Component (Fixed Layout - Simplified)
 
 
3
  """
4
  import gradio as gr
5
 
6
  def create_header():
7
  """
8
- 創建 Header:
9
- 1. 功能鍵區 (懸浮固定 - 使用 elem_id 強制定位)
10
- 2. 標題區 (普通流式佈局)
 
11
  """
12
- # 1. 功能按鈕區
13
- # 🔥 重點:移除內部的 gr.Row,只用 gr.Group,並給它一個唯一的 elem_id
14
- with gr.Group(elem_id="floating-menu"):
15
- home_btn = gr.Button("🏠", elem_classes="dock-btn")
16
- theme_btn = gr.Button("🌓", elem_classes="dock-btn")
17
- settings_btn = gr.Button("⚙️", elem_classes="dock-btn")
18
- doc_btn = gr.Button("📖", elem_classes="dock-btn")
19
-
20
- # 2. 左側標題區
21
- with gr.Row(elem_classes="header-row"):
22
- with gr.Column(scale=1):
23
  gr.HTML("""
24
  <div class="app-header-left">
25
- <h1 style="margin: 0; font-size: 1.8rem;">✨ LifeFlow AI</h1>
26
- <p style="margin: 0; color: #64748b; font-size: 0.9rem;">Intelligent Trip Planner</p>
 
 
 
 
 
 
 
27
  </div>
28
  """)
29
 
 
 
 
 
 
 
 
 
30
  return home_btn, theme_btn, settings_btn, doc_btn
 
1
  """
2
+ LifeFlow AI - Header Component (Optimized)
3
+ ✅ 修復佈局問題,使用 Row/Column 避免 fixed positioning
4
+ ✅ 響應式友好設計
5
  """
6
  import gradio as gr
7
 
8
  def create_header():
9
  """
10
+ 創建優化後的 Header:
11
+ - 使用 Row/Column 佈局避免 fixed positioning 問題
12
+ - 響應式設計友好
13
+ - 按鈕水平排列
14
  """
15
+ with gr.Row(elem_classes="app-header-container"):
16
+ # 左側:Logo 和標題
17
+ with gr.Column(scale=4, min_width=300):
 
 
 
 
 
 
 
 
18
  gr.HTML("""
19
  <div class="app-header-left">
20
+ <h1 style="margin: 0; font-size: 2rem; font-weight: 800;
21
+ background: linear-gradient(to right, #6366f1, #ec4899);
22
+ -webkit-background-clip: text;
23
+ -webkit-text-fill-color: transparent;">
24
+ ✨ LifeFlow AI
25
+ </h1>
26
+ <p style="margin: 5px 0 0 0; color: #64748b; font-size: 0.95rem;">
27
+ Intelligent Trip Planner
28
+ </p>
29
  </div>
30
  """)
31
 
32
+ # 右側:功能按鈕
33
+ with gr.Column(scale=1, min_width=200):
34
+ with gr.Row(elem_classes="header-controls"):
35
+ home_btn = gr.Button("🏠", size="sm", elem_classes="header-btn")
36
+ theme_btn = gr.Button("🌓", size="sm", elem_classes="header-btn")
37
+ settings_btn = gr.Button("⚙️", size="sm", elem_classes="header-btn")
38
+ doc_btn = gr.Button("📖", size="sm", elem_classes="header-btn")
39
+
40
  return home_btn, theme_btn, settings_btn, doc_btn
ui/components/input_form.py CHANGED
@@ -1,5 +1,6 @@
1
  """
2
- LifeFlow AI - Input Form Component (UX Improved)
 
3
  """
4
  import gradio as gr
5
 
@@ -9,10 +10,18 @@ def create_input_form(agent_stream_html):
9
  with gr.Group(elem_classes="glass-card") as input_area:
10
  gr.Markdown("### 📝 What's your plan today?")
11
 
12
- # 1. Agent 狀態區 (改為較小的視覺)
13
- agent_stream_output = gr.HTML(value=agent_stream_html, label="Agent Status")
14
 
15
- # 2. 主要輸入區
 
 
 
 
 
 
 
 
 
 
16
  user_input = gr.Textbox(
17
  label="Describe your tasks",
18
  placeholder="e.g., I need to visit the dentist at 10am, then buy groceries from Costco, and pick up a package...",
@@ -20,26 +29,24 @@ def create_input_form(agent_stream_html):
20
  elem_id="main-input"
21
  )
22
 
23
- # 3. 快速範例 (Quick Prompts) - UX 大加分
 
 
 
 
 
 
 
 
24
  gr.Examples(
25
  examples=[
26
- ["Plan a trip to visit Taipei 101, then have lunch at Din Tai Fung, and go to the National Palace Museum."],
27
- ["I need to go to the hospital for a checkup at 9 AM, then buy some flowers, and meet a friend for coffee at 2 PM."],
28
  ["Errands run: Post office, bank, and supermarket. Start at 10 AM, finish by 1 PM."]
29
  ],
30
  inputs=user_input,
31
  label="Quick Start Examples"
32
  )
33
 
34
- gr.Markdown("---")
35
-
36
- # 4. 位置設定 (使用 Accordion 收納次要資訊)
37
- with gr.Accordion("📍 Location Settings", open=False):
38
- auto_location = gr.Checkbox(label="Auto-detect my location (Simulated)", value=False)
39
- with gr.Group(visible=True) as location_inputs:
40
- with gr.Row():
41
- lat_input = gr.Number(label="Latitude", value=25.033, precision=6, scale=1)
42
- lon_input = gr.Number(label="Longitude", value=121.565, precision=6, scale=1)
43
 
44
  # 5. 主按鈕
45
  analyze_btn = gr.Button("🚀 Analyze & Plan Trip", variant="primary", size="lg")
 
1
  """
2
+ LifeFlow AI - Input Form Component (Fixed Layout)
3
+ ✅ 強制將 AI 狀態欄位移至按鈕上方
4
  """
5
  import gradio as gr
6
 
 
10
  with gr.Group(elem_classes="glass-card") as input_area:
11
  gr.Markdown("### 📝 What's your plan today?")
12
 
 
 
13
 
14
+ # 這裡的 HTML 元件會顯示串流文字
15
+ agent_stream_output = gr.HTML(
16
+ value=agent_stream_html,
17
+ label="Agent Status",
18
+ elem_classes="agent-stream-box-step1",
19
+ visible=True # 確保預設可見(即使是空的),佔據空間
20
+ )
21
+
22
+ gr.Markdown("---")
23
+
24
+ # 1. 主要輸入區
25
  user_input = gr.Textbox(
26
  label="Describe your tasks",
27
  placeholder="e.g., I need to visit the dentist at 10am, then buy groceries from Costco, and pick up a package...",
 
29
  elem_id="main-input"
30
  )
31
 
32
+ # 2. 位置設定
33
+ with gr.Accordion("📍 Location Settings", open=False):
34
+ auto_location = gr.Checkbox(label="Auto-detect my location (Simulated)", value=False)
35
+ with gr.Group(visible=True) as location_inputs:
36
+ with gr.Row():
37
+ lat_input = gr.Number(label="Latitude", value=25.033, precision=6, scale=1)
38
+ lon_input = gr.Number(label="Longitude", value=121.565, precision=6, scale=1)
39
+
40
+ # 3. 快速範例
41
  gr.Examples(
42
  examples=[
43
+ ["Plan a trip to visit Taipei 101, then have lunch at Din Tai Fung."],
 
44
  ["Errands run: Post office, bank, and supermarket. Start at 10 AM, finish by 1 PM."]
45
  ],
46
  inputs=user_input,
47
  label="Quick Start Examples"
48
  )
49
 
 
 
 
 
 
 
 
 
 
50
 
51
  # 5. 主按鈕
52
  analyze_btn = gr.Button("🚀 Analyze & Plan Trip", variant="primary", size="lg")
ui/components/progress_stepper.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LifeFlow AI - Progress Stepper Component
3
+ ✅ 顯示用戶當前所在步驟
4
+ ✅ 視覺化流程進度
5
+ """
6
+ import gradio as gr
7
+
8
+
9
+ def create_progress_stepper(current_step: int = 1):
10
+ """
11
+ 創建流程進度條
12
+
13
+ Args:
14
+ current_step: 當前步驟 (1-4)
15
+ 1: Analyze
16
+ 2: Confirm
17
+ 3: Optimize
18
+ 4: Review
19
+
20
+ Returns:
21
+ HTML 組件
22
+ """
23
+ steps = [
24
+ {'num': 1, 'label': 'Analyze', 'icon': '📋', 'desc': 'Extract tasks'},
25
+ {'num': 2, 'label': 'Confirm', 'icon': '✅', 'desc': 'Review & modify'},
26
+ {'num': 3, 'label': 'Optimize', 'icon': '⚡', 'desc': 'Route planning'},
27
+ {'num': 4, 'label': 'Review', 'icon': '🎉', 'desc': 'Check results'}
28
+ ]
29
+
30
+ html = '<div class="progress-stepper">'
31
+
32
+ for i, step in enumerate(steps):
33
+ is_active = step['num'] == current_step
34
+ is_complete = step['num'] < current_step
35
+
36
+ state_class = ''
37
+ if is_complete:
38
+ state_class = 'complete'
39
+ elif is_active:
40
+ state_class = 'active'
41
+
42
+ html += f"""
43
+ <div class="step {state_class}">
44
+ <div class="step-number">{step['icon']}</div>
45
+ <div class="step-label">{step['label']}</div>
46
+ <div class="step-desc">{step['desc']}</div>
47
+ </div>
48
+ """
49
+
50
+ # 添加連接線
51
+ if i < len(steps) - 1:
52
+ connector_class = 'complete' if is_complete else ''
53
+ html += f'<div class="step-connector {connector_class}"></div>'
54
+
55
+ html += '</div>'
56
+
57
+ # 內嵌樣式
58
+ css = """
59
+ <style>
60
+ .progress-stepper {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: space-between;
64
+ padding: 24px;
65
+ background: white;
66
+ border-radius: 16px;
67
+ margin-bottom: 24px;
68
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
69
+ }
70
+
71
+ .step {
72
+ display: flex;
73
+ flex-direction: column;
74
+ align-items: center;
75
+ position: relative;
76
+ flex: 1;
77
+ min-width: 80px;
78
+ }
79
+
80
+ .step-number {
81
+ width: 56px;
82
+ height: 56px;
83
+ border-radius: 50%;
84
+ background: #e2e8f0;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ font-size: 24px;
89
+ transition: all 0.3s ease;
90
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
91
+ margin-bottom: 8px;
92
+ }
93
+
94
+ .step.active .step-number {
95
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
96
+ color: white;
97
+ box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.2), 0 4px 8px rgba(0, 0, 0, 0.15);
98
+ transform: scale(1.1);
99
+ animation: pulse-ring 2s infinite;
100
+ }
101
+
102
+ .step.complete .step-number {
103
+ background: #10b981;
104
+ color: white;
105
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
106
+ }
107
+
108
+ .step-label {
109
+ margin-top: 8px;
110
+ font-size: 14px;
111
+ font-weight: 600;
112
+ color: #64748b;
113
+ transition: color 0.3s;
114
+ }
115
+
116
+ .step.active .step-label {
117
+ color: #6366f1;
118
+ font-weight: 700;
119
+ }
120
+
121
+ .step.complete .step-label {
122
+ color: #10b981;
123
+ }
124
+
125
+ .step-desc {
126
+ font-size: 11px;
127
+ color: #94a3b8;
128
+ margin-top: 4px;
129
+ text-align: center;
130
+ }
131
+
132
+ .step-connector {
133
+ flex: 1;
134
+ height: 3px;
135
+ background: #e2e8f0;
136
+ margin: 0 8px;
137
+ transition: background 0.3s;
138
+ position: relative;
139
+ top: -20px;
140
+ }
141
+
142
+ .step-connector.complete {
143
+ background: #10b981;
144
+ }
145
+
146
+ @keyframes pulse-ring {
147
+ 0% {
148
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7), 0 4px 8px rgba(0, 0, 0, 0.15);
149
+ }
150
+ 50% {
151
+ box-shadow: 0 0 0 8px rgba(99, 102, 241, 0), 0 4px 8px rgba(0, 0, 0, 0.15);
152
+ }
153
+ 100% {
154
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0), 0 4px 8px rgba(0, 0, 0, 0.15);
155
+ }
156
+ }
157
+
158
+ /* 響應式 */
159
+ @media (max-width: 768px) {
160
+ .progress-stepper {
161
+ padding: 16px 8px;
162
+ }
163
+
164
+ .step {
165
+ min-width: 60px;
166
+ }
167
+
168
+ .step-number {
169
+ width: 44px;
170
+ height: 44px;
171
+ font-size: 18px;
172
+ }
173
+
174
+ .step-label {
175
+ font-size: 12px;
176
+ }
177
+
178
+ .step-desc {
179
+ display: none;
180
+ }
181
+
182
+ .step-connector {
183
+ margin: 0 4px;
184
+ }
185
+ }
186
+
187
+ @media (max-width: 480px) {
188
+ .progress-stepper {
189
+ flex-wrap: wrap;
190
+ gap: 12px;
191
+ }
192
+
193
+ .step-connector {
194
+ display: none;
195
+ }
196
+
197
+ .step {
198
+ flex-direction: row;
199
+ gap: 8px;
200
+ width: calc(50% - 6px);
201
+ }
202
+
203
+ .step-number {
204
+ width: 36px;
205
+ height: 36px;
206
+ font-size: 16px;
207
+ }
208
+ }
209
+ </style>
210
+ """
211
+
212
+ return gr.HTML(css + html)
213
+
214
+
215
+ def update_stepper(step_number: int):
216
+ """更新進度條到指定步驟"""
217
+ return create_progress_stepper(step_number)
ui/renderers.py CHANGED
@@ -1,508 +1,169 @@
1
- """
2
- LifeFlow AI - UI Renderers
3
- 負責生成 HTML 字串與前端展示元件
4
- """
5
  from datetime import datetime
6
  from config import AGENTS_INFO
7
 
8
 
9
- # ui/renderers.py
10
-
11
- def create_agent_stream_output(text: str = None) -> str:
12
- """創建 Agent 串流輸出 HTML (支援動態傳入文字)"""
13
-
14
- # 如果沒有傳入文字,顯示預設值
15
- if not text:
16
- display_content = 'Ready to analyze your tasks...<span class="stream-cursor"></span>'
17
- else:
18
- # 這裡直接使用傳入的 text
19
- display_content = f'{text}<span class="stream-cursor"></span>'
20
-
21
- return f"""
22
- <div class="stream-container">
23
- <div class="stream-text" style="white-space: pre-wrap; line-height: 1.6; min-height: 60px;">{display_content}</div>
24
- </div>
25
- <style>
26
- .stream-text {{
27
- font-family: 'Inter', sans-serif;
28
- color: #334155;
29
- font-size: 1.05rem;
30
- }}
31
- .stream-cursor {{
32
- display: inline-block;
33
- width: 8px;
34
- height: 18px;
35
- background-color: #6366f1;
36
- margin-left: 5px;
37
- animation: blink 1s infinite;
38
- vertical-align: sub;
39
- }}
40
- @keyframes blink {{ 0%, 100% {{ opacity: 1; }} 50% {{ opacity: 0; }} }}
41
- </style>
42
- """
43
-
44
-
45
- def create_agent_card_enhanced(agent_key: str, status: str = "idle", message: str = "") -> str:
46
- agent = AGENTS_INFO.get(agent_key, {})
47
- color = agent.get("color", "#6366f1")
48
-
49
- status_color = "#94a3b8" # gray
50
- active_class = ""
51
-
52
- if status == "working":
53
- status_color = "#f59e0b" # orange
54
- active_class = "working" # 🔥 這裡對應 CSS 的動畫 class
55
- elif status == "complete":
56
- status_color = "#10b981" # green
57
-
58
- # 確保 active_class 被加入 div class 列表
59
- return f"""
60
- <div class="agent-card-mini {active_class}" style="border-top: 3px solid {color}">
61
- <div class="agent-avatar-mini">{agent.get("avatar", "🤖")}</div>
62
- <div class="agent-name-mini">{agent.get("name", "Agent")}</div>
63
- <span class="agent-status-dot" style="background-color: {status_color}"></span>
64
- <div style="font-size: 10px; color: #64748b; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;">{message}</div>
65
- </div>
66
- """
67
-
68
- def generate_chat_history_html_bubble(session) -> str:
69
- """生成對話氣泡 HTML (使用 session 物件)"""
70
- # 注意:這裡依賴 session 物件的 chat_history 屬性
71
- if not session.chat_history:
72
- return """
73
- <div style="text-align: center; padding: 40px; color: #94a3b8;">
74
- <div style="font-size: 40px; margin-bottom: 10px;">👋</div>
75
- <p>Hi! I'm LifeFlow. Tell me your plans, or ask me to modify the tasks above.</p>
76
- </div>
77
- """
78
 
79
- html = '<div class="chat-history-container">'
80
- for msg in session.chat_history:
81
- role = msg["role"] # user or assistant
82
- role_class = "user" if role == "user" else "assistant"
83
 
84
- html += f"""
85
- <div class="chat-message {role_class}">
86
- <div class="chat-bubble">
87
- {msg["message"]}
88
- <div class="chat-time">{msg["time"]}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </div>
90
  </div>
91
  """
92
- html += '</div>'
93
- return html
94
-
95
- def create_timeline_html_enhanced(timeline):
96
- if not timeline: return "<div>No timeline data</div>"
97
-
98
- html = '<div class="timeline-container">'
99
- for i, stop in enumerate(timeline):
100
- time = stop.get("time", "--:--")
101
- location = stop.get("location", "Unknown")
102
- weather = stop.get("weather", "")
103
 
104
- icon = "📍"
105
- if i == 0:
106
- icon = "🏠"
107
- elif i == len(timeline) - 1:
108
- icon = "🏁"
109
- elif "food" in location.lower():
110
- icon = "🍽️"
111
- elif "hospital" in location.lower():
112
- icon = "🏥"
113
-
114
- html += f"""
115
- <div class="timeline-item">
116
- <div class="timeline-left">
117
- <div class="timeline-icon-box">{icon}</div>
118
  </div>
119
- <div class="timeline-content-card">
120
- <div style="display:flex; justify-content:space-between; margin-bottom:5px;">
121
- <span style="font-weight:bold; color:#334155; font-size:1.1rem;">{location}</span>
122
- <span style="background:#eff6ff; color:#3b82f6; padding:2px 8px; border-radius:12px; font-size:0.8rem; font-weight:600;">{time}</span>
123
- </div>
124
- <div style="font-size:0.9rem; color:#64748b;">
125
- {f'🌤️ {weather}' if weather else ''}
126
- </div>
127
  </div>
128
  </div>
129
- """
130
- html += '</div>'
131
  return html
132
 
133
- def create_task_card(task_num: int, task_title: str, priority: str,
134
- time_window: dict, duration: str, location: str, icon: str = "📋") -> str:
135
- priority_color_map = {
136
- "HIGH": "#FF6B6B",
137
- "MEDIUM": "#F5A623",
138
- "LOW": "#7ED321"
139
- }
140
-
141
- # 處理 time_window 邏輯
142
- display_time = "Anytime"
143
- if isinstance(time_window, dict):
144
- start = time_window.get('earliest_time', None)
145
- end = time_window.get('latest_time', None)
146
- start_str = datetime.fromisoformat(start).strftime("%H:%M") if start else "Before"
147
- end_str = datetime.fromisoformat(end).strftime("%H:%M") if end else "After"
148
-
149
- if start_str == "Before" and end_str == "After":
150
- display_time = "Anytime"
151
- elif start_str == "After":
152
- display_time = f"After {end_str}"
153
- elif end_str == "Before":
154
- display_time = f"Before {start_str}"
155
- else:
156
- display_time = f"{start_str} - {end_str}"
157
- elif time_window:
158
- display_time = str(time_window)
159
-
160
- priority_class = f"priority-{priority.lower()}"
161
- task_color = priority_color_map.get(priority, "#999")
162
 
 
 
163
  return f"""
164
- <div class="task-card" style="--task-priority-color: {task_color};">
165
- <div class="task-header">
166
- <div class="task-title">
167
- <span class="task-icon">{icon}</span>
168
- <span>#{task_num}: {task_title}</span>
169
- </div>
170
- <span class="priority-badge {priority_class}">{priority}</span>
171
- </div>
172
- <div class="task-details">
173
- <div class="task-detail-item">
174
- <span>🕐</span>
175
- <span>{display_time}</span>
176
- </div>
177
- <div class="task-detail-item">
178
- <span>⏱️</span>
179
- <span>{duration}</span>
180
  </div>
181
- <div class="task-detail-item">
182
- <span>📍</span>
183
- <span>{location}</span>
184
  </div>
185
  </div>
186
- </div>
187
- """
188
-
189
- def create_summary_card(total_tasks: int, high_priority: int, total_time: int) -> str:
190
- return f"""
191
- <div class="summary-card">
192
- <h3 style="margin: 0 0 15px 0; font-size: 20px;">📋 Task Summary</h3>
193
- <div class="summary-stats">
194
- <div class="stat-item">
195
- <span class="stat-value">{total_tasks}</span>
196
- <span class="stat-label">Tasks</span>
197
- </div>
198
- <div class="stat-item">
199
- <span class="stat-value">{high_priority}</span>
200
- <span class="stat-label">High Priority</span>
201
- </div>
202
- <div class="stat-item">
203
- <span class="stat-value">{total_time}</span>
204
- <span class="stat-label">min total</span>
205
  </div>
206
- </div>
207
- </div>
208
- """
209
-
210
- def get_reasoning_html_reversed(reasoning_messages: list = None) -> str:
211
- """獲取推理過程 HTML(反向排序,最新在上)"""
212
- if not reasoning_messages:
213
- return '<div class="reasoning-timeline"><p style="text-align: center; opacity: 0.6;">No reasoning messages yet...</p></div>'
214
-
215
- items_html = ""
216
- for msg in reversed(reasoning_messages[-20:]):
217
- agent_info = AGENTS_INFO.get(msg.get('agent', 'planner'), {})
218
- color = agent_info.get('color', '#4A90E2')
219
- avatar = agent_info.get('avatar', '🤖')
220
-
221
- items_html += f"""
222
- <div class="reasoning-item" style="--agent-color: {color};">
223
- <div class="reasoning-header">
224
- <span>{avatar}</span>
225
- <span class="reasoning-agent">{msg.get('agent', 'Agent')}</span>
226
- <span class="reasoning-time">{msg.get('time', '')}</span>
227
  </div>
228
- <div class="reasoning-content">{msg.get('message', '')}</div>
229
- </div>
230
- """
231
-
232
- return f'<div class="reasoning-timeline">{items_html}</div>'
233
-
234
- def create_celebration_animation() -> str:
235
- """創建慶祝動畫(純 CSS 撒花效果)"""
236
- import random
237
- confetti_html = ""
238
- colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2']
239
-
240
- for i in range(100):
241
- left = random.randint(0, 100)
242
- delay = random.uniform(0, 2)
243
- duration = random.uniform(3, 6)
244
- rotation = random.randint(0, 360)
245
- color = random.choice(colors)
246
- size = random.randint(8, 15)
247
-
248
- confetti_html += f"""
249
- <div class="confetti" style="
250
- left: {left}%;
251
- animation-delay: {delay}s;
252
- animation-duration: {duration}s;
253
- background-color: {color};
254
- width: {size}px;
255
- height: {size}px;
256
- transform: rotate({rotation}deg);
257
- "></div>
258
- """
259
-
260
- return f"""
261
- <div class="celebration-overlay">
262
- {confetti_html}
263
- </div>
264
- <style>
265
- @keyframes confetti-fall {{
266
- 0% {{ transform: translateY(-100vh) rotate(0deg); opacity: 1; }}
267
- 100% {{ transform: translateY(100vh) rotate(720deg); opacity: 0; }}
268
- }}
269
- @keyframes celebration-fade {{
270
- 0% {{ opacity: 1; }}
271
- 80% {{ opacity: 1; }}
272
- 100% {{ opacity: 0; }}
273
- }}
274
- .celebration-overlay {{
275
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
276
- pointer-events: none; z-index: 9999; overflow: hidden;
277
- animation: celebration-fade 6s forwards;
278
- }}
279
- .confetti {{
280
- position: absolute; top: -20px;
281
- animation: confetti-fall linear infinite; opacity: 0.9;
282
- }}
283
- </style>
284
- """
285
-
286
- def create_result_visualization(task_list: list, structured_data: dict) -> str:
287
- """創建詳細的結果視覺化卡片"""
288
- metrics = structured_data.get("metrics", {})
289
- traffic = structured_data.get("traffic_summary", {})
290
- timeline = structured_data.get("timeline", [])
291
-
292
- completed_tasks = metrics.get("completed_tasks", 0)
293
- total_tasks = metrics.get("total_tasks", 0)
294
- total_dist_km = traffic.get("total_distance_km", 0)
295
- total_dur_min = traffic.get("total_duration_min", 0)
296
-
297
- hours = int(total_dur_min // 60)
298
- mins = int(total_dur_min % 60)
299
- time_str = f"{hours}h {mins}m" if hours > 0 else f"{mins}min"
300
-
301
- efficiency = metrics.get("route_efficiency_pct", 90)
302
- time_saved_val = metrics.get("time_saved_min", 0)
303
- dist_saved_val_km = metrics.get("distance_saved_m", 0) / 1000
304
-
305
- # ... (為簡潔起見,這裡保留原有的 HTML 拼接邏輯,只是函數位置改變)
306
- # 這裡的代碼與原 utils.py 中的邏輯完全一致,只是為了空間我縮略了中間的 HTML string
307
- # 在實際實施時,請複製原 utils.py 中完整的 HTML 模板代碼
308
-
309
- html = f"""
310
- <div class="result-visualization">
311
- <div class="result-header">
312
- <h2 style="color: #50C878; margin: 0; font-size: 28px; display: flex; align-items: center; gap: 10px;">
313
- <span class="celebration-icon" style="font-size: 36px; animation: bounce 1s infinite;">🎉</span>
314
- <span>Planning Complete!</span>
315
- </h2>
316
- <p style="margin: 10px 0 0 0; opacity: 0.8; font-size: 14px;">Optimized based on real-time traffic & weather</p>
317
- </div>
318
- <div class="quick-summary" style="margin-top: 25px;">
319
- <div class="metric-value">{completed_tasks} / {total_tasks}</div>
320
  </div>
321
  </div>
322
  """
323
- # 注意:請確保將完整的 HTML 邏輯搬移過來
324
-
325
- # 為了確保功能正常,這裡是一個簡化的佔位符,請使用原來的完整代碼替換這裡
326
- # 實作時,請將原 utils.py 中 create_result_visualization 的完整內容複製到這裡
327
- return html # 替換為完整 HTML
328
 
329
 
330
- def create_metrics_cards(metrics, traffic_summary):
331
- """生成左側的簡單 Metrics (三張卡片)"""
332
- if traffic_summary is None: traffic_summary = {}
333
- if metrics is None: metrics = {}
334
 
335
- total_distance = traffic_summary.get("total_distance_km", 0)
336
- total_duration = traffic_summary.get("total_duration_min", 0)
337
- efficiency = metrics.get("route_efficiency_pct", 90)
338
-
339
- # 根據效率改變顏色
340
- eff_color = "#50C878" if efficiency >= 80 else "#F5A623"
 
 
 
 
341
 
342
  return f"""
343
- <div class="metrics-container">
344
- <div class="metric-card">
345
- <h3>📏 Distance</h3>
346
- <p class="metric-value">{total_distance:.1f} km</p>
347
- </div>
348
- <div class="metric-card">
349
- <h3>⏱️ Duration</h3>
350
- <p class="metric-value">{total_duration:.0f} min</p>
351
  </div>
352
- <div class="metric-card" style="border-bottom: 3px solid {eff_color}">
353
- <h3>⚡ Efficiency</h3>
354
- <p class="metric-value" style="color: {eff_color}">{efficiency:.0f}%</p>
355
  </div>
356
  </div>
357
  """
358
 
359
 
360
- # 🔥 [Full Restoration] 恢復完整的結果視覺化
361
- def create_result_visualization(task_list: list, structured_data: dict) -> str:
362
- """創建詳細的結果視覺化卡片"""
363
- metrics = structured_data.get("metrics", {})
364
- traffic = structured_data.get("traffic_summary", {})
365
- timeline = structured_data.get("timeline", [])
366
-
367
- completed_tasks = metrics.get("completed_tasks", 0)
368
- total_tasks = metrics.get("total_tasks", 0)
369
- total_dist_km = traffic.get("total_distance_km", 0)
370
- total_dur_min = traffic.get("total_duration_min", 0)
371
-
372
- hours = int(total_dur_min // 60)
373
- mins = int(total_dur_min % 60)
374
- time_str = f"{hours}h {mins}m" if hours > 0 else f"{mins}min"
375
-
376
- efficiency = metrics.get("route_efficiency_pct", 90)
377
- time_saved_pct = metrics.get("time_improvement_pct", 0)
378
- dist_saved_pct = metrics.get("distance_improvement_pct", 0)
379
- time_saved_val = metrics.get("time_saved_min", 0)
380
- dist_saved_val_km = metrics.get("distance_saved_m", 0) / 1000
381
-
382
- # 開始構建 HTML
383
- html = f"""
384
- <div class="result-visualization">
385
- <div class="result-header">
386
- <h2 style="color: #50C878; margin: 0; font-size: 28px; display: flex; align-items: center; gap: 10px;">
387
- <span class="celebration-icon" style="font-size: 36px; animation: bounce 1s infinite;">🎉</span>
388
- <span>Planning Complete!</span>
389
- </h2>
390
- <p style="margin: 10px 0 0 0; opacity: 0.8; font-size: 14px;">Optimized based on real-time traffic & weather</p>
391
- </div>
392
-
393
- <div class="quick-summary" style="margin-top: 25px;">
394
- <h3 style="font-size: 18px; margin: 0 0 15px 0; color: #333;">📊 Quick Summary</h3>
395
- <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
396
- <div class="summary-metric">
397
- <div class="metric-icon">🎯</div>
398
- <div class="metric-content">
399
- <div class="metric-value">{completed_tasks} / {total_tasks}</div>
400
- <div class="metric-label">Tasks Completed</div>
401
- </div>
402
- </div>
403
- <div class="summary-metric">
404
- <div class="metric-icon">⏱️</div>
405
- <div class="metric-content">
406
- <div class="metric-value">{time_str}</div>
407
- <div class="metric-label">Total Duration</div>
408
- </div>
409
- </div>
410
- <div class="summary-metric">
411
- <div class="metric-icon">🚗</div>
412
- <div class="metric-content">
413
- <div class="metric-value">{total_dist_km:.1f} km</div>
414
- <div class="metric-label">Total Distance</div>
415
- </div>
416
- </div>
417
- <div class="summary-metric success">
418
- <div class="metric-icon">⚡</div>
419
- <div class="metric-content">
420
- <div class="metric-value">{efficiency:.0f}%</div>
421
- <div class="metric-label">Efficiency Score</div>
422
- </div>
423
- </div>
424
- </div>
425
  </div>
 
426
 
427
- <div class="timeline-preview" style="margin-top: 25px;">
428
- <h3 style="font-size: 18px; margin: 0 0 15px 0; color: #333;">🗓️ Timeline Preview</h3>
429
- <div class="timeline-container">
430
- """
431
-
432
- # 綁定時間線數據
433
  for i, stop in enumerate(timeline):
434
- time = stop.get("time", "")
435
- loc_name = stop.get("location", "")
436
- weather = stop.get("weather", "")
437
 
438
- icon = "🏁" if i == 0 else ("🔚" if i == len(timeline) - 1 else "📍")
 
 
 
439
 
440
  html += f"""
441
- <div class="timeline-item">
442
- <div class="timeline-time">{time}</div>
443
- <div class="timeline-connector">━━</div>
444
- <div class="timeline-task">
445
- <span class="timeline-icon">{icon}</span>
446
- <div class="timeline-info">
447
- <span class="timeline-title">{loc_name}</span>
448
- <span class="timeline-meta">{weather}</span>
449
- </div>
450
- </div>
451
  </div>
 
 
 
452
  """
 
 
453
 
454
- if i < len(timeline) - 1:
455
- next_travel = timeline[i + 1].get("travel_time_from_prev", "0 mins")
456
- html += f"""
457
- <div class="timeline-travel">
458
- <div class="travel-arrow">↓</div>
459
- <div class="travel-time">Drive: {next_travel}</div>
460
- </div>
461
- """
462
 
463
- html += """
464
- </div>
465
- </div>
466
 
467
- <div class="optimization-metrics" style="margin-top: 25px;">
468
- <h3 style="font-size: 18px; margin: 0 0 15px 0; color: #333;">📈 Optimization Metrics</h3>
469
- <div class="metrics-grid">
470
- """
471
 
472
- viz_metrics = [
473
- ("Route Efficiency", efficiency, "#50C878"),
474
- ("Time Saved", time_saved_pct, "#4A90E2"),
475
- ("Distance Reduced", dist_saved_pct, "#F5A623")
476
- ]
477
 
478
- for label, value, color in viz_metrics:
479
- if value is None: value = 0
480
- display_val = min(max(value, 0), 100)
481
- html += f"""
482
- <div class="metric-bar">
483
- <div class="metric-bar-header">
484
- <span class="metric-bar-label">{label}</span>
485
- <span class="metric-bar-value">{value:.1f}%</span>
486
- </div>
487
- <div class="metric-bar-track">
488
- <div class="metric-bar-fill" style="width: {display_val}%; background: {color};"></div>
489
- </div>
490
- </div>
491
- """
492
 
493
- html += f"""
494
- </div>
495
- <div class="optimization-stats" style="margin-top: 15px; display: flex; gap: 20px; flex-wrap: wrap;">
496
- <div class="stat-badge">
497
- <span class="stat-badge-icon">⏱️</span>
498
- <span class="stat-badge-text">Saved: <strong>{time_saved_val} min</strong></span>
499
- </div>
500
- <div class="stat-badge">
501
- <span class="stat-badge-icon">📏</span>
502
- <span class="stat-badge-text">Reduced: <strong>{dist_saved_val_km:.1f} km</strong></span>
503
- </div>
504
- </div>
505
- </div>
506
- </div>
507
- """
508
- return html
 
1
+ # ui/renderers.py
 
 
 
2
  from datetime import datetime
3
  from config import AGENTS_INFO
4
 
5
 
6
+ def _format_iso_time(iso_str: str) -> str:
7
+ if not iso_str or iso_str == "N/A": return ""
8
+ try:
9
+ dt = datetime.fromisoformat(iso_str)
10
+ return dt.strftime("%H:%M")
11
+ except Exception:
12
+ if "T" in iso_str: return iso_str.split("T")[1][:5]
13
+ return iso_str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
 
 
 
 
15
 
16
+ def create_agent_stream_output(text: str = None) -> str:
17
+ return f'<div style="font-family: monospace; color: #334155;">{text}</div>'
18
+
19
+
20
+ def create_agent_dashboard(status_dict: dict) -> str:
21
+ leader_key = 'team'
22
+ member_keys = ['scout', 'weatherman', 'optimizer', 'navigator', 'presenter']
23
+
24
+ def _render_card(key):
25
+ info = AGENTS_INFO.get(key, {})
26
+ state = status_dict.get(key, {})
27
+ status = state.get('status', 'idle')
28
+ msg = state.get('message', 'Standby')
29
+ active_class = "working" if status == "working" else ""
30
+ icon = info.get('icon', '🤖')
31
+ name = info.get('name', key.title())
32
+ role = info.get('role', 'Agent')
33
+ color = info.get('color', '#6366f1')
34
+
35
+ return f"""
36
+ <div class="agent-card-wrap {active_class}">
37
+ <div class="agent-card-inner" style="border-top: 3px solid {color}">
38
+ <div class="agent-avatar">{icon}</div>
39
+ <div class="agent-name">{name}</div>
40
+ <div class="agent-role">{role}</div>
41
+ <div class="status-badge">{msg}</div>
42
  </div>
43
  </div>
44
  """
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ html = f"""
47
+ <div class="agent-war-room">
48
+ <div class="org-chart">
49
+ <div class="org-level">
50
+ {_render_card(leader_key)}
 
 
 
 
 
 
 
 
 
51
  </div>
52
+ <div class="connector-line"></div>
53
+ <div class="connector-horizontal"></div>
54
+ <div class="org-level">
55
+ {''.join([_render_card(k) for k in member_keys])}
 
 
 
 
56
  </div>
57
  </div>
58
+ </div>
59
+ """
60
  return html
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ def create_summary_card(total_tasks: int, high_priority: int, total_time: int, location: str = "Taipei City",
64
+ date: str = "Today") -> str:
65
  return f"""
66
+ <div style="background: #f8fafc; border-radius: 12px; padding: 16px; margin-bottom: 20px; border: 1px solid #e2e8f0;">
67
+ <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
68
+ <div>
69
+ <div style="font-size: 0.8rem; color: #64748b; font-weight: 600;">TRIP SUMMARY</div>
70
+ <div style="font-size: 1.1rem; font-weight: 700; color: #1e293b;">{location}</div>
71
+ <div style="font-size: 0.9rem; color: #6366f1;">📅 {date}</div>
 
 
 
 
 
 
 
 
 
 
72
  </div>
73
+ <div style="text-align: right;">
74
+ <div style="font-size: 2rem; font-weight: 800; color: #6366f1; line-height: 1;">{total_tasks}</div>
75
+ <div style="font-size: 0.8rem; color: #64748b;">Tasks</div>
76
  </div>
77
  </div>
78
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
79
+ <div style="background: white; padding: 8px; border-radius: 8px; text-align: center; border: 1px solid #e2e8f0;">
80
+ <div style="font-size: 0.8rem; color: #64748b;">Duration</div>
81
+ <div style="font-weight: 600; color: #334155;">{total_time} min</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  </div>
83
+ <div style="background: white; padding: 8px; border-radius: 8px; text-align: center; border: 1px solid #e2e8f0;">
84
+ <div style="font-size: 0.8rem; color: #64748b;">High Prio</div>
85
+ <div style="font-weight: 600; color: #ef4444;">{high_priority}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  </div>
88
  </div>
89
  """
 
 
 
 
 
90
 
91
 
92
+ def create_task_card(task_num: int, task_title: str, priority: str, time_window: dict, duration: str, location: str,
93
+ icon: str = "📋") -> str:
94
+ p_color = {"HIGH": "#ef4444", "MEDIUM": "#f59e0b", "LOW": "#10b981"}.get(priority.upper(), "#94a3b8")
 
95
 
96
+ display_time = "Anytime"
97
+ if isinstance(time_window, dict):
98
+ s_clean = _format_iso_time(time_window.get('earliest_time', ''))
99
+ e_clean = _format_iso_time(time_window.get('latest_time', ''))
100
+ if s_clean and e_clean:
101
+ display_time = f"{s_clean} - {e_clean}"
102
+ elif s_clean:
103
+ display_time = f"After {s_clean}"
104
+ elif time_window:
105
+ display_time = str(time_window)
106
 
107
  return f"""
108
+ <div class="task-card-item" style="border-left: 4px solid {p_color};">
109
+ <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
110
+ <div style="font-weight: 700; color: #334155; display: flex; align-items: center; gap: 8px;">
111
+ <span style="background:#f1f5f9; padding:4px; border-radius:6px;">{icon}</span>
112
+ {task_title}
113
+ </div>
114
+ <span style="font-size: 0.7rem; font-weight: 700; color: {p_color}; background: {p_color}15; padding: 2px 8px; border-radius: 12px; height: fit-content;">{priority}</span>
 
115
  </div>
116
+ <div style="font-size: 0.85rem; color: #64748b; display: flex; flex-direction: column; gap: 4px;">
117
+ <div style="display: flex; align-items: center; gap: 6px;"><span>📍</span> {location}</div>
118
+ <div style="display: flex; align-items: center; gap: 6px;"><span>🕒</span> {display_time} <span style="color:#cbd5e1">|</span>{duration}</div>
119
  </div>
120
  </div>
121
  """
122
 
123
 
124
+ # 🔥🔥🔥 Updated Timeline Function
125
+ def create_timeline_html_enhanced(timeline):
126
+ if not timeline:
127
+ return """
128
+ <div style="text-align: center; padding: 20px; color: #94a3b8;">
129
+ No timeline data available yet.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  </div>
131
+ """
132
 
133
+ html = '<div class="timeline-container">'
 
 
 
 
 
134
  for i, stop in enumerate(timeline):
135
+ location = stop.get('location', 'Unknown')
136
+ time_str = stop.get('time', '')
137
+ if not time_str or time_str == "N/A": time_str = "Flexible Time"
138
 
139
+ weather_str = stop.get('weather', '')
140
+ weather_html = ""
141
+ if weather_str and weather_str != "N/A":
142
+ weather_html = f'<div class="timeline-meta">🌤️ {weather_str}</div>'
143
 
144
  html += f"""
145
+ <div class="timeline-stop">
146
+ <div class="timeline-left">
147
+ <div class="timeline-marker"></div>
148
+ </div>
149
+ <div class="timeline-card">
150
+ <div class="timeline-header">
151
+ <div class="timeline-location">{location}</div>
152
+ <div class="timeline-time-badge">🕒 {time_str}</div>
 
 
153
  </div>
154
+ {weather_html}
155
+ </div>
156
+ </div>
157
  """
158
+ html += '</div>'
159
+ return html
160
 
 
 
 
 
 
 
 
 
161
 
162
+ # Empty Placeholders
163
+ def create_metrics_cards(metrics, traffic): return ""
 
164
 
 
 
 
 
165
 
166
+ def create_result_visualization(tasks, data): return ""
 
 
 
 
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ def generate_chat_history_html_bubble(session): return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ui/theme.py CHANGED
@@ -1,391 +1,354 @@
1
- """
2
- LifeFlow AI - Theme System
3
- 提供現代化、毛玻璃風格的主題和樣式管理
4
- """
5
 
6
  def get_enhanced_css() -> str:
7
  return """
8
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
9
  <style>
 
10
  :root {
11
- --primary-color: #6366f1; /* Indigo */
12
  --primary-dark: #4f46e5;
13
- --secondary-color: #10b981; /* Emerald */
14
- --accent-color: #f59e0b; /* Amber */
15
- --background-gradient: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
16
  --glass-bg: rgba(255, 255, 255, 0.7);
17
  --glass-border: 1px solid rgba(255, 255, 255, 0.5);
18
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
19
- --radius-lg: 16px;
 
 
 
20
  --radius-md: 12px;
 
 
21
  --font-main: 'Inter', sans-serif;
22
  }
23
 
 
24
  body, .gradio-container {
25
  font-family: var(--font-main) !important;
26
- background: #f8fafc !important;
27
- }
28
-
29
- /* ============= 毛玻璃卡片風格 ============= */
30
- .glass-card {
31
- background: var(--glass-bg);
32
- backdrop-filter: blur(12px);
33
- -webkit-backdrop-filter: blur(12px);
34
- border: var(--glass-border);
35
- border-radius: var(--radius-lg);
36
- box-shadow: var(--shadow-lg);
37
  }
38
 
39
- /* ============= 標題優化 ============= */
40
- .app-header {
41
- text-align: center;
42
- padding: 40px 0 20px;
43
- animation: fade-in 0.8s ease-out;
44
- }
45
- .app-header h1 {
46
- font-size: 3.5rem !important;
47
- font-weight: 800 !important;
48
- background: linear-gradient(to right, #6366f1, #ec4899);
49
- -webkit-background-clip: text;
50
- -webkit-text-fill-color: transparent;
51
- margin-bottom: 0.5rem !important;
52
- letter-spacing: -0.02em;
53
- }
54
- .app-header p {
55
- font-size: 1.2rem !important;
56
- color: #64748b;
57
- font-weight: 400;
58
- }
59
-
60
- /* ============= Header 優化 (Flex Layout) ============= */
61
- .header-row {
62
  align-items: center !important;
63
- margin-bottom: 20px !important;
64
- padding: 10px 20px !important;
65
- background: rgba(255, 255, 255, 0.5);
66
- backdrop-filter: blur(10px);
67
- border-radius: 16px;
68
- border: 1px solid rgba(255, 255, 255, 0.5);
69
- }
70
- .app-header-left h1 {
71
- background: linear-gradient(to right, #6366f1, #ec4899);
72
- -webkit-background-clip: text;
73
- -webkit-text-fill-color: transparent;
74
- }
75
-
76
- /* 按鈕微調 */
77
- .icon-btn {
78
- background: transparent !important;
79
- border: 1px solid #e2e8f0 !important;
80
- box-shadow: none !important;
81
- }
82
- .icon-btn:hover {
83
- background: #f1f5f9 !important;
84
- border-color: #cbd5e1 !important;
85
- }
86
-
87
- /* ============= 懸浮功能選單 (Fixed Top-Right) ============= */
88
- #floating-menu {
89
- position: fixed !important;
90
- top: 20px !important;
91
- right: 20px !important;
92
- z-index: 99999 !important;
93
-
94
- /* 外觀 */
95
- background: rgba(255, 255, 255, 0.95) !important;
96
- backdrop-filter: blur(12px);
97
- border: 1px solid rgba(255, 255, 255, 0.8);
98
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
99
- border-radius: 50px !important;
100
- padding: 5px 10px !important;
101
-
102
- /* 🔥 核彈級排版修正:使用 Grid 🔥 */
103
- display: grid !important;
104
- /* 強制分為 4 個欄位,每個欄位寬度自動 */
105
- grid-template-columns: auto auto auto auto !important;
106
- gap: 8px !important;
107
-
108
- /* 強制重置寬度,不讓它佔滿 */
109
- width: fit-content !important;
110
- min-width: auto !important;
111
  }
112
 
113
- /* 2. 強制重置 Gradio 內部的 Layout 容器 */
114
- #floating-menu > * {
115
- display: contents !important; /* 忽略中間層的 div,直接讓按鈕參與 Grid 排列 */
 
116
  }
117
 
118
- /* 3. 按鈕本體設定 */
119
- .dock-btn {
120
  display: flex !important;
 
 
121
  align-items: center !important;
122
- justify-content: center !important;
123
-
124
- width: 40px !important;
125
- height: 40px !important;
126
- border-radius: 50% !important;
127
-
128
- background: transparent !important;
129
- border: none !important;
130
- box-shadow: none !important;
131
-
132
- margin: 0 !important;
133
- padding: 0 !important;
134
- font-size: 1.2rem !important;
135
- }
136
-
137
- .dock-btn:hover {
138
- background: #f1f5f9 !important;
139
- transform: translateY(-2px);
140
- transition: all 0.2s ease;
141
  }
142
 
143
- /* 🔥 強制重置 Gradio 內部包裝層 🔥 */
144
- /* 這是造成垂直排列的主因,Gradio 預設會把元件包在 width:100% div 裡 */
145
- .header-controls > * {
146
- min-width: auto !important;
147
- width: auto !important;
148
- flex: 0 0 auto !important;
149
- margin: 0 !important;
150
  padding: 0 !important;
151
- border: none !important;
152
- background: transparent !important;
153
- }
154
-
155
- /* 按鈕本體樣式優化 */
156
- .header-controls button {
157
- display: flex !important;
158
- align-items: center !important;
159
- justify-content: center !important;
160
-
161
- width: 38px !important; /* 固定按鈕大小 */
162
- height: 38px !important;
163
  border-radius: 50% !important;
164
-
165
  background: transparent !important;
166
- border: 1px solid transparent !important;
167
  box-shadow: none !important;
168
- padding: 0 !important;
169
- font-size: 1.2rem !important;
170
- line-height: 1 !important;
171
  color: #64748b !important;
 
 
 
 
 
172
  }
173
-
174
- .header-controls button:hover {
175
  background: #f1f5f9 !important;
176
- color: #6366f1 !important;
177
- transform: translateY(-1px);
178
- transition: all 0.2s ease;
179
- box-shadow: 0 2px 5px rgba(0,0,0,0.05) !important;
 
 
 
 
180
  }
181
 
182
- /* ============= Agent 呼吸動畫 (關鍵修改) ============= */
183
  @keyframes breathing-glow {
184
- 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); border-color: #6366f1; }
185
- 70% { box-shadow: 0 0 0 6px rgba(99, 102, 241, 0); border-color: #818cf8; }
186
- 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); border-color: #6366f1; }
187
  }
188
 
189
- .agent-card-mini.working {
190
- animation: breathing-glow 2s infinite;
191
- background: linear-gradient(to bottom right, #fff, #eff6ff);
192
- transform: scale(1.05); /* 稍微放大 */
193
- z-index: 10;
194
- }
195
-
196
- /* ============= AI Conversation 優化 ============= */
197
- .reasoning-timeline {
198
- max-height: 400px;
199
- overflow-y: auto;
200
- padding-right: 10px;
201
- /* 讓最新的訊息看起來像是在終端機中彈出 */
202
- display: flex;
203
- flex-direction: column-reverse;
204
- }
205
-
206
- /* ============= 按鈕優化 ============= */
207
- button.primary {
208
- background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)) !important;
209
- border: none !important;
210
- box-shadow: 0 4px 6px rgba(99, 102, 241, 0.3) !important;
211
- transition: all 0.2s ease !important;
212
- }
213
- button.primary:hover {
214
- transform: translateY(-2px);
215
- box-shadow: 0 8px 12px rgba(99, 102, 241, 0.4) !important;
216
  }
217
 
218
- /* ============= Agent 狀態卡片 (Grid Layout) ============= */
219
  .agent-grid {
220
  display: grid;
221
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
222
  gap: 12px;
223
  margin-top: 10px;
224
  }
 
225
  .agent-card-mini {
226
  background: white;
227
- padding: 10px;
228
  border-radius: var(--radius-md);
229
- border: 1px solid #e2e8f0;
230
  display: flex;
231
  flex-direction: column;
232
  align-items: center;
233
  text-align: center;
234
- transition: all 0.3s ease;
 
 
235
  }
236
- .agent-card-mini.active {
237
- border-color: var(--primary-color);
238
- box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
239
- transform: scale(1.02);
 
 
 
240
  }
241
- .agent-avatar-mini { font-size: 24px; margin-bottom: 4px; }
242
- .agent-name-mini { font-weight: 600; font-size: 0.85rem; color: #1e293b; }
243
- .agent-status-dot {
244
- height: 8px; width: 8px; border-radius: 50%; display: inline-block; margin-top: 4px;
 
 
245
  }
246
 
247
- /* ============= 對話框樣式 (Chat Bubbles) ============= */
 
 
 
 
 
248
  .chat-history {
249
- display: flex;
250
- flex-direction: column;
251
- gap: 16px;
252
- padding: 20px;
253
- background: #fff;
254
- border-radius: var(--radius-lg);
 
 
 
 
 
 
 
 
 
255
  border: 1px solid #e2e8f0;
256
- max-height: 500px;
257
- overflow-y: auto;
258
- }
259
- .chat-message {
260
- display: flex;
261
- align-items: flex-end;
262
- gap: 10px;
263
- max-width: 85%;
264
- }
265
- .chat-message.user {
266
- align-self: flex-end;
267
- flex-direction: row-reverse;
268
- }
269
- .chat-message.assistant {
270
- align-self: flex-start;
271
- }
272
- .chat-bubble {
273
- padding: 12px 16px;
274
- border-radius: 18px;
275
- font-size: 0.95rem;
276
- line-height: 1.5;
277
- position: relative;
278
- }
279
- .chat-message.user .chat-bubble {
280
- background: var(--primary-color);
281
- color: white;
282
- border-bottom-right-radius: 4px;
283
- }
284
- .chat-message.assistant .chat-bubble {
285
- background: #f1f5f9;
286
- color: #334155;
287
- border-bottom-left-radius: 4px;
288
  }
289
- .chat-time {
290
- font-size: 0.7rem;
291
- opacity: 0.7;
292
- margin-top: 4px;
293
- text-align: right;
294
  }
295
 
296
- /* ============= Timeline 優化 ============= */
297
  .timeline-container {
298
  position: relative;
299
- padding: 20px 0;
 
300
  }
 
 
301
  .timeline-container::before {
302
  content: '';
303
  position: absolute;
304
- left: 24px;
305
- top: 30px;
306
- bottom: 30px;
307
  width: 2px;
308
  background: #e2e8f0;
309
  z-index: 0;
310
  }
 
 
311
  .timeline-item {
312
  position: relative;
313
- display: flex;
 
314
  gap: 20px;
315
  margin-bottom: 24px;
316
  z-index: 1;
 
317
  }
 
 
318
  .timeline-left {
319
- min-width: 50px;
320
  display: flex;
321
  flex-direction: column;
322
  align-items: center;
 
 
323
  }
324
- .timeline-icon-box {
325
- width: 48px;
326
- height: 48px;
 
327
  border-radius: 50%;
328
  background: white;
329
- border: 2px solid var(--primary-color);
330
- display: flex;
331
- align-items: center;
332
- justify-content: center;
333
- font-size: 20px;
334
- box-shadow: 0 4px 6px rgba(0,0,0,0.05);
335
  }
336
- .timeline-content-card {
 
 
337
  flex: 1;
338
- background: white;
339
- padding: 16px;
340
- border-radius: var(--radius-md);
341
- border: 1px solid #e2e8f0;
342
- box-shadow: 0 2px 4px rgba(0,0,0,0.02);
343
- transition: transform 0.2s;
 
344
  }
345
- .timeline-content-card:hover {
 
346
  transform: translateX(4px);
347
- border-color: var(--primary-color);
 
348
  }
349
 
350
- /* ============= Metric Cards ============= */
351
- .metrics-container {
352
- display: grid;
353
- grid-template-columns: repeat(3, 1fr);
354
- gap: 15px;
355
- margin-bottom: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
356
  }
357
- .metric-card {
 
 
 
 
 
 
358
  background: white;
359
- padding: 15px;
360
- border-radius: var(--radius-md);
361
- text-align: center;
362
  border: 1px solid #e2e8f0;
363
- box-shadow: 0 2px 4px rgba(0,0,0,0.02);
364
- }
365
- .metric-value {
366
- font-size: 1.5rem;
367
- font-weight: 700;
368
- color: #1e293b;
369
- margin: 5px 0;
370
  }
371
 
372
- /* ============= 動畫定義 ============= */
373
- @keyframes fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
374
- @keyframes pulse-ring { 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); } 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); } }
 
 
375
 
376
- /* Dark Mode Overrides (Minimal) */
377
- .theme-dark body { background: #0f172a !important; }
378
- .theme-dark .chat-history, .theme-dark .timeline-content-card, .theme-dark .metric-card, .theme-dark .agent-card-mini {
379
- background: #1e293b;
380
- border-color: #334155;
381
- color: #e2e8f0;
382
  }
383
- .theme-dark h1, .theme-dark h2, .theme-dark h3, .theme-dark p, .theme-dark span {
384
- color: #e2e8f0 !important;
 
 
385
  }
386
- .theme-dark .chat-message.assistant .chat-bubble {
387
- background: #334155;
388
- color: #e2e8f0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  }
390
  </style>
391
  """
 
1
+ # ui/theme.py
2
+ # ui/theme.py
 
 
3
 
4
  def get_enhanced_css() -> str:
5
  return """
6
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
7
  <style>
8
+ /* ============= CSS Variables ============= */
9
  :root {
10
+ --primary-color: #6366f1;
11
  --primary-dark: #4f46e5;
12
+ --secondary-color: #10b981;
13
+ --accent-color: #f59e0b;
14
+ --danger-color: #ef4444;
15
  --glass-bg: rgba(255, 255, 255, 0.7);
16
  --glass-border: 1px solid rgba(255, 255, 255, 0.5);
17
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
18
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
19
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
20
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
21
+ --radius-sm: 8px;
22
  --radius-md: 12px;
23
+ --radius-lg: 16px;
24
+ --radius-xl: 20px;
25
  --font-main: 'Inter', sans-serif;
26
  }
27
 
28
+ /* ============= Base Styles ============= */
29
  body, .gradio-container {
30
  font-family: var(--font-main) !important;
31
+ background: #f8fafc !important;
 
 
 
 
 
 
 
 
 
 
32
  }
33
 
34
+ * { box-sizing: border-box; }
35
+
36
+ /* ============= 1. Header (保留 Backup 樣式) ============= */
37
+ .app-header-container {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  align-items: center !important;
39
+ margin-bottom: 24px !important;
40
+ padding: 16px 24px !important;
41
+ background: rgba(255, 255, 255, 0.8) !important;
42
+ backdrop-filter: blur(12px) !important;
43
+ -webkit-backdrop-filter: blur(12px) !important;
44
+ border-radius: var(--radius-lg) !important;
45
+ border: 1px solid rgba(255, 255, 255, 0.5) !important;
46
+ box-shadow: var(--shadow-md) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
 
49
+ .app-header-left {
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: 4px;
53
  }
54
 
55
+ .header-controls {
 
56
  display: flex !important;
57
+ gap: 8px !important;
58
+ justify-content: flex-end !important;
59
  align-items: center !important;
60
+ width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  }
62
 
63
+ .header-btn {
64
+ min-width: 44px !important;
65
+ max-width: 44px !important;
66
+ height: 44px !important;
 
 
 
67
  padding: 0 !important;
 
 
 
 
 
 
 
 
 
 
 
 
68
  border-radius: 50% !important;
 
69
  background: transparent !important;
70
+ border: 1px solid #e2e8f0 !important;
71
  box-shadow: none !important;
72
+ font-size: 1.25rem !important;
 
 
73
  color: #64748b !important;
74
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
75
+ cursor: pointer !important;
76
+ display: flex !important;
77
+ align-items: center !important;
78
+ justify-content: center !important;
79
  }
80
+
81
+ .header-btn:hover {
82
  background: #f1f5f9 !important;
83
+ border-color: var(--primary-color) !important;
84
+ color: var(--primary-color) !important;
85
+ transform: translateY(-2px) !important;
86
+ box-shadow: var(--shadow-md) !important;
87
+ }
88
+
89
+ .header-btn:active {
90
+ transform: translateY(0) !important;
91
  }
92
 
93
+ /* ============= 2. Animations & Agent Cards (保留 Backup 樣式) ============= */
94
  @keyframes breathing-glow {
95
+ 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.6), 0 0 20px rgba(99, 102, 241, 0.3); border-color: #6366f1; }
96
+ 50% { box-shadow: 0 0 0 8px rgba(99, 102, 241, 0), 0 0 30px rgba(99, 102, 241, 0.5); border-color: #818cf8; }
97
+ 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0), 0 0 20px rgba(99, 102, 241, 0.3); border-color: #6366f1; }
98
  }
99
 
100
+ @keyframes pulse-scale {
101
+ 0%, 100% { transform: scale(1); }
102
+ 50% { transform: scale(1.03); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
 
105
+ /* Agent Grid (用於 Step 3 的 Agent 卡片基礎) */
106
  .agent-grid {
107
  display: grid;
108
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
109
  gap: 12px;
110
  margin-top: 10px;
111
  }
112
+
113
  .agent-card-mini {
114
  background: white;
115
+ padding: 12px;
116
  border-radius: var(--radius-md);
117
+ border: 2px solid #e2e8f0;
118
  display: flex;
119
  flex-direction: column;
120
  align-items: center;
121
  text-align: center;
122
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
123
+ position: relative;
124
+ overflow: hidden;
125
  }
126
+
127
+ .agent-card-mini.working {
128
+ animation: breathing-glow 2s ease-in-out infinite !important;
129
+ background: linear-gradient(135deg, #ffffff 0%, #eff6ff 100%) !important;
130
+ border-width: 2px !important;
131
+ z-index: 10 !important;
132
+ position: relative !important;
133
  }
134
+
135
+ .agent-card-mini.working::before {
136
+ content: '';
137
+ position: absolute; top: -50%; left: -50%; width: 200%; height: 200%;
138
+ background: radial-gradient(circle, rgba(99, 102, 241, 0.1) 0%, transparent 70%);
139
+ animation: pulse-scale 2s ease-in-out infinite;
140
  }
141
 
142
+ .agent-card-mini:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
143
+ .agent-avatar-mini { font-size: 28px; margin-bottom: 6px; position: relative; z-index: 1; }
144
+ .agent-name-mini { font-weight: 600; font-size: 0.85rem; color: #1e293b; position: relative; z-index: 1; }
145
+ .agent-status-dot { height: 8px; width: 8px; border-radius: 50%; display: inline-block; margin-top: 6px; position: relative; z-index: 1; }
146
+
147
+ /* ============= 3. Chat & Tasks (保留 Backup 樣式) ============= */
148
  .chat-history {
149
+ display: flex; flex-direction: column; gap: 16px; padding: 20px;
150
+ background: #fff; border-radius: var(--radius-lg); border: 1px solid #e2e8f0;
151
+ max-height: 500px; overflow-y: auto;
152
+ }
153
+ .chat-message { display: flex; align-items: flex-end; gap: 10px; max-width: 85%; animation: fade-in 0.3s ease-out; }
154
+ .chat-message.user { align-self: flex-end; flex-direction: row-reverse; }
155
+ .chat-message.assistant { align-self: flex-start; }
156
+ .chat-bubble { padding: 12px 16px; border-radius: 18px; font-size: 0.95rem; line-height: 1.5; position: relative; box-shadow: var(--shadow-sm); }
157
+ .chat-message.user .chat-bubble { background: var(--primary-color); color: white; border-bottom-right-radius: 4px; }
158
+ .chat-message.assistant .chat-bubble { background: #f1f5f9; color: #334155; border-bottom-left-radius: 4px; }
159
+ .chat-time { font-size: 0.7rem; opacity: 0.7; margin-top: 4px; text-align: right; }
160
+
161
+ /* Task Cards */
162
+ .task-card-item {
163
+ background: white;
164
  border: 1px solid #e2e8f0;
165
+ border-radius: 12px;
166
+ padding: 16px;
167
+ margin-bottom: 12px;
168
+ transition: transform 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  }
170
+ .task-card-item:hover {
171
+ border-color: var(--primary-color);
172
+ transform: translateY(-2px);
173
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
 
174
  }
175
 
176
+ /* ============= 4. Timeline Redesign (Step 4 修正) ============= */
177
  .timeline-container {
178
  position: relative;
179
+ padding: 10px 0 10px 20px;
180
+ margin-top: 10px;
181
  }
182
+
183
+ /* 垂直連接線 */
184
  .timeline-container::before {
185
  content: '';
186
  position: absolute;
187
+ left: 29px;
188
+ top: 20px;
189
+ bottom: 20px;
190
  width: 2px;
191
  background: #e2e8f0;
192
  z-index: 0;
193
  }
194
+
195
+ /* 時間軸項目 - 強制橫排 */
196
  .timeline-item {
197
  position: relative;
198
+ display: flex !important;
199
+ flex-direction: row !important;
200
  gap: 20px;
201
  margin-bottom: 24px;
202
  z-index: 1;
203
+ align-items: stretch;
204
  }
205
+
206
+ /* 左側圓點 */
207
  .timeline-left {
 
208
  display: flex;
209
  flex-direction: column;
210
  align-items: center;
211
+ min-width: 20px;
212
+ padding-top: 4px;
213
  }
214
+
215
+ .timeline-marker {
216
+ width: 20px;
217
+ height: 20px;
218
  border-radius: 50%;
219
  background: white;
220
+ border: 4px solid var(--primary-color);
221
+ box-shadow: 0 0 0 4px #fff;
222
+ z-index: 2;
 
 
 
223
  }
224
+
225
+ /* 右側卡片 */
226
+ .timeline-card {
227
  flex: 1;
228
+ background: white !important;
229
+ border: 1px solid #e2e8f0 !important;
230
+ border-radius: 12px !important;
231
+ padding: 16px !important;
232
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important;
233
+ transition: transform 0.2s, box-shadow 0.2s;
234
+ display: block !important;
235
  }
236
+
237
+ .timeline-card:hover {
238
  transform: translateX(4px);
239
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05) !important;
240
+ border-color: var(--primary-color) !important;
241
  }
242
 
243
+ .timeline-header {
244
+ display: flex;
245
+ justify-content: space-between;
246
+ align-items: flex-start;
247
+ margin-bottom: 8px;
248
+ gap: 10px;
249
+ }
250
+
251
+ .timeline-location { font-size: 1rem; font-weight: 700; color: #1e293b; line-height: 1.3; }
252
+
253
+ .timeline-time-badge {
254
+ background: #f1f5f9; color: #64748b; font-size: 0.75rem; font-weight: 600;
255
+ padding: 4px 10px; border-radius: 20px; white-space: nowrap; display: flex; align-items: center; gap: 4px;
256
+ }
257
+
258
+ .timeline-meta {
259
+ display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: #64748b;
260
+ background: #f8fafc; padding: 8px; border-radius: 8px; margin-top: 8px;
261
  }
262
+
263
+ .timeline-item:first-child .timeline-marker { border-color: #10b981 !important; }
264
+ .timeline-item:last-child .timeline-marker { border-color: #ef4444 !important; }
265
+
266
+ /* ============= 5. Step 3 War Room (新樣式) ============= */
267
+ .live-report-wrapper {
268
+ flex: 1;
269
  background: white;
 
 
 
270
  border: 1px solid #e2e8f0;
271
+ border-radius: 16px;
272
+ padding: 30px;
273
+ min-height: 600px;
274
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
 
 
 
275
  }
276
 
277
+ .report-title {
278
+ font-size: 1.2rem; font-weight: 700; color: #1e293b;
279
+ margin-bottom: 16px; display: flex; align-items: center; gap: 8px;
280
+ border-bottom: 2px solid #f1f5f9; padding-bottom: 12px;
281
+ }
282
 
283
+ .war-room-wrapper {
284
+ width: 450px; flex-shrink: 0; display: flex; flex-direction: column; gap: 20px; position: sticky; top: 20px;
 
 
 
 
285
  }
286
+
287
+ .agent-war-room {
288
+ background: white; border-radius: 16px; border: 1px solid #e2e8f0; padding: 24px;
289
+ display: flex; flex-direction: column; align-items: center; box-shadow: 0 2px 4px rgba(0,0,0,0.05);
290
  }
291
+
292
+ .org-chart { width: 100%; display: flex; flex-direction: column; align-items: center; gap: 20px; }
293
+ .org-level { display: flex !important; flex-direction: row !important; flex-wrap: wrap; justify-content: center; gap: 12px; width: 100%; z-index: 2; }
294
+ .connector-line { width: 2px; height: 20px; background: #cbd5e1; }
295
+ .connector-horizontal { height: 2px; width: 80%; background: #cbd5e1; margin-top: -20px; margin-bottom: 10px; }
296
+
297
+ /* Step 3 的 Agent Card (複用 mini card 但調整寬度) */
298
+ .agent-card-wrap { width: 110px; text-align: center; display: flex; flex-direction: column; align-items: center; }
299
+ .agent-card-inner {
300
+ background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 10px;
301
+ display: flex; flex-direction: column; align-items: center; width: 100%; transition: transform 0.2s;
302
+ }
303
+ .agent-card-wrap.working .agent-card-inner {
304
+ background: white; border-color: #6366f1;
305
+ box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15); animation: pulse 2s infinite;
306
+ }
307
+ .agent-avatar { font-size: 24px; margin-bottom: 4px; }
308
+ .agent-name { font-weight: 700; font-size: 0.8rem; color: #1e293b; }
309
+ .agent-role { font-size: 0.65rem; color: #64748b; text-transform: uppercase; }
310
+ .status-badge { font-size: 0.65rem; padding: 2px 6px; border-radius: 8px; background: #e2e8f0; color: #64748b; margin-top: 4px; }
311
+ .agent-card-wrap.working .status-badge { background: #e0e7ff; color: #6366f1; }
312
+
313
+ /* ============= 6. Utils & Layouts ============= */
314
+ .step-container { max-width: 1400px; margin: 0 auto; padding: 0 16px; animation: fade-in 0.4s ease-out; }
315
+ .centered-input-container { max-width: 1000px !important; margin: 0 auto; padding: 40px 0; }
316
+
317
+ .panel-container { background: white; border: 1px solid #e2e8f0; border-radius: 16px; height: 650px !important; display: flex !important; flex-direction: column !important; overflow: hidden; }
318
+ .panel-header { padding: 16px 20px; background: #f8fafc; border-bottom: 1px solid #e2e8f0; font-weight: 700; color: var(--text-main); flex-shrink: 0; }
319
+ .scrollable-content { flex: 1; overflow-y: auto !important; padding: 20px; background: #fff; }
320
+
321
+ .chat-input-row { padding: 12px !important; border-top: 1px solid #e2e8f0; background: white; align-items: center !important; }
322
+
323
+ .split-view-container { display: flex; gap: 24px; height: calc(100vh - 140px); min-height: 600px; }
324
+ .split-left-panel { flex: 1; overflow-y: auto; }
325
+ .split-right-panel { flex: 2; position: sticky; top: 100px; height: 100%; }
326
+
327
+ /* Animations */
328
+ @keyframes fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
329
+ @keyframes slide-in-left { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } }
330
+ @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); } 70% { box-shadow: 0 0 0 6px rgba(0,0,0,0); } 100% { box-shadow: 0 0 0 0 rgba(0,0,0,0); } }
331
+
332
+ /* Scrollbar */
333
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
334
+ ::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; }
335
+ ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
336
+ ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
337
+
338
+ /* Dark Mode Support */
339
+ .theme-dark body, .theme-dark .gradio-container { background: #0f172a !important; }
340
+ .theme-dark .app-header-container, .theme-dark .chat-history, .theme-dark .timeline-card, .theme-dark .metric-card, .theme-dark .agent-card-mini, .theme-dark .agent-card-inner, .theme-dark .live-report-wrapper, .theme-dark .agent-war-room, .theme-dark .task-card-item {
341
+ background: #1e293b !important; border-color: #334155 !important; color: #e2e8f0 !important;
342
+ }
343
+ .theme-dark h1, .theme-dark h2, .theme-dark h3, .theme-dark p, .theme-dark span, .theme-dark .agent-name { color: #e2e8f0 !important; }
344
+ .theme-dark .chat-message.assistant .chat-bubble { background: #334155 !important; color: #e2e8f0 !important; }
345
+ .theme-dark .agent-name-mini, .theme-dark .metric-card h3 { color: #cbd5e1 !important; }
346
+
347
+ /* Step 1 Log */
348
+ .agent-stream-box-step1 {
349
+ background: #f8fafc; border-radius: 8px; padding: 12px; border: 1px solid #e2e8f0;
350
+ min-height: 60px; max-height: 150px; overflow-y: auto; font-family: monospace; font-size: 0.9rem;
351
+ margin-bottom: 16px; margin-top: 10px;
352
  }
353
  </style>
354
  """