File size: 10,207 Bytes
8a2dcce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Gradio Blocks layout for the DSA Tutor Chatbot.

Custom layout only (per spec) — no gr.ChatInterface. Structure:

    Top bar: title, auth status, Login/Logout buttons
    Sign-up dialog (modal-ish accordion, opened via a "Sign up" link)
    Main row:
        [ Toggleable history panel ]  [ Chat window + input + send/new chat ]

Streaming: the assistant's reply is revealed progressively via
ui.callbacks.stream_last_response (see that module's docstring for why
it's simulated rather than a true token stream).
"""

import gradio as gr

from ui import callbacks
from ui.theme import CUSTOM_CSS, dsa_theme


def build_ui(flask_app) -> gr.Blocks:
    callbacks.init_app(flask_app)

    with gr.Blocks(theme=dsa_theme, css=CUSTOM_CSS, title="DSA Tutor Chatbot") as demo:
        auth_state = gr.State(None)          # {"user_id":.., "email":..} | None
        session_state = gr.State(None)       # active ChatSession.id | None
        anon_history = gr.State([])          # rolling history for anonymous users
        history_visible_state = gr.State(False)  # tracks History panel visibility

        # ---------------------------------------------------------------
        # Top bar
        # ---------------------------------------------------------------
        with gr.Row(elem_id="topbar"):
            gr.Markdown("### 🧮 DSA Tutor Chatbot", elem_id="topbar-title")
            with gr.Row(elem_id="topbar-actions"):
                auth_status_md = gr.Markdown("Not logged in", elem_id="auth-status")
                history_toggle_btn = gr.Button("📜 History", size="sm", scale=0, elem_classes=["small-btn"])
                login_btn = gr.Button("Log in", size="sm", scale=0, elem_classes=["small-btn"])
                signup_btn = gr.Button("Sign up", size="sm", scale=0, elem_classes=["small-btn"])
                logout_btn = gr.Button("Log out", size="sm", scale=0, visible=False, elem_classes=["small-btn"])

        # ---------------------------------------------------------------
        # Login popover
        # ---------------------------------------------------------------
        with gr.Group(visible=False) as login_panel:
            gr.Markdown("#### Log in")
            login_email = gr.Textbox(label="Email")
            login_password = gr.Textbox(label="Password", type="password")
            with gr.Row():
                login_submit = gr.Button("Log in", variant="primary", size="sm", elem_classes=["small-btn"])
                login_cancel = gr.Button("Cancel", size="sm", elem_classes=["small-btn"])
            login_status = gr.Markdown("")

        # ---------------------------------------------------------------
        # Sign-up dialog
        # ---------------------------------------------------------------
        with gr.Group(visible=False) as signup_panel:
            gr.Markdown("#### Create an account")
            signup_email = gr.Textbox(label="Email")
            signup_password = gr.Textbox(label="Password (min 8 characters)", type="password")
            with gr.Row():
                signup_submit = gr.Button("Sign up", variant="primary", size="sm", elem_classes=["small-btn"])
                signup_cancel = gr.Button("Cancel", size="sm", elem_classes=["small-btn"])
            signup_status = gr.Markdown("")

        # ---------------------------------------------------------------
        # Main area: history panel (toggleable) + chat
        # ---------------------------------------------------------------
        with gr.Row(elem_id="main-row"):
            with gr.Column(scale=1, visible=False, elem_id="history-panel") as history_panel:
                gr.Markdown("**Previous sessions**")
                session_list = gr.Radio(choices=[], label="", elem_classes=["session-item"])
                with gr.Row():
                    refresh_sessions_btn = gr.Button("Refresh", size="sm", elem_classes=["small-btn"])
                    delete_session_btn = gr.Button("Delete selected", size="sm", elem_classes=["small-btn"])

            with gr.Column(scale=4, elem_id="chat-column"):
                chatbot = gr.Chatbot(
                    label="DSA Tutor",
                    type="messages",
                    height=360,
                    elem_id="chat-window",
                    avatar_images=(None, None),
                )
                with gr.Row(elem_id="input-row"):
                    msg_box = gr.Textbox(
                        placeholder="Ask about any DSA topic (e.g. 'Explain merge sort')...",
                        show_label=False,
                        scale=8,
                    )
                    send_btn = gr.Button("Send", variant="primary", scale=1, size="sm")
                new_chat_btn = gr.Button("🆕 New chat", size="sm", elem_classes=["small-btn"])

        # ---------------------------------------------------------------
        # Wiring: top bar toggles
        # ---------------------------------------------------------------
        def _toggle_history(is_visible):
            new_visible = not is_visible
            return new_visible, gr.update(visible=new_visible)

        history_toggle_btn.click(
            _toggle_history,
            inputs=[history_visible_state],
            outputs=[history_visible_state, history_panel],
        )

        login_btn.click(lambda: gr.update(visible=True), outputs=login_panel)
        login_cancel.click(lambda: gr.update(visible=False), outputs=login_panel)

        signup_btn.click(lambda: gr.update(visible=True), outputs=signup_panel)
        signup_cancel.click(lambda: gr.update(visible=False), outputs=signup_panel)

        def _auth_ui_update(auth_state_value):
            if auth_state_value:
                status = f"Logged in as **{auth_state_value['email']}**"
                return (
                    status,
                    gr.update(visible=False),  # login_btn
                    gr.update(visible=False),  # signup_btn
                    gr.update(visible=True),   # logout_btn
                )
            return (
                "Not logged in",
                gr.update(visible=True),
                gr.update(visible=True),
                gr.update(visible=False),
            )

        def _on_login(email, password):
            auth_state_value, message, is_error = callbacks.do_login(email, password)
            status_md, login_show, signup_show, logout_show = _auth_ui_update(auth_state_value)
            panel_visible = gr.update(visible=is_error)
            return auth_state_value, status_md, login_show, signup_show, logout_show, panel_visible, message

        login_submit.click(
            _on_login,
            inputs=[login_email, login_password],
            outputs=[auth_state, auth_status_md, login_btn, signup_btn, logout_btn, login_panel, login_status],
        )

        def _on_signup(email, password):
            auth_state_value, message, is_error = callbacks.do_signup(email, password)
            status_md, login_show, signup_show, logout_show = _auth_ui_update(auth_state_value)
            panel_visible = gr.update(visible=is_error)
            return auth_state_value, status_md, login_show, signup_show, logout_show, panel_visible, message

        signup_submit.click(
            _on_signup,
            inputs=[signup_email, signup_password],
            outputs=[auth_state, auth_status_md, login_btn, signup_btn, logout_btn, signup_panel, signup_status],
        )

        def _on_logout():
            auth_state_value, message, _ = callbacks.do_logout()
            status_md, login_show, signup_show, logout_show = _auth_ui_update(auth_state_value)
            return auth_state_value, status_md, login_show, signup_show, logout_show, [], None, []

        logout_btn.click(
            _on_logout,
            outputs=[
                auth_state, auth_status_md, login_btn, signup_btn, logout_btn,
                chatbot, session_state, anon_history,
            ],
        )

        # ---------------------------------------------------------------
        # Wiring: chat
        # ---------------------------------------------------------------
        def _send_and_stream(user_message, chat_display, auth_state_value, session_state_value, anon_hist):
            chat_display, session_state_value, anon_hist, cleared = callbacks.send_message(
                user_message, chat_display, auth_state_value, session_state_value, anon_hist
            )
            for partial in callbacks.stream_last_response(chat_display):
                yield partial, session_state_value, anon_hist, cleared

        send_btn.click(
            _send_and_stream,
            inputs=[msg_box, chatbot, auth_state, session_state, anon_history],
            outputs=[chatbot, session_state, anon_history, msg_box],
        )
        msg_box.submit(
            _send_and_stream,
            inputs=[msg_box, chatbot, auth_state, session_state, anon_history],
            outputs=[chatbot, session_state, anon_history, msg_box],
        )

        new_chat_btn.click(
            callbacks.new_chat,
            outputs=[chatbot, session_state, anon_history],
        )

        # ---------------------------------------------------------------
        # Wiring: history panel
        # ---------------------------------------------------------------
        refresh_sessions_btn.click(
            callbacks.refresh_sessions,
            inputs=[auth_state],
            outputs=[session_list],
        )

        session_list.change(
            callbacks.open_session,
            inputs=[session_list, auth_state],
            outputs=[chatbot, session_state],
        )

        delete_session_btn.click(
            callbacks.delete_session,
            inputs=[session_list, auth_state],
            outputs=[chatbot, session_state],
        ).then(
            callbacks.refresh_sessions,
            inputs=[auth_state],
            outputs=[session_list],
        )

        # Refresh the session list whenever a login/signup succeeds.
        auth_state.change(
            callbacks.refresh_sessions,
            inputs=[auth_state],
            outputs=[session_list],
        )

    return demo