""" LifeFlow AI - Main Application (Refactored) Controller Layer: 負責 UI 佈局與 Service 連接 """ import gradio as gr from typing import List # ===== Core & Data Models ===== from core.session import UserSession from services.planner_service import PlannerService # ===== UI & Config ===== from config import APP_TITLE, DEFAULT_SETTINGS from ui.theme import get_enhanced_css from ui.renderers import ( create_agent_stream_output, create_agent_card_enhanced, get_reasoning_html_reversed, generate_chat_history_html_bubble ) from core.visualizers import create_animated_map # ===== UI Components ===== from ui.components.header import create_header, create_top_controls from ui.components.input_form import create_input_form, toggle_location_inputs from ui.components.confirmation import create_confirmation_area from ui.components.results import create_team_area, create_result_area, create_tabs from ui.components.modals import create_settings_modal, create_doc_modal class LifeFlowAI: def __init__(self): self.service = PlannerService() def _get_agent_outputs(self, active_agent: str = None, status: str = "idle", message: str = "Waiting") -> List[str]: """ 輔助函數:生成 6 個 Agent 卡片的 HTML 列表 用於在 Gradio 中更新 agent_displays """ agents = ['planner', 'scout', 'optimizer', 'validator', 'weather', 'traffic'] outputs = [] for agent in agents: if agent == active_agent: outputs.append(create_agent_card_enhanced(agent, status, message)) else: # 簡單處理:非活動 Agent 顯示 Idle 或保持原狀 (這裡簡化為 Idle,可根據需求優化) outputs.append(create_agent_card_enhanced(agent, "idle", "On standby")) return outputs def analyze_wrapper(self, user_input, auto_loc, lat, lon, session_data): """Step 1: 呼叫 Service 並將結果轉換為 Gradio Updates""" session = UserSession.from_dict(session_data) # 呼叫 Service Generator iterator = self.service.run_step1_analysis(user_input, auto_loc, lat, lon, session) for event in iterator: evt_type = event.get("type") # 準備 Agent 狀態 agent_status = event.get("agent_status", ("planner", "idle", "Waiting")) agent_outputs = self._get_agent_outputs(*agent_status) # 獲取推理過程 HTML # 注意:event 中可能已更新 session,從 session 獲取最新 reasoning current_session = event.get("session", session) reasoning_html = get_reasoning_html_reversed(current_session.reasoning_messages) if evt_type == "stream": yield ( create_agent_stream_output().replace("Ready to analyze...", event.get("stream_text", "")), gr.HTML(), # Task Summary (Empty) gr.HTML(), # Task List (Empty) reasoning_html, gr.update(visible=False), # Confirm Area gr.update(visible=False), # Chat Input gr.HTML(), # Chat History f"Processing: {agent_status[2]}", # Status Bar *agent_outputs, current_session.to_dict() ) elif evt_type == "complete": # 完成時生成 Task List HTML task_html = self.service.generate_task_list_html(current_session) # 生成 Summary HTML (這裡簡化調用,實際可從 service 獲取) summary_html = f"
Found {len(current_session.task_list)} tasks
" # 更新所有 Agent 為完成 final_agents = self._get_agent_outputs("planner", "complete", "Tasks ready") yield ( create_agent_stream_output().replace("Ready...", event.get("stream_text", "")), gr.HTML(value=summary_html), gr.HTML(value=task_html), reasoning_html, gr.update(visible=True), # Confirm Area Show gr.update(visible=False), generate_chat_history_html_bubble(current_session), "✓ Tasks extracted", *final_agents, current_session.to_dict() ) elif evt_type == "error": err_msg = event.get("message", "Unknown error") error_agents = self._get_agent_outputs("planner", "idle", "Error") yield ( f"
Error: {err_msg}
", gr.HTML(), gr.HTML(), reasoning_html, gr.update(visible=False), gr.update(visible=False), gr.HTML(), f"Error: {err_msg}", *error_agents, current_session.to_dict() ) def chat_wrapper(self, msg, session_data): """Chat Logic Wrapper""" session = UserSession.from_dict(session_data) iterator = self.service.modify_task_chat(msg, session) for event in iterator: current_session = event.get("session", session) chat_html = generate_chat_history_html_bubble(current_session) task_html = self.service.generate_task_list_html(current_session) yield ( chat_html, task_html, current_session.to_dict() ) def step2_wrapper(self, session_data): """Step 2 Wrapper""" session = UserSession.from_dict(session_data) result = self.service.run_step2_search(session) current_session = result.get("session", session) reasoning_html = get_reasoning_html_reversed(current_session.reasoning_messages) agent_outputs = self._get_agent_outputs("scout", "working", "Searching POIs...") return ( reasoning_html, "🗺️ Scout is searching...", *agent_outputs, current_session.to_dict() ) def step3_wrapper(self, session_data): """Step 3 Wrapper""" session = UserSession.from_dict(session_data) iterator = self.service.run_step3_team(session) for event in iterator: current_session = event.get("session", session) if event["type"] == "complete": yield (event.get("report_html", ""), current_session.to_dict()) elif event["type"] == "error": yield (f"Error: {event.get('message')}", current_session.to_dict()) # 可以根據需要處理中間狀態更新 def step4_wrapper(self, session_data): """Step 4 Wrapper""" session = UserSession.from_dict(session_data) result = self.service.run_step4_finalize(session) current_session = result.get("session", session) if result["type"] == "success": agent_outputs = self._get_agent_outputs("team", "complete", "Done") return ( result["timeline_html"], result["metrics_html"], result["result_html"], result["map_fig"], gr.update(visible=True), # Show Map Tab gr.update(visible=False), # Hide Team Area "🎉 Planning completed!", *agent_outputs, current_session.to_dict() ) else: # Error handling default_map = create_animated_map() agent_outputs = self._get_agent_outputs("team", "idle", "Error") err = result.get("message", "Error") return ( f"Error: {err}", "", "", default_map, gr.update(), gr.update(), f"Error: {err}", *agent_outputs, current_session.to_dict() ) def save_settings(self, google_key, weather_key, gemini_key, model, session_data): """Settings Save Wrapper""" session = UserSession.from_dict(session_data) session.custom_settings['google_maps_api_key'] = google_key session.custom_settings['openweather_api_key'] = weather_key session.custom_settings['gemini_api_key'] = gemini_key session.custom_settings['model'] = model return "✅ Settings saved locally!", session.to_dict() def build_interface(self): with gr.Blocks(title=APP_TITLE) as demo: gr.HTML(get_enhanced_css()) create_header() theme_btn, settings_btn, doc_btn = create_top_controls() # State session_state = gr.State(value=UserSession().to_dict()) with gr.Row(): # Left Column with gr.Column(scale=2, min_width=400): (input_area, agent_stream_output, user_input, auto_location, location_inputs, lat_input, lon_input, analyze_btn) = create_input_form( create_agent_stream_output() ) (task_confirm_area, task_summary_display, task_list_display, exit_btn_inline, ready_plan_btn) = create_confirmation_area() # Team Area team_area, agent_displays = create_team_area(create_agent_card_enhanced) # Result Area (result_area, result_display, timeline_display, metrics_display) = create_result_area( create_animated_map) # Right Column with gr.Column(scale=3, min_width=500): status_bar = gr.Textbox(label="📊 Status", value="Waiting for input...", interactive=False, max_lines=1) (tabs, report_tab, map_tab, report_output, map_output, reasoning_output, chat_input_area, chat_history_output, chat_input, chat_send) = create_tabs( create_animated_map, get_reasoning_html_reversed() ) # Modals (settings_modal, google_maps_key, openweather_key, gemini_api_key, model_choice, close_settings_btn, save_settings_btn, settings_status) = create_settings_modal() doc_modal, close_doc_btn = create_doc_modal() # ===== Event Binding ===== auto_location.change(fn=toggle_location_inputs, inputs=[auto_location], outputs=[location_inputs]) # Step 1: Analyze analyze_btn.click( fn=self.analyze_wrapper, inputs=[user_input, auto_location, lat_input, lon_input, session_state], outputs=[ agent_stream_output, task_summary_display, task_list_display, reasoning_output, task_confirm_area, chat_input_area, chat_history_output, status_bar, *agent_displays, session_state ] ).then( fn=lambda: (gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)), outputs=[input_area, task_confirm_area, chat_input_area] ) # Chat chat_send.click( fn=self.chat_wrapper, inputs=[chat_input, session_state], outputs=[chat_history_output, task_list_display, session_state] ).then(fn=lambda: "", outputs=[chat_input]) # Exit exit_btn_inline.click( fn=lambda: ( gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), "", create_agent_stream_output(), "Ready...", UserSession().to_dict() ), outputs=[ input_area, task_confirm_area, chat_input_area, result_area, team_area, report_tab, map_tab, user_input, agent_stream_output, status_bar, session_state ] ) # Step 2, 3, 4 Sequence ready_plan_btn.click( fn=lambda: (gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(selected="ai_conversation_tab")), outputs=[task_confirm_area, chat_input_area, team_area, tabs] ).then( fn=self.step2_wrapper, inputs=[session_state], outputs=[reasoning_output, status_bar, *agent_displays, session_state] ).then( fn=self.step3_wrapper, inputs=[session_state], outputs=[report_output, session_state] ).then( fn=lambda: (gr.update(visible=True), gr.update(visible=True), gr.update(selected="report_tab")), outputs=[report_tab, map_tab, tabs] ).then( fn=self.step4_wrapper, inputs=[session_state], outputs=[ timeline_display, metrics_display, result_display, map_output, map_tab, team_area, status_bar, *agent_displays, session_state ] ).then(fn=lambda: gr.update(visible=True), outputs=[result_area]) # Settings & Docs Handlers settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal]) close_settings_btn.click(fn=lambda: gr.update(visible=False), outputs=[settings_modal]) save_settings_btn.click( fn=self.save_settings, inputs=[google_maps_key, openweather_key, gemini_api_key, model_choice, session_state], outputs=[settings_status, session_state] ) theme_btn.click(fn=None, js=""" () => { const container = document.querySelector('.gradio-container'); if (container) { container.classList.toggle('theme-dark'); const isDark = container.classList.contains('theme-dark'); localStorage.setItem('lifeflow-theme', isDark ? 'dark' : 'light'); } } """) doc_btn.click(fn=lambda: gr.update(visible=True), outputs=[doc_modal]) close_doc_btn.click(fn=lambda: gr.update(visible=False), outputs=[doc_modal]) return demo def main(): app = LifeFlowAI() demo = app.build_interface() demo.launch(server_name="0.0.0.0", server_port=7860, share=True, show_error=True) if __name__ == "__main__": main()