File size: 18,398 Bytes
fbcd46b
6b71d3d
 
 
fbcd46b
 
 
6b71d3d
 
4abc17c
6b71d3d
 
 
 
aba9311
6b71d3d
50a7f50
6b71d3d
aba9311
6b71d3d
 
aba9311
 
4abc17c
fbcd46b
aba9311
fbcd46b
6b71d3d
 
 
 
 
 
 
 
 
 
 
747f356
 
6b71d3d
 
747f356
6b71d3d
 
 
 
 
747f356
6b71d3d
50a7f50
aba9311
 
 
 
 
 
6b71d3d
aba9311
6b71d3d
 
 
 
 
 
 
 
aba9311
6b71d3d
50a7f50
6b71d3d
aba9311
6b71d3d
 
 
 
 
 
 
 
aba9311
50a7f50
6b71d3d
 
 
 
 
 
 
 
 
 
 
 
 
 
50a7f50
aba9311
6b71d3d
 
 
 
 
 
 
 
aba9311
 
 
 
 
 
6b71d3d
 
 
 
 
fbcd46b
6b71d3d
aba9311
a7c1db4
6b71d3d
 
 
a7c1db4
6b71d3d
 
a7c1db4
6b71d3d
 
 
 
 
 
 
 
 
 
 
50a7f50
6b71d3d
aba9311
a7c1db4
50a7f50
aba9311
6b71d3d
50a7f50
 
 
6b71d3d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aba9311
 
 
 
3b3daa9
6b71d3d
 
3b3daa9
6b71d3d
 
 
 
 
 
 
3b3daa9
aba9311
747f356
a7c1db4
6b71d3d
 
747f356
 
6b71d3d
747f356
6b71d3d
 
4abc17c
6b71d3d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4abc17c
 
6b71d3d
 
 
 
 
3b3daa9
a7c1db4
aba9311
6b71d3d
 
 
fbcd46b
6b71d3d
 
c98862f
6b71d3d
 
 
 
aba9311
3b3daa9
6b71d3d
 
a7c1db4
 
6b71d3d
aba9311
3b3daa9
6b71d3d
4abc17c
6b71d3d
 
 
 
 
 
 
4abc17c
6b71d3d
a7c1db4
6b71d3d
 
 
 
 
 
 
 
 
 
 
 
a7c1db4
fbcd46b
 
6b71d3d
 
 
 
fbcd46b
 
4abc17c
fbcd46b
6b71d3d
fbcd46b
 
a328f28
1
2
3
4
5
6
7
8
9
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
"""
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()