Marco310 commited on
Commit
747f356
·
1 Parent(s): c98862f

Fix: fixed state reset bug and enforce sticky horizontal header layout

Browse files
Files changed (3) hide show
  1. app.py +63 -27
  2. ui/components/header.py +16 -16
  3. ui/theme.py +95 -0
app.py CHANGED
@@ -33,6 +33,23 @@ class LifeFlowAI:
33
  def __init__(self):
34
  self.service = PlannerService()
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  def _get_agent_outputs(self, active_agent: str = None, status: str = "idle", message: str = "Waiting") -> List[str]:
37
  """
38
  輔助函數:生成 6 個 Agent 卡片的 HTML 列表
@@ -199,9 +216,40 @@ class LifeFlowAI:
199
  session.custom_settings['openweather_api_key'] = weather_key
200
  session.custom_settings['gemini_api_key'] = gemini_key
201
  session.custom_settings['model'] = model
202
- return "✅ Settings saved locally!", session.to_dict()
 
 
 
203
 
204
  def build_interface(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  with gr.Blocks(title=APP_TITLE) as demo:
206
  gr.HTML(get_enhanced_css())
207
  #create_header()
@@ -211,6 +259,7 @@ class LifeFlowAI:
211
  # State
212
  session_state = gr.State(value=UserSession().to_dict())
213
 
 
214
  with gr.Row():
215
  # Left Column
216
  with gr.Column(scale=2, min_width=400):
@@ -231,7 +280,9 @@ class LifeFlowAI:
231
 
232
  # Right Column
233
  with gr.Column(scale=3, min_width=500):
234
- status_bar = gr.Textbox(label="📊 Status", value="Waiting for input...", interactive=False,
 
 
235
  max_lines=1)
236
  (tabs, report_tab, map_tab, report_output, map_output, reasoning_output,
237
  chat_input_area, chat_history_output, chat_input, chat_send) = create_tabs(
@@ -245,28 +296,6 @@ class LifeFlowAI:
245
  doc_modal, close_doc_btn = create_doc_modal()
246
 
247
  # ===== Event Binding =====
248
- def reset_app(old_session_data):
249
- # 1. 讀取舊 Session
250
- old_session = UserSession.from_dict(old_session_data)
251
-
252
- # 2. 創建新 Session (數據歸零)
253
- new_session = UserSession()
254
-
255
- # 3. 🔥 關鍵:繼承舊的設定 (API Keys, Model)
256
- new_session.custom_settings = old_session.custom_settings
257
-
258
- # 4. 回傳重置後的 UI 狀態
259
- return (
260
- gr.update(visible=True), gr.update(visible=False), # Input Show, Confirm Hide
261
- gr.update(visible=False), gr.update(visible=False), # Chat Hide, Result Hide
262
- gr.update(visible=False), gr.update(visible=False), # Team Hide, Report Hide
263
- gr.update(visible=False), "", # Map Hide, Textbox Clear
264
- create_agent_stream_output(),
265
- "Ready...",
266
- new_session.to_dict() # Reset Session but keep keys
267
- )
268
-
269
-
270
  auto_location.change(fn=toggle_location_inputs, inputs=[auto_location], outputs=[location_inputs])
271
 
272
  # Step 1: Analyze
@@ -290,17 +319,18 @@ class LifeFlowAI:
290
  outputs=[chat_history_output, task_list_display, session_state]
291
  ).then(fn=lambda: "", outputs=[chat_input])
292
 
 
293
  home_btn.click(
294
  fn=reset_app,
295
  inputs=[session_state],
296
  outputs=[
297
  input_area, task_confirm_area, chat_input_area, result_area,
298
  team_area, report_tab, map_tab, user_input,
299
- agent_stream_output, status_bar, session_state
300
  ]
301
  )
302
 
303
- # 綁定原有的 Exit 按鈕 (加入 inputs)
304
  exit_btn_inline.click(
305
  fn=reset_app,
306
  inputs=[session_state],
@@ -311,6 +341,8 @@ class LifeFlowAI:
311
  ]
312
  )
313
 
 
 
314
  # Step 2, 3, 4 Sequence
315
  ready_plan_btn.click(
316
  fn=lambda: (gr.update(visible=False), gr.update(visible=False),
@@ -339,10 +371,11 @@ class LifeFlowAI:
339
  # Settings & Docs Handlers
340
  settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal])
341
  close_settings_btn.click(fn=lambda: gr.update(visible=False), outputs=[settings_modal])
 
342
  save_settings_btn.click(
343
  fn=self.save_settings,
344
  inputs=[google_maps_key, openweather_key, gemini_api_key, model_choice, session_state],
345
- outputs=[settings_status, session_state]
346
  )
347
 
348
  theme_btn.click(fn=None, js="""
