"""
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()