Marco310 commited on
Commit
4abc17c
·
1 Parent(s): a6744e0

rebuild app.py

Browse files
app.py CHANGED
@@ -1,1072 +1,532 @@
1
  """
2
- LifeFlow AI - v11 English + Compact Layout
3
- ✅ English version
4
- ✅ Compact layout (horizontal on widescreen)
5
- ✅ Responsive design
6
- ✅ Optimized for HuggingFace Spaces
7
  """
8
 
 
 
 
 
9
  import gradio as gr
10
- import plotly.graph_objects as go
11
- from datetime import datetime, time
12
- import json
13
- from typing import Dict, List, Optional, Tuple
14
  import time as time_module
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- class LifeFlowInteractive:
18
- """LifeFlow AI Interactive Demo v11"""
19
 
20
  def __init__(self):
21
- self.team = None
22
- self.current_step = 0
23
  self.task_list = []
24
- self.poi_results = []
25
- self.chat_history = []
26
  self.reasoning_messages = []
27
  self.planning_completed = False
28
- self.agent_raw_output = ""
29
-
30
- # Settings
31
- self.settings = {
32
- 'google_maps_api_key': '',
33
- 'openweather_api_key': '',
34
- 'anthropic_api_key': '',
35
- 'model': 'claude-sonnet-4-20250514'
36
- }
37
-
38
- # Agent info
39
- self.agents_info = {
40
- "planner": {
41
- "name": "Planner",
42
- "avatar": "👔",
43
- "role": "Chief Planning Officer",
44
- "color": "#4A90E2"
45
- },
46
- "scout": {
47
- "name": "Scout",
48
- "avatar": "🕵️",
49
- "role": "Location Scout Expert",
50
- "color": "#50C878"
51
- },
52
- "optimizer": {
53
- "name": "Optimizer",
54
- "avatar": "🤖",
55
- "role": "AI Optimization Engine",
56
- "color": "#F5A623"
57
- },
58
- "validator": {
59
- "name": "Validator",
60
- "avatar": "🛡️",
61
- "role": "Quality Assurance Expert",
62
- "color": "#7ED321"
63
- },
64
- "weather": {
65
- "name": "Weather",
66
- "avatar": "🌈",
67
- "role": "Weather Analyst",
68
- "color": "#50E3C2"
69
- },
70
- "traffic": {
71
- "name": "Traffic",
72
- "avatar": "🚗",
73
- "role": "Traffic Planner",
74
- "color": "#FF6B6B"
75
- }
76
- }
77
-
78
- def create_agent_card(self, agent_key: str, status: str = "idle", message: str = ""):
79
- """Create Agent card (compact version)"""
80
- agent = self.agents_info[agent_key]
81
-
82
- status_icons = {
83
- "idle": "⚪",
84
- "thinking": "💭",
85
- "using_tool": "🔧",
86
- "working": "⚙️",
87
- "completed": "✅",
88
- "waiting": "⏸️",
89
- "error": "❌"
90
- }
91
-
92
- icon = status_icons.get(status, "⚪")
93
- breathing_class = "breathing" if status in ["working", "using_tool"] else ""
94
-
95
- return f"""
96
- <div class="{breathing_class}" style="
97
- background: var(--background-fill-secondary);
98
- border-left: 3px solid {agent['color']};
99
- border-radius: 6px;
100
- padding: 10px;
101
- margin: 6px 0;
102
- ">
103
- <div style="display: flex; align-items: center; gap: 8px;">
104
- <div style="font-size: 24px;">{agent['avatar']}</div>
105
- <div style="flex-grow: 1;">
106
- <div style="display: flex; justify-content: space-between; align-items: center;">
107
- <strong style="color: var(--body-text-color); font-size: 14px;">
108
- {agent['name']}
109
- </strong>
110
- <span style="font-size: 16px;">{icon}</span>
111
- </div>
112
- {f'<div style="color: var(--body-text-color); opacity: 0.7; font-size: 12px; margin-top: 4px;">{message}</div>' if message else ''}
113
- </div>
114
- </div>
115
- </div>
116
- """
117
-
118
- def add_reasoning_message(self, agent: str, message: str, msg_type: str = "info"):
119
- """Add reasoning message"""
120
- icon_map = {
121
- "thinking": "💭",
122
- "tool": "����",
123
- "success": "✅",
124
- "info": "ℹ️",
125
- "warning": "⚠️",
126
- "error": "❌"
127
- }
128
- icon = icon_map.get(msg_type, "ℹ️")
129
-
130
- agent_name = self.agents_info.get(agent, {}).get("name", agent)
131
-
132
- self.reasoning_messages.append({
133
- "agent": agent_name,
134
- "message": message,
135
- "icon": icon,
136
- "timestamp": datetime.now().strftime("%H:%M:%S")
137
- })
138
-
139
- def get_reasoning_html(self) -> str:
140
- """Generate reasoning HTML (compact scrollable)"""
141
- if not self.reasoning_messages:
142
- return """
143
- <div style="
144
- padding: 15px;
145
- text-align: center;
146
- color: var(--body-text-color);
147
- opacity: 0.6;
148
- background: var(--background-fill-secondary);
149
- border-radius: 6px;
150
- ">
151
- 💬 AI conversation log will appear here
152
- </div>
153
- """
154
-
155
- messages_html = ""
156
- for msg in self.reasoning_messages:
157
- messages_html += f"""
158
- <div style="
159
- padding: 8px;
160
- margin: 6px 0;
161
- background: var(--background-fill-secondary);
162
- border-radius: 6px;
163
- border-left: 2px solid #4A90E2;
164
- ">
165
- <div style="display: flex; align-items: center; gap: 6px; margin-bottom: 4px;">
166
- <span style="font-size: 14px;">{msg['icon']}</span>
167
- <strong style="color: var(--body-text-color); font-size: 13px;">{msg['agent']}</strong>
168
- <span style="color: var(--body-text-color); opacity: 0.5; font-size: 11px; margin-left: auto;">
169
- {msg['timestamp']}
170
- </span>
171
- </div>
172
- <div style="color: var(--body-text-color); opacity: 0.85; padding-left: 20px; font-size: 12px;">
173
- {msg['message']}
174
- </div>
175
- </div>
176
- """
177
 
178
- return f"""
179
- <div style="
180
- max-height: 400px;
181
- overflow-y: auto;
182
- padding: 10px;
183
- background: var(--background-fill-primary);
184
- border-radius: 6px;
185
- border: 1px solid var(--border-color-primary);
186
- ">
187
- {messages_html}
188
- </div>
189
  """
190
-
191
- def create_task_list_ui(self, tasks: List[Dict]) -> str:
192
- """Create task list UI (compact)"""
193
- if not tasks:
194
- return "<div style='color: var(--body-text-color);'>No tasks yet</div>"
195
-
196
- priority_colors = {
197
- "HIGH": "#FF6B6B",
198
- "MEDIUM": "#F5A623",
199
- "LOW": "#7ED321"
200
- }
201
-
202
- category_icons = {
203
- "medical": "🏥",
204
- "shopping": "🛒",
205
- "postal": "📮",
206
- "dining": "🍽️",
207
- "financial": "🏦",
208
- "other": "📋"
209
- }
210
-
211
- html = "<div style='display: flex; flex-direction: column; gap: 8px;'>"
212
-
213
- for i, task in enumerate(tasks, 1):
214
- priority = task.get('priority', 'MEDIUM')
215
- category = task.get('category', 'other')
216
- icon = category_icons.get(category, "📋")
217
- color = priority_colors.get(priority, "#7ED321")
218
- time_window = task.get('time_window', 'Anytime')
219
-
220
- html += f"""
221
- <div style="
222
- background: var(--background-fill-secondary);
223
- border-left: 3px solid {color};
224
- border-radius: 6px;
225
- padding: 10px;
226
- ">
227
- <div style="display: flex; align-items: center; gap: 6px; margin-bottom: 4px;">
228
- <span style="font-size: 20px;">{icon}</span>
229
- <strong style="color: var(--body-text-color); font-size: 13px;">
230
- Task {i}: {task['description']}
231
- </strong>
232
- </div>
233
- <div style="color: var(--body-text-color); opacity: 0.7; font-size: 11px;">
234
- <span style="background: {color}; color: white; padding: 2px 6px; border-radius: 3px; margin-right: 6px;">
235
- {priority}
236
- </span>
237
- <span>⏰ {time_window}</span>
238
- </div>
239
- </div>
240
- """
241
-
242
- html += "</div>"
243
- return html
244
-
245
- def create_task_summary(self, tasks: List[Dict]) -> str:
246
- """Create task summary (compact)"""
247
- high_count = sum(1 for t in tasks if t.get('priority') == 'HIGH')
248
- medium_count = sum(1 for t in tasks if t.get('priority') == 'MEDIUM')
249
- low_count = sum(1 for t in tasks if t.get('priority') == 'LOW')
250
-
251
- return f"""
252
- <div style="
253
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
254
- padding: 15px;
255
- border-radius: 6px;
256
- color: white;
257
- margin-top: 10px;
258
- ">
259
- <h4 style="margin: 0 0 10px 0; font-size: 14px;">📊 Task Summary</h4>
260
- <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">
261
- <div style="text-align: center;">
262
- <div style="font-size: 24px; font-weight: bold;">{len(tasks)}</div>
263
- <div style="opacity: 0.9; font-size: 11px;">Total</div>
264
- </div>
265
- <div style="text-align: center;">
266
- <div style="font-size: 24px; font-weight: bold;">{high_count}</div>
267
- <div style="opacity: 0.9; font-size: 11px;">High Priority</div>
268
- </div>
269
- <div style="text-align: center;">
270
- <div style="font-size: 24px; font-weight: bold;">{medium_count + low_count}</div>
271
- <div style="opacity: 0.9; font-size: 11px;">Med/Low</div>
272
- </div>
273
- </div>
274
- </div>
275
  """
 
 
276
 