@@ -359,6 +392,9 @@ class LifeFlowAI:
359
  doc_btn.click(fn=lambda: gr.update(visible=True), outputs=[doc_modal])
360
  close_doc_btn.click(fn=lambda: gr.update(visible=False), outputs=[doc_modal])
361
 
 
 
 
362
  return demo
363
 
364
 
 
33
  def __init__(self):
34
  self.service = PlannerService()
35
 
36
+ def _check_api_status(self, session: UserSession) -> str:
37
+ """檢查 API Key 狀態並回傳對應的 Status 文字"""
38
+ # 1. 檢查 Session 中的自定義設定
39
+ has_gemini = bool(session.custom_settings.get("gemini_api_key"))
40
+ has_google = bool(session.custom_settings.get("google_maps_api_key"))
41
+
42
+ # 2. 如果沒有自定義,也可以檢查系統預設 (可選)
43
+ # 這裡假設我們強制要求用戶輸入或確認
44
+
45
+ if has_gemini and has_google:
46
+ return "✅ System Ready - Waiting for input..."
47
+ else:
48
+ missing = []
49
+ if not has_gemini: missing.append("Gemini Key")
50
+ if not has_google: missing.append("Google Maps Key")
51
+ return f"⚠️ Missing Keys: {', '.join(missing)} - Please configure Settings ↗"
52
+
53
  def _get_agent_outputs(self, active_agent: str = None, status: str = "idle", message: str = "Waiting") -> List[str]:
54
  """
55
  輔助函數:生成 6 個 Agent 卡片的 HTML 列表
 
216
  session.custom_settings['openweather_api_key'] = weather_key
217
  session.custom_settings['gemini_api_key'] = gemini_key
218
  session.custom_settings['model'] = model
219
+
220
+ new_status = self._check_api_status(session)
221
+
222
+ return "✅ Settings saved locally!", session.to_dict(), new_status
223
 
224
  def build_interface(self):
225
+ def reset_app(old_session_data):
226
+ # 1. 讀取舊 Session
227
+ old_session = UserSession.from_dict(old_session_data)
228
+
229
+ # 2. 創建新 Session (數據歸零)
230
+ new_session = UserSession()
231
+
232
+ # 3. 🔥 關鍵:繼承舊的設定 (API Keys, Model)
233
+ new_session.custom_settings = old_session.custom_settings
234
+
235
+ status_msg = self._check_api_status(new_session)
236
+
237
+ # 4. 回傳重置後的 UI 狀態
238
+ return (
239
+ gr.update(visible=True), gr.update(visible=False), # Input Show, Confirm Hide
240
+ gr.update(visible=False), gr.update(visible=False), # Chat Hide, Result Hide
241
+ gr.update(visible=False), gr.update(visible=False), # Team Hide, Report Hide
242
+ gr.update(visible=False), "", # Map Hide, Textbox Clear
243
+ create_agent_stream_output(),
244
+ status_msg,
245
+ new_session.to_dict() # Reset Session but keep keys
246
+ )
247
+
248
+ def initial_check(session_data):
249
+ s = UserSession.from_dict(session_data)
250
+ # 如果環境變數有值,可以在這裡預先填入 custom_settings (視需求而定)
251
+ return self._check_api_status(s)
252
+
253
  with gr.Blocks(title=APP_TITLE) as demo:
254
  gr.HTML(get_enhanced_css())
255
  #create_header()
 
259
  # State
260
  session_state = gr.State(value=UserSession().to_dict())
261
 
262
+
263
  with gr.Row():
264
  # Left Column
265
  with gr.Column(scale=2, min_width=400):
 
280
 
281
  # Right Column
282
  with gr.Column(scale=3, min_width=500):
283
+ status_bar = gr.Textbox(label="📊 Status",
284
+ value="⚠️ Checking System Status...",
285
+ interactive=False,
286
  max_lines=1)
287
  (tabs, report_tab, map_tab, report_output, map_output, reasoning_output,
288
  chat_input_area, chat_history_output, chat_input, chat_send) = create_tabs(
 
296
  doc_modal, close_doc_btn = create_doc_modal()
297
 
298
  # ===== Event Binding =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  auto_location.change(fn=toggle_location_inputs, inputs=[auto_location], outputs=[location_inputs])
300
 
301
  # Step 1: Analyze
 
319
  outputs=[chat_history_output, task_list_display, session_state]
320
  ).then(fn=lambda: "", outputs=[chat_input])
321
 
322
+ # 更新 Home 鍵綁定
323
  home_btn.click(
324
  fn=reset_app,
325
  inputs=[session_state],
326
  outputs=[
327
  input_area, task_confirm_area, chat_input_area, result_area,
328
  team_area, report_tab, map_tab, user_input,
329
+ agent_stream_output, status_bar, session_state # 確保 status_bar 在列表裡
330
  ]
331
  )
332
 
333
+ # 更新 Exit 鍵綁定 (同上)
334
  exit_btn_inline.click(
335
  fn=reset_app,
336
  inputs=[session_state],
 
341
  ]
342
  )
343
 
344
+
345
+
346
  # Step 2, 3, 4 Sequence
347
  ready_plan_btn.click(
348
  fn=lambda: (gr.update(visible=False), gr.update(visible=False),
 
371
  # Settings & Docs Handlers
372
  settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal])
373
  close_settings_btn.click(fn=lambda: gr.update(visible=False), outputs=[settings_modal])
374
+
375
  save_settings_btn.click(
376
  fn=self.save_settings,
377
  inputs=[google_maps_key, openweather_key, gemini_api_key, model_choice, session_state],
378
+ outputs=[settings_status, session_state, status_bar] # 增加 status_bar
379
  )
380
 
381
  theme_btn.click(fn=None, js="""
 
