Marco310 commited on
Commit
b5e87e1
·
1 Parent(s): fcc5d97

Feat: Upgrade map to Folium with Google Maps style UI, Card Popup, and Layer Control

Browse files
app.py CHANGED
@@ -296,7 +296,8 @@ class LifeFlowAI:
296
 
297
  # Right: Hero Map
298
  with gr.Column(scale=2, elem_classes="split-right-panel"):
299
- map_view = gr.Plot(label="Route Map", show_label=False)
 
300
 
301
  # Modals & Events
302
  (settings_modal, g_key, w_key, llm_provider,
@@ -380,7 +381,7 @@ class LifeFlowAI:
380
  def main():
381
  app = LifeFlowAI()
382
  demo = app.build_interface()
383
- demo.launch(server_name="0.0.0.0", server_port=7860, share=True, show_error=True)
384
  #7860
385
  if __name__ == "__main__":
386
  main()
 
296
 
297
  # Right: Hero Map
298
  with gr.Column(scale=2, elem_classes="split-right-panel"):
299
+ #map_view = gr.Plot(label="Route Map", show_label=False)
300
+ map_view = gr.HTML(label="Route Map")
301
 
302
  # Modals & Events
303
  (settings_modal, g_key, w_key, llm_provider,
 
381
  def main():
382
  app = LifeFlowAI()
383
  demo = app.build_interface()
384
+ demo.launch(server_name="0.0.0.0", server_port=8080, share=True, show_error=True)
385
  #7860
386
  if __name__ == "__main__":
387
  main()
core/visualizers.py CHANGED
@@ -1,113 +1,333 @@
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
8
  from src.infra.logger import get_logger
9
 
10
  logger = get_logger(__name__)
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):
60
- coords = stop.get("coordinates", {})
61
- lat = coords.get("lat")
62
- lng = coords.get("lng")
63
-
64
- if lat and lng:
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
-
113
- return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import folium
2
+ from folium import plugins
 
 
 
 
3
  from core.helpers import decode_polyline
4
  from src.infra.logger import get_logger
5
 
6
  logger = get_logger(__name__)
7
 
8
+ # --- CSS 樣式:讓 Popup 變成漂亮的資訊卡片 ---
9
+ CSS_STYLE = """
10
+ <style>
11
+ .map-card {
12
+ font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
13
+ width: 300px;
14
+ background: white;
15
+ border-radius: 8px;
16
+ overflow: hidden;
17
+ box-shadow: 0 4px 15px rgba(0,0,0,0.15);
18
+ }
19
+ .card-header {
20
+ padding: 12px 15px;
21
+ color: white;
22
+ font-weight: bold;
23
+ font-size: 14px;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: space-between;
27
+ }
28
+ .card-body {
29
+ padding: 15px;
30
+ font-size: 13px;
31
+ color: #333;
32
+ background-color: #fff;
33
+ }
34
+ .card-subtitle {
35
+ font-weight: 600;
36
+ margin-bottom: 10px;
37
+ font-size: 14px;
38
+ color: #2c3e50;
39
+ border-bottom: 1px solid #eee;
40
+ padding-bottom: 8px;
41
+ }
42
+ .info-grid {
43
+ display: grid;
44
+ grid-template-columns: 1fr 1fr;
45
+ gap: 8px;
46
+ }
47
+ .info-item {
48
+ background: #f8f9fa;
49
+ padding: 8px;
50
+ border-radius: 6px;
51
+ text-align: center;
52
+ border: 1px solid #e9ecef;
53
+ }
54
+ .info-label {
55
+ font-size: 10px;
56
+ color: #7f8c8d;
57
+ text-transform: uppercase;
58
+ margin-bottom: 2px;
59
+ letter-spacing: 0.5px;
60
+ }
61
+ .info-value {
62
+ font-weight: bold;
63
+ font-size: 12px;
64
+ color: #2c3e50;
65
+ }
66
+ .cost-badge {
67
+ background: #ffebee;
68
+ color: #c62828;
69
+ padding: 4px 10px;
70
+ border-radius: 12px;
71
+ font-size: 11px;
72
+ font-weight: bold;
73
+ display: inline-block;
74
+ margin-top: 10px;
75
+ width: 100%;
76
+ text-align: center;
77
+ box-sizing: border-box;
78
+ }
79
+ </style>
80
+ """
81
+
82
+
83
+ def create_popup_html(title, subtitle, color, metrics, is_alternative=False):
84
+ """
85
+ 產生漂亮的 HTML 卡片字串
86
  """
87
+ # 定義顏色代碼
88
+ bg_color = {
89
+ 'green': '#2ecc71', # Stop 1 (Start)
90
+ 'blue': '#3498db', # Waypoints
91
+ 'red': '#e74c3c', # Stop N (End)
92
+ 'gray': '#7f8c8d', # Alternative
93
+ 'route': '#4285F4' # Route Leg
94
+ }.get(color, '#34495e')
95
+
96
+ # 構建數據網格
97
+ grid_html = ""
98
+ for label, value in metrics.items():
99
+ if value: # 只顯示有值的欄位
100
+ grid_html += f"""
101
+ <div class="info-item">
102
+ <div class="info-label">{label}</div>
103
+ <div class="info-value">{value}</div>
104
+ </div>
105
+ """
106
+
107
+ # 備用方案的額外標示
108
+ extra_html = ""
109
+ if is_alternative:
110
+ extra_html = """
111
+ <div class="cost-badge">⚠️ 替代方案 (Alternative Option)</div>
112
+ """
113
+
114
+ html = f"""
115
+ <div class="map-card">
116
+ <div class="card-header" style="background: {bg_color};">
117
+ <span>{title}</span>
118
+ <i class="fa fa-info-circle" style="opacity: 0.8;"></i>
119
+ </div>
120
+ <div class="card-body">
121
+ <div class="card-subtitle">{subtitle}</div>
122
+ <div class="info-grid">
123
+ {grid_html}
124
+ </div>
125
+ {extra_html}
126
+ </div>
127
+ </div>
128
  """
129
+ return html
130
+
131
 
132
+ def create_animated_map(structured_data=None):
133
+ """
134
+ LifeFlow AI - Interactive Map Generator
135
+ 產生包含完整資訊、圖層控制與美化 UI 的 HTML 地圖。
136
+ """
137
+ # 1. 初始化地圖 (預設台北)
138
  center_lat, center_lon = 25.033, 121.565
139
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=13, tiles="OpenStreetMap")
140
+
141
+ # 注入 CSS 樣式
142
+ m.get_root().html.add_child(folium.Element(CSS_STYLE))
143
 
 
144
  if not structured_data:
145
+ return m._repr_html_()
146
+
147
+ try:
148
+ # --- 資料提取 ---
149
+ timeline = structured_data.get("timeline", [])
150
+ precise_result = structured_data.get("precise_traffic_result", {})
151
+ legs = precise_result.get("legs", [])
152
+ tasks_detail = structured_data.get("tasks_detail", [])
153
+ raw_tasks = structured_data.get("tasks", [])
154
+
155
+ # 建立查找表 (Lookup Maps)
156
+ # Stop Index -> Location Name (用於路線顯示)
157
+ index_to_name = {stop.get("stop_index"): stop.get("location") for stop in timeline}
158
+
159
+ # POI ID -> Name (用於替代點顯示,解決只有 ID 沒有名字的問題)
160
+ poi_id_to_name = {}
161
+ for t in raw_tasks:
162
+ for cand in t.get("candidates", []):
163
+ if cand.get("poi_id"):
164
+ poi_id_to_name[cand["poi_id"]] = cand.get("name")
165
+
166
+ bounds = []
167
+
168
+ # 定義主題色輪替
169
+ THEMES = [
170
+ ('#2ecc71', 'green'), # Stop 1
171
+ ('#3498db', 'blue'), # Stop 2
172
+ ('#e74c3c', 'red'), # Stop 3
173
+ ('#f39c12', 'orange') # Others
174
+ ]
175
+
176
+ # --- Layer 1: 路線 (Route) ---
177
+ route_group = folium.FeatureGroup(name="🚗 行駛路徑 (Main Route)", show=True)
178
+
179
+ # 1.1 背景特效 (AntPath)
180
+ all_coords = []
181
+ for leg in legs:
182
+ if leg.get("polyline"):
183
+ all_coords.extend(decode_polyline(leg["polyline"]))
184
+
185
+ if all_coords:
186
+ plugins.AntPath(
187
+ locations=all_coords, color="#555", weight=8, dash_array=[10, 20],
188
+ opacity=0.2, pulse_color='#FFFFFF', hardware_acceleration=True
189
+ ).add_to(route_group)
190
+
191
+ # 1.2 前景互動線 (PolyLine)
192
+ for i, leg in enumerate(legs):
193
+ poly_str = leg.get("polyline")
194
+ if not poly_str: continue
195
+
196
+ decoded = decode_polyline(poly_str)
197
+ bounds.extend(decoded)
198
+
199
+ # 準備數據
200
+ dist = leg.get("distance_meters", 0)
201
+ dur = leg.get("duration_seconds", 0) // 60
202
+ from_idx = leg.get("from_index")
203
+ to_idx = leg.get("to_index")
204
+ from_n = index_to_name.get(from_idx, f"Point {from_idx}")
205
+ to_n = index_to_name.get(to_idx, f"Point {to_idx}")
206
+
207
+ popup_html = create_popup_html(
208
+ title=f"LEG {i + 1} ROUTE",
209
+ subtitle=f"{from_n} {to_n}",
210
+ color="route",
211
+ metrics={
212
+ "預估時間": f"{dur} min",
213
+ "行駛距離": f"{dist / 1000:.1f} km"
214
+ }
215
+ )
216
+
217
+ folium.PolyLine(
218
+ locations=decoded, color="#4285F4", weight=6, opacity=0.9,
219
+ tooltip=f"Leg {i + 1}: {dur} min",
220
+ popup=folium.Popup(popup_html, max_width=320)
221
+ ).add_to(route_group)
222
+
223
+ route_group.add_to(m)
224
+
225
+ # --- Layer 2: 備用方案 (Alternatives) ---
226
+ # 依據 Task 分組,讓使用者可以開關特定站點的備用方案
227
+ for idx, task in enumerate(tasks_detail):
228
+ # 決定顏色主題 (跳過起點,對應到中間站點)
229
+ theme_idx = (idx + 1) % len(THEMES)
230
+ theme_color, theme_name = THEMES[theme_idx]
231
+
232
+ group_name = f"🔹 站點 {idx + 1} 備選 ({theme_name.upper()})"
233
+ alt_group = folium.FeatureGroup(name=group_name, show=True)
234
+
235
+ chosen = task.get("chosen_poi", {})
236
+ center_lat, center_lng = chosen.get("lat"), chosen.get("lng")
237
+
238
+ if center_lat and center_lng:
239
+ for alt in task.get("alternative_pois", []):
240
+ alat, alng = alt.get("lat"), alt.get("lng")
241
+ if alat and alng:
242
+ bounds.append([alat, alng])
243
+
244
+ # A. 虛線連接 (主站點 -> 備用點)
245
+ folium.PolyLine(
246
+ locations=[[center_lat, center_lng], [alat, alng]],
247
+ color=theme_color, weight=2, dash_array='5, 5', opacity=0.6
248
+ ).add_to(alt_group)
249
+
250
+ # B. 備用點 Marker
251
+ poi_name = poi_id_to_name.get(alt.get("poi_id"), "Alternative Option")
252
+ extra_min = alt.get("delta_travel_time_min", 0)
253
+ extra_dist = alt.get("delta_travel_distance_m", 0)
254
+
255
+ popup_html = create_popup_html(
256
+ title="ALTERNATIVE POI",
257
+ subtitle=poi_name,
258
+ color="gray",
259
+ metrics={
260
+ "額外時間": f"+{extra_min} min",
261
+ "額外距離": f"+{extra_dist} m"
262
+ },
263
+ is_alternative=True
264
+ )
265
+
266
+ folium.CircleMarker(
267
+ location=[alat, alng], radius=7,
268
+ color=theme_color, fill=True, fill_color="white", fill_opacity=1,
269
+ popup=folium.Popup(popup_html, max_width=320),
270
+ tooltip=f"備選: {poi_name}"
271
+ ).add_to(alt_group)
272
+
273
+ alt_group.add_to(m)
274
+
275
+ # --- Layer 3: 主要站點 (Main Stops) ---
276
+ stops_group = folium.FeatureGroup(name="📍 主要行程站點", show=True)
277
+
278
+ for i, stop in enumerate(timeline):
279
+ coords = stop.get("coordinates", {})
280
+ lat, lng = coords.get("lat"), coords.get("lng")
281
+
282
+ if lat and lng:
283
+ bounds.append([lat, lng])
284
+
285
+ # 決定顏色
286
+ if i == 0:
287
+ color_code, theme_name = THEMES[0] # Green
288
+ elif i == len(timeline) - 1:
289
+ color_code, theme_name = THEMES[2] # Red
290
+ else:
291
+ color_code, theme_name = THEMES[1] # Blue
292
+
293
+ loc_name = stop.get("location", "")
294
+
295
+ popup_html = create_popup_html(
296
+ title=f"STOP {i + 1}",
297
+ subtitle=loc_name,
298
+ color=theme_name,
299
+ metrics={
300
+ "抵達時間": stop.get("time", ""),
301
+ "天氣": stop.get("weather", ""),
302
+ "空氣品質": stop.get("aqi", {}).get("label", "")
303
+ }
304
+ )
305
+
306
+ # Icon 設定
307
+ icon_type = 'flag-checkered' if i == len(timeline) - 1 else ('play' if i == 0 else 'utensils')
308
+ icon = folium.Icon(color=theme_name, icon=icon_type, prefix='fa')
309
+
310
+ folium.Marker(
311
+ location=[lat, lng], icon=icon,
312
+ popup=folium.Popup(popup_html, max_width=320),
313
+ tooltip=f"第 {i + 1} 站: {loc_name}"
314
+ ).add_to(stops_group)
315
+
316
+ stops_group.add_to(m)
317
+
318
+ # --- 控制元件 ---
319
+ folium.LayerControl(collapsed=False).add_to(m)
320
+
321
+ if bounds:
322
+ m.fit_bounds(bounds, padding=(50, 50))
323
+
324
+ except Exception as e:
325
+ logger.error(f"Folium map error: {e}", exc_info=True)
326
+ return m._repr_html_()
327
+
328
+ return m._repr_html_()
329
+
330
+ if __name__ == "__main__":
331
+ test_data = {'status': 'OK', 'total_travel_time_min': 19, 'total_travel_distance_m': 7567, 'metrics': {'total_tasks': 2, 'completed_tasks': 2, 'completion_rate_pct': 100.0, 'original_distance_m': 15478, 'optimized_distance_m': 7567, 'distance_saved_m': 7911, 'distance_improvement_pct': 51.1, 'original_duration_min': 249, 'optimized_duration_min': 229, 'time_saved_min': 20, 'time_improvement_pct': 8.4, 'route_efficiency_pct': 91.7}, 'route': [{'step': 0, 'node_index': 0, 'arrival_time': '2025-11-27T10:00:00+08:00', 'departure_time': '2025-11-27T10:00:00+08:00', 'type': 'depot', 'task_id': None, 'poi_id': None, 'service_duration_min': 0}, {'step': 1, 'node_index': 1, 'arrival_time': '2025-11-27T10:17:57+08:00', 'departure_time': '2025-11-27T12:17:57+08:00', 'type': 'task_poi', 'task_id': '1', 'poi_id': 'ChIJH56c2rarQjQRphD9gvC8BhI', 'service_duration_min': 120}, {'step': 2, 'node_index': 7, 'arrival_time': '2025-11-27T12:19:05+08:00', 'departure_time': '2025-11-27T13:49:05+08:00', 'type': 'task_poi', 'task_id': '2', 'poi_id': 'ChIJQXcl6LarQjQRGUMnQ18F0lE', 'service_duration_min': 90}], 'visited_tasks': ['1', '2'], 'skipped_tasks': [], 'tasks_detail': [{'task_id': '1', 'priority': 'HIGH', 'visited': True, 'chosen_poi': {'node_index': 1, 'poi_id': 'ChIJH56c2rarQjQRphD9gvC8BhI', 'lat': 25.033976, 'lng': 121.56453889999999, 'interval_idx': 0}, 'alternative_pois': []}, {'task_id': '2', 'priority': 'HIGH', 'visited': True, 'chosen_poi': {'node_index': 7, 'poi_id': 'ChIJQXcl6LarQjQRGUMnQ18F0lE', 'lat': 25.033337099999997, 'lng': 121.56465960000001, 'interval_idx': 0}, 'alternative_pois': [{'node_index': 10, 'poi_id': 'ChIJwQWwVe6rQjQRRGA4WzYdO2U', 'lat': 25.036873699999997, 'lng': 121.5679503, 'interval_idx': 0, 'delta_travel_time_min': 7, 'delta_travel_distance_m': 2003}, {'node_index': 6, 'poi_id': 'ChIJ01XRzrurQjQRnp5ZsHbAAuE', 'lat': 25.039739800000003, 'lng': 121.5665985, 'interval_idx': 0, 'delta_travel_time_min': 9, 'delta_travel_distance_m': 2145}, {'node_index': 5, 'poi_id': 'ChIJaeY0sNCrQjQRBmpF8-RmywQ', 'lat': 25.0409656, 'lng': 121.5429975, 'interval_idx': 0, 'delta_travel_time_min': 24, 'delta_travel_distance_m': 6549}]}], 'tasks': [{'task_id': '1', 'priority': 'HIGH', 'service_duration_min': 120, 'time_window': {'earliest_time': '2025-11-27T10:00:00+08:00', 'latest_time': '2025-11-27T22:00:00+08:00'}, 'candidates': [{'poi_id': 'ChIJH56c2rarQjQRphD9gvC8BhI', 'name': 'Taipei 101', 'lat': 25.033976, 'lng': 121.56453889999999, 'rating': None, 'time_window': None}]}, {'task_id': '2', 'priority': 'HIGH', 'service_duration_min': 90, 'time_window': {'earliest_time': '2025-11-27T11:30:00+08:00', 'latest_time': '2025-11-27T14:30:00+08:00'}, 'candidates': [{'poi_id': 'ChIJbbvUtW6pQjQRLvK71hSUXN8', 'name': 'Din Tai Fung Mitsukoshi Nanxi Restaurant', 'lat': 25.0523074, 'lng': 121.5211037, 'rating': 4.4, 'time_window': None}, {'poi_id': 'ChIJA-U6X-epQjQR-T9BLmEfUlc', 'name': 'Din Tai Fung Xinsheng Branch', 'lat': 25.033889, 'lng': 121.5321338, 'rating': 4.6, 'time_window': None}, {'poi_id': 'ChIJbTKSE4KpQjQRXDZZI57v-pM', 'name': 'Din Tai Fung Xinyi Branch', 'lat': 25.0335035, 'lng': 121.53011799999999, 'rating': 4.4, 'time_window': None}, {'poi_id': 'ChIJaeY0sNCrQjQRBmpF8-RmywQ', 'name': 'Din Tai Fung Fuxing Restaurant', 'lat': 25.0409656, 'lng': 121.5429975, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJ01XRzrurQjQRnp5ZsHbAAuE', 'name': 'Din Tai Fung A4 Branch', 'lat': 25.039739800000003, 'lng': 121.5665985, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJQXcl6LarQjQRGUMnQ18F0lE', 'name': 'Din Tai Fung 101', 'lat': 25.033337099999997, 'lng': 121.56465960000001, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJ7y2qTJeuQjQRbpDcQImxLO0', 'name': 'Din Tai Fung Tianmu Restaurant', 'lat': 25.105072000000003, 'lng': 121.52447500000001, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJxRJqSBmoQjQR3gtSHvJgkZk', 'name': 'Din Tai Fung Mega City Restaurant', 'lat': 25.0135467, 'lng': 121.46675080000001, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJwQWwVe6rQjQRRGA4WzYdO2U', 'name': 'Din Tai Fung A13 Branch', 'lat': 25.036873699999997, 'lng': 121.5679503, 'rating': 4.5, 'time_window': None}, {'poi_id': 'ChIJ34CbayyoQjQRDbCGQgDk_RY', 'name': 'Ding Tai Feng', 'lat': 25.0059716, 'lng': 121.48668440000002, 'rating': 3.6, 'time_window': None}]}], 'global_info': {'language': 'en-US', 'plan_type': 'TRIP', 'departure_time': '2025-11-27T10:00:00+08:00', 'start_location': {'name': 'Taipei Main Station', 'lat': 25.0474428, 'lng': 121.5170955}}, 'traffic_summary': {'total_distance_km': 7.567, 'total_duration_min': 15}, 'precise_traffic_result': {'total_distance_meters': 7567, 'total_duration_seconds': 925, 'total_residence_time_minutes': 210, 'total_time_seconds': 13525, 'start_time': '2025-11-27 10:00:00+08:00', 'end_time': '2025-11-27 05:45:25+00:00', 'stops': [{'lat': 25.0474428, 'lng': 121.5170955}, {'lat': 25.033976, 'lng': 121.56453889999999}, {'lat': 25.033337099999997, 'lng': 121.56465960000001}], 'legs': [{'from_index': 0, 'to_index': 1, 'travel_mode': 'DRIVE', 'distance_meters': 7366, 'duration_seconds': 857, 'departure_time': '2025-11-27T02:00:00+00:00', 'polyline': 'm_{wCqttdVQbAu@KwDe@ELMTKl@C\\@LC^@PLPRJdC\\LLe@bEIj@_@|@I\\?P}EGMERqAAyAf@iD\\sC`AqKTmDJYXoDn@oFH{A`@{PNoB\\wChAyFfCaLrAcHnAyH|BeMZcCRmCNoGVcJPoI@{CIwBc@mHw@aNGeCD}BpBm^Ak@LyDB_@KmD]qDu@iGNs@SuASsBdUPZ@FcNP{ODcItBfANBxClA^D`A@As@D{@HyGpFBtD?pADlKB?|@FD`@@lAE'}, {'from_index': 1, 'to_index': 2, 'travel_mode': 'DRIVE', 'distance_meters': 201, 'duration_seconds': 68, 'departure_time': '2025-11-27T04:14:17+00:00', 'polyline': 'mmxwC}d~dVfBAXGJON]l@?CrC'}]}, 'solved_waypoints': [{'lat': 25.0474428, 'lng': 121.5170955}, {'lat': 25.033976, 'lng': 121.56453889999999}, {'lat': 25.033337099999997, 'lng': 121.56465960000001}], 'timeline': [{'stop_index': 0, 'time': '10:00', 'location': 'start point', 'address': '', 'weather': 'Rain, 20.76°C', 'aqi': {'aqi': 2, 'label': 'AQI 2 🟡'}, 'travel_time_from_prev': '0 mins', 'coordinates': {'lat': 25.0474428, 'lng': 121.5170955}}, {'stop_index': 1, 'time': '10:14', 'location': 'Taipei 101', 'address': '', 'weather': 'Rain, 20.75°C', 'aqi': {'aqi': 2, 'label': 'AQI 2 🟡'}, 'travel_time_from_prev': '14 mins', 'coordinates': {'lat': 25.033976, 'lng': 121.56453889999999}}, {'stop_index': 2, 'time': '12:15', 'location': 'Din Tai Fung Mitsukoshi Nanxi Restaurant', 'address': '', 'weather': 'Rain, 20.75°C', 'aqi': {'aqi': 2, 'label': 'AQI 2 🟡'}, 'travel_time_from_prev': '1 mins', 'coordinates': {'lat': 25.033337099999997, 'lng': 121.56465960000001}}]}
332
+
333
+ create_animated_map(structured_data=test_data)
services/planner_service.py CHANGED
@@ -608,21 +608,7 @@ class PlannerService:
608
  metrics = structured_data.get("metrics", {})
609
  traffic = structured_data.get("traffic_summary", {})
610
 
611
- print("metrics\n", metrics)
612
-
613
  # 🔥 組合 Summary Tab 的內容
614
- # 包含: 總結卡片 + 演算法快樂表 + 時間軸
615
- task_count = len(session.task_list)
616
- high_prio = sum(1 for t in session.task_list if t.get("priority") == "HIGH")
617
- total_time = traffic.get("total_duration_min", 0)
618
-
619
- summary_card = create_summary_card(task_count, high_prio, int(total_time))
620
- metrics_viz = create_result_visualization(session.task_list, structured_data)
621
-
622
- # 將三個元件合併為一個 HTML (用於 Summary Tab)
623
- # 順序: Card -> Viz -> Timeline
624
- full_summary_html = f"{summary_card}<br/>{metrics_viz}<br/><h3>📍 Timeline</h3>{timeline_html}"
625
-
626
  task_count = len(session.task_list)
627
  high_prio = sum(1 for t in session.task_list if t.get("priority") == "HIGH")
628
 
@@ -637,11 +623,12 @@ class PlannerService:
637
  efficiency = metrics.get("route_efficiency_pct", 0)
638
  saved_dist_m = metrics.get("distance_saved_m", 0)
639
  saved_time_min = metrics.get("time_saved_min", 0)
 
 
640
 
641
- summary_card = create_summary_card(task_count, high_prio, int(total_time))
642
 
643
  # B. 進階數據快樂表 (AI Efficiency & Distance)
644
- # 根據效率分數改變顏色 (高於 80% 綠色,否則橘色)
645
  eff_color = "#047857" if efficiency >= 80 else "#d97706"
646
  eff_bg = "#ecfdf5" if efficiency >= 80 else "#fffbeb"
647
  eff_border = "#a7f3d0" if efficiency >= 80 else "#fde68a"
@@ -675,14 +662,14 @@ class PlannerService:
675
  full_summary_html = f"{summary_card}{ai_stats_html}<h3>📍 Itinerary Timeline</h3>{timeline_html}"
676
 
677
  # C. 其他 Tab 內容
678
- task_list_html = self.generate_task_list_html(session)
679
 
680
  # 3. Map
681
  map_fig = create_animated_map(structured_data)
682
- if isinstance(map_fig, str):
683
- logger.error(f"CRITICAL: map_fig is a string ('{map_fig}'). Creating default map.")
684
- from core.visualizers import create_animated_map as default_map_gen
685
- map_fig = default_map_gen(None)
686
 
687
  # 4. Task List
688
  task_list_html = self.generate_task_list_html(session)
 
608
  metrics = structured_data.get("metrics", {})
609
  traffic = structured_data.get("traffic_summary", {})
610
 
 
 
611
  # 🔥 組合 Summary Tab 的內容
 
 
 
 
 
 
 
 
 
 
 
 
612
  task_count = len(session.task_list)
613
  high_prio = sum(1 for t in session.task_list if t.get("priority") == "HIGH")
614
 
 
623
  efficiency = metrics.get("route_efficiency_pct", 0)
624
  saved_dist_m = metrics.get("distance_saved_m", 0)
625
  saved_time_min = metrics.get("time_saved_min", 0)
626
+ start_location = structured_data.get("global_info", {}).get("start_location", {}).get("name", "N/A")
627
+ date = structured_data.get("global_info", {}).get("departure_time", "N/A")
628
 
629
+ summary_card = create_summary_card(task_count, high_prio, int(total_time), start_location, date)
630
 
631
  # B. 進階數據快樂表 (AI Efficiency & Distance)
 
632
  eff_color = "#047857" if efficiency >= 80 else "#d97706"
633
  eff_bg = "#ecfdf5" if efficiency >= 80 else "#fffbeb"
634
  eff_border = "#a7f3d0" if efficiency >= 80 else "#fde68a"
 
662
  full_summary_html = f"{summary_card}{ai_stats_html}<h3>📍 Itinerary Timeline</h3>{timeline_html}"
663
 
664
  # C. 其他 Tab 內容
665
+ # task_list_html = self.generate_task_list_html(session)
666
 
667
  # 3. Map
668
  map_fig = create_animated_map(structured_data)
669
+ #if isinstance(map_fig, str):
670
+ # logger.error(f"CRITICAL: map_fig is a string ('{map_fig}'). Creating default map.")
671
+ # from core.visualizers import create_animated_map as default_map_gen
672
+ # map_fig = default_map_gen(None)
673
 
674
  # 4. Task List
675
  task_list_html = self.generate_task_list_html(session)
src/tools/reader_toolkit.py CHANGED
@@ -24,7 +24,6 @@ class ReaderToolkit(Toolkit):
24
  logger.warning(f"🔄 Auto-Correcting: Switching to latest Session ID: {latest_id}")
25
  data = poi_repo.load(latest_id)
26
 
27
-
28
  if not data:
29
  return "CRITICAL_ERROR: Ref ID not found."
30
  if not data.get("timeline"):
 
24
  logger.warning(f"🔄 Auto-Correcting: Switching to latest Session ID: {latest_id}")
25
  data = poi_repo.load(latest_id)
26
 
 
27
  if not data:
28
  return "CRITICAL_ERROR: Ref ID not found."
29
  if not data.get("timeline"):