277
- def create_trip_result_summary(self, result: Dict) -> str:
278
- """Create trip result summary (compact, left side)"""
279
- stops = result.get('route', {}).get('stops', [])
280
-
281
- return f"""
282
- <div style="
283
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
284
- padding: 20px;
285
- border-radius: 8px;
286
- color: white;
287
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
288
- ">
289
- <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 15px;">
290
- <span style="font-size: 36px;">🎉</span>
291
- <div>
292
- <h3 style="margin: 0; font-size: 18px;">Trip Planning Complete!</h3>
293
- <p style="margin: 5px 0 0 0; opacity: 0.9; font-size: 12px;">Optimized {len(stops)} stops</p>
294
- </div>
295
- </div>
296
-
297
- <div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 6px; margin-bottom: 12px;">
298
- <h4 style="margin: 0 0 8px 0; font-size: 13px;">⏰ Schedule</h4>
299
- <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; font-size: 11px;">
300
- <div>• Start: <strong>08:30</strong></div>
301
- <div>• Finish: <strong>11:25</strong> ✅</div>
302
- <div>• Deadline: <strong>15:00</strong></div>
303
- <div>• Buffer: <strong>3h 35m</strong> 🎯</div>
304
- </div>
305
- </div>
306
-
307
- <div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 6px;">
308
- <h4 style="margin: 0 0 8px 0; font-size: 13px;">📍 Route</h4>
309
- <div style="font-size: 11px; line-height: 1.6;">
310
- {' → '.join([stop['name'] for stop in stops])}
311
- </div>
312
- </div>
313
-
314
- <div style="margin-top: 12px; padding: 10px; background: rgba(255,255,255,0.1); border-radius: 6px; font-size: 11px;">
315
- 💡 Check the <strong>Full Report</strong> tab for detailed analysis
316
- </div>
317
- </div>
318
- """
319
-
320
- def create_agent_text_output(self, result: Dict) -> str:
321
- """Create Agent text output (for right side tab)"""
322
- stops = result.get('route', {}).get('stops', [])
323
-
324
- output = "=" * 80 + "\n"
325
- output += "LIFEFLOW AI - COMPLETE TRIP PLANNING REPORT\n"
326
- output += "=" * 80 + "\n\n"
327
-
328
- output += "【PLANNING SUMMARY】\n"
329
- output += f"Total Stops: {len(stops)}\n"
330
- output += f"Start Time: 08:30\n"
331
- output += f"Estimated Completion: 11:25\n"
332
- output += f"Deadline: 15:00\n"
333
- output += f"Buffer Time: 3h 35m\n\n"
334
-
335
- output += "=" * 80 + "\n"
336
- output += "【PLANNER AGENT ANALYSIS】\n"
337
- output += "=" * 80 + "\n"
338
- output += "✓ Identified 3 tasks\n"
339
- output += "✓ Task 1: Visit hospital (Medical - HIGH)\n"
340
- output += " - Time window: 08:00-12:00\n"
341
- output += " - Duration: 45 minutes\n"
342
- output += "✓ Task 2: Buy groceries (Shopping - MEDIUM)\n"
343
- output += " - Time window: Anytime\n"
344
- output += " - Duration: 30 minutes\n"
345
- output += "✓ Task 3: Mail package (Postal - HIGH)\n"
346
- output += " - Time window: 09:00-15:00\n"
347
- output += " - Duration: 20 minutes\n\n"
348
-
349
- output += "【TIME CONSTRAINT ANALYSIS】\n"
350
- output += "- Hospital time window is tight, must prioritize\n"
351
- output += "- Post office deadline is 15:00, must complete on time\n"
352
- output += "- Supermarket time is flexible, can be used as buffer\n\n"
353
-
354
- output += "=" * 80 + "\n"
355
- output += "【SCOUT AGENT SEARCH RESULTS】\n"
356
- output += "=" * 80 + "\n"
357
- output += "✓ POI search complete\n\n"
358
-
359
- for i, stop in enumerate(stops, 1):
360
- output += f"Stop {i}: {stop['name']}\n"
361
- output += f" Address: {stop['address']}\n"
362
- output += f" Phone: {stop['phone']}\n"
363
- output += f" Rating: {stop['rating']} ⭐\n"
364
- output += f" Distance: {'Start' if i == 1 else f'{0.5 * i}km'}\n\n"
365
-
366
- output += "【DISTANCE MATRIX】\n"
367
- output += "- Start → NTU Hospital: 0.8km (15 min)\n"
368
- output += "- Hospital → PX Mart: 0.6km (12 min)\n"
369
- output += "- PX Mart → Post Office: 0.8km (15 min)\n\n"
370
-
371
- output += "=" * 80 + "\n"
372
- output += "【OPTIMIZER AGENT ROUTE OPTIMIZATION】\n"
373
- output += "=" * 80 + "\n"
374
- output += "✓ Using OR-Tools TSPTW solver\n\n"
375
-
376
- output += "【OPTIMIZATION PROCESS】\n"
377
- output += "Phase 1: Greedy Construction\n"
378
- output += " - Initial route: Hospital → Supermarket → Post Office\n"
379
- output += " - Total distance: 2.2km\n"
380
- output += " - Total time: 95 minutes\n\n"
381
-
382
- output += "Phase 2: 2-opt Improvement\n"
383
- output += " - Attempting order swap...\n"
384
- output += " - Current order is optimal\n"
385
- output += " - No improvement needed\n\n"
386
-
387
- output += "Phase 3: Time Window Validation\n"
388
- output += " - Hospital: 09:00 ∈ [08:00, 12:00] ✓\n"
389
- output += " - Supermarket: 10:15 ∈ [Anytime] ✓\n"
390
- output += " - Post Office: 11:05 ∈ [09:00, 15:00] ✓\n\n"
391
-
392
- output += "【OPTIMAL ROUTE】\n"
393
- output += "Stop Order: Hospital → Supermarket → Post Office\n"
394
- output += "Total Distance: 2.2km\n"
395
- output += "Total Time: 95 minutes (travel + stops)\n"
396
- output += "Completion: 11:25\n"
397
- output += "Deadline Met: ✓ (3h 35m early)\n\n"
398
-
399
- output += "=" * 80 + "\n"
400
- output += "【VALIDATOR AGENT FEASIBILITY CHECK】\n"
401
- output += "=" * 80 + "\n"
402
- output += "✓ Comprehensive feasibility check passed\n\n"
403
-
404
- output += "【CHECK ITEMS】\n"
405
- output += "✓ Time window constraints: All satisfied\n"
406
- output += "✓ Deadline constraint: Met (11:25 < 15:00)\n"
407
- output += "✓ Priority handling: All HIGH tasks completed\n"
408
- output += "✓ Business hours: All POIs open during visit\n"
409
- output += "✓ Route continuity: No backtracking\n\n"
410
-
411
- output += "【RISK ASSESSMENT】\n"
412
- output += "- Risk Level: LOW\n"
413
- output += "- Sufficient buffer for unexpected delays\n"
414
- output += "- Hospital may have wait time, buffer allocated\n\n"
415
-
416
- output += "【SCORE】\n"
417
- output += "Overall Score: 98/100 (Excellent)\n"
418
- output += " - Time Efficiency: 95/100\n"
419
- output += " - Route Optimization: 100/100\n"
420
- output += " - Constraint Satisfaction: 100/100\n"
421
- output += " - Practicality: 95/100\n\n"
422
-
423
- output += "=" * 80 + "\n"
424
- output += "【WEATHER AGENT ANALYSIS】\n"
425
- output += "=" * 80 + "\n"
426
- output += "✓ Weather data obtained\n\n"
427
- output += "【CURRENT WEATHER】\n"
428
- output += "- Condition: Sunny ☀️\n"
429
- output += "- Temperature: 23°C\n"
430
- output += "- Precipitation: 10%\n"
431
- output += "- Wind: 5 m/s\n\n"
432
- output += "【WEATHER IMPACT】\n"
433
- output += "- Impact on trip: None\n"
434
- output += "- Recommendation: Good weather for travel\n\n"
435
-
436
- output += "=" * 80 + "\n"
437
- output += "【TRAFFIC AGENT ANALYSIS】\n"
438
- output += "=" * 80 + "\n"
439
- output += "✓ Traffic data analyzed\n\n"
440
- output += "【TRAFFIC FORECAST】\n"
441
- output += "- 08:30-09:00: Light congestion (morning rush)\n"
442
- output += "- 09:00-11:00: Good conditions\n"
443
- output += "- 11:00-11:30: Good conditions\n\n"
444
- output += "【RECOMMENDED DEPARTURE】\n"
445
- output += "- Recommended: 08:30 (as planned)\n"
446
- output += "- Alternative: 08:15 (avoid some congestion)\n\n"
447
- output += "【ESTIMATED TRAVEL TIME】\n"
448
- output += "- Start → Hospital: 18 min (with congestion)\n"
449
- output += "- Hospital → Supermarket: 12 min\n"
450
- output += "- Supermarket → Post Office: 15 min\n"
451
- output += "- Total Travel: 45 minutes\n\n"
452
-
453
- output += "=" * 80 + "\n"
454
- output += "【TEAM MODEL FINAL DECISION】\n"
455
- output += "=" * 80 + "\n"
456
- output += "✓ All agent reports synthesized\n\n"
457
-
458
- output += "【DECISION RATIONALE】\n"
459
- output += "1. Planner identified 3 tasks with clear time constraints\n"
460
- output += "2. Scout found high-quality POIs (all rated 4.3+)\n"
461
- output += "3. Optimizer confirmed current order is optimal\n"
462
- output += "4. Validator verified all constraints satisfied\n"
463
- output += "5. Weather shows good conditions, no adjustments\n"
464
- output += "6. Traffic shows acceptable conditions, proceed as planned\n\n"
465
-
466
- output += "【FINAL PLAN】\n"
467
- output += "Adopt Optimizer's route order\n"
468
- output += "Recommend departure at 08:30\n"
469
- output += "Expected completion at 11:25\n"
470
- output += "Safety margin: 3h 35m\n\n"
471
-
472
- output += "【RECOMMENDATIONS】\n"
473
- output += "1. Depart at 08:30 to ensure completion of all tasks\n"
474
- output += "2. Hospital visit may take longer, buffer allocated\n"
475
- output += "3. After post office, can have lunch nearby\n"
476
- output += "4. If delayed, can skip supermarket (MEDIUM priority)\n\n"
477
-
478
- output += "=" * 80 + "\n"
479
- output += "Planning completed: " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "\n"
480
- output += "=" * 80 + "\n"
481
-
482
- return output
483
-
484
- def create_map(self, result: Optional[Dict] = None) -> go.Figure:
485
- """Create map"""
486
- if not result:
487
- fig = go.Figure()
488
- fig.update_layout(
489
- title="Map will appear after planning",
490
- height=500,
491
- template="plotly_dark"
492
- )
493
- return fig
494
-
495
- stops = [
496
- {"name": "Start", "lat": 25.0330, "lng": 121.5654},
497
- {"name": "NTU Hospital", "lat": 25.0408, "lng": 121.5318},
498
- {"name": "PX Mart", "lat": 25.0428, "lng": 121.5298},
499
- {"name": "Post Office", "lat": 25.0468, "lng": 121.5358}
500
- ]
501
-
502
- fig = go.Figure()
503
-
504
- lats = [stop['lat'] for stop in stops]
505
- lngs = [stop['lng'] for stop in stops]
506
-
507
- fig.add_trace(go.Scattermapbox(
508
- lat=lats,
509
- lon=lngs,
510
- mode='lines+markers',
511
- marker=dict(size=15, color='red'),
512
- line=dict(width=3, color='blue'),
513
- text=[stop['name'] for stop in stops],
514
- hoverinfo='text'
515
- ))
516
-
517
- fig.update_layout(
518
- mapbox=dict(
519
- style='open-street-map',
520
- center=dict(lat=25.04, lon=121.53),
521
- zoom=13
522
- ),
523
- height=500,
524
- margin=dict(l=0, r=0, t=0, b=0)
525
  )
526
 
527
- return fig
528
-
529
- def step1_analyze_tasks(self, user_input: str):
530
- """Step 1: Analyze tasks"""
531
- self.reasoning_messages = []
532
-
533
- self.add_reasoning_message("planner", "Analyzing user requirements...", "thinking")
534
  time_module.sleep(0.5)
535
 
536
- self.add_reasoning_message("planner", "Using tool: parse_requirements()", "tool")
537
- time_module.sleep(0.5)
538
-
539
- self.add_reasoning_message("planner", "✅ Identified 3 tasks: hospital, supermarket, post office", "success")
 
 
 
 
 
 
 
 
540
 
541
- self.add_reasoning_message("planner", "Using tool: extract_time_windows()", "tool")
542
  time_module.sleep(0.5)
543
 
544
- self.add_reasoning_message("planner", "✅ Time windows extracted", "success")
545
-
546
  self.task_list = [
547
- {
548
- "description": "Visit NTU Hospital",
549
- "category": "medical",
550
- "priority": "HIGH",
551
- "time_window": "08:00-12:00"
552
- },
553
- {
554
- "description": "Buy groceries at PX Mart",
555
- "category": "shopping",
556
- "priority": "MEDIUM",
557
- "time_window": "Anytime"
558
- },
559
- {
560
- "description": "Mail package at post office",
561
- "category": "postal",
562
- "priority": "HIGH",
563
- "time_window": "09:00-15:00"
564
- }
565
  ]
566
 
567
- agent_updates = [
568
- self.create_agent_card("planner", "completed", "✅ Analysis complete"),
569
- self.create_agent_card("scout", "idle", "On standby"),
570
- self.create_agent_card("optimizer", "idle", "On standby"),
571
- self.create_agent_card("validator", "idle", "On standby"),
572
- self.create_agent_card("weather", "idle", "On standby"),
573
- self.create_agent_card("traffic", "idle", "On standby")
574
- ]
575
-
576
- task_list_ui = self.create_task_list_ui(self.task_list)
577
- task_summary = self.create_task_summary(self.task_list)
578
-
579
- return (
580
- *agent_updates,
581
- self.get_reasoning_html(),
582
- task_list_ui,
583
- task_summary,
584
- gr.update(visible=True),
585
- gr.update(visible=True),
586
- [],
587
- "✅ Task analysis complete"
588
- )
589
 
590
- def step2_search_pois(self):
591
- """Step 2: Search POIs"""
592
- self.add_reasoning_message("scout", "Starting POI search...", "thinking")
593
- time_module.sleep(0.5)
594
 
595
- agent_updates = [
596
- self.create_agent_card("planner", "completed", "✅ Complete"),
597
- self.create_agent_card("scout", "working", "🔧 Searching POIs..."),
598
- self.create_agent_card("optimizer", "waiting", "⏸️ Waiting"),
599
- self.create_agent_card("validator", "waiting", "⏸️ Waiting"),
600
- self.create_agent_card("weather", "waiting", "⏸️ Waiting"),
601
- self.create_agent_card("traffic", "waiting", "⏸️ Waiting")
602
- ]
603
 
604
- yield (*agent_updates, self.get_reasoning_html(), "", gr.update(), gr.update(), gr.update(), gr.update(),
605
- "🔍 Searching locations...")
606
 
607
- self.add_reasoning_message("scout", "Using tool: search_poi(query='hospital')", "tool")
608
- time_module.sleep(0.8)
 
 
 
 
 
 
 
 
 
 
609
 
610
- self.add_reasoning_message("scout", "✅ Found NTU Hospital (0.8km, 4.8★)", "success")
 
 
 
 
 
 
 
 
 
 
 
 
 
611
 
612
- self.add_reasoning_message("scout", "Using tool: search_poi(query='supermarket')", "tool")
613
- time_module.sleep(0.8)
 
614
 
615
- self.add_reasoning_message("scout", "✅ Found PX Mart (1.2km, 24/7)", "success")
 
 
 
 
616
 
617
- self.add_reasoning_message("scout", "Using tool: search_poi(query='post office')", "tool")
618
- time_module.sleep(0.8)
619
 
620
- self.add_reasoning_message("scout", "✅ Found Post Office (1.8km, until 17:30)", "success")
 
621
 
622
- self.add_reasoning_message("scout", "Using tool: calculate_distance_matrix()", "tool")
623
- time_module.sleep(0.5)
 
 
 
 
 
 
 
624
 
625
- self.add_reasoning_message("scout", "✅ Distance matrix calculated", "success")
626
 
627
  agent_updates = [
628
- self.create_agent_card("planner", "completed", " Complete"),
629
- self.create_agent_card("scout", "completed", " POI search complete"),
630
- self.create_agent_card("optimizer", "waiting", "⏸️ Waiting"),
631
- self.create_agent_card("validator", "waiting", "⏸️ Waiting"),
632
- self.create_agent_card("weather", "waiting", "⏸️ Waiting"),
633
- self.create_agent_card("traffic", "waiting", "⏸️ Waiting")
634
  ]
635
 
636
- yield (*agent_updates, self.get_reasoning_html(), "", gr.update(), gr.update(), gr.update(), gr.update(),
637
- "✅ POI search complete")
638
 
639
  def step3_optimize_route(self):
640
- """Step 3: Optimize route"""
641
- self.add_reasoning_message("optimizer", "Starting route optimization...", "thinking")
642
- time_module.sleep(0.5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
 
644
  agent_updates = [
645
- self.create_agent_card("planner", "completed", " Complete"),
646
- self.create_agent_card("scout", "completed", " Complete"),
647
- self.create_agent_card("optimizer", "working", "⚙️ Optimizing..."),
648
- self.create_agent_card("validator", "waiting", "⏸️ Waiting"),
649
- self.create_agent_card("weather", "waiting", "⏸️ Waiting"),
650
- self.create_agent_card("traffic", "waiting", "⏸️ Waiting")
651
  ]
652
 
653
- yield (*agent_updates, self.get_reasoning_html(), "", gr.update(), gr.update(), gr.update(), gr.update(),
654
- "⚙️ Optimizing route...")
655
-
656
- self.add_reasoning_message("optimizer", "Using tool: optimize_with_ortools()", "tool")
657
- time_module.sleep(1.0)
658
-
659
- self.add_reasoning_message("optimizer", "✅ TSPTW solved, improved 15%", "success")
660
 
661
- self.add_reasoning_message("validator", "Validating feasibility...", "thinking")
662
- time_module.sleep(0.5)
 
 
 
 
663
 
664
- agent_updates = [
665
- self.create_agent_card("planner", "completed", "✅ Complete"),
666
- self.create_agent_card("scout", "completed", "✅ Complete"),
667
- self.create_agent_card("optimizer", "completed", "✅ Optimized"),
668
- self.create_agent_card("validator", "working", "🛡️ Validating..."),
669
- self.create_agent_card("weather", "waiting", "⏸️ Waiting"),
670
- self.create_agent_card("traffic", "waiting", "⏸️ Waiting")
671
- ]
672
 
673
- yield (*agent_updates, self.get_reasoning_html(), "", gr.update(), gr.update(), gr.update(), gr.update(),
674
- "🛡️ Validating...")
675
 
676
- self.add_reasoning_message("validator", "Using tool: validate_time_windows()", "tool")
677
- time_module.sleep(0.8)
678
 
679
- self.add_reasoning_message("validator", "✅ All time windows satisfied", "success")
 
 
680
 
681
- self.add_reasoning_message("validator", "Using tool: check_deadline()", "tool")
682
- time_module.sleep(0.8)
683
-
684
- self.add_reasoning_message("validator", "✅ Deadline check passed (3h35m buffer)", "success")
685
-
686
- self.add_reasoning_message("weather", "Checking weather...", "thinking")
687
- time_module.sleep(0.5)
688
 
689
  agent_updates = [
690
- self.create_agent_card("planner", "completed", " Complete"),
691
- self.create_agent_card("scout", "completed", " Complete"),
692
- self.create_agent_card("optimizer", "completed", "✅ Complete"),
693
- self.create_agent_card("validator", "completed", "✅ Validated"),
694
- self.create_agent_card("weather", "working", "🌈 Checking..."),
695
- self.create_agent_card("traffic", "waiting", "⏸️ Waiting")
696
  ]
697
 
698
- yield (*agent_updates, self.get_reasoning_html(), "", gr.update(), gr.update(), gr.update(), gr.update(),
699
- "🌈 Checking weather...")
 
 
 
 
 
700
 
701
- self.add_reasoning_message("weather", "Using tool: check_weather()", "tool")
702
- time_module.sleep(0.8)
 
 
 
 
 
703
 
704
- self.add_reasoning_message("weather", "✅ Sunny, 23°C, perfect for travel", "success")
 
 
 
 
 
 
705
 
706
- self.add_reasoning_message("traffic", "Checking traffic...", "thinking")
707
- time_module.sleep(0.5)
 
 
 
 
 
708
 
709
- agent_updates = [
710
- self.create_agent_card("planner", "completed", "✅ Complete"),
711
- self.create_agent_card("scout", "completed", "✅ Complete"),
712
- self.create_agent_card("optimizer", "completed", "✅ Complete"),
713
- self.create_agent_card("validator", "completed", "✅ Complete"),
714
- self.create_agent_card("weather", "completed", "✅ Good weather"),
715
- self.create_agent_card("traffic", "working", "🚗 Checking...")
716
- ]
 
717
 
718
- yield (*agent_updates, self.get_reasoning_html(), "", gr.update(), gr.update(), gr.update(), gr.update(),
719
- "🚗 Checking traffic...")
 
 
 
 
 
 
 
720
 
721
- self.add_reasoning_message("traffic", "Using tool: check_traffic()", "tool")
722
- time_module.sleep(0.8)
 
 
723
 
724
- self.add_reasoning_message("traffic", " Good road conditions", "success")
725
 
726
- agent_updates = [
727
- self.create_agent_card("planner", "completed", "✅ Complete"),
728
- self.create_agent_card("scout", "completed", "✅ Complete"),
729
- self.create_agent_card("optimizer", "completed", "✅ Complete"),
730
- self.create_agent_card("validator", "completed", "✅ Complete"),
731
- self.create_agent_card("weather", "completed", "✅ Complete"),
732
- self.create_agent_card("traffic", "completed", "✅ Complete")
733
- ]
734
 
735
- result = {
736
- 'route': {
737
- 'stops': [
738
- {'name': 'NTU Hospital', 'address': 'No. 1, Changde St., Taipei', 'arrival_time': '09:00',
739
- 'duration': '45',
740
- 'rating': '4.8', 'phone': '02-2312-3456'},
741
- {'name': 'PX Mart', 'address': 'Roosevelt Rd. Sec. 3, Taipei', 'arrival_time': '10:15',
742
- 'duration': '30', 'rating': '4.5', 'phone': '02-2362-7890'},
743
- {'name': 'Main Post Office', 'address': 'Zhongxiao W. Rd. Sec. 1, Taipei', 'arrival_time': '11:05',
744
- 'duration': '20', 'rating': '4.3', 'phone': '02-2311-1234'}
745
- ]
746
- }
747
- }
 
 
 
 
 
 
748
 
749
- trip_result = self.create_trip_result_summary(result)
750
- agent_text = self.create_agent_text_output(result)
751
- map_fig = self.create_map(result)
752
 
753
- yield (
754
- *agent_updates,
755
- self.get_reasoning_html(),
756
- trip_result,
757
- agent_text,
758
- map_fig,
759
- gr.update(visible=True),
760
- gr.update(visible=True),
761
- "🎉 Planning complete!"
 
 
762
  )
763
 
764
- def modify_task_chat(self, message: str, history: List):
765
- """Modify tasks via chat"""
766
- if not message:
767
- return history, self.create_task_list_ui(self.task_list)
768
-
769
- history.append({"role": "user", "content": message})
770
- history.append({"role": "assistant", "content": "Noted. I've recorded your request."})
771
- return history, self.create_task_list_ui(self.task_list)
772
-
773
- def save_settings(self, google_key: str, weather_key: str, anthropic_key: str, model: str):
774
- """Save settings"""
775
- self.settings['google_maps_api_key'] = google_key
776
- self.settings['openweather_api_key'] = weather_key
777
- self.settings['anthropic_api_key'] = anthropic_key
778
- self.settings['model'] = model
779
- return "✅ Settings saved"
780
-
781
  def build_interface(self):
782
- """Build Gradio interface (compact layout)"""
783
- with gr.Blocks(
784
- theme=gr.themes.Soft(
785
- primary_hue="blue",
786
- secondary_hue="green",
787
- neutral_hue="slate",
788
- font=gr.themes.GoogleFont("Inter"),
789
- font_mono=gr.themes.GoogleFont("IBM Plex Mono")
790
- ),
791
- css="""
792
- .breathing {
793
- animation: breathing 2s ease-in-out infinite;
794
- }
795
- @keyframes breathing {
796
- 0%, 100% { opacity: 1; transform: scale(1); }
797
- 50% { opacity: 0.9; transform: scale(1.02); }
798
- }
799
- .compact-container {
800
- max-width: 1400px;
801
- margin: 0 auto;
802
- }
803
- """,
804
- title="LifeFlow AI - Intelligent Trip Planning"
805
- ) as demo:
806
- gr.Markdown("""
807
- # 🚀 LifeFlow AI - Intelligent Trip Planning System
808
-
809
- **Multi-Agent AI System** for optimizing daily errands and activities
810
- """)
811
-
812
- with gr.Row(elem_classes="compact-container"):
813
- # Left: Input + Tasks + Agents + Results (narrower)
814
- with gr.Column(scale=2, min_width=350):
815
- # Input area
816
- with gr.Group(visible=True) as input_area:
817
- gr.Markdown("### 📝 Describe Your Needs")
818
-
819
- user_input = gr.Textbox(
820
- label="Requirements",
821
- placeholder="e.g., Visit hospital tomorrow morning, buy groceries, mail package before 3pm",
822
- lines=3
823
- )
824
-
825
- with gr.Row():
826
- start_location = gr.Textbox(
827
- label="Start",
828
- placeholder="Taipei",
829
- scale=2
830
- )
831
- start_time = gr.Textbox(
832
- label="Time",
833
- placeholder="08:30",
834
- scale=1
835
- )
836
-
837
- deadline = gr.Textbox(
838
- label="Deadline (optional)",
839
- placeholder="e.g., 15:00"
840
- )
841
-
842
- analyze_btn = gr.Button("🚀 Start AI Analysis", variant="primary", size="lg")
843
-
844
- # Task confirmation
845
- with gr.Group(visible=False) as task_confirm_area:
846
- gr.Markdown("### ✅ Confirm Tasks")
847
-
848
- task_list_display = gr.HTML()
849
- task_summary_display = gr.HTML()
850
-
851
- with gr.Row():
852
- back_btn = gr.Button("⬅️ Back", variant="secondary", scale=1)
853
- confirm_btn = gr.Button("✅ Confirm & Plan", variant="primary", scale=2)
854
-
855
- # Trip result summary
856
- with gr.Group(visible=False) as trip_result_area:
857
- trip_result_display = gr.HTML()
858
-
859
- # AI Team
860
- with gr.Group(visible=False) as team_area:
861
- gr.Markdown("### 🤖 AI Expert Team")
862
- agent_displays = []
863
- for agent_key in ['planner', 'scout', 'optimizer', 'validator', 'weather', 'traffic']:
864
- agent_card = gr.HTML(value=self.create_agent_card(agent_key, "idle", "On standby"))
865
- agent_displays.append(agent_card)
866
-
867
- # Right: Status + Chat + Tabs (wider)
868
  with gr.Column(scale=3, min_width=500):
869
- # Status bar
870
- with gr.Group():
871
- status_bar = gr.Textbox(
872
- label="📊 Status",
873
- value="Waiting for input...",
874
- interactive=False,
875
- max_lines=1
876
- )
877
-
878
- # Chat modification
879
- with gr.Group(visible=False) as chat_modify_area:
880
- gr.Markdown("### 💬 Modify Tasks")
881
-
882
- chatbot = gr.Chatbot(
883
- type='messages',
884
- value=[],
885
- height=120,
886
- show_label=False
887
- )
888
-
889
- with gr.Row():
890
- chat_input = gr.Textbox(
891
- placeholder="e.g., Change task 2 to high priority",
892
- show_label=False,
893
- scale=4
894
- )
895
- chat_send = gr.Button("Send", variant="primary", scale=1)
896
-
897
- # Tabs
898
- with gr.Tabs() as tabs:
899
- # Tab 1: AI Conversation
900
- with gr.Tab("💬 AI Conversation", id="chat_tab"):
901
- reasoning_output = gr.HTML(
902
- value=self.get_reasoning_html()
903
- )
904
-
905
- # Tab 2: Full Report
906
- with gr.Tab("📊 Full Report", id="summary_tab", visible=False) as summary_tab:
907
- gr.Markdown("*Team Agent complete analysis (plain text)*")
908
- complete_summary_output = gr.Textbox(
909
- show_label=False,
910
- lines=20,
911
- max_lines=25,
912
- interactive=False
913
- )
914
-
915
- # Tab 3: Map
916
- with gr.Tab("🗺️ Route Map", id="map_tab", visible=False) as map_tab:
917
- map_output = gr.Plot(value=self.create_map(), show_label=False)
918
-
919
- # Tab 4: Settings
920
- with gr.Tab("⚙️ Settings", id="settings_tab"):
921
- gr.Markdown("### 🔑 API Keys")
922
-
923
- google_maps_key = gr.Textbox(
924
- label="Google Maps API Key",
925
- placeholder="AIzaSy...",
926
- type="password"
927
- )
928
-
929
- openweather_key = gr.Textbox(
930
- label="OpenWeather API Key",
931
- type="password"
932
- )
933
-
934
- anthropic_key = gr.Textbox(
935
- label="Anthropic API Key",
936
- placeholder="sk-ant-...",
937
- type="password"
938
- )
939
-
940
- gr.Markdown("### 🤖 Model")
941
-
942
- model_choice = gr.Dropdown(
943
- label="Select Model",
944
- choices=[
945
- "claude-sonnet-4-20250514",
946
- "claude-opus-4-20250514",
947
- "gpt-4o",
948
- "gemini-pro"
949
- ],
950
- value="claude-sonnet-4-20250514"
951
- )
952
-
953
- save_settings_btn = gr.Button("💾 Save", variant="primary")
954
- settings_status = gr.Textbox(label="Status", value="", interactive=False)
955
-
956
- # Examples
957
- gr.Examples(
958
- examples=[
959
- ["Visit hospital tomorrow morning, buy groceries, mail package before 3pm", "Taipei", "08:30",
960
- "15:00"]
961
- ],
962
- inputs=[user_input, start_location, start_time, deadline]
963
  )
964
 
965
- # Event bindings
966
  analyze_btn.click(
967
  fn=self.step1_analyze_tasks,
968
- inputs=[user_input],
969
  outputs=[
970
- *agent_displays,
971
- reasoning_output,
972
- task_list_display,
973
- task_summary_display,
974
- task_confirm_area,
975
- chat_modify_area,
976
- chatbot,
977
- status_bar
978
  ]
979
  ).then(
980
- fn=lambda: (gr.update(visible=False), gr.update(visible=True)),
981
- outputs=[input_area, task_confirm_area]
982
- )
983
-
984
- back_btn.click(
985
  fn=lambda: (
986
- gr.update(visible=True),
987
- gr.update(visible=False),
988
- gr.update(visible=False),
989
- "Waiting for input..."
990
  ),
991
- outputs=[input_area, task_confirm_area, chat_modify_area, status_bar]
992
  )
993
 
 
994
  chat_send.click(
995
  fn=self.modify_task_chat,
996
- inputs=[chat_input, chatbot],
997
- outputs=[chatbot, task_list_display]
998
  ).then(
999
- fn=lambda: "",
1000
  outputs=[chat_input]
1001
  )
1002
 
1003
- chat_input.submit(
1004
- fn=self.modify_task_chat,
1005
- inputs=[chat_input, chatbot],
1006
- outputs=[chatbot, task_list_display]
1007
- ).then(
1008
- fn=lambda: "",
1009
- outputs=[chat_input]
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
  )
1011
 
1012
- confirm_btn.click(
 
 
1013
  fn=lambda: (
1014
- gr.update(visible=False),
1015
- gr.update(visible=False),
1016
- gr.update(visible=True)
 
1017
  ),
1018
- outputs=[task_confirm_area, chat_modify_area, team_area]
1019
  ).then(
 
1020
  fn=self.step2_search_pois,
1021
- outputs=[
1022
- *agent_displays,
1023
- reasoning_output,
1024
- trip_result_display,
1025
- complete_summary_output,
1026
- map_output,
1027
- map_tab,
1028
- summary_tab,
1029
- status_bar
1030
- ]
1031
  ).then(
 
1032
  fn=self.step3_optimize_route,
1033
- outputs=[
1034
- *agent_displays,
1035
- reasoning_output,
1036
- trip_result_display,
1037
- complete_summary_output,
1038
- map_output,
1039
- map_tab,
1040
- summary_tab,
1041
- status_bar
1042
- ]
1043
  ).then(
 
1044
  fn=lambda: (
1045
- gr.update(visible=True),
1046
- gr.update(selected="summary_tab")
 
1047
  ),
1048
- outputs=[trip_result_area, tabs]
 
 
 
 
 
 
 
 
 
 
1049
  )
1050
 
 
 
 
 
 
 
 
 
 
1051
  save_settings_btn.click(
1052
  fn=self.save_settings,
1053
  inputs=[google_maps_key, openweather_key, anthropic_key, model_choice],
1054
  outputs=[settings_status]
1055
  )
1056
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1057
  return demo
1058
 
1059
 
1060
  def main():
1061
- app = LifeFlowInteractive()
1062
  demo = app.build_interface()
1063
- demo.launch(
1064
- server_name="0.0.0.0",
1065
- server_port=7860, # HuggingFace default 7860
1066
- share=False,
1067
- show_error=True
1068
- )
1069
-
1070
 
1071
  if __name__ == "__main__":
1072
  main()
 
1
  """
2
+ LifeFlow AI - Main Application v3.1
 
 
 
 
3
  """
4
 
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
  import gradio as gr
10
+ from datetime import datetime
 
 
 
11
  import time as time_module
12
 
13
+ # 導入配置
14
+ from config import DEFAULT_SETTINGS, APP_TITLE
15
+
16
+ # 導入 UI 組件
17
+ from ui.theme import get_enhanced_css
18
+ from ui.components.header import create_header, create_top_controls
19
+ from ui.components.input_form import create_input_form, toggle_location_inputs
20
+ from ui.components.confirmation import create_confirmation_area, create_exit_button
21
+ from ui.components.results import create_team_area, create_result_area, create_tabs
22
+ from ui.components.modals import create_settings_modal, create_doc_modal
23
+
24
+ # 導入核心工具
25
+ from core.utils import (
26
+ create_agent_stream_output, create_agent_card_enhanced,
27
+ create_task_card, create_summary_card, create_animated_map,
28
+ get_reasoning_html_reversed, create_celebration_animation,
29
+ create_result_visualization
30
+ )
31
+
32
 
33
+ class LifeFlowAI:
34
+ """LifeFlow AI - v3.1 主類"""
35
 
36
  def __init__(self):
37
+ self.settings = DEFAULT_SETTINGS.copy()
 
38
  self.task_list = []
 
 
39
  self.reasoning_messages = []
40
  self.planning_completed = False
41
+ self.chat_history = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ def step1_analyze_tasks(self, user_input, auto_location, lat, lon):
 
 
 
 
 
 
 
 
 
 
44
  """
45
+ Step 1: 分析任務 - 帶串流輸出
46
+ 完成後進入 Step 2 (Confirmation)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  """
48
+ if not user_input.strip():
49
+ return self._empty_step1_outputs()
50
 
51
+ # 🔧 串流輸出 1: 開始分析
52
+ stream_str = "🤔 Analyzing your request..."
53
+ stream_html = self._create_stream_html(stream_str)
54
+ yield (
55
+ stream_html, "", "", get_reasoning_html_reversed(),
56
+ gr.update(visible=False), gr.update(visible=False),
57
+ "", # chat_history_output
58
+ "Starting analysis...",
59
+ *[create_agent_card_enhanced("planner", "working", "Analyzing..."),
60
+ *[create_agent_card_enhanced(k, "idle", "On standby")
61
+ for k in ["scout", "optimizer", "validator", "weather", "traffic"]]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  )
63
 
 
 
 
 
 
 
 
64
  time_module.sleep(0.5)
65
 
66
+ stream_str += "\n📋 Extracting tasks from your input..."
67
+ # 🔧 串流輸出 2: 提取任務
68
+ stream_html = self._create_stream_html(stream_str)
69
+ yield (
70
+ stream_html, "", "", get_reasoning_html_reversed(),
71
+ gr.update(visible=False), gr.update(visible=False),
72
+ "", # chat_history_output
73
+ "Extracting tasks...",
74
+ *[create_agent_card_enhanced("planner", "working", "Extracting tasks..."),
75
+ *[create_agent_card_enhanced(k, "idle", "On standby")
76
+ for k in ["scout", "optimizer", "validator", "weather", "traffic"]]]
77
+ )
78
 
 
79
  time_module.sleep(0.5)
80
 
81
+ # 模擬提取的任務
 
82
  self.task_list = [
83
+ {"id": 1, "title": "Visit hospital", "priority": "HIGH",
84
+ "time": "08:00-12:00", "duration": "45 minutes", "location": "Hospital nearby", "icon": "🏥"},
85
+ {"id": 2, "title": "Buy groceries", "priority": "MEDIUM",
86
+ "time": "Anytime", "duration": "30 minutes", "location": "Supermarket", "icon": "🛒"},
87
+ {"id": 3, "title": "Mail package", "priority": "HIGH",
88
+ "time": "Before 15:00", "duration": "20 minutes", "location": "Post office", "icon": "📮"}
 
 
 
 
 
 
 
 
 
 
 
 
89
  ]
90
 
91
+ # 添加推理訊息
92
+ self._add_reasoning("planner", "Analyzing input tasks...")
93
+ self._add_reasoning("planner", f"Extracted {len(self.task_list)} tasks")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
+ # 生成摘要和任務卡片
96
+ high_priority = sum(1 for t in self.task_list if t["priority"] == "HIGH")
97
+ total_time = sum(int(t["duration"].split()[0]) for t in self.task_list)
 
98
 
99
+ summary_html = create_summary_card(len(self.task_list), high_priority, total_time)
100
+ task_list_html = self._generate_task_list_html()
 
 
 
 
 
 
101
 
102
+ stream_str += "\n✅ Analysis complete! Please review your tasks."
103
+ stream_html = self._create_stream_html(stream_str)
104
 
105
+ # Step 1 完成,準備進入 Step 2
106
+ yield (
107
+ stream_html, summary_html, task_list_html,
108
+ get_reasoning_html_reversed(self.reasoning_messages),
109
+ gr.update(visible=True), # task_confirm_area
110
+ gr.update(visible=False), # chat_input_area (先隱藏)
111
+ self._generate_chat_welcome_html(), # chat_history_output
112
+ "Tasks extracted successfully ✓",
113
+ *[create_agent_card_enhanced("planner", "complete", "Tasks extracted"),
114
+ *[create_agent_card_enhanced(k, "idle", "On standby")
115
+ for k in ["scout", "optimizer", "validator", "weather", "traffic"]]]
116
+ )
117
 
118
+ def modify_task_chat(self, user_message):
119
+ """
120
+ 處理用戶在 Chat with LifeFlow Tab 中的修改請求
121
+ 這在 Step 2 時可用
122
+ """
123
+ if not user_message.strip():
124
+ return self._generate_chat_history_html(), self._generate_task_list_html()
125
+
126
+ # 添加用戶消息
127
+ self.chat_history.append({
128
+ "role": "user",
129
+ "message": user_message,
130
+ "time": datetime.now().strftime("%H:%M:%S")
131
+ })
132
 
133
+ # 模擬 AI 回應
134
+ time_module.sleep(0.3)
135
+ ai_response = f"✅ I've updated your tasks based on your request: '{user_message}'"
136
 
137
+ self.chat_history.append({
138
+ "role": "assistant",
139
+ "message": ai_response,
140
+ "time": datetime.now().strftime("%H:%M:%S")
141
+ })
142
 
143
+ # 更新 reasoning
144
+ self._add_reasoning("planner", f"User requested: {user_message}")
145
 
146
+ # 返回更新的聊天歷史和任務列表
147
+ return self._generate_chat_history_html(), self._generate_task_list_html()
148
 
149
+ def step2_search_pois(self):
150
+ """
151
+ Step 2.5: 搜索 POI - 專家團隊開始工作
152
+ Scout Agent 開始工作
153
+ """
154
+ time_module.sleep(1)
155
+ self._add_reasoning("scout", "Searching for POIs...")
156
+ self._add_reasoning("scout", "Found hospital: NTU Hospital (800m, 4.8★)")
157
+ self._add_reasoning("scout", "Found supermarket: PX Mart (1.2km)")
158
 
159
+ reasoning_html = get_reasoning_html_reversed(self.reasoning_messages)
160
 
161
  agent_updates = [
162
+ create_agent_card_enhanced("planner", "complete", "Tasks ready"),
163
+ create_agent_card_enhanced("scout", "working", "Searching POIs..."),
164
+ *[create_agent_card_enhanced(k, "idle", "On standby")
165
+ for k in ["optimizer", "validator", "weather", "traffic"]]
 
 
166
  ]
167
 
168
+ return reasoning_html, "🗺️ Searching for locations...", *agent_updates
 
169
 
170
  def step3_optimize_route(self):
171
+ """Step 3: 優化路線 - Optimizer 開始工作"""
172
+ time_module.sleep(1)
173
+ self._add_reasoning("optimizer", "Running TSPTW solver...")
174
+ self._add_reasoning("optimizer", "Optimized route: Hospital → Supermarket → Post Office")
175
+
176
+ report = """
177
+ ## 🎯 Optimization Complete
178
+
179
+ ### Route Details:
180
+ 1. **🏥 Hospital** (09:00 - 10:00)
181
+ 2. **🛒 Supermarket** (10:15 - 10:45)
182
+ 3. **📮 Post Office** (11:05 - 11:25)
183
+
184
+ ### Metrics:
185
+ - ✅ Total distance: 2.8 km
186
+ - ✅ Total time: 95 minutes
187
+ - ✅ All deadlines met
188
+ - ✅ Minimal travel distance
189
+ - ✅ Weather conditions favorable
190
+ """
191
 
192
  agent_updates = [
193
+ create_agent_card_enhanced("planner", "complete", "Analysis done"),
194
+ create_agent_card_enhanced("scout", "complete", "POI search done"),
195
+ create_agent_card_enhanced("optimizer", "working", "Optimizing route..."),
196
+ *[create_agent_card_enhanced(k, "idle", "On standby")
197
+ for k in ["validator", "weather", "traffic"]]
 
198
  ]
199
 
200
+ reasoning_html = get_reasoning_html_reversed(self.reasoning_messages)
201
+ return reasoning_html, report, "🎯 Optimizing route...", *agent_updates
 
 
 
 
 
202
 
203
+ def step4_finalize(self):
204
+ """Step 4: 完成並生成報告(含慶祝動畫和增強視覺化)"""
205
+ time_module.sleep(1)
206
+ self._add_reasoning("validator", "Validating route quality...")
207
+ self._add_reasoning("weather", "Checking weather conditions...")
208
+ self._add_reasoning("traffic", "Analyzing traffic patterns...")
209
 
210
+ self.planning_completed = True
 
 
 
 
 
 
 
211
 
212
+ # 生成慶祝動畫
213
+ celebration_html = create_celebration_animation()
214
 
215
+ # 生成詳細的結果視覺化
216
+ result_html = create_result_visualization(self.task_list) + celebration_html
217
 
218
+ # 簡化的 timeline metrics(保留兼容性,但現在主要信息在 result_html 中)
219
+ timeline_html = "<h3>🗓️ Detailed Timeline</h3><p>See the complete timeline in the result panel</p>"
220
+ metrics_html = "<h3>📈 Performance Metrics</h3><p>All optimization metrics are displayed above</p>"
221
 
222
+ map_fig = create_animated_map()
 
 
 
 
 
 
223
 
224
  agent_updates = [
225
+ create_agent_card_enhanced(k, "complete", "Task complete")
226
+ for k in ["planner", "scout", "optimizer", "validator", "weather", "traffic"]
 
 
 
 
227
  ]
228
 
229
+ return (
230
+ timeline_html, metrics_html, result_html, map_fig,
231
+ gr.update(visible=True), # map_tab
232
+ gr.update(visible=False), # team_area (隱藏,任務完成)
233
+ "🎉 Planning complete! Review your optimized route.",
234
+ *agent_updates
235
+ )
236
 
237
+ def save_settings(self, google_key, weather_key, anthropic_key, model):
238
+ """保存設定"""
239
+ self.settings['google_maps_api_key'] = google_key
240
+ self.settings['openweather_api_key'] = weather_key
241
+ self.settings['anthropic_api_key'] = anthropic_key
242
+ self.settings['model'] = model
243
+ return "✅ Settings saved successfully!"
244
 
245
+ def _create_stream_html(self, message):
246
+ """創建串流輸出 HTML"""
247
+ return f"""
248
+ <div class="stream-container">
249
+ <div class="stream-text">{message}<span class="stream-cursor"></span></div>
250
+ </div>
251
+ """
252
 
253
+ def _add_reasoning(self, agent, message):
254
+ """添加推理訊息"""
255
+ self.reasoning_messages.append({
256
+ 'agent': agent,
257
+ 'message': message,
258
+ 'time': datetime.now().strftime("%H:%M:%S")
259
+ })
260
 
261
+ def _generate_task_list_html(self):
262
+ """生成任務列表 HTML"""
263
+ html = ""
264
+ for task in self.task_list:
265
+ html += create_task_card(
266
+ task["id"], task["title"], task["priority"],
267
+ task["time"], task["duration"], task["location"], task["icon"]
268
+ )
269
+ return html
270
 
271
+ def _generate_chat_welcome_html(self):
272
+ """生成 Chat 歡迎訊息"""
273
+ return """
274
+ <div style="padding: 20px; text-align: center;">
275
+ <h3 style="color: #4A90E2; margin-bottom: 10px;">💬 Chat with LifeFlow</h3>
276
+ <p style="opacity: 0.8;">You can now modify your tasks by chatting with me!</p>
277
+ <p style="opacity: 0.6; font-size: 0.9em;">Try: "Change task 2 to high priority" or "Add a new task"</p>
278
+ </div>
279
+ """
280
 
281
+ def _generate_chat_history_html(self):
282
+ """生成聊天歷史 HTML"""
283
+ if not self.chat_history:
284
+ return self._generate_chat_welcome_html()
285
 
286
+ html = '<div class="chat-history-container" style="max-height: 400px; overflow-y: auto;">'
287
 
288
+ for msg in self.chat_history:
289
+ role = msg["role"]
290
+ message = msg["message"]
291
+ time = msg["time"]
 
 
 
 
292
 
293
+ if role == "user":
294
+ html += f'''
295
+ <div style="margin-bottom: 15px; text-align: right;">
296
+ <div style="display: inline-block; max-width: 70%; background: #E3F2FD; padding: 10px 15px; border-radius: 15px 15px 0 15px;">
297
+ <div style="font-size: 0.9em; font-weight: 500;">{message}</div>
298
+ <div style="font-size: 0.75em; opacity: 0.6; margin-top: 5px;">{time}</div>
299
+ </div>
300
+ </div>
301
+ '''
302
+ else:
303
+ html += f'''
304
+ <div style="margin-bottom: 15px; text-align: left;">
305
+ <div style="display: inline-block; max-width: 70%; background: #F5F5F5; padding: 10px 15px; border-radius: 15px 15px 15px 0;">
306
+ <div style="font-size: 0.85em; color: #4A90E2; font-weight: 600; margin-bottom: 5px;">🤖 LifeFlow AI</div>
307
+ <div style="font-size: 0.9em;">{message}</div>
308
+ <div style="font-size: 0.75em; opacity: 0.6; margin-top: 5px;">{time}</div>
309
+ </div>
310
+ </div>
311
+ '''
312
 
313
+ html += '</div>'
314
+ return html
 
315
 
316
+ def _empty_step1_outputs(self):
317
+ """返回空的 Step 1 輸出"""
318
+ return (
319
+ create_agent_stream_output(),
320
+ "", "", get_reasoning_html_reversed(),
321
+ gr.update(visible=False),
322
+ gr.update(visible=False),
323
+ self._generate_chat_welcome_html(),
324
+ "Please enter your tasks",
325
+ *[create_agent_card_enhanced(k, "idle", "On standby")
326
+ for k in ["planner", "scout", "optimizer", "validator", "weather", "traffic"]]
327
  )
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  def build_interface(self):
330
+ """構建 Gradio 界面"""
331
+ with gr.Blocks(title=APP_TITLE) as demo:
332
+ # 注入 CSS 樣式
333
+ gr.HTML(get_enhanced_css())
334
+ # Header
335
+ create_header()
336
+
337
+ # Top Controls (Theme, Settings & Doc)
338
+ theme_btn, settings_btn, doc_btn = create_top_controls()
339
+
340
+ # Main Layout
341
+ with gr.Row():
342
+ # ========== Left Column (主操作區) ==========
343
+ with gr.Column(scale=2, min_width=400):
344
+ # Step 1: Input Form
345
+ (input_area, agent_stream_output, user_input, auto_location,
346
+ location_inputs, lat_input, lon_input, analyze_btn) = create_input_form(
347
+ create_agent_stream_output()
348
+ )
349
+
350
+ # Step 2: Confirmation Area (包含 Exit 和 Ready to plan 按鈕)
351
+ (task_confirm_area, task_summary_display,
352
+ task_list_display, exit_btn_inline, ready_plan_btn) = create_confirmation_area()
353
+
354
+ # Step 2.5/3: Team Area (取代 Confirmation Area)
355
+ team_area, agent_displays = create_team_area(create_agent_card_enhanced)
356
+
357
+ # Step 3: Result Area (最終結果展示)
358
+ (result_area, result_display,
359
+ timeline_display, metrics_display) = create_result_area(create_animated_map)
360
+
361
+ # ========== Right Column (狀態 + Tabs) ==========
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  with gr.Column(scale=3, min_width=500):
363
+ status_bar = gr.Textbox(
364
+ label="📊 Status",
365
+ value="Waiting for input...",
366
+ interactive=False,
367
+ max_lines=1
368
+ )
369
+
370
+ # Tabs (包含新的 Chat with LifeFlow Tab)
371
+ (tabs, report_tab, map_tab, report_output, map_output, reasoning_output,
372
+ chat_input_area, chat_history_output, chat_input, chat_send) = create_tabs(
373
+ create_animated_map,
374
+ get_reasoning_html_reversed()
375
+ )
376
+
377
+ # Modals
378
+ (settings_modal, google_maps_key, openweather_key, anthropic_key,
379
+ model_choice, close_settings_btn, save_settings_btn,
380
+ settings_status) = create_settings_modal()
381
+
382
+ doc_modal, close_doc_btn = create_doc_modal()
383
+
384
+ # ============= Event Handlers =============
385
+
386
+ # Auto location toggle
387
+ auto_location.change(
388
+ fn=toggle_location_inputs,
389
+ inputs=[auto_location],
390
+ outputs=[location_inputs]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  )
392
 
393
+ # ====== Step 1: Analyze button ======
394
  analyze_btn.click(
395
  fn=self.step1_analyze_tasks,
396
+ inputs=[user_input, auto_location, lat_input, lon_input],
397
  outputs=[
398
+ agent_stream_output, task_summary_display, task_list_display,
399
+ reasoning_output, task_confirm_area, chat_input_area,
400
+ chat_history_output, status_bar, *agent_displays
 
 
 
 
 
401
  ]
402
  ).then(
403
+ # Step 1 → Step 2: 隱藏輸入區,顯示確認區 + 開啟 Chat 輸入框
 
 
 
 
404
  fn=lambda: (
405
+ gr.update(visible=False), # input_area
406
+ gr.update(visible=True), # task_confirm_area
407
+ gr.update(visible=True) # chat_input_area (開啟聊天功能)
 
408
  ),
409
+ outputs=[input_area, task_confirm_area, chat_input_area]
410
  )
411
 
412
+ # ====== Step 2: Chat with LifeFlow (任務修改) ======
413
  chat_send.click(
414
  fn=self.modify_task_chat,
415
+ inputs=[chat_input],
416
+ outputs=[chat_history_output, task_list_display]
417
  ).then(
418
+ fn=lambda: "", # 清空輸入框
419
  outputs=[chat_input]
420
  )
421
 
422
+ # ====== Step 2: Exit button ======
423
+ exit_btn_inline.click(
424
+ fn=lambda: (
425
+ gr.update(visible=True), # input_area
426
+ gr.update(visible=False), # task_confirm_area
427
+ gr.update(visible=False), # chat_input_area (關閉聊天)
428
+ gr.update(visible=False), # result_area
429
+ gr.update(visible=False), # team_area
430
+ gr.update(visible=False), # report_tab
431
+ gr.update(visible=False), # map_tab
432
+ "", # user_input
433
+ create_agent_stream_output(), # agent_stream_output
434
+ self._generate_chat_welcome_html(), # chat_history_output (重置)
435
+ "Ready to start planning..." # status_bar
436
+ ),
437
+ outputs=[
438
+ input_area, task_confirm_area, chat_input_area, result_area,
439
+ team_area, report_tab, map_tab, user_input,
440
+ agent_stream_output, chat_history_output, status_bar
441
+ ]
442
  )
443
 
444
+ # ====== Step 2 → Step 2.5: Ready to Plan button ======
445
+ ready_plan_btn.click(
446
+ # 隱藏確認區和聊天輸入,顯示專家團隊,切換到 AI Conversation Tab
447
  fn=lambda: (
448
+ gr.update(visible=False), # task_confirm_area
449
+ gr.update(visible=False), # chat_input_area (關閉聊天輸入)
450
+ gr.update(visible=True), # team_area (顯示專家團隊)
451
+ gr.update(selected="ai_conversation_tab") # 切換到 AI Conversation Tab
452
  ),
453
+ outputs=[task_confirm_area, chat_input_area, team_area, tabs]
454
  ).then(
455
+ # Step 2.5: Scout 開始工作
456
  fn=self.step2_search_pois,
457
+ outputs=[reasoning_output, status_bar, *agent_displays]
 
 
 
 
 
 
 
 
 
458
  ).then(
459
+ # Step 3: Optimizer 開始工作,切換到 Full Report Tab
460
  fn=self.step3_optimize_route,
461
+ outputs=[reasoning_output, report_output, status_bar, *agent_displays]
 
 
 
 
 
 
 
 
 
462
  ).then(
463
+ # 顯示 Report 和 Map Tabs,並切換到 Full Report
464
  fn=lambda: (
465
+ gr.update(visible=True), # report_tab
466
+ gr.update(visible=True), # map_tab
467
+ gr.update(selected="report_tab") # 切換到 Full Report Tab
468
  ),
469
+ outputs=[report_tab, map_tab, tabs]
470
+ ).then(
471
+ # Step 4: 完成規劃
472
+ fn=self.step4_finalize,
473
+ outputs=[
474
+ timeline_display, metrics_display, result_display,
475
+ map_output, map_tab, team_area, status_bar, *agent_displays
476
+ ]
477
+ ).then(
478
+ fn=lambda: gr.update(visible=True),
479
+ outputs=[result_area]
480
  )
481
 
482
+ # ====== Settings ======
483
+ settings_btn.click(
484
+ fn=lambda: gr.update(visible=True),
485
+ outputs=[settings_modal]
486
+ )
487
+ close_settings_btn.click(
488
+ fn=lambda: gr.update(visible=False),
489
+ outputs=[settings_modal]
490
+ )
491
  save_settings_btn.click(
492
  fn=self.save_settings,
493
  inputs=[google_maps_key, openweather_key, anthropic_key, model_choice],
494
  outputs=[settings_status]
495
  )
496
 
497
+ # ====== Theme Toggle ======
498
+ theme_btn.click(
499
+ fn=None,
500
+ js="""
501
+ () => {
502
+ const container = document.querySelector('.gradio-container');
503
+ if (container) {
504
+ container.classList.toggle('theme-dark');
505
+ const isDark = container.classList.contains('theme-dark');
506
+ localStorage.setItem('lifeflow-theme', isDark ? 'dark' : 'light');
507
+ console.log('Theme toggled:', isDark ? 'dark' : 'light');
508
+ }
509
+ }
510
+ """
511
+ )
512
+
513
+ # ====== Documentation ======
514
+ doc_btn.click(
515
+ fn=lambda: gr.update(visible=True),
516
+ outputs=[doc_modal]
517
+ )
518
+ close_doc_btn.click(
519
+ fn=lambda: gr.update(visible=False),
520
+ outputs=[doc_modal]
521
+ )
522
+
523
  return demo
524
 
525
 
526
  def main():
527
+ app = LifeFlowAI()
528
  demo = app.build_interface()
529
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False, show_error=True)
 
 
 
 
 
 
530
 
531
  if __name__ == "__main__":
532
  main()
config.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LifeFlow AI - Configuration
3
+ """
4
+
5
+ APP_TITLE = "LifeFlow AI - Intelligent Daily Trip Planner"
6
+
7
+ AGENTS_INFO = {
8
+ 'planner': {
9
+ 'name': 'Planner',
10
+ 'role': 'Task Analyzer',
11
+ 'icon': '📋',
12
+ 'color': '#f093fb'
13
+ },
14
+ 'scout': {
15
+ 'name': 'Scout',
16
+ 'role': 'POI Searcher',
17
+ 'icon': '🗺️',
18
+ 'color': '#4facfe'
19
+ },
20
+ 'optimizer': {
21
+ 'name': 'Optimizer',
22
+ 'role': 'Route Optimizer',
23
+ 'icon': '⚡',
24
+ 'color': '#43e97b'
25
+ },
26
+ 'validator': {
27
+ 'name': 'Validator',
28
+ 'role': 'Feasibility Checker',
29
+ 'icon': '✅',
30
+ 'color': '#fa709a'
31
+ },
32
+ 'weather': {
33
+ 'name': 'Weather',
34
+ 'role': 'Weather Advisor',
35
+ 'icon': '🌤️',
36
+ 'color': '#feca57'
37
+ },
38
+ 'traffic': {
39
+ 'name': 'Traffic',
40
+ 'role': 'Traffic Analyzer',
41
+ 'icon': '🚗',
42
+ 'color': '#ff6348'
43
+ }
44
+ }
45
+
46
+ DEFAULT_SETTINGS = {
47
+ 'google_maps_api_key': '',
48
+ 'openweather_api_key': '',
49
+ 'anthropic_api_key': '',
50
+ 'model': 'claude-sonnet-4-20250514'
51
+ }
52
+
53
+ MODEL_CHOICES = [
54
+ 'claude-sonnet-4-20250514',
55
+ 'claude-sonnet-3-5-20241022',
56
+ 'gpt-4-turbo',
57
+ 'gemini-pro'
58
+ ]
ui/__init__.py ADDED
File without changes
ui/components/__init__.py ADDED
File without changes
ui/components/confirmation.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LifeFlow AI - Confirmation Component
3
+ Step 2: 任務確認區域
4
+ ✅ 移除獨立的 chat_modify_area (已整合到 Tab 中)
5
+ """
6
+ import gradio as gr
7
+
8
+
9
+ def create_confirmation_area():
10
+ """
11
+ 創建任務確認區域
12
+ ✅ Exit 和 Ready to plan 並排
13
+ """
14
+ with gr.Group(visible=False) as task_confirm_area:
15
+ gr.Markdown("### ✅ Confirm Your Tasks")
16
+ task_summary_display = gr.HTML()
17
+ task_list_display = gr.HTML()
18
+
19
+ # 按鈕並排顯示(Exit 左邊,Ready to plan 右邊)
20
+ with gr.Row():
21
+ exit_btn = gr.Button(
22
+ "🏠 Exit",
23
+ size="lg",
24
+ scale=1,
25
+ elem_id="exit-button-inline"
26
+ )
27
+ ready_plan_btn = gr.Button(
28
+ "✅ Ready to plan",
29
+ variant="primary",
30
+ size="lg",
31
+ scale=2,
32
+ elem_classes="ready-plan-button"
33
+ )
34
+
35
+ return task_confirm_area, task_summary_display, task_list_display, exit_btn, ready_plan_btn
36
+
37
+
38
+ def create_exit_button():
39
+ """
40
+ 創建獨立的 Exit 按鈕(用於其他地方)
41
+ """
42
+ exit_btn = gr.Button(
43
+ "🏠 Exit",
44
+ size="lg",
45
+ visible=False,
46
+ elem_id="exit-button"
47
+ )
48
+ return exit_btn
ui/components/header.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LifeFlow AI - Header Component
3
+ 應用標題和頂部控制按鈕
4
+ """
5
+ import gradio as gr
6
+
7
+
8
+ def create_header():
9
+ """創建應用 Header - 更大的標題"""
10
+ header_html = """
11
+ <div class="app-header">
12
+ <h1>✨ LifeFlow AI</h1>
13
+ <p>Your Intelligent Daily Trip Planning Assistant</p>
14
+ </div>
15
+ """
16
+ return gr.HTML(header_html)
17
+
18
+
19
+ def create_top_controls():
20
+ """創建頂部控制按鈕(Theme Toggle, Settings & Doc)- 使用絕對定位"""
21
+ # 不使用 gr.Row,直接創建獨立按鈕
22
+ theme_btn = gr.Button("🌓", elem_id="theme-toggle", size="sm")
23
+ settings_btn = gr.Button("⚙️", elem_id="settings-btn", size="sm")
24
+ doc_btn = gr.Button("📖", elem_id="doc-btn", size="sm")
25
+
26
+ return theme_btn, settings_btn, doc_btn
ui/components/input_form.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LifeFlow AI - Input Form Component
3
+ Step 1: 用戶輸入表單
4
+ """
5
+ import gradio as gr
6
+
7
+
8
+ def create_input_form(agent_stream_html):
9
+ """創建輸入表單"""
10
+ with gr.Group(visible=True) as input_area:
11
+ gr.Markdown("### 📝 Tell us your plans")
12
+ agent_stream_output = gr.HTML(value=agent_stream_html)
13
+ user_input = gr.Textbox(
14
+ label="What do you need to do?",
15
+ placeholder="e.g., Visit hospital tomorrow morning, buy groceries, mail package before 3pm",
16
+ lines=3
17
+ )
18
+ gr.Markdown("---")
19
+ auto_location = gr.Checkbox(label="📍 Auto-detect my location", value=False)
20
+
21
+ with gr.Group(visible=True) as location_inputs:
22
+ with gr.Row():
23
+ lat_input = gr.Number(label="Latitude", value=25.033, precision=6)
24
+ lon_input = gr.Number(label="Longitude", value=121.565, precision=6)
25
+
26
+ analyze_btn = gr.Button("🚀 Analyze & Plan", variant="primary", size="lg")
27
+
28
+ return (input_area, agent_stream_output, user_input, auto_location,
29
+ location_inputs, lat_input, lon_input, analyze_btn)
30
+
31
+
32
+ def toggle_location_inputs(auto_location):
33
+ """切換位置輸入顯示"""
34
+ return gr.update(visible=not auto_location)
ui/components/modals.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LifeFlow AI - Settings & Documentation Modals
3
+ 設定和文檔模態框
4
+ """
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ current_dir = Path(__file__).parent.parent.parent
9
+ sys.path.insert(0, str(current_dir))
10
+
11
+ import gradio as gr
12
+ from config import MODEL_CHOICES
13
+
14
+
15
+ def create_settings_modal():
16
+ """創建設定模態框"""
17
+ with gr.Group(visible=False) as settings_modal:
18
+ gr.Markdown("## ⚙️ Settings")
19
+ gr.Markdown("### 🔑 API Keys")
20
+ google_maps_key = gr.Textbox(label="Google Maps API Key", type="password")
21
+ openweather_key = gr.Textbox(label="OpenWeather API Key", type="password")
22
+ anthropic_key = gr.Textbox(label="Anthropic API Key", type="password")
23
+
24
+ gr.Markdown("### 🤖 Model")
25
+ model_choice = gr.Dropdown(
26
+ choices=MODEL_CHOICES,
27
+ value=MODEL_CHOICES[0],
28
+ label="Select Model"
29
+ )
30
+
31
+ with gr.Row():
32
+ close_settings_btn = gr.Button("❌ Close", scale=1)
33
+ save_settings_btn = gr.Button("💾 Save", variant="primary", scale=2)
34
+
35
+ settings_status = gr.Textbox(label="Status", value="")
36
+
37
+ return (settings_modal, google_maps_key, openweather_key, anthropic_key,
38
+ model_choice, close_settings_btn, save_settings_btn, settings_status)
39
+
40
+
41
+ def create_doc_modal():
42
+ """創建文檔模態框"""
43
+ doc_content = """
44
+ ## 📖 Documentation
45
+
46
+ ### How to Use LifeFlow AI
47
+
48
+ #### Step 1: Input Your Tasks
49
+ 1. Describe what you need to do today
50
+ 2. Choose whether to auto-detect location or enter manually
51
+ 3. Click "🚀 Analyze & Plan"
52
+
53
+ #### Step 2: Review & Confirm
54
+ 1. Check the extracted tasks
55
+ 2. Modify if needed using the chat
56
+ 3. Click "✅ Ready to plan" to start optimization
57
+
58
+ #### Step 3: Get Your Plan
59
+ 1. Watch the AI agents work together
60
+ 2. View the optimized route on the map
61
+ 3. Read the full report
62
+
63
+ ### Features
64
+ - 🤖 **Multi-Agent AI**: 6 specialized agents work together
65
+ - 🗺️ **Smart Routing**: Optimizes for distance and time
66
+ - ⚡ **Real-time Updates**: See AI reasoning process
67
+ - 🎨 **Responsive Design**: Works on all devices
68
+
69
+ ### Tips
70
+ - Be specific about time constraints
71
+ - Mention priorities (urgent, important, etc.)
72
+ - Include any special requirements
73
+
74
+ ### Support
75
+ For issues or questions, contact: support@lifeflow.ai
76
+ """
77
+
78
+ with gr.Group(visible=False) as doc_modal:
79
+ gr.Markdown(doc_content)
80
+ close_doc_btn = gr.Button("❌ Close")
81
+
82
+ return doc_modal, close_doc_btn
ui/components/results.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LifeFlow AI - Results Component
3
+ Step 3: 結果展示
4
+ ✅ 新增 Chat with LifeFlow Tab
5
+ ✅ 支援任務修改對話
6
+ """
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ current_dir = Path(__file__).parent.parent.parent
11
+ sys.path.insert(0, str(current_dir))
12
+
13
+ import gradio as gr
14
+ from config import AGENTS_INFO
15
+
16
+
17
+ def create_team_area(create_agent_card_func):
18
+ """創建 AI Team 展示區域"""
19
+ with gr.Group(visible=False) as team_area:
20
+ gr.Markdown("### 🤖 AI Expert Team")
21
+ agent_displays = []
22
+ for agent_key in ['planner', 'scout', 'optimizer', 'validator', 'weather', 'traffic']:
23
+ agent_card = gr.HTML(value=create_agent_card_func(agent_key, "idle", "On standby"))
24
+ agent_displays.append(agent_card)
25
+
26
+ return team_area, agent_displays
27
+
28
+
29
+ def create_result_area(create_animated_map_func):
30
+ """創建結果展示區域"""
31
+ with gr.Group(visible=False) as result_area:
32
+ result_display = gr.HTML()
33
+ timeline_display = gr.HTML()
34
+ metrics_display = gr.HTML()
35
+
36
+ return result_area, result_display, timeline_display, metrics_display
37
+
38
+
39
+ def create_tabs(create_animated_map_func, reasoning_html):
40
+ """
41
+ 創建標籤頁
42
+ Tab 順序: Chat with LifeFlow (default) → Full Report → Route Map → AI Conversation
43
+ """
44
+ with gr.Tabs(selected="chat_lifeflow_tab") as tabs:
45
+ # 🆕 Tab 1: Chat with LifeFlow (用於任務修改,Step 1 默認選擇)
46
+ with gr.Tab("💬 Chat with LifeFlow", id="chat_lifeflow_tab"):
47
+ gr.Markdown("*Ask me to modify your tasks*", elem_classes="tab-subtitle")
48
+
49
+ # Chat 輸出區域(顯示對話歷史)
50
+ chat_history_output = gr.HTML(
51
+ value='<div style="padding: 20px; text-align: center; opacity: 0.6;">Chat will be available after task analysis...</div>',
52
+ elem_classes="chat-history"
53
+ )
54
+
55
+ # Chat 輸入區域(Step 1 時隱藏,Step 2 時顯示)
56
+ with gr.Group(visible=False, elem_classes="chat-input-group") as chat_input_area:
57
+ with gr.Row():
58
+ chat_input = gr.Textbox(
59
+ placeholder="e.g., Change task 2 to high priority, or add a new task...",
60
+ show_label=False,
61
+ scale=4,
62
+ elem_classes="chat-input"
63
+ )
64
+ chat_send = gr.Button("Send", variant="primary", scale=1)
65
+
66
+ # Tab 2: Full Report (專家推理過程,Step 3 時變為默認)
67
+ with gr.Tab("📊 Full Report", id="report_tab", visible=False) as report_tab:
68
+ gr.Markdown("*AI Expert Team Reasoning Process*", elem_classes="tab-subtitle")
69
+ report_output = gr.Markdown()
70
+
71
+ # Tab 3: Route Map
72
+ with gr.Tab("🗺️ Route Map", id="map_tab", visible=False) as map_tab:
73
+ map_output = gr.Plot(value=create_animated_map_func(), show_label=False)
74
+
75
+ # Tab 4: AI Conversation (原有的 reasoning stream)
76
+ with gr.Tab("🔍 AI Conversation", id="ai_conversation_tab"):
77
+ gr.Markdown("*Real-time AI reasoning*", elem_classes="tab-subtitle")
78
+ reasoning_output = gr.HTML(value=reasoning_html)
79
+
80
+ return (tabs, report_tab, map_tab, report_output, map_output, reasoning_output,
81
+ chat_input_area, chat_history_output, chat_input, chat_send)
ui/theme.py ADDED
@@ -0,0 +1,593 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LifeFlow AI - Theme System
3
+ 提供完整的主題和樣式管理,支援動態主題切換
4
+ """
5
+
6
+
7
+ def get_enhanced_css() -> str:
8
+ """
9
+ 改進的 CSS 系統:
10
+ 1. ✅ 修復色調功能 - 確保主題顏色正確應用
11
+ 2. ✅ 完整支援深淺模式切換
12
+ 3. ✅ 修復 Exit 按鈕樣式(Step 2)
13
+ 4. ✅ 調整 Setting 和 Doc 按鈕位置和大小
14
+ """
15
+ return """
16
+ <style>
17
+ :root {
18
+ /* 品牌色 - 固定不變,保持品牌識別度 */
19
+ --primary-color: #4A90E2;
20
+ --secondary-color: #50C878;
21
+ --accent-color: #F5A623;
22
+ --primary-glow: rgba(74, 144, 226, 0.3);
23
+
24
+ /* 功能色 */
25
+ --success-color: #7ED321;
26
+ --warning-color: #F5A623;
27
+ --danger-color: #FF6B6B;
28
+ }
29
+
30
+ /* ============= 動畫效果 ============= */
31
+ @keyframes pulse {
32
+ 0%, 100% { transform: scale(1); }
33
+ 50% { transform: scale(1.05); }
34
+ }
35
+ @keyframes breathing {
36
+ 0%, 100% { box-shadow: 0 0 10px var(--agent-glow, rgba(74, 144, 226, 0.3)); }
37
+ 50% { box-shadow: 0 0 25px var(--agent-glow, rgba(74, 144, 226, 0.6)), 0 0 40px var(--agent-glow, rgba(74, 144, 226, 0.3)); }
38
+ }
39
+ @keyframes slide-in-left {
40
+ from { transform: translateX(-100%); opacity: 0; }
41
+ to { transform: translateX(0); opacity: 1; }
42
+ }
43
+ @keyframes fade-in {
44
+ from { opacity: 0; }
45
+ to { opacity: 1; }
46
+ }
47
+ @keyframes blink {
48
+ 0%, 49% { opacity: 1; }
49
+ 50%, 100% { opacity: 0; }
50
+ }
51
+
52
+ /* ============= 全域設定 ============= */
53
+ .gradio-container {
54
+ background: var(--background-fill-primary) !important;
55
+ }
56
+
57
+ /* ============= 🔧 修復:Header 標題 ============= */
58
+ .app-header {
59
+ text-align: center;
60
+ padding: 40px 20px 30px 20px;
61
+ position: relative;
62
+ }
63
+
64
+ .app-header h1 {
65
+ font-size: 64px !important;
66
+ margin: 0 !important;
67
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
68
+ -webkit-background-clip: text;
69
+ -webkit-text-fill-color: transparent;
70
+ background-clip: text;
71
+ font-weight: 700 !important;
72
+ }
73
+
74
+ .app-header p {
75
+ font-size: 20px !important;
76
+ color: var(--body-text-color);
77
+ opacity: 0.8;
78
+ margin-top: 10px !important;
79
+ }
80
+
81
+ /* ============= 🔧 修復:主題切換按鈕 ============= */
82
+ #theme-toggle {
83
+ position: fixed !important;
84
+ top: 20px !important;
85
+ right: 110px !important;
86
+ z-index: 1000 !important;
87
+ width: 40px !important;
88
+ height: 40px !important;
89
+ min-width: 40px !important;
90
+ max-width: 40px !important;
91
+ padding: 0 !important;
92
+ font-size: 18px !important;
93
+ border-radius: 10px !important;
94
+ background: var(--background-fill-secondary) !important;
95
+ border: 1.5px solid var(--border-color-primary) !important;
96
+ box-shadow: 0 2px 6px rgba(0,0,0,0.1) !important;
97
+ transition: all 0.3s ease !important;
98
+ cursor: pointer !important;
99
+ }
100
+
101
+ #theme-toggle:hover {
102
+ transform: translateY(-2px) !important;
103
+ box-shadow: 0 4px 10px rgba(245, 166, 35, 0.3) !important;
104
+ background: var(--accent-color) !important;
105
+ color: white !important;
106
+ }
107
+
108
+ /* ============= 🔧 修復:Settings 按鈕 ============= */
109
+ #settings-btn {
110
+ position: fixed !important;
111
+ top: 20px !important;
112
+ right: 60px !important;
113
+ z-index: 1000 !important;
114
+ width: 40px !important;
115
+ height: 40px !important;
116
+ min-width: 40px !important;
117
+ max-width: 40px !important;
118
+ padding: 0 !important;
119
+ font-size: 18px !important;
120
+ border-radius: 10px !important;
121
+ background: var(--background-fill-secondary) !important;
122
+ border: 1.5px solid var(--border-color-primary) !important;
123
+ box-shadow: 0 2px 6px rgba(0,0,0,0.1) !important;
124
+ transition: all 0.3s ease !important;
125
+ cursor: pointer !important;
126
+ }
127
+
128
+ #settings-btn:hover {
129
+ transform: translateY(-2px) !important;
130
+ box-shadow: 0 4px 10px rgba(74, 144, 226, 0.3) !important;
131
+ background: var(--primary-color) !important;
132
+ color: white !important;
133
+ }
134
+
135
+ /* ============= 🔧 修復:Doc 按鈕 ============= */
136
+ #doc-btn {
137
+ position: fixed !important;
138
+ top: 20px !important;
139
+ right: 10px !important;
140
+ z-index: 1000 !important;
141
+ width: 40px !important;
142
+ height: 40px !important;
143
+ min-width: 40px !important;
144
+ max-width: 40px !important;
145
+ padding: 0 !important;
146
+ font-size: 18px !important;
147
+ border-radius: 10px !important;
148
+ background: var(--background-fill-secondary) !important;
149
+ border: 1.5px solid var(--border-color-primary) !important;
150
+ box-shadow: 0 2px 6px rgba(0,0,0,0.1) !important;
151
+ transition: all 0.3s ease !important;
152
+ cursor: pointer !important;
153
+ }
154
+
155
+ #doc-btn:hover {
156
+ transform: translateY(-2px) !important;
157
+ box-shadow: 0 4px 10px rgba(74, 144, 226, 0.3) !important;
158
+ background: var(--primary-color) !important;
159
+ color: white !important;
160
+ }
161
+
162
+ /* ============= 🔧 修復:Exit 按鈕樣式 (Step 2 - 頂部獨立) ============= */
163
+ #exit-button {
164
+ width: 100% !important;
165
+ height: 50px !important;
166
+ font-size: 16px !important;
167
+ font-weight: 600 !important;
168
+ border-radius: 10px !important;
169
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
170
+ color: white !important;
171
+ border: none !important;
172
+ box-shadow: 0 3px 10px rgba(102, 126, 234, 0.3) !important;
173
+ transition: all 0.3s ease !important;
174
+ margin-bottom: 15px !important;
175
+ cursor: pointer;
176
+ }
177
+
178
+ #exit-button:hover {
179
+ transform: translateY(-2px);
180
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.5) !important;
181
+ }
182
+
183
+ /* ============= 🔧 新增:Exit 按鈕樣式 (內聯版本 - 與 Ready to plan 並排) ============= */
184
+ #exit-button-inline {
185
+ height: 50px !important;
186
+ font-size: 15px !important;
187
+ font-weight: 600 !important;
188
+ border-radius: 10px !important;
189
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
190
+ color: white !important;
191
+ border: none !important;
192
+ box-shadow: 0 3px 10px rgba(102, 126, 234, 0.3) !important;
193
+ transition: all 0.3s ease !important;
194
+ cursor: pointer;
195
+ }
196
+
197
+ #exit-button-inline:hover {
198
+ transform: translateY(-2px);
199
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.5) !important;
200
+ }
201
+
202
+ /* ============= 串流輸出框 ============= */
203
+ .stream-container {
204
+ background: var(--background-fill-secondary);
205
+ border-radius: 10px;
206
+ padding: 15px;
207
+ min-height: 150px;
208
+ max-height: 300px;
209
+ overflow-y: auto;
210
+ margin: 15px 0;
211
+ border: 1px solid var(--border-color-primary);
212
+ font-family: 'Monaco', 'Courier New', monospace;
213
+ font-size: 13px;
214
+ line-height: 1.6;
215
+ }
216
+ .stream-text {
217
+ color: var(--body-text-color);
218
+ white-space: pre-wrap;
219
+ word-wrap: break-word;
220
+ }
221
+ .stream-cursor {
222
+ display: inline-block;
223
+ width: 8px;
224
+ height: 16px;
225
+ background: var(--primary-color);
226
+ animation: blink 1s infinite;
227
+ margin-left: 2px;
228
+ }
229
+
230
+ /* ============= Agent 卡片 ============= */
231
+ .agent-card {
232
+ background: var(--background-fill-secondary);
233
+ border-radius: 12px;
234
+ padding: 15px;
235
+ margin: 10px 0;
236
+ border-left: 4px solid var(--agent-color, var(--primary-color));
237
+ transition: all 0.3s ease;
238
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
239
+ }
240
+ .agent-card:hover {
241
+ transform: translateX(5px);
242
+ box-shadow: 0 4px 12px var(--agent-glow, rgba(74, 144, 226, 0.2));
243
+ }
244
+ .agent-card.active {
245
+ animation: breathing 2s infinite;
246
+ border-left-width: 6px;
247
+ }
248
+ .agent-header {
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 10px;
252
+ margin-bottom: 8px;
253
+ }
254
+ .agent-avatar {
255
+ font-size: 28px;
256
+ filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
257
+ }
258
+ .agent-info h3 {
259
+ margin: 0;
260
+ font-size: 16px;
261
+ color: var(--body-text-color);
262
+ }
263
+ .agent-role {
264
+ font-size: 12px;
265
+ color: var(--body-text-color);
266
+ opacity: 0.7;
267
+ }
268
+ .agent-status {
269
+ display: inline-block;
270
+ padding: 4px 12px;
271
+ border-radius: 12px;
272
+ font-size: 11px;
273
+ font-weight: 600;
274
+ margin-top: 8px;
275
+ }
276
+ .status-idle {
277
+ background: rgba(128, 128, 128, 0.2);
278
+ color: var(--body-text-color);
279
+ }
280
+ .status-working {
281
+ background: var(--primary-glow);
282
+ color: var(--primary-color);
283
+ animation: pulse 1.5s infinite;
284
+ }
285
+ .status-complete {
286
+ background: rgba(126, 211, 33, 0.2);
287
+ color: var(--success-color);
288
+ }
289
+ .agent-message {
290
+ margin-top: 8px;
291
+ padding: 8px;
292
+ background: var(--background-fill-primary);
293
+ border-radius: 6px;
294
+ font-size: 13px;
295
+ color: var(--body-text-color);
296
+ opacity: 0.9;
297
+ }
298
+
299
+ /* ============= Task 卡片 ============= */
300
+ .task-card {
301
+ background: var(--background-fill-secondary);
302
+ border-radius: 10px;
303
+ padding: 16px;
304
+ margin: 12px 0;
305
+ border-left: 4px solid var(--task-priority-color, #999);
306
+ box-shadow: 0 2px 6px rgba(0,0,0,0.08);
307
+ transition: all 0.3s ease;
308
+ }
309
+ .task-card:hover {
310
+ transform: translateY(-2px);
311
+ box-shadow: 0 4px 12px rgba(0,0,0,0.12);
312
+ }
313
+ .task-header {
314
+ display: flex;
315
+ justify-content: space-between;
316
+ align-items: center;
317
+ margin-bottom: 10px;
318
+ }
319
+ .task-title {
320
+ font-size: 16px;
321
+ font-weight: 600;
322
+ color: var(--body-text-color);
323
+ display: flex;
324
+ align-items: center;
325
+ gap: 10px;
326
+ }
327
+ .task-icon {
328
+ font-size: 24px;
329
+ }
330
+ .priority-badge {
331
+ padding: 4px 12px;
332
+ border-radius: 12px;
333
+ font-size: 11px;
334
+ font-weight: 700;
335
+ text-transform: uppercase;
336
+ }
337
+ .priority-high {
338
+ background: rgba(255, 107, 107, 0.2);
339
+ color: #FF6B6B;
340
+ }
341
+ .priority-medium {
342
+ background: rgba(245, 166, 35, 0.2);
343
+ color: #F5A623;
344
+ }
345
+ .priority-low {
346
+ background: rgba(126, 211, 33, 0.2);
347
+ color: #7ED321;
348
+ }
349
+ .task-details {
350
+ display: flex;
351
+ gap: 20px;
352
+ font-size: 13px;
353
+ color: var(--body-text-color);
354
+ opacity: 0.8;
355
+ margin-top: 10px;
356
+ }
357
+ .task-detail-item {
358
+ display: flex;
359
+ align-items: center;
360
+ gap: 6px;
361
+ }
362
+
363
+ /* ============= Summary Card ============= */
364
+ .summary-card {
365
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
366
+ border-radius: 12px;
367
+ padding: 20px;
368
+ color: white;
369
+ margin: 15px 0;
370
+ box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
371
+ }
372
+ .summary-stats {
373
+ display: flex;
374
+ justify-content: space-around;
375
+ margin-top: 15px;
376
+ }
377
+ .stat-item {
378
+ text-align: center;
379
+ }
380
+ .stat-value {
381
+ font-size: 28px;
382
+ font-weight: bold;
383
+ display: block;
384
+ }
385
+ .stat-label {
386
+ font-size: 12px;
387
+ opacity: 0.9;
388
+ margin-top: 5px;
389
+ }
390
+
391
+ /* ============= 🔧 優化:Ready to Plan 按鈕樣式(內聯佈局) ============= */
392
+ .ready-plan-button {
393
+ height: 50px !important;
394
+ font-size: 16px !important;
395
+ font-weight: 700 !important;
396
+ border-radius: 10px !important;
397
+ background: linear-gradient(135deg, #50C878 0%, #7ED321 100%) !important;
398
+ color: white !important;
399
+ border: none !important;
400
+ box-shadow: 0 3px 12px rgba(80, 200, 120, 0.4) !important;
401
+ transition: all 0.3s ease !important;
402
+ cursor: pointer;
403
+ }
404
+
405
+ .ready-plan-button:hover {
406
+ transform: translateY(-2px);
407
+ box-shadow: 0 5px 20px rgba(80, 200, 120, 0.6) !important;
408
+ background: linear-gradient(135deg, #5DD68D 0%, #8FE63F 100%) !important;
409
+ }
410
+
411
+ .ready-plan-button:active {
412
+ transform: translateY(0);
413
+ }
414
+
415
+ /* ============= Reasoning Timeline ============= */
416
+ .reasoning-timeline {
417
+ padding: 20px 0;
418
+ }
419
+ .reasoning-item {
420
+ padding: 15px;
421
+ margin: 10px 0;
422
+ border-radius: 8px;
423
+ background: var(--background-fill-secondary);
424
+ border-left: 3px solid var(--agent-color, var(--primary-color));
425
+ animation: fade-in 0.5s ease;
426
+ }
427
+ .reasoning-header {
428
+ display: flex;
429
+ align-items: center;
430
+ gap: 10px;
431
+ margin-bottom: 8px;
432
+ }
433
+ .reasoning-agent {
434
+ font-weight: 600;
435
+ color: var(--agent-color, var(--primary-color));
436
+ }
437
+ .reasoning-time {
438
+ font-size: 11px;
439
+ opacity: 0.6;
440
+ margin-left: auto;
441
+ }
442
+ .reasoning-content {
443
+ color: var(--body-text-color);
444
+ opacity: 0.9;
445
+ line-height: 1.6;
446
+ }
447
+
448
+ /* ============= Modal 樣式 ============= */
449
+ .modal-overlay {
450
+ position: fixed;
451
+ top: 0;
452
+ left: 0;
453
+ right: 0;
454
+ bottom: 0;
455
+ background: rgba(0, 0, 0, 0.5);
456
+ z-index: 1000;
457
+ display: flex;
458
+ align-items: center;
459
+ justify-content: center;
460
+ }
461
+ .modal-content {
462
+ background: var(--background-fill-primary);
463
+ border-radius: 16px;
464
+ padding: 30px;
465
+ max-width: 600px;
466
+ width: 90%;
467
+ max-height: 80vh;
468
+ overflow-y: auto;
469
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
470
+ }
471
+
472
+ /* ============= Responsive ============= */
473
+ @media (max-width: 768px) {
474
+ #top-controls {
475
+ top: 10px;
476
+ right: 10px;
477
+ }
478
+ #top-controls button {
479
+ width: 40px !important;
480
+ height: 40px !important;
481
+ font-size: 18px !important;
482
+ }
483
+ .agent-card {
484
+ padding: 12px;
485
+ }
486
+ .task-card {
487
+ padding: 12px;
488
+ }
489
+ }
490
+
491
+ /* ============= 滾動條美化 ============= */
492
+ ::-webkit-scrollbar {
493
+ width: 8px;
494
+ height: 8px;
495
+ }
496
+ ::-webkit-scrollbar-track {
497
+ background: var(--background-fill-secondary);
498
+ }
499
+ ::-webkit-scrollbar-thumb {
500
+ background: var(--primary-color);
501
+ border-radius: 4px;
502
+ }
503
+ ::-webkit-scrollbar-thumb:hover {
504
+ background: var(--secondary-color);
505
+ }
506
+ /* ============= 🔧 主題切換 - 深色模式樣式 ============= */
507
+ .theme-dark {
508
+ background: #1a1a1a !important;
509
+ }
510
+
511
+ .theme-dark .gradio-container {
512
+ background: #1a1a1a !important;
513
+ }
514
+
515
+ .theme-dark [data-testid="block"] {
516
+ background: #2d2d2d !important;
517
+ }
518
+
519
+ .theme-dark .gr-box,
520
+ .theme-dark .gr-form,
521
+ .theme-dark .gr-input,
522
+ .theme-dark .gr-group {
523
+ background: #2d2d2d !important;
524
+ border-color: #444444 !important;
525
+ }
526
+
527
+ .theme-dark p,
528
+ .theme-dark label,
529
+ .theme-dark span,
530
+ .theme-dark h1,
531
+ .theme-dark h2,
532
+ .theme-dark h3 {
533
+ color: #e0e0e0 !important;
534
+ }
535
+
536
+ .theme-dark input,
537
+ .theme-dark textarea {
538
+ background: #3a3a3a !important;
539
+ color: #e0e0e0 !important;
540
+ border-color: #555555 !important;
541
+ }
542
+
543
+ .theme-dark button {
544
+ background: #3a3a3a !important;
545
+ color: #e0e0e0 !important;
546
+ border-color: #555555 !important;
547
+ }
548
+
549
+ /* 保持品牌色按鈕的顏色 */
550
+ .theme-dark #exit-button,
551
+ .theme-dark #exit-button-inline,
552
+ .theme-dark .ready-plan-button,
553
+ .theme-dark #theme-toggle,
554
+ .theme-dark #settings-btn,
555
+ .theme-dark #doc-btn {
556
+ /* 保持原有的漸變色 */
557
+ filter: brightness(0.9);
558
+ }
559
+ </style>
560
+
561
+ <script>
562
+ // 主題切換功能 - 頁面載入時執行
563
+ (function() {
564
+ // 等待 Gradio 完全載入
565
+ function initTheme() {
566
+ const savedTheme = localStorage.getItem('lifeflow-theme');
567
+ console.log('Initializing theme, saved:', savedTheme);
568
+
569
+ if (savedTheme === 'dark') {
570
+ const container = document.querySelector('.gradio-container');
571
+ if (container) {
572
+ container.classList.add('theme-dark');
573
+ document.body.classList.add('theme-dark');
574
+ console.log('Dark theme applied on load');
575
+ } else {
576
+ // 如果還沒找到容器,稍後再試
577
+ setTimeout(initTheme, 100);
578
+ }
579
+ }
580
+ }
581
+
582
+ // 頁面載入完成後執行
583
+ if (document.readyState === 'loading') {
584
+ document.addEventListener('DOMContentLoaded', initTheme);
585
+ } else {
586
+ initTheme();
587
+ }
588
+
589
+ // 也在 window load 時再次檢查
590
+ window.addEventListener('load', initTheme);
591
+ })();
592
+ </script>
593
+ """