392
  doc_btn.click(fn=lambda: gr.update(visible=True), outputs=[doc_modal])
393
  close_doc_btn.click(fn=lambda: gr.update(visible=False), outputs=[doc_modal])
394
 
395
+ demo.load(fn=initial_check,
396
+ inputs=[session_state],
397
+ outputs=[status_bar])
398
  return demo
399
 
400
 
ui/components/header.py CHANGED
@@ -1,16 +1,25 @@
1
  """
2
- LifeFlow AI - Header Component (Layout Improved)
3
  """
4
  import gradio as gr
5
 
6
  def create_header():
7
  """
8
- 創建整合式 Header:左側標題,右側控制區
9
- 回傳按鈕物件供 app.py 綁定事件
 
10
  """
11
- with gr.Row(elem_classes="header-row", elem_id="header-container"):
12
- # 左側:標題區
13
- with gr.Column(scale=6, min_width=200):
 
 
 
 
 
 
 
 
14
  gr.HTML("""
15
  <div class="app-header-left">
16
  <h1 style="margin: 0; font-size: 1.8rem;">✨ LifeFlow AI</h1>
@@ -18,13 +27,4 @@ def create_header():
18
  </div>
19
  """)
20
 
21
- # 右側:功能按鈕區 (包含 Home 鍵)
22
- with gr.Column(scale=2, min_width=200):
23
- with gr.Row(elem_classes="header-controls"):
24
- # 移除 tooltip 參數,保留其他設定
25
- home_btn = gr.Button("🏠", size="sm", min_width=40, elem_classes="icon-btn")
26
- theme_btn = gr.Button("🌓", size="sm", min_width=40, elem_classes="icon-btn")
27
- settings_btn = gr.Button("⚙️", size="sm", min_width=40, elem_classes="icon-btn")
28
- doc_btn = gr.Button("📖", size="sm", min_width=40, elem_classes="icon-btn")
29
-
30
- return home_btn, theme_btn, settings_btn, doc_btn
 
1
  """
2
+ LifeFlow AI - Header Component (Fixed Layout - Simplified)
3
  """
4
  import gradio as gr
5
 
6
  def create_header():
7
  """
8
+ 創建 Header
9
+ 1. 功能鍵區 (懸浮固定 - 使用 elem_id 強制定位)
10
+ 2. 標題區 (普通流式佈局)
11
  """
12
+ # 1. 功能按鈕區
13
+ # 🔥 重點:移除內部的 gr.Row,只用 gr.Group,並給它一個唯一的 elem_id
14
+ with gr.Group(elem_id="floating-menu"):
15
+ home_btn = gr.Button("🏠", elem_classes="dock-btn")
16
+ theme_btn = gr.Button("🌓", elem_classes="dock-btn")
17
+ settings_btn = gr.Button("⚙️", elem_classes="dock-btn")
18
+ doc_btn = gr.Button("📖", elem_classes="dock-btn")
19
+
20
+ # 2. 左側標題區
21
+ with gr.Row(elem_classes="header-row"):
22
+ with gr.Column(scale=1):
23
  gr.HTML("""
24
  <div class="app-header-left">
25
  <h1 style="margin: 0; font-size: 1.8rem;">✨ LifeFlow AI</h1>
 
27
  </div>
28
  """)
29
 
30
+ return home_btn, theme_btn, settings_btn, doc_btn
 
 
 
 
 
 
 
 
 
ui/theme.py CHANGED
@@ -83,6 +83,101 @@ def get_enhanced_css() -> str:
83
  background: #f1f5f9 !important;
84
  border-color: #cbd5e1 !important;
85
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  /* ============= Agent 呼吸動畫 (關鍵修改) ============= */
88
  @keyframes breathing-glow {
 
83
  background: #f1f5f9 !important;
84
  border-color: #cbd5e1 !important;
85
  }
86
+
87
+ /* ============= 懸浮功能選單 (Fixed Top-Right) ============= */
88
+ #floating-menu {
89
+ position: fixed !important;
90
+ top: 20px !important;
91
+ right: 20px !important;
92
+ z-index: 99999 !important;
93
+
94
+ /* 外觀 */
95
+ background: rgba(255, 255, 255, 0.95) !important;
96
+ backdrop-filter: blur(12px);
97
+ border: 1px solid rgba(255, 255, 255, 0.8);
98
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
99
+ border-radius: 50px !important;
100
+ padding: 5px 10px !important;
101
+
102
+ /* 🔥 核彈級排版修正:使用 Grid 🔥 */
103
+ display: grid !important;
104
+ /* 強制分為 4 個欄位,每個欄位寬度自動 */
105
+ grid-template-columns: auto auto auto auto !important;
106
+ gap: 8px !important;
107
+
108
+ /* 強制重置寬度,不讓它佔滿 */
109
+ width: fit-content !important;
110
+ min-width: auto !important;
111
+ }
112
+
113
+ /* 2. 強制重置 Gradio 內部的 Layout 容器 */
114
+ #floating-menu > * {
115
+ display: contents !important; /* 忽略中間層的 div,直接讓按鈕參與 Grid 排列 */
116
+ }
117
+
118
+ /* 3. 按鈕本體設定 */
119
+ .dock-btn {
120
+ display: flex !important;
121
+ align-items: center !important;
122
+ justify-content: center !important;
123
+
124
+ width: 40px !important;
125
+ height: 40px !important;
126
+ border-radius: 50% !important;
127
+
128
+ background: transparent !important;
129
+ border: none !important;
130
+ box-shadow: none !important;
131
+
132
+ margin: 0 !important;
133
+ padding: 0 !important;
134
+ font-size: 1.2rem !important;
135
+ }
136
+
137
+ .dock-btn:hover {
138
+ background: #f1f5f9 !important;
139
+ transform: translateY(-2px);
140
+ transition: all 0.2s ease;
141
+ }
142
+
143
+ /* 🔥 強制重置 Gradio 內部包裝層 🔥 */
144
+ /* 這是造成垂直排列的主因,Gradio 預設會把元件包在 width:100% 的 div 裡 */
145
+ .header-controls > * {
146
+ min-width: auto !important;
147
+ width: auto !important;
148
+ flex: 0 0 auto !important;
149
+ margin: 0 !important;
150
+ padding: 0 !important;
151
+ border: none !important;
152
+ background: transparent !important;
153
+ }
154
+
155
+ /* 按鈕本體樣式優化 */
156
+ .header-controls button {
157
+ display: flex !important;
158
+ align-items: center !important;
159
+ justify-content: center !important;
160
+
161
+ width: 38px !important; /* 固定按鈕大小 */
162
+ height: 38px !important;
163
+ border-radius: 50% !important;
164
+
165
+ background: transparent !important;
166
+ border: 1px solid transparent !important;
167
+ box-shadow: none !important;
168
+ padding: 0 !important;
169
+ font-size: 1.2rem !important;
170
+ line-height: 1 !important;
171
+ color: #64748b !important;
172
+ }
173
+
174
+ .header-controls button:hover {
175
+ background: #f1f5f9 !important;
176
+ color: #6366f1 !important;
177
+ transform: translateY(-1px);
178
+ transition: all 0.2s ease;
179
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05) !important;
180
+ }
181
 
182
  /* ============= Agent 呼吸動畫 (關鍵修改) ============= */
183
  @keyframes breathing-glow {