Spaces:
Running
Running
feat: Optimize UI/UX
Browse files- app.py +25 -8
- services/planner_service.py +16 -10
- ui/components/header.py +12 -12
- ui/components/input_form.py +89 -30
- ui/components/modals.py +61 -27
- ui/theme.py +197 -4
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 |
-
|
| 40 |
has_google = bool(session.custom_settings.get("google_maps_api_key"))
|
| 41 |
-
msg = "✅ System Ready" if (
|
| 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,
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 345 |
sess = UserSession.from_dict(s_data)
|
| 346 |
-
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
|
| 349 |
def main():
|
| 350 |
app = LifeFlowAI()
|
| 351 |
demo = app.build_interface()
|
| 352 |
-
demo.launch(server_name="0.0.0.0", server_port=
|
| 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
|
| 179 |
-
|
| 180 |
-
if not weather_key:
|
|
|
|
| 181 |
|
| 182 |
-
if not
|
| 183 |
raise ValueError("🔑 Missing Gemini API Key. Please configure Settings.")
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 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 |
-
-
|
| 12 |
-
-
|
| 13 |
-
- 按鈕水平排列
|
| 14 |
"""
|
|
|
|
| 15 |
with gr.Row(elem_classes="app-header-container"):
|
| 16 |
-
# 左側:Logo 和標題
|
| 17 |
-
with gr.Column(scale=
|
| 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 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 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
|
| 28 |
lines=3,
|
| 29 |
elem_id="main-input"
|
| 30 |
)
|
| 31 |
|
| 32 |
-
# 2. 位置設定
|
| 33 |
-
with gr.Accordion("📍 Location Settings", open=
|
| 34 |
-
auto_location = gr.Checkbox(label="Auto-detect my location
|
| 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 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
inputs=user_input,
|
| 47 |
-
label="Quick Start Examples"
|
| 48 |
-
)
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
#
|
| 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 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 58 |
-
|
| 59 |
align-items: center !important;
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
"""
|