LifeFlow-AI / app.py
Marco310's picture
feat: Complete UI/UX redesign and fix critical workflow bugs
6b71d3d
raw
history blame
18.4 kB
"""
LifeFlow AI - Main Application (Fixed Output Mismatch)
โœ… ไฟฎๅพฉ Step 4 ๅ›žๅ‚ณๅ€ผๆ•ธ้‡ไธๅŒน้…ๅฐŽ่‡ด็š„ AttributeError
โœ… ็ขบไฟ outputs ๆธ…ๅ–ฎๅŒ…ๅซๆ‰€ๆœ‰ 7 ๅ€‹ๅ…ƒไปถ
"""
import gradio as gr
import inspect
from datetime import datetime
from ui.theme import get_enhanced_css
from ui.components.header import create_header
from ui.components.progress_stepper import create_progress_stepper, update_stepper
from ui.components.input_form import create_input_form, toggle_location_inputs
from ui.components.modals import create_settings_modal, create_doc_modal
from ui.renderers import (
create_agent_dashboard,
create_summary_card,
create_task_card
)
from core.session import UserSession
from services.planner_service import PlannerService
from core.visualizers import create_animated_map
class LifeFlowAI:
def __init__(self):
self.service = PlannerService()
def _get_dashboard_html(self, active_agent: str = None, status: str = "idle", message: str = "Waiting") -> str:
agents = ['team', 'scout', 'optimizer', 'navigator', 'weatherman', 'presenter']
status_dict = {a: {'status': 'idle', 'message': 'Standby'} for a in agents}
if active_agent in status_dict:
status_dict[active_agent] = {'status': status, 'message': message}
if active_agent != 'team' and status == 'working':
status_dict['team'] = {'status': 'working', 'message': f'Monitoring {active_agent}...'}
return create_agent_dashboard(status_dict)
def _check_api_status(self, session_data):
session = UserSession.from_dict(session_data)
has_gemini = bool(session.custom_settings.get("gemini_api_key"))
has_google = bool(session.custom_settings.get("google_maps_api_key"))
msg = "โœ… System Ready" if (has_gemini and has_google) else "โš ๏ธ Missing API Keys"
return msg
def _get_gradio_chat_history(self, session):
history = []
for msg in session.chat_history:
history.append({"role": msg["role"], "content": msg["message"]})
return history
# ================= Event Wrappers =================
def analyze_wrapper(self, user_input, auto_loc, lat, lon, session_data):
session = UserSession.from_dict(session_data)
iterator = self.service.run_step1_analysis(user_input, auto_loc, lat, lon, session)
for event in iterator:
evt_type = event.get("type")
current_session = event.get("session", session)
if evt_type == "error":
yield (
gr.update(), # step1
gr.update(), # step2
gr.update(), # step3
gr.HTML(f"<div style='color:red'>{event.get('message')}</div>"), # s1_stream
gr.update(), # task_list
gr.update(), # task_summary
gr.update(), # chatbot
current_session.to_dict() # state
)
return
if evt_type == "stream":
yield (
gr.update(visible=True),
gr.update(visible=False),
gr.update(visible=False),
event.get("stream_text", ""),
gr.update(),
gr.update(),
gr.update(),
current_session.to_dict()
)
elif evt_type == "complete":
tasks_html = self.service.generate_task_list_html(current_session)
date_str = datetime.now().strftime("%Y-%m-%d")
loc_str = "Selected Location"
if current_session.lat and current_session.lng:
loc_str = f"{current_session.lat:.2f}, {current_session.lng:.2f}"
summary_html = create_summary_card(len(current_session.task_list), event.get("high_priority", 0), event.get("total_time", 0), location=loc_str, date=date_str)
chat_history = self._get_gradio_chat_history(current_session)
if not chat_history:
chat_history = [
{"role": "assistant", "content": event.get('stream_text', "")},
{"role": "assistant", "content": "Hi! I'm LifeFlow. Tell me if you want to change priorities, add stops, or adjust times."}]
current_session.chat_history.append({"role": "assistant", "message": "Hi! I'm LifeFlow...", "time": ""})
yield (
gr.update(visible=False), # Hide S1
gr.update(visible=True), # Show S2
gr.update(visible=False), # Hide S3
"", # Clear Stream
gr.HTML(tasks_html),
gr.HTML(summary_html),
chat_history,
current_session.to_dict()
)
def chat_wrapper(self, msg, session_data):
session = UserSession.from_dict(session_data)
iterator = self.service.modify_task_chat(msg, session)
for event in iterator:
sess = event.get("session", session)
tasks_html = self.service.generate_task_list_html(sess)
summary_html = event.get("summary_html", gr.HTML())
gradio_history = self._get_gradio_chat_history(sess)
yield (gradio_history, tasks_html, summary_html, sess.to_dict())
def step3_wrapper(self, session_data):
session = UserSession.from_dict(session_data)
# Init variables
log_content = ""
report_content = ""
tasks_html = self.service.generate_task_list_html(session)
init_dashboard = self._get_dashboard_html('team', 'working', 'Initializing...')
# HTML Content (No Indentation)
loading_html = inspect.cleandoc("""
<div style="text-align: center; padding: 60px 20px; color: #64748b;">
<div style="font-size: 48px; margin-bottom: 20px; animation: pulse 1.5s infinite;">๐Ÿง </div>
<div style="font-size: 1.2rem; font-weight: 600; color: #334155;">AI Team is analyzing your request...</div>
<div style="font-size: 0.9rem; margin-top: 8px;">Checking routes, weather, and optimizing schedule.</div>
</div>
<style>@keyframes pulse { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.7; } 100% { transform: scale(1); opacity: 1; } }</style>
""")
init_log = '<div style="padding: 10px; color: #94a3b8; font-style: italic;">Waiting for agents...</div>'
yield (init_dashboard, init_log, loading_html, tasks_html, session.to_dict())
iterator = self.service.run_step3_team(session)
for event in iterator:
sess = event.get("session", session)
evt_type = event.get("type")
if evt_type == "report_stream":
report_content = event.get("content", "")
if evt_type == "reasoning_update":
agent, status, msg = event.get("agent_status")
time_str = datetime.now().strftime('%H:%M:%S')
log_entry = f"""
<div style="margin-bottom: 8px; border-left: 3px solid #6366f1; padding-left: 10px;">
<div style="font-size: 0.75rem; color: #94a3b8;">{time_str} โ€ข {agent.upper()}</div>
<div style="color: #334155; font-size: 0.9rem;">{msg}</div>
</div>
"""
log_content = log_entry + log_content
dashboard_html = self._get_dashboard_html(agent, status, msg)
log_html = f'<div style="height: 500px; overflow-y: auto; padding: 10px; background: #fff;">{log_content}</div>'
current_report = report_content + "\n\n" if report_content else loading_html
yield (dashboard_html, log_html, current_report, tasks_html, sess.to_dict())
if evt_type == "complete":
final_report = event.get("report_html", report_content)
final_log = f"""
<div style="margin-bottom: 8px; border-left: 3px solid #10b981; padding-left: 10px; background: #ecfdf5; padding: 10px;">
<div style="font-weight: bold; color: #059669;">โœ… Planning Completed</div>
</div>
{log_content}
"""
final_log_html = f'<div style="height: 500px; overflow-y: auto; padding: 10px; background: #fff;">{final_log}</div>'
yield (
self._get_dashboard_html('team', 'complete', 'Planning Finished'),
final_log_html,
final_report,
tasks_html,
sess.to_dict()
)
def step4_wrapper(self, session_data):
session = UserSession.from_dict(session_data)
result = self.service.run_step4_finalize(session)
# ๐Ÿ”ฅ Wrapper ๅ›žๅ‚ณ 7 ๅ€‹ๅ€ผ
if result['type'] == 'success':
return (
gr.update(visible=False), # 1. Step 3 Hide
gr.update(visible=True), # 2. Step 4 Show
result['summary_tab_html'], # 3. Summary Tab HTML
result['report_md'], # 4. Report Tab Markdown
result['task_list_html'], # 5. Task List HTML
result['map_fig'], # 6. Map Plot
session.to_dict() # 7. Session State
)
else:
return (
gr.update(visible=True), gr.update(visible=False),
"", "", "", None,
session.to_dict()
)
# ================= Main UI Builder =================
def build_interface(self):
with gr.Blocks(title="LifeFlow AI", css=".gradio-container {max-width: 100% !important; padding: 0;}") as demo:
gr.HTML(get_enhanced_css())
session_state = gr.State(UserSession().to_dict())
with gr.Column(elem_classes="step-container"):
home_btn, theme_btn, settings_btn, doc_btn = create_header()
stepper = create_progress_stepper(1)
status_bar = gr.Markdown("Ready", visible=False)
# STEP 1
with gr.Group(visible=True, elem_classes="step-container centered-input-container") as step1_container:
(input_area, s1_stream_output, user_input, auto_loc, loc_group, lat_in, lon_in, analyze_btn) = create_input_form("")
# STEP 2
with gr.Group(visible=False, elem_classes="step-container") as step2_container:
gr.Markdown("### โœ… Review & Refine Tasks")
with gr.Row(elem_classes="step2-split-view"):
with gr.Column(scale=1):
with gr.Group(elem_classes="panel-container"):
gr.HTML('<div class="panel-header">๐Ÿ“‹ Your Tasks</div>')
with gr.Group(elem_classes="scrollable-content"):
task_summary_box = gr.HTML()
task_list_box = gr.HTML()
with gr.Column(scale=1):
with gr.Group(elem_classes="panel-container chat-panel-native"):
chatbot = gr.Chatbot(label="AI Assistant", type="messages", height=540, elem_classes="native-chatbot", bubble_full_width=False)
with gr.Row(elem_classes="chat-input-row"):
chat_input = gr.Textbox(show_label=False, placeholder="Type to modify tasks...", container=False, scale=5, autofocus=True)
chat_send = gr.Button("โžค", variant="primary", scale=1, min_width=50)
with gr.Row(elem_classes="action-footer"):
back_btn = gr.Button("โ† Back", variant="secondary", scale=1)
plan_btn = gr.Button("๐Ÿš€ Start Planning", variant="primary", scale=2)
# STEP 3
with gr.Group(visible=False, elem_classes="step-container") as step3_container:
gr.Markdown("### ๐Ÿค– AI Team Operations")
with gr.Group(elem_classes="agent-dashboard-container"):
agent_dashboard = gr.HTML(value=self._get_dashboard_html())
with gr.Row():
with gr.Column(scale=2):
with gr.Tabs():
with gr.Tab("๐Ÿ“ Full Report"):
with gr.Group(elem_classes="live-report-wrapper"):
live_report_md = gr.Markdown()
with gr.Tab("๐Ÿ“‹ Task List"):
with gr.Group(elem_classes="panel-container"):
with gr.Group(elem_classes="scrollable-content"):
task_list_s3 = gr.HTML()
with gr.Column(scale=1):
gr.Markdown("### โšก Activity Log")
with gr.Group(elem_classes="panel-container"):
planning_log = gr.HTML(value="Waiting...")
# STEP 4
with gr.Group(visible=False, elem_classes="step-container") as step4_container:
with gr.Row():
# Left: Tabs (Summary / Report / Tasks)
with gr.Column(scale=1, elem_classes="split-left-panel"):
with gr.Tabs():
with gr.Tab("๐Ÿ“Š Summary"):
summary_tab_output = gr.HTML()
with gr.Tab("๐Ÿ“ Full Report"):
report_tab_output = gr.Markdown()
with gr.Tab("๐Ÿ“‹ Task List"):
task_list_tab_output = gr.HTML()
# Right: Hero Map
with gr.Column(scale=2, elem_classes="split-right-panel"):
map_view = gr.Plot(label="Route Map", show_label=False)
# Modals & Events
(settings_modal, g_key, w_key, gem_key, model_sel, close_set, save_set, set_stat) = create_settings_modal()
doc_modal, close_doc_btn = create_doc_modal()
settings_btn.click(fn=lambda: gr.update(visible=True), outputs=[settings_modal])
close_set.click(fn=lambda: gr.update(visible=False), outputs=[settings_modal])
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])
theme_btn.click(fn=None, js="() => { document.querySelector('.gradio-container').classList.toggle('theme-dark'); }")
analyze_event = analyze_btn.click(
fn=self.analyze_wrapper,
inputs=[user_input, auto_loc, lat_in, lon_in, session_state],
outputs=[step1_container, step2_container, step3_container, s1_stream_output, task_list_box, task_summary_box, chatbot, session_state]
).then(fn=lambda: update_stepper(2), outputs=[stepper])
chat_send.click(fn=self.chat_wrapper, inputs=[chat_input, session_state], outputs=[chatbot, task_list_box, task_summary_box, session_state]).then(fn=lambda: "", outputs=[chat_input])
chat_input.submit(fn=self.chat_wrapper, inputs=[chat_input, session_state], outputs=[chatbot, task_list_box, task_summary_box, session_state]).then(fn=lambda: "", outputs=[chat_input])
step3_start = plan_btn.click(
fn=lambda: (gr.update(visible=False), gr.update(visible=True), update_stepper(3)),
outputs=[step2_container, step3_container, stepper]
).then(
fn=self.step3_wrapper,
inputs=[session_state],
outputs=[agent_dashboard, planning_log, live_report_md, task_list_s3, session_state],
show_progress="hidden"
)
step3_start.then(
fn=self.step4_wrapper,
inputs=[session_state],
# ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ ้—œ้ตไฟฎๆญฃ๏ผš้€™่ฃกๅˆ—ๅ‡บไบ† 7 ๅ€‹ Outputs๏ผŒๅฟ…้ ˆๅฐๆ‡‰ Wrapper ๅ›žๅ‚ณ็š„ 7 ๅ€‹ๅ€ผ
outputs=[
step3_container, # 1. Hide Step 3
step4_container, # 2. Show Step 4
summary_tab_output, # 3. Summary Tab
report_tab_output, # 4. Report Tab
task_list_tab_output, # 5. Task List Tab
map_view, # 6. Map
session_state # 7. State
]
).then(fn=lambda: update_stepper(4), outputs=[stepper])
def reset_all(session_data):
old_session = UserSession.from_dict(session_data)
new_session = UserSession()
new_session.custom_settings = old_session.custom_settings
return (gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), update_stepper(1), new_session.to_dict(), "")
home_btn.click(fn=reset_all, inputs=[session_state], outputs=[step1_container, step2_container, step3_container, step4_container, stepper, session_state, user_input])
back_btn.click(fn=reset_all, inputs=[session_state], outputs=[step1_container, step2_container, step3_container, step4_container, stepper, session_state, user_input])
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])
auto_loc.change(fn=toggle_location_inputs, inputs=auto_loc, outputs=loc_group)
demo.load(fn=self._check_api_status, inputs=[session_state], outputs=[status_bar])
return demo
def save_settings(self, g, w, gem, m, s_data):
sess = UserSession.from_dict(s_data)
sess.custom_settings.update({'google_maps_api_key': g, 'openweather_api_key': w, 'gemini_api_key': gem, 'model': m})
return "โœ… Saved", sess.to_dict(), "Settings Updated"
def main():
app = LifeFlowAI()
demo = app.build_interface()
demo.launch(server_name="0.0.0.0", server_port=8080, share=True, show_error=True)
if __name__ == "__main__":
main()