Marco310 commited on
Commit
a3b49e8
·
1 Parent(s): 760373c

feat: Optimize UI/UX

Browse files
app.py CHANGED
@@ -36,9 +36,9 @@ class LifeFlowAI:
36
 
37
  def _check_api_status(self, session_data):
38
  session = UserSession.from_dict(session_data)
39
- has_gemini = bool(session.custom_settings.get("gemini_api_key"))
40
  has_google = bool(session.custom_settings.get("google_maps_api_key"))
41
- msg = "✅ System Ready" if (has_gemini and has_google) else "⚠️ Missing API Keys"
42
  return msg
43
 
44
  def _get_gradio_chat_history(self, session):
@@ -50,6 +50,7 @@ class LifeFlowAI:
50
  # ================= Event Wrappers =================
51
 
52
  def analyze_wrapper(self, user_input, auto_loc, lat, lon, session_data):
 
53
  session = UserSession.from_dict(session_data)
54
  iterator = self.service.run_step1_analysis(user_input, auto_loc, lat, lon, session)
55
  for event in iterator:
@@ -215,6 +216,7 @@ class LifeFlowAI:
215
 
216
  with gr.Column(elem_classes="step-container"):
217
  home_btn, theme_btn, settings_btn, doc_btn = create_header()
 
218
  stepper = create_progress_stepper(1)
219
  status_bar = gr.Markdown("Ready", visible=False)
220
 
@@ -283,7 +285,9 @@ class LifeFlowAI:
283
  map_view = gr.Plot(label="Route Map", show_label=False)
284
 
285
  # Modals & Events
286
- (settings_modal, g_key, w_key, gem_key, model_sel, close_set, save_set, set_stat) = create_settings_modal()
 
 
287
  doc_modal, close_doc_btn = create_doc_modal()
288
 
289
  settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal])
@@ -335,21 +339,34 @@ class LifeFlowAI:
335
  home_btn.click(fn=reset_all, inputs=[session_state], outputs=[step1_container, step2_container, step3_container, step4_container, stepper, session_state, user_input])
336
  back_btn.click(fn=reset_all, inputs=[session_state], outputs=[step1_container, step2_container, step3_container, step4_container, stepper, session_state, user_input])
337
 
338
- save_set.click(fn=self.save_settings, inputs=[g_key, w_key, gem_key, model_sel, session_state], outputs=[set_stat, session_state, status_bar])
 
 
 
 
339
  auto_loc.change(fn=toggle_location_inputs, inputs=auto_loc, outputs=loc_group)
340
  demo.load(fn=self._check_api_status, inputs=[session_state], outputs=[status_bar])
341
 
342
  return demo
343
 
344
- def save_settings(self, g, w, gem, m, s_data):
 
345
  sess = UserSession.from_dict(s_data)
346
- sess.custom_settings.update({'google_maps_api_key': g, 'openweather_api_key': w, 'gemini_api_key': gem, 'model': m})
347
- return "✅ Saved", sess.to_dict(), "Settings Updated"
 
 
 
 
 
 
 
 
348
 
349
  def main():
350
  app = LifeFlowAI()
351
  demo = app.build_interface()
352
- demo.launch(server_name="0.0.0.0", server_port=7860, share=True, show_error=True)
353
 
354
  if __name__ == "__main__":
355
  main()
 
36
 
37
  def _check_api_status(self, session_data):
38
  session = UserSession.from_dict(session_data)
39
+ has_model_api = bool(session.custom_settings.get("model_api"))
40
  has_google = bool(session.custom_settings.get("google_maps_api_key"))
41
+ msg = "✅ System Ready" if (has_model_api and has_google) else "⚠️ Missing API Keys"
42
  return msg
43
 
44
  def _get_gradio_chat_history(self, session):
 
50
  # ================= Event Wrappers =================
51
 
52
  def analyze_wrapper(self, user_input, auto_loc, lat, lon, session_data):
53
+
54
  session = UserSession.from_dict(session_data)
55
  iterator = self.service.run_step1_analysis(user_input, auto_loc, lat, lon, session)
56
  for event in iterator:
 
216
 
217
  with gr.Column(elem_classes="step-container"):
218
  home_btn, theme_btn, settings_btn, doc_btn = create_header()
219
+
220
  stepper = create_progress_stepper(1)
221
  status_bar = gr.Markdown("Ready", visible=False)
222
 
 
285
  map_view = gr.Plot(label="Route Map", show_label=False)
286
 
287
  # Modals & Events
288
+ (settings_modal, g_key, w_key, llm_provider,
289
+ model_key, model_sel, close_set, save_set, set_stat) = create_settings_modal()
290
+
291
  doc_modal, close_doc_btn = create_doc_modal()
292
 
293
  settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal])
 
339
  home_btn.click(fn=reset_all, inputs=[session_state], outputs=[step1_container, step2_container, step3_container, step4_container, stepper, session_state, user_input])
340
  back_btn.click(fn=reset_all, inputs=[session_state], outputs=[step1_container, step2_container, step3_container, step4_container, stepper, session_state, user_input])
341
 
342
+ save_set.click(fn=self.save_settings,
343
+ inputs=[g_key, w_key, llm_provider, model_key, model_sel, session_state],
344
+ outputs=[set_stat, session_state, status_bar]
345
+ ).then(fn=lambda: gr.update(visible=False), outputs=[settings_modal])
346
+
347
  auto_loc.change(fn=toggle_location_inputs, inputs=auto_loc, outputs=loc_group)
348
  demo.load(fn=self._check_api_status, inputs=[session_state], outputs=[status_bar])
349
 
350
  return demo
351
 
352
+ # 接收參數中加入了 prov (對應 UI 上的 llm_provider)
353
+ def save_settings(self, g, w, prov, model_api, m, s_data):
354
  sess = UserSession.from_dict(s_data)
355
+
356
+ # 更新設定字典,加入 provider
357
+ sess.custom_settings.update({
358
+ 'google_maps_api_key': g,
359
+ 'openweather_api_key': w,
360
+ 'llm_provider': prov, # ✅ 新增這行:儲存供應商
361
+ 'model_api_key': model_api,
362
+ 'model': m
363
+ })
364
+ return gr.update(visible=False), sess.to_dict(), "✅ Settings Updated"
365
 
366
  def main():
367
  app = LifeFlowAI()
368
  demo = app.build_interface()
369
+ demo.launch(server_name="0.0.0.0", server_port=8080, share=True, show_error=True)
370
 
371
  if __name__ == "__main__":
372
  main()
services/planner_service.py CHANGED
@@ -170,22 +170,25 @@ class PlannerService:
170
  return session
171
 
172
  # 1. API Key 檢查
173
- gemini_key = session.custom_settings.get("gemini_api_key")
174
  google_key = session.custom_settings.get("google_maps_api_key")
175
  weather_key = session.custom_settings.get("openweather_api_key")
176
 
 
 
 
177
  import os
178
- if not gemini_key: gemini_key = os.getenv("GEMINI_API_KEY", "")
179
- if not google_key: google_key = os.getenv("GOOGLE_MAPS_API_KEY", "")
180
- if not weather_key: weather_key = os.getenv("OPENWEATHER_API_KEY", "")
 
181
 
182
- if not gemini_key:
183
  raise ValueError("🔑 Missing Gemini API Key. Please configure Settings.")
184
 
185
- # 2. 準備 Models
186
- planner_model = Gemini(id='gemini-2.5-flash', thinking_budget=2048, api_key=gemini_key)
187
- main_model = Gemini(id='gemini-2.5-flash', thinking_budget=1024, api_key=gemini_key)
188
- lite_model = Gemini(id="gemini-2.5-flash-lite", api_key=gemini_key)
189
 
190
  models_dict = {
191
  "team": main_model, "scout": main_model, "optimizer": lite_model,
@@ -318,10 +321,13 @@ class PlannerService:
318
 
319
  try:
320
  task_list_data = json.loads(json_data)
 
 
 
 
321
  session.task_list = self._convert_task_list_to_ui_format(task_list_data)
322
  except Exception as e:
323
  logger.error(f"Failed to parse task_list: {e}")
324
- print(json_data)
325
  session.task_list = []
326
 
327
  # 🛡️ 檢查 2: Planner 是否回傳空列表
 
170
  return session
171
 
172
  # 1. API Key 檢查
 
173
  google_key = session.custom_settings.get("google_maps_api_key")
174
  weather_key = session.custom_settings.get("openweather_api_key")
175
 
176
+ provider = session.custom_settings.get("llm_provider")
177
+ model_api_key = session.custom_settings.get("model_api_key")
178
+
179
  import os
180
+ if not google_key:
181
+ google_key = os.getenv("GOOGLE_MAPS_API_KEY", "")
182
+ if not weather_key:
183
+ weather_key = os.getenv("OPENWEATHER_API_KEY", "")
184
 
185
+ if not model_api_key:
186
  raise ValueError("🔑 Missing Gemini API Key. Please configure Settings.")
187
 
188
+ if provider.lower() == "gemini":
189
+ planner_model = Gemini(id='gemini-2.5-flash', thinking_budget=1024, api_key=model_api_key)
190
+ main_model = Gemini(id='gemini-2.5-flash', thinking_budget=1024, api_key=model_api_key)
191
+ lite_model = Gemini(id="gemini-2.5-flash-lite", api_key=model_api_key)
192
 
193
  models_dict = {
194
  "team": main_model, "scout": main_model, "optimizer": lite_model,
 
321
 
322
  try:
323
  task_list_data = json.loads(json_data)
324
+ if task_list_data["global_info"]["start_location"].lower() == "user location":
325
+ logger.info("Using detected location for start_location")
326
+ task_list_data["global_info"]["start_location"] = {"lat": lat, "lng": lon}
327
+
328
  session.task_list = self._convert_task_list_to_ui_format(task_list_data)
329
  except Exception as e:
330
  logger.error(f"Failed to parse task_list: {e}")
 
331
  session.task_list = []
332
 
333
  # 🛡️ 檢查 2: Planner 是否回傳空列表
ui/components/header.py CHANGED
@@ -8,13 +8,13 @@ import gradio as gr
8
  def create_header():
9
  """
10
  創建優化後的 Header:
11
- - 使用 Row/Column 佈局避免 fixed positioning 問題
12
- - 響應式設計友好
13
- - 按鈕水平排列
14
  """
 
15
  with gr.Row(elem_classes="app-header-container"):
16
- # 左側:Logo 和標題
17
- with gr.Column(scale=4, min_width=300):
18
  gr.HTML("""
19
  <div class="app-header-left">
20
  <h1 style="margin: 0; font-size: 2rem; font-weight: 800;
@@ -29,12 +29,12 @@ def create_header():
29
  </div>
30
  """)
31
 
32
- # 右側:功能按鈕
33
- with gr.Column(scale=1, min_width=200):
34
- with gr.Row(elem_classes="header-controls"):
35
- home_btn = gr.Button("🏠", size="sm", elem_classes="header-btn")
36
- theme_btn = gr.Button("🌓", size="sm", elem_classes="header-btn")
37
- settings_btn = gr.Button("⚙️", size="sm", elem_classes="header-btn")
38
- doc_btn = gr.Button("📖", size="sm", elem_classes="header-btn")
39
 
40
  return home_btn, theme_btn, settings_btn, doc_btn
 
8
  def create_header():
9
  """
10
  創建優化後的 Header:
11
+ - Logo 區塊保留在原有容器中
12
+ - 按鈕區塊移出 app-header-container,避免被父級的 backdrop-filter 捕獲
 
13
  """
14
+ # 1. Header 容器 (只放 Logo)
15
  with gr.Row(elem_classes="app-header-container"):
16
+ # 左側:Logo 和標題 (佔滿寬度,因為按鈕已經飛出去了)
17
+ with gr.Column(scale=1):
18
  gr.HTML("""
19
  <div class="app-header-left">
20
  <h1 style="margin: 0; font-size: 2rem; font-weight: 800;
 
29
  </div>
30
  """)
31
 
32
+ # 2. 功能按鈕 (移出上面的 Row,獨立存在)
33
+ # 因為 CSS 設定了 position: fixed,它們會自動飛到右上角,不受文檔流影響
34
+ with gr.Row(elem_classes="header-controls"):
35
+ home_btn = gr.Button("🏠", size="sm", elem_classes="header-btn")
36
+ theme_btn = gr.Button("🌓", size="sm", elem_classes="header-btn")
37
+ settings_btn = gr.Button("⚙️", size="sm", elem_classes="header-btn")
38
+ doc_btn = gr.Button("📖", size="sm", elem_classes="header-btn")
39
 
40
  return home_btn, theme_btn, settings_btn, doc_btn
ui/components/input_form.py CHANGED
@@ -1,58 +1,117 @@
1
- """
2
- LifeFlow AI - Input Form Component (Fixed Layout)
3
- ✅ 強制將 AI 狀態欄位移至按鈕上方
4
- """
5
  import gradio as gr
6
 
 
7
  def create_input_form(agent_stream_html):
8
- """創建優化後的輸入表單"""
9
 
10
  with gr.Group(elem_classes="glass-card") as input_area:
11
  gr.Markdown("### 📝 What's your plan today?")
12
 
13
-
14
- # 這裡的 HTML 元件會顯示串流文字
15
- agent_stream_output = gr.HTML(
16
- value=agent_stream_html,
17
- label="Agent Status",
18
- elem_classes="agent-stream-box-step1",
19
- visible=True # 確保預設可見(即使是空的),佔據空間
20
- )
21
-
22
- gr.Markdown("---")
23
-
24
  # 1. 主要輸入區
25
  user_input = gr.Textbox(
26
  label="Describe your tasks",
27
- placeholder="e.g., I need to visit the dentist at 10am, then buy groceries from Costco, and pick up a package...",
28
  lines=3,
29
  elem_id="main-input"
30
  )
31
 
32
- # 2. 位置設定
33
- with gr.Accordion("📍 Location Settings", open=False):
34
- auto_location = gr.Checkbox(label="Auto-detect my location (Simulated)", value=False)
35
  with gr.Group(visible=True) as location_inputs:
36
  with gr.Row():
37
  lat_input = gr.Number(label="Latitude", value=25.033, precision=6, scale=1)
38
  lon_input = gr.Number(label="Longitude", value=121.565, precision=6, scale=1)
39
 
40
- # 3. 快速範例
41
- gr.Examples(
42
- examples=[
43
- ["Plan a trip to visit Taipei 101, then have lunch at Din Tai Fung."],
44
- ["Errands run: Post office, bank, and supermarket. Start at 10 AM, finish by 1 PM."]
45
- ],
46
- inputs=user_input,
47
- label="Quick Start Examples"
48
- )
49
 
 
 
 
 
50
 
51
- # 5. 主按鈕
52
  analyze_btn = gr.Button("🚀 Analyze & Plan Trip", variant="primary", size="lg")
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  return (input_area, agent_stream_output, user_input, auto_location,
55
  location_inputs, lat_input, lon_input, analyze_btn)
56
 
 
57
  def toggle_location_inputs(auto_loc):
58
  return gr.update(visible=not auto_loc)
 
 
 
 
 
1
  import gradio as gr
2
 
3
+
4
  def create_input_form(agent_stream_html):
5
+ """創建優化後的輸入表單 - 放棄 gr.Examples,改用純按鈕"""
6
 
7
  with gr.Group(elem_classes="glass-card") as input_area:
8
  gr.Markdown("### 📝 What's your plan today?")
9
 
 
 
 
 
 
 
 
 
 
 
 
10
  # 1. 主要輸入區
11
  user_input = gr.Textbox(
12
  label="Describe your tasks",
13
+ placeholder="e.g., I need to visit the dentist at 10am...",
14
  lines=3,
15
  elem_id="main-input"
16
  )
17
 
18
+ # 2. 位置設定 (保持不變)
19
+ with gr.Accordion("📍 Location Settings", open=True):
20
+ auto_location = gr.Checkbox(label="Auto-detect my location", value=False)
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, scale=1)
24
  lon_input = gr.Number(label="Longitude", value=121.565, precision=6, scale=1)
25
 
26
+ # 3. 快速範例 (⭐⭐ 重製版:使用 Button 取代 Examples ⭐⭐)
27
+ gr.Markdown("##### ⚡ Quick Start Examples")
28
+
29
+ # 定義範例文字
30
+ ex_text_1 = "Plan a trip to visit Taipei 101, then have lunch at Din Tai Fung."
31
+ ex_text_2 = "Errands run: Post office, bank, and supermarket. Start at 10 AM, finish by 1 PM."
 
 
 
32
 
33
+ with gr.Column(elem_classes="example-container"):
34
+ # 這是真的按鈕,絕對會垂直排列,絕對不會被壓縮
35
+ btn_ex1 = gr.Button(ex_text_1, size="sm", variant="secondary", elem_classes="example-btn")
36
+ btn_ex2 = gr.Button(ex_text_2, size="sm", variant="secondary", elem_classes="example-btn")
37
 
38
+ # 4. 主按鈕
39
  analyze_btn = gr.Button("🚀 Analyze & Plan Trip", variant="primary", size="lg")
40
 
41
+ # 5. Agent 狀態區
42
+ gr.Markdown("---")
43
+ gr.Markdown("### 🤖 Agent Status")
44
+ agent_stream_output = gr.HTML(
45
+ value=agent_stream_html,
46
+ elem_classes="agent-stream-box-step1",
47
+ visible=True
48
+ )
49
+
50
+ geo_js = """
51
+ (is_auto, curr_lat, curr_lon) => {
52
+ if (!is_auto) {
53
+ // 如果是「取消勾選」,保持原值不變
54
+ return [curr_lat, curr_lon];
55
+ }
56
+
57
+ // 如果是「勾選」,開始定位
58
+ return new Promise((resolve) => {
59
+ if (!navigator.geolocation) {
60
+ alert("Geolocation is not supported by your browser.");
61
+ resolve([curr_lat, curr_lon]); // 失敗回傳原值
62
+ return;
63
+ }
64
+
65
+ // 顯示讀取中的狀態 (可選)
66
+ // document.body.style.cursor = 'wait';
67
+
68
+ navigator.geolocation.getCurrentPosition(
69
+ (position) => {
70
+ // document.body.style.cursor = 'default';
71
+ // 成功!回傳經緯度
72
+ resolve([position.coords.latitude, position.coords.longitude]);
73
+ },
74
+ (error) => {
75
+ // document.body.style.cursor = 'default';
76
+ let msg = "Unknown error";
77
+ switch(error.code) {
78
+ case 1: msg = "Permission denied. Please allow location access."; break;
79
+ case 2: msg = "Position unavailable."; break;
80
+ case 3: msg = "Timeout."; break;
81
+ }
82
+ alert("📍 Location Error: " + msg);
83
+ resolve([curr_lat, curr_lon]); // 失敗回傳原值
84
+ },
85
+ { enableHighAccuracy: true, timeout: 8000 }
86
+ );
87
+ });
88
+ }
89
+ """
90
+
91
+ # 綁定事件 1: 控制輸入框顯示/隱藏 (Python 邏輯)
92
+ auto_location.change(
93
+ fn=lambda x: gr.update(visible=not x),
94
+ inputs=[auto_location],
95
+ outputs=[location_inputs]
96
+ )
97
+
98
+ # 綁定事件 2: 執行 JS 定位 (JS 邏輯)
99
+ # 注意 inputs 包含了 lat/lon,這樣 JS 才能在失敗/取消時把原值傳回來
100
+ auto_location.change(
101
+ fn=None,
102
+ inputs=[auto_location, lat_input, lon_input],
103
+ outputs=[lat_input, lon_input],
104
+ js=geo_js
105
+ )
106
+
107
+ # ⭐⭐ 綁定點擊事件 ⭐⭐
108
+ # 點擊按鈕 -> 把按鈕文字填入 user_input
109
+ btn_ex1.click(lambda: ex_text_1, outputs=user_input)
110
+ btn_ex2.click(lambda: ex_text_2, outputs=user_input)
111
+
112
  return (input_area, agent_stream_output, user_input, auto_location,
113
  location_inputs, lat_input, lon_input, analyze_btn)
114
 
115
+
116
  def toggle_location_inputs(auto_loc):
117
  return gr.update(visible=not auto_loc)
ui/components/modals.py CHANGED
@@ -1,35 +1,69 @@
1
- """
2
- LifeFlow AI - Settings & Documentation Modals
3
- """
4
 
5
  import gradio as gr
6
- from config import MODEL_CHOICES
7
 
8
 
9
  def create_settings_modal():
10
- """創建設定模態框"""
11
- with gr.Group(visible=False) as settings_modal:
12
- gr.Markdown("## ⚙️ Settings")
13
- gr.Markdown("### 🔑 API Keys")
14
- google_maps_key = gr.Textbox(label="Google Maps API Key", type="password")
15
- openweather_key = gr.Textbox(label="OpenWeather API Key", type="password")
16
- gemini_api_key = gr.Textbox(label="Gemini API Key", type="password")
17
-
18
- gr.Markdown("### 🤖 Model")
19
- model_choice = gr.Dropdown(
20
- choices=MODEL_CHOICES,
21
- value=MODEL_CHOICES[0],
22
- label="Select Model"
23
- )
24
-
25
- with gr.Row():
26
- close_settings_btn = gr.Button(" Close", scale=1)
27
- save_settings_btn = gr.Button("💾 Save", variant="primary", scale=2)
28
-
29
- settings_status = gr.Textbox(label="Status", value="")
30
-
31
- return (settings_modal, google_maps_key, openweather_key, gemini_api_key,
32
- model_choice, close_settings_btn, save_settings_btn, settings_status)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
 
35
  def create_doc_modal():
 
1
+ import gradio as gr
 
 
2
 
3
  import gradio as gr
 
4
 
5
 
6
  def create_settings_modal():
7
+ """
8
+ 創建設定彈窗 (Modal) - 雙欄佈局優化版
9
+ 回傳 9 個元件 (新增了 llm_provider)
10
+ """
11
+ with gr.Group(visible=False, elem_classes="modal-overlay", elem_id="settings-modal") as modal:
12
+ with gr.Group(elem_classes="modal-box"):
13
+ with gr.Row(elem_classes="modal-header"):
14
+ gr.Markdown("### ⚙️ System Configuration", elem_classes="modal-title")
15
+
16
+ with gr.Column(elem_classes="modal-content"):
17
+ with gr.Tabs():
18
+ # === 分頁 1: API Keys ===
19
+ with gr.TabItem("🔑 API Keys"):
20
+ gr.Markdown("Configure your service credentials below.", elem_classes="tab-desc")
21
+
22
+ g_key = gr.Textbox(label="Google Maps Platform Key", placeholder="AIza...", type="password")
23
+ w_key = gr.Textbox(label="OpenWeatherMap Key", placeholder="Enter key...",
24
+ type="password")
25
+
26
+ # 修改開始:將原本單獨的 gem_key 改為左右佈局 ⭐
27
+ with gr.Row(equal_height=True): # 確保高度對齊
28
+ # 左側:供應商選擇 (短)
29
+ with gr.Column(scale=1, min_width=100):
30
+ llm_provider = gr.Dropdown(
31
+ choices=["Gemini"], #, "OpenAI", "Anthropic"
32
+ value="Gemini",
33
+ label="Provider",
34
+ interactive=True,
35
+ elem_id="provider-dropdown" # 方便 CSS 微調
36
+ )
37
+
38
+ # 右側:API Key 輸入 (長)
39
+ with gr.Column(scale=3):
40
+ gem_key = gr.Textbox(
41
+ label="Model API Key",
42
+ placeholder="sk-...",
43
+ type="password"
44
+ )
45
+ # ⭐ 修改結束 ⭐
46
+
47
+ # === 分頁 2: Model Settings ===
48
+ with gr.TabItem("🤖 Model Settings"):
49
+ gr.Markdown("Select the AI model engine for trip planning.", elem_classes="tab-desc")
50
+ model_sel = gr.Dropdown(
51
+ choices=["gemini-2.5-flash"], #, "gemini-1.5-flash", "gpt-4o"
52
+ value="gemini-2.5-flash",
53
+ label="AI Model",
54
+ interactive=True,
55
+ info="Only support 2.5-flash"
56
+ # "Only models supported by your selected provider will be shown."
57
+ )
58
+
59
+ set_stat = gr.Markdown(value="", visible=True)
60
+
61
+ with gr.Row(elem_classes="modal-footer"):
62
+ close_btn = gr.Button("Cancel", variant="secondary")
63
+ save_btn = gr.Button("💾 Save Configuration", variant="primary")
64
+
65
+ # 🔥 重要:這裡現在回傳 9 個變數 (新增了 llm_provider)
66
+ return modal, g_key, w_key, llm_provider, gem_key, model_sel, close_btn, save_btn, set_stat
67
 
68
 
69
  def create_doc_modal():
ui/theme.py CHANGED
@@ -53,11 +53,26 @@ def get_enhanced_css() -> str:
53
  }
54
 
55
  .header-controls {
 
 
 
 
 
56
  display: flex !important;
57
- gap: 8px !important;
58
- justify-content: flex-end !important;
59
  align-items: center !important;
60
- width: 100%;
 
 
 
 
 
 
 
 
 
 
61
  }
62
 
63
  .header-btn {
@@ -89,7 +104,7 @@ def get_enhanced_css() -> str:
89
  .header-btn:active {
90
  transform: translateY(0) !important;
91
  }
92
-
93
  /* ============= 2. Animations & Agent Cards (保留 Backup 樣式) ============= */
94
  @keyframes breathing-glow {
95
  0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.6), 0 0 20px rgba(99, 102, 241, 0.3); border-color: #6366f1; }
@@ -350,5 +365,183 @@ def get_enhanced_css() -> str:
350
  min-height: 60px; max-height: 150px; overflow-y: auto; font-family: monospace; font-size: 0.9rem;
351
  margin-bottom: 16px; margin-top: 10px;
352
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  </style>
354
  """
 
53
  }
54
 
55
  .header-controls {
56
+ position: fixed !important; /* 關鍵:脫離文檔流,固定在視窗 */
57
+ top: 25px !important; /* 距離視窗頂部 25px */
58
+ right: 25px !important; /* 距離視窗右側 25px */
59
+ z-index: 99999 !important; /* 關鍵:設得夠大,確保浮在所有內容最上面 */
60
+
61
  display: flex !important;
62
+ flex-direction: row !important;
63
+ gap: 10px !important;
64
  align-items: center !important;
65
+
66
+ /* 視覺優化:半透明毛玻璃膠囊背景 */
67
+ background: rgba(255, 255, 255, 0.85) !important;
68
+ backdrop-filter: blur(12px) !important; /* 背景模糊效果 */
69
+ padding: 8px 16px !important;
70
+ border-radius: 99px !important; /* 圓形膠囊狀 */
71
+ border: 1px solid rgba(255, 255, 255, 0.6) !important;
72
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1) !important;
73
+
74
+ width: auto !important; /* 讓寬度隨按鈕數量自動縮放,不要佔滿整行 */
75
+ min-width: 0 !important;
76
  }
77
 
78
  .header-btn {
 
104
  .header-btn:active {
105
  transform: translateY(0) !important;
106
  }
107
+
108
  /* ============= 2. Animations & Agent Cards (保留 Backup 樣式) ============= */
109
  @keyframes breathing-glow {
110
  0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.6), 0 0 20px rgba(99, 102, 241, 0.3); border-color: #6366f1; }
 
365
  min-height: 60px; max-height: 150px; overflow-y: auto; font-family: monospace; font-size: 0.9rem;
366
  margin-bottom: 16px; margin-top: 10px;
367
  }
368
+
369
+ /* ============= 新版 Quick Start Buttons ============= */
370
+
371
+ /* 容器稍微給點間距 */
372
+ .example-container {
373
+ gap: 8px !important;
374
+ margin-bottom: 16px !important;
375
+ }
376
+
377
+ /* 把按鈕偽裝成卡片列表 */
378
+ .example-btn {
379
+ text-align: left !important; /* 文字靠左 */
380
+ justify-content: flex-start !important; /* Flex 內容靠左 */
381
+ height: auto !important; /* 高度自動 */
382
+ white-space: normal !important; /* ⭐ 允許換行 */
383
+ word-break: break-word !important; /* 長字換行 */
384
+ padding: 12px 16px !important;
385
+ background: #f8fafc !important;
386
+ border: 1px solid #e2e8f0 !important;
387
+ color: #475569 !important;
388
+ font-weight: 400 !important; /* 字體不要太粗 */
389
+ }
390
+
391
+ .example-btn:hover {
392
+ background: white !important;
393
+ border-color: var(--primary-color) !important;
394
+ color: var(--primary-color) !important;
395
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05) !important;
396
+ transform: translateY(-1px);
397
+ }
398
+
399
+ /* ============= 設定彈窗 (Settings Modal) ============= */
400
+
401
+ /* 1. 全螢幕遮罩層 (Overlay) */
402
+ .modal-overlay {
403
+ position: fixed !important;
404
+ top: 0 !important;
405
+ left: 0 !important;
406
+ width: 100vw !important;
407
+ height: 100vh !important;
408
+ background: rgba(0, 0, 0, 0.6) !important; /* 半透明黑色背景 */
409
+ backdrop-filter: blur(4px) !important; /* 背景模糊效果 */
410
+ z-index: 100000 !important; /* 必須比 Header 的 99999 還大 */
411
+ display: flex !important;
412
+ align-items: center !important; /* 垂直置中 */
413
+ justify-content: center !important; /* 水平置中 */
414
+ padding: 20px !important;
415
+ }
416
+
417
+ /* 2. 彈窗本體 (Modal Box) */
418
+ .modal-box {
419
+ background: white !important;
420
+ width: 100% !important;
421
+ max-width: 500px !important; /* 限制最大寬度 */
422
+ border-radius: 16px !important;
423
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !important;
424
+ border: 1px solid rgba(255, 255, 255, 0.3) !important;
425
+ overflow: hidden !important;
426
+ padding: 0 !important; /* 移除 Gradio 預設內距 */
427
+ position: relative !important;
428
+ animation: modal-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
429
+ }
430
+
431
+ /* 3. 彈窗內部佈局微調 */
432
+ .modal-header {
433
+ padding: 20px 24px 0 24px !important;
434
+ background: white !important;
435
+ }
436
+
437
+ .modal-title h3 {
438
+ margin: 0 !important;
439
+ font-size: 1.5rem !important;
440
+ color: #1e293b !important;
441
+ }
442
+
443
+ .modal-content {
444
+ padding: 0 24px 10px 24px !important;
445
+ max-height: 60vh !important;
446
+ overflow-y: auto !important; /* 內容太長時可捲動 */
447
+ }
448
+
449
+ .modal-footer {
450
+ padding: 16px 24px 24px 24px !important;
451
+ background: #f8fafc !important;
452
+ border-top: 1px solid #e2e8f0 !important;
453
+ display: flex !important;
454
+ justify-content: flex-end !important;
455
+ gap: 10px !important;
456
+ }
457
+
458
+ /* 彈出動畫 */
459
+ @keyframes modal-pop {
460
+ 0% { transform: scale(0.95) translateY(10px); opacity: 0; }
461
+ 100% { transform: scale(1) translateY(0); opacity: 1; }
462
+ }
463
+
464
+ /* ============= API Key 佈局優化 ============= */
465
+
466
+ /* 1. 縮小左右欄位的間距 (原本約 20px -> 改為 6px) */
467
+ .api-row {
468
+ gap: 6px !important;
469
+ }
470
+
471
+ /* 2. 修正 Dropdown 在窄欄位時文字被切斷的問題 */
472
+ #provider-dropdown .wrap-inner {
473
+ padding-right: 25px !important; /* 預留空間給箭頭 */
474
+ }
475
+
476
+ #provider-dropdown input {
477
+ text-overflow: ellipsis !important;
478
+ min-width: 0 !important;
479
+ }
480
+
481
+ /* 讓 Dropdown 的選單箭頭不要擠到文字 */
482
+ #provider-dropdown svg {
483
+ margin-right: -5px !important;
484
+ }
485
+ .agent-war-room {
486
+ background: white !important; /* 強制純白不透明 */
487
+ border: 1px solid #cbd5e1 !important; /* 加深邊框顏色 */
488
+ border-radius: 16px !important;
489
+ padding: 24px !important;
490
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important; /* 加強陰影 */
491
+ opacity: 1 !important;
492
+ }
493
+
494
+ /* 2. Agent 小卡片:確保它是獨立的實體 */
495
+ .agent-card-inner {
496
+ background: #f8fafc !important; /* 淺灰底色,區分層次 */
497
+ border: 1px solid #cbd5e1 !important; /* 明顯的邊框 */
498
+ border-radius: 12px !important;
499
+ opacity: 1 !important; /* 拒絕透明 */
500
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05) !important;
501
+ transition: all 0.2s !important;
502
+ }
503
+
504
+ /* 3. 正在工作的卡片:亮起來 */
505
+ .agent-card-wrap.working .agent-card-inner {
506
+ background: white !important;
507
+ border-color: #6366f1 !important; /* 亮紫色邊框 */
508
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2) !important; /* 發光效果 */
509
+ }
510
+
511
+ /* 4. ⭐ 關鍵:文字顏色強制矯正 ⭐ */
512
+ /* 不管外面主題怎麼變,這裡的字一定要是深色的 */
513
+ .agent-name {
514
+ color: #0f172a !important; /* 深黑藍色 */
515
+ font-weight: 700 !important;
516
+ font-size: 0.85rem !important;
517
+ opacity: 1 !important;
518
+ }
519
+
520
+ .agent-role {
521
+ color: #475569 !important; /* 深灰色 */
522
+ font-weight: 600 !important;
523
+ opacity: 1 !important;
524
+ }
525
+
526
+ .status-badge {
527
+ color: #334155 !important;
528
+ background: #e2e8f0 !important;
529
+ border: 1px solid #cbd5e1 !important;
530
+ font-weight: 600 !important;
531
+ opacity: 1 !important;
532
+ }
533
+
534
+ /* 工作的狀態標籤 */
535
+ .agent-card-wrap.working .status-badge {
536
+ background: #e0e7ff !important;
537
+ color: #4338ca !important;
538
+ border-color: #818cf8 !important;
539
+ }
540
+
541
+ /* 連接線也要加深,不然看不到 */
542
+ .connector-line, .connector-horizontal {
543
+ background: #94a3b8 !important; /* 加深灰色 */
544
+ opacity: 0.6 !important;
545
+ }
546
  </style>
547
  """