SamCChauhan commited on
Commit
eb6fac8
·
unverified ·
1 Parent(s): 758d51e

Refactor app to use NiceGUI and async features

Browse files
Files changed (1) hide show
  1. app.py +270 -306
app.py CHANGED
@@ -1,25 +1,26 @@
1
- import gradio as gr
2
  from google import genai
3
  from google.genai import types
4
  from PIL import Image
5
  import datetime
6
  import os
7
- import time
 
8
 
9
  # --- 1. SETUP & MODULAR PROMPT LIBRARY ---
10
  API_KEY = os.environ.get('GOOGLE_API_KEY')
11
 
12
  try:
13
  if not API_KEY:
14
- print("⚠️ Warning: GOOGLE_API_KEY not found. Check your Space Secrets.")
 
15
  client = genai.Client(api_key=API_KEY)
16
  except Exception as e:
17
  print(f"❌ Initialization Error: {e}")
18
 
19
- # Stable flash model ID
20
  MODEL_ID = "gemini-2.5-flash"
21
 
22
- # --- SYSTEM PROMPT FRAGMENTS ---
23
  BASE_PERSONA = """
24
  ROLE: You are 'Code Mentor,' a Coding Trainer Chatbot intended for use in a high-school programming classroom.
25
  VISION: You are a MULTIMODAL AI. You have vision capabilities. You can seamlessly see, read, and analyze uploaded images, screenshots of code or errors, flowcharts, and architecture diagrams.
@@ -92,220 +93,11 @@ def build_system_prompt(mode, language, course):
92
  prompt_parts.append(TRANSPARENCY_AND_ASSESSMENT)
93
  return "\n\n".join(prompt_parts)
94
 
95
- # --- 2. LOGIC FUNCTIONS ---
96
- def chat_logic(message, history, mode, language, course):
97
- if not language or not course:
98
- yield "⚠️ **Configuration Required:** Please select a **Course Curriculum** and a **Target Language** from the sidebar before we start coding!"
99
- return
100
-
101
- current_instruction = build_system_prompt(mode, language, course)
102
- gemini_history = []
103
-
104
- # 1. BUILD HISTORY (Properly passing Images & Text to Memory)
105
- for msg in history:
106
- role = "user" if msg["role"] == "user" else "model"
107
- raw_content = msg["content"]
108
- parts_list = []
109
-
110
- if isinstance(raw_content, list):
111
- for item in raw_content:
112
- if isinstance(item, dict):
113
- if item.get("type") == "text" and "text" in item:
114
- parts_list.append(types.Part.from_text(text=item["text"]))
115
- elif item.get("type") == "file" and "file" in item:
116
- path = item["file"].get("path")
117
- if path:
118
- ext = path.split('.')[-1].lower()
119
- if ext in ['png', 'jpg', 'jpeg', 'webp', 'gif']:
120
- try:
121
- img = Image.open(path)
122
- parts_list.append(types.Part.from_image(img))
123
- except Exception as e:
124
- print(f"Error loading history image: {e}")
125
- else:
126
- try: # Handle previously uploaded .py, .txt, etc.
127
- with open(path, "r", encoding="utf-8") as f:
128
- parts_list.append(types.Part.from_text(text=f"\n\n--- Uploaded File: {os.path.basename(path)} ---\n{f.read()}"))
129
- except:
130
- pass
131
- else:
132
- text_content = str(raw_content)
133
- if text_content.strip():
134
- parts_list.append(types.Part.from_text(text=text_content))
135
-
136
- if parts_list:
137
- gemini_history.append(types.Content(role=role, parts=parts_list))
138
-
139
- try:
140
- chat = client.chats.create(
141
- model=MODEL_ID,
142
- config=types.GenerateContentConfig(
143
- system_instruction=current_instruction,
144
- temperature=0.7 if mode == "Socratic" else 0.2
145
- ),
146
- history=gemini_history
147
- )
148
-
149
- # 2. CURRENT MESSAGE PAYLOAD
150
- user_text = message.get("text", "")
151
- user_files = message.get("files", [])
152
-
153
- payload = []
154
- if user_text.strip():
155
- payload.append(user_text)
156
-
157
- for file_item in user_files:
158
- path = file_item.get("path") if isinstance(file_item, dict) else file_item
159
- ext = path.split('.')[-1].lower()
160
-
161
- if ext in ['png', 'jpg', 'jpeg', 'webp', 'gif']:
162
- try:
163
- img = Image.open(path)
164
- payload.append(img)
165
- except Exception as e:
166
- print(f"Error loading image: {e}")
167
- else:
168
- try:
169
- with open(path, "r", encoding="utf-8") as f:
170
- file_text = f.read()
171
- payload.append(f"\n\n--- Uploaded File: {os.path.basename(path)} ---\n{file_text}")
172
- except Exception as ex:
173
- print(f"Could not read file {path}: {ex}")
174
-
175
- if not payload:
176
- yield "⚠️ Please provide some text or an image."
177
- return
178
-
179
- response_stream = chat.send_message_stream(payload)
180
- full_response = ""
181
- for chunk in response_stream:
182
- if chunk.text:
183
- for char in chunk.text:
184
- full_response += char
185
- yield full_response
186
- time.sleep(0.015)
187
- except Exception as e:
188
- yield f"🤖 Technical Hiccup: {str(e)}"
189
-
190
- def save_transcript(history):
191
- if not history: return None
192
- filename = f"DACodeX_Transcript_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.txt"
193
- transcript_text = "DACODEX MENTOR SESSION\n" + "="*30 + "\n\n"
194
-
195
- for msg in history:
196
- prefix = "STUDENT" if msg["role"] == "user" else "MENTOR"
197
- raw_content = msg["content"]
198
-
199
- if isinstance(raw_content, list):
200
- texts = []
201
- for item in raw_content:
202
- if isinstance(item, dict):
203
- if item.get("type") == "text" and "text" in item:
204
- texts.append(item["text"])
205
- elif item.get("type") == "file" and "file" in item:
206
- path = item["file"].get("path")
207
- if path:
208
- ext = path.split('.')[-1].lower()
209
- if ext in ['png', 'jpg', 'jpeg', 'webp', 'gif']:
210
- texts.append(f"[Uploaded Image: {os.path.basename(path)}]")
211
- else:
212
- texts.append(f"[Uploaded File: {os.path.basename(path)}]")
213
- text_content = "\n".join(texts)
214
- else:
215
- text_content = str(raw_content)
216
-
217
- transcript_text += f"{prefix}:\n{text_content}\n\n"
218
-
219
- with open(filename, "w", encoding="utf-8") as f:
220
- f.write(transcript_text)
221
- return filename
222
-
223
- def archive_and_clear(history, current_storage):
224
- if not history: return current_storage, [], gr.update(choices=[item[0] for item in current_storage])
225
- timestamp = datetime.datetime.now().strftime("%H:%M:%S")
226
- label = f"Session {timestamp} ({len(history)} messages)"
227
- new_storage = [(label, history)] + current_storage
228
- new_choices = [item[0] for item in new_storage]
229
- return new_storage, [], gr.update(choices=new_choices, value=None)
230
-
231
- def load_from_history(selected_label, current_storage):
232
- if not selected_label: return gr.update()
233
- for label, history in current_storage:
234
- if label == selected_label: return history
235
- return []
236
-
237
- def toggle_sidebar_func(is_visible):
238
- new_state = not is_visible
239
- button_text = "◀ Hide Sidebar" if new_state else "▶ Show Sidebar"
240
- return new_state, gr.update(visible=new_state), gr.update(value=button_text)
241
-
242
- # --- 3. THEME & ADVANCED CSS ---
243
- dacodex_theme = gr.themes.Base(
244
- primary_hue=gr.themes.colors.red,
245
- neutral_hue=gr.themes.colors.slate,
246
- font=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
247
- ).set(
248
- body_background_fill="#09090b",
249
- block_background_fill="#121217",
250
- block_border_color="#27272a",
251
- body_text_color="#e4e4e7",
252
- button_primary_background_fill="#dc2626",
253
- button_primary_background_fill_hover="#ef4444",
254
- block_label_text_color="#D1D5DB"
255
- )
256
-
257
- custom_css = """
258
- @keyframes flicker {
259
- 0% { opacity: 0.97; }
260
- 5% { opacity: 0.9; }
261
- 10% { opacity: 0.97; }
262
- 100% { opacity: 1; }
263
- }
264
- .landing-container {
265
- height: 90vh;
266
- display: flex;
267
- flex-direction: column;
268
- justify-content: center;
269
- align-items: center;
270
- background: radial-gradient(circle at center, #1e1b4b 0%, #09090b 100%);
271
- animation: flicker 0.15s infinite;
272
- text-align: center;
273
- }
274
- .start-btn {
275
- border: 1px solid #ef4444 !important;
276
- box-shadow: 0 0 15px rgba(220, 38, 38, 0.4);
277
- letter-spacing: 2px;
278
- font-weight: bold !important;
279
- padding: 20px 40px !important;
280
- font-size: 1.2em !important;
281
- transition: all 0.3s ease !important;
282
- margin-top: 20px;
283
- cursor: pointer;
284
- }
285
- .start-btn:hover {
286
- box-shadow: 0 0 30px rgba(239, 68, 68, 0.8);
287
- transform: scale(1.05) !important;
288
- }
289
- #chatbot-window {
290
- border-left: 2px solid #dc2626;
291
- background: rgba(18, 18, 23, 0.8);
292
- }
293
- .info-popup {
294
- background: #1a1a23;
295
- border: 1px solid #dc2626;
296
- padding: 15px;
297
- border-radius: 8px;
298
- margin-bottom: 15px;
299
- box-shadow: 0 0 10px rgba(220, 38, 38, 0.2);
300
- }
301
- .sidebar-btn {
302
- margin-top: 10px !important;
303
- }
304
- .toggle-btn {
305
- width: 150px !important;
306
- margin-bottom: 10px !important;
307
- }
308
- """
309
 
310
  def get_logo(width=400, height=100):
311
  return f"""
@@ -327,99 +119,271 @@ def get_logo(width=400, height=100):
327
  </div>
328
  """
329
 
330
- # --- 4. BUILD UI ---
331
- with gr.Blocks() as demo:
332
- session_storage = gr.State([])
333
- sidebar_state = gr.State(True)
334
-
335
- # PRE-SCREEN (Landing Page)
336
- with gr.Column(visible=True, elem_classes="landing-container") as landing_page:
337
- gr.HTML(get_logo(width=600, height=150))
338
- gr.Markdown("### // SYSTEM STATUS: ONLINE\n// ACADEMIC CORE: READY")
339
- start_button = gr.Button("INITIALIZE INTERFACE", variant="primary", elem_classes="start-btn")
340
-
341
- # MAIN APP (The Chat Interface)
342
- with gr.Column(visible=False) as main_app:
343
- gr.HTML(get_logo(width=300, height=80))
344
-
345
- with gr.Row():
346
- with gr.Column(scale=1) as sidebar_col:
347
- # Info Popup Section
348
- info_btn = gr.Button("ℹ️ > Quick Guide", size="sm", variant="secondary")
349
-
350
- with gr.Column(visible=False, elem_classes="info-popup") as info_panel:
351
- gr.Markdown("""
352
- **<u>Teaching Protocol:</u>**
353
- * **Socratic:** AI hints and asks questions to guide you.
354
- * **Direct:** AI explains concepts and gives examples immediately.
355
- **<u>Upload Images & Code:</u>**
356
- Use the 📎 icon in the chat bar to upload screenshots of errors, flowcharts, or even raw `.py` files!
357
- **<u>Archive Current Session:</u>**
358
- Saves current chat in 'Previous Chats' and creates a new session.
359
- """)
360
- close_info_btn = gr.Button("Close Guide", size="sm")
361
-
362
- gr.Markdown("---")
363
- mode_selector = gr.Radio(choices=["Socratic", "Direct"], value="Socratic", label="Teaching Protocol")
364
- course_selector = gr.Dropdown(choices=["AP CS A", "AP CSP", "C++ Fundamentals", "Web Development 101", "Intro to Python", "AP Cybersecurity", "Other"], value="Intro to Python", label="Course Curriculum")
365
- language_selector = gr.Dropdown(choices=["Java", "Python", "JavaScript", "C++", "C#", "SQL"], value="Python", label="Target Language")
366
-
367
- gr.Markdown("---")
368
- gr.Markdown("### Session Archives")
369
- history_dropdown = gr.Dropdown(choices=[], label="Previous Chats", interactive=True)
370
- clear_btn = gr.Button("Archive Current Session", variant="secondary", elem_classes="sidebar-btn")
371
-
372
- gr.Markdown("---")
373
- download_btn = gr.Button("Download Text File", variant="primary", elem_classes="sidebar-btn")
374
- transcript_file = gr.File(label="Download Ready", visible=False)
375
-
376
- with gr.Column(scale=4):
377
- toggle_sidebar_btn = gr.Button("◀ Hide Sidebar", size="sm", elem_classes="toggle-btn")
378
-
379
- chat_ui = gr.ChatInterface(
380
- fn=chat_logic,
381
- additional_inputs=[mode_selector, language_selector, course_selector],
382
- chatbot=gr.Chatbot(height=600, elem_id="chatbot-window", label="DACodeX"),
383
- multimodal=True
384
- )
385
 
386
- # --- UI LOGIC / EVENTS ---
387
- def start_app():
388
- return gr.update(visible=False), gr.update(visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
- def toggle_info(show):
391
- return gr.update(visible=show)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
- start_button.click(fn=start_app, outputs=[landing_page, main_app])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
 
395
- info_btn.click(fn=lambda: toggle_info(True), outputs=info_panel)
396
- close_info_btn.click(fn=lambda: toggle_info(False), outputs=info_panel)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
- clear_btn.click(
399
- archive_and_clear,
400
- inputs=[chat_ui.chatbot, session_storage],
401
- outputs=[session_storage, chat_ui.chatbot, history_dropdown]
402
- )
403
 
404
- download_btn.click(
405
- save_transcript,
406
- inputs=[chat_ui.chatbot],
407
- outputs=[transcript_file]
408
- ).then(
409
- lambda: gr.update(visible=True), None, transcript_file
410
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
 
412
- history_dropdown.change(
413
- load_from_history,
414
- inputs=[history_dropdown, session_storage],
415
- outputs=[chat_ui.chatbot]
416
- )
 
 
 
417
 
418
- toggle_sidebar_btn.click(
419
- toggle_sidebar_func,
420
- inputs=[sidebar_state],
421
- outputs=[sidebar_state, sidebar_col, toggle_sidebar_btn]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  )
423
-
424
- if __name__ == "__main__":
425
- demo.launch(theme=dacodex_theme, css=custom_css)
 
1
+ from nicegui import ui, app, events
2
  from google import genai
3
  from google.genai import types
4
  from PIL import Image
5
  import datetime
6
  import os
7
+ import asyncio
8
+ import io
9
 
10
  # --- 1. SETUP & MODULAR PROMPT LIBRARY ---
11
  API_KEY = os.environ.get('GOOGLE_API_KEY')
12
 
13
  try:
14
  if not API_KEY:
15
+ print("⚠️ Warning: GOOGLE_API_KEY not found. Set it in your environment variables.")
16
+ # Using the async client for non-blocking UI updates in NiceGUI
17
  client = genai.Client(api_key=API_KEY)
18
  except Exception as e:
19
  print(f"❌ Initialization Error: {e}")
20
 
 
21
  MODEL_ID = "gemini-2.5-flash"
22
 
23
+ # --- SYSTEM PROMPT FRAGMENTS (PRESERVED) ---
24
  BASE_PERSONA = """
25
  ROLE: You are 'Code Mentor,' a Coding Trainer Chatbot intended for use in a high-school programming classroom.
26
  VISION: You are a MULTIMODAL AI. You have vision capabilities. You can seamlessly see, read, and analyze uploaded images, screenshots of code or errors, flowcharts, and architecture diagrams.
 
93
  prompt_parts.append(TRANSPARENCY_AND_ASSESSMENT)
94
  return "\n\n".join(prompt_parts)
95
 
96
+
97
+ # --- STATE MANAGEMENT ---
98
+ chat_history = [] # Stores UI messages
99
+ session_storage = {} # Stores archived sessions
100
+ pending_uploads = [] # Temporary storage for files before sending
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  def get_logo(width=400, height=100):
103
  return f"""
 
119
  </div>
120
  """
121
 
122
+ # --- UI COMPONENTS & LOGIC ---
123
+
124
+ @ui.page('/')
125
+ def main_page():
126
+ # --- STYLING ---
127
+ ui.add_css("""
128
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;800&display=swap');
129
+ body { background-color: #09090b; color: #e4e4e7; font-family: 'JetBrains Mono', monospace; }
130
+
131
+ @keyframes flicker {
132
+ 0% { opacity: 0.97; } 5% { opacity: 0.9; } 10% { opacity: 0.97; } 100% { opacity: 1; }
133
+ }
134
+ .landing-container {
135
+ height: 100vh;
136
+ background: radial-gradient(circle at center, #1e1b4b 0%, #09090b 100%);
137
+ animation: flicker 0.15s infinite;
138
+ }
139
+ .start-btn {
140
+ border: 1px solid #ef4444 !important;
141
+ box-shadow: 0 0 15px rgba(220, 38, 38, 0.4);
142
+ letter-spacing: 2px;
143
+ transition: all 0.3s ease !important;
144
+ }
145
+ .start-btn:hover {
146
+ box-shadow: 0 0 30px rgba(239, 68, 68, 0.8);
147
+ transform: scale(1.05) !important;
148
+ }
149
+
150
+ /* Message Text Colors - Forced White */
151
+ .q-message-text { background-color: #121217 !important; border: 1px solid #27272a; }
152
+ .q-message-text--sent { background-color: #dc2626 !important; border: none; }
153
+ .q-message-text-content { color: #ffffff !important; }
154
+ .q-message-name { color: #D1D5DB !important; }
155
+
156
+ .drawer-bg { background-color: #121217 !important; border-right: 1px solid #27272a; }
157
+ """)
158
+
159
+ ui.colors(primary='#dc2626', secondary='#121217', accent='#ef4444')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
+ # --- 1. LANDING PAGE ---
162
+ with ui.column().classes('w-full items-center justify-center landing-container') as landing_view:
163
+ ui.html(get_logo(width=600, height=150))
164
+ ui.markdown("### // SYSTEM STATUS: ONLINE\n// ACADEMIC CORE: READY").classes('text-center')
165
+ start_btn = ui.button("INITIALIZE INTERFACE").classes('start-btn mt-4 px-8 py-4 text-lg font-bold rounded text-white')
166
+
167
+ # --- 2. SIDEBAR ---
168
+ with ui.left_drawer(value=False).classes('drawer-bg p-4') as drawer:
169
+ ui.html(get_logo(width=200, height=60)).classes('mb-4')
170
+
171
+ with ui.dialog() as info_dialog, ui.card().classes('bg-[#1a1a23] border border-[#dc2626] text-white'):
172
+ ui.markdown("""
173
+ **<u>Teaching Protocol:</u>**
174
+ * **Socratic:** AI hints and asks questions to guide you.
175
+ * **Direct:** AI explains concepts and gives examples immediately.
176
+ **<u>Upload Images & Code:</u>**
177
+ Use the 📎 icon in the chat bar to upload screenshots of errors, flowcharts, or even raw `.py` files!
178
+ **<u>Archive Current Session:</u>**
179
+ Saves current chat in 'Previous Chats' and creates a new session.
180
+ """)
181
+ ui.button('Close', on_click=info_dialog.close)
182
+
183
+ ui.button("ℹ️ Quick Guide", on_click=info_dialog.open).props('outline rounded size=sm').classes('w-full mb-4 text-white')
184
+ ui.separator()
185
+
186
+ mode_select = ui.select(["Socratic", "Direct"], value="Socratic", label="Teaching Protocol").classes('w-full mt-2 text-white')
187
+ course_select = ui.select(["AP CS A", "AP CSP", "C++ Fundamentals", "Web Development 101", "Intro to Python", "AP Cybersecurity", "Other"], value="Intro to Python", label="Course Curriculum").classes('w-full mt-2 text-white')
188
+ language_select = ui.select(["Java", "Python", "JavaScript", "C++", "C#", "SQL"], value="Python", label="Target Language").classes('w-full mt-2 text-white')
189
+
190
+ ui.separator().classes('my-4')
191
+ ui.label("Session Archives").classes('text-lg font-bold text-gray-300')
192
+
193
+ history_dropdown = ui.select([], label="Previous Chats").classes('w-full mt-2 text-white')
194
+
195
+ def archive_session():
196
+ if not chat_history: return
197
+ timestamp = datetime.datetime.now().strftime("%H:%M:%S")
198
+ label = f"Session {timestamp} ({len(chat_history)} msgs)"
199
+ session_storage[label] = chat_history.copy()
200
+ history_dropdown.options = list(session_storage.keys())
201
+ history_dropdown.update()
202
+ chat_history.clear()
203
+ render_messages.refresh()
204
+
205
+ ui.button("Archive Current Session", on_click=archive_session).props('outline rounded').classes('w-full mt-2 text-white')
206
+
207
+ def load_session(e):
208
+ if e.value in session_storage:
209
+ chat_history.clear()
210
+ chat_history.extend(session_storage[e.value])
211
+ render_messages.refresh()
212
 
213
+ history_dropdown.on_value_change(load_session)
214
+
215
+ ui.separator().classes('my-4')
216
+
217
+ def download_transcript():
218
+ if not chat_history: return
219
+ transcript_text = "DACODEX MENTOR SESSION\n" + "="*30 + "\n\n"
220
+ for msg in chat_history:
221
+ prefix = "STUDENT" if msg["role"] == "user" else "MENTOR"
222
+ transcript_text += f"{prefix}:\n{msg['raw_text']}\n\n"
223
+
224
+ # Create file bytes for download
225
+ filename = f"DACodeX_Transcript_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.txt"
226
+ file_bytes = transcript_text.encode('utf-8')
227
+ ui.download(file_bytes, filename)
228
+
229
+ ui.button("Download Text File", on_click=download_transcript).classes('w-full mt-2 start-btn text-white')
230
 
231
+ # --- 3. MAIN CHAT AREA ---
232
+ with ui.column().classes('w-full h-screen relative') as main_chat_view:
233
+ main_chat_view.set_visibility(False)
234
+
235
+ # Header row for drawer toggle
236
+ with ui.row().classes('w-full p-4 border-b border-[#27272a] bg-[#121217] items-center z-10'):
237
+ ui.button(icon='menu', on_click=drawer.toggle).props('flat round dense color=white')
238
+ ui.label('DACodeX - Academic Core').classes('text-xl font-bold ml-2 text-white')
239
+
240
+ # Chat Messages Area
241
+ with ui.scroll_area().classes('flex-grow w-full p-4 pb-32') as scroll_area:
242
+ @ui.refreshable
243
+ def render_messages():
244
+ for msg in chat_history:
245
+ with ui.chat_message(text=msg['text'], name=msg['name'], sent=msg['sent']):
246
+ # Render images if attached
247
+ for img_html in msg.get('images', []):
248
+ ui.html(img_html).classes('max-w-xs rounded mt-2')
249
+
250
+ render_messages()
251
 
252
+ # Input Area (Pinned to bottom)
253
+ with ui.row().classes('absolute bottom-0 w-full p-4 bg-[#09090b] border-t border-[#27272a] items-end z-10'):
254
+
255
+ def handle_upload(e: events.UploadEventArguments):
256
+ filename = e.name
257
+ ext = filename.split('.')[-1].lower()
258
+ content = e.content.read()
259
+
260
+ if ext in ['png', 'jpg', 'jpeg', 'webp', 'gif']:
261
+ try:
262
+ img = Image.open(io.BytesIO(content))
263
+ pending_uploads.append({'type': 'image', 'data': img, 'name': filename})
264
+ ui.notify(f"Attached Image: {filename}", type='positive')
265
+ except Exception as ex:
266
+ ui.notify(f"Error loading image: {ex}", color='negative')
267
+ else:
268
+ try:
269
+ text_content = content.decode('utf-8', errors='ignore')
270
+ pending_uploads.append({'type': 'text', 'data': f"\n\n--- Uploaded File: {filename} ---\n{text_content}", 'name': filename})
271
+ ui.notify(f"Attached File: {filename}", type='positive')
272
+ except Exception as ex:
273
+ ui.notify(f"Could not read file {filename}: {ex}", color='negative')
274
+
275
+ upload_element.reset()
276
 
277
+ # The invisible uploader component
278
+ upload_element = ui.upload(multiple=True, auto_upload=True, on_upload=handle_upload).classes('absolute w-0 h-0 opacity-0 overflow-hidden -z-10')
279
+
280
+ # The visible icon button that triggers the hidden uploader's file dialog via JS
281
+ ui.button(icon='attach_file', on_click=lambda: ui.run_javascript('document.querySelector(".q-uploader__input")?.click()')).props('flat round dense color=white').classes('mb-2')
282
 
283
+ text_input = ui.input(placeholder="Type your message...").classes('flex-grow mx-2').props('outlined dark rounded')
284
+
285
+ async def send_message():
286
+ user_text = text_input.value.strip()
287
+ if not user_text and not pending_uploads:
288
+ ui.notify("Please provide some text or an image.", color='warning')
289
+ return
290
+
291
+ # 1. Build Payload and UI Message
292
+ payload = []
293
+ images_for_ui = []
294
+ raw_text_record = user_text
295
+
296
+ if user_text:
297
+ payload.append(user_text)
298
+
299
+ for item in pending_uploads:
300
+ if item['type'] == 'image':
301
+ payload.append(item['data'])
302
+ raw_text_record += f"\n[Uploaded Image: {item['name']}]"
303
+ # For UI display, convert PIL to base64
304
+ import base64
305
+ buffered = io.BytesIO()
306
+ item['data'].save(buffered, format="PNG")
307
+ img_str = base64.b64encode(buffered.getvalue()).decode()
308
+ images_for_ui.append(f'<img src="data:image/png;base64,{img_str}" />')
309
+ elif item['type'] == 'text':
310
+ payload.append(item['data'])
311
+ raw_text_record += f"\n[Uploaded File: {item['name']}]"
312
+
313
+ # 2. Add User Message to UI
314
+ chat_history.append({
315
+ 'text': user_text if user_text else "📎 (Attachments)",
316
+ 'name': 'Student',
317
+ 'sent': True,
318
+ 'role': 'user',
319
+ 'raw_text': raw_text_record,
320
+ 'images': images_for_ui
321
+ })
322
+
323
+ text_input.value = ""
324
+ pending_uploads.clear()
325
+ render_messages.refresh()
326
+ scroll_area.scroll_to(percent=1)
327
 
328
+ # 3. Setup GenAI API Call Structure
329
+ current_instruction = build_system_prompt(mode_select.value, language_select.value, course_select.value)
330
+
331
+ gemini_history = []
332
+ for msg in chat_history[:-1]: # Exclude the message we just added (it goes in payload)
333
+ role = msg['role']
334
+ # Simplified history recreation for API
335
+ gemini_history.append(types.Content(role=role, parts=[types.Part.from_text(text=msg['raw_text'])]))
336
 
337
+ try:
338
+ # 4. Async API Call to prevent GUI Freezing
339
+ chat = client.aio.chats.create(
340
+ model=MODEL_ID,
341
+ config=types.GenerateContentConfig(
342
+ system_instruction=current_instruction,
343
+ temperature=0.7 if mode_select.value == "Socratic" else 0.2
344
+ ),
345
+ history=gemini_history
346
+ )
347
+
348
+ # Add empty AI message to UI
349
+ chat_history.append({'text': '', 'name': 'DACodeX', 'sent': False, 'role': 'model', 'raw_text': ''})
350
+ render_messages.refresh()
351
+ scroll_area.scroll_to(percent=1)
352
+
353
+ response_stream = await chat.send_message_stream(payload)
354
+ full_response = ""
355
+
356
+ async for chunk in response_stream:
357
+ if chunk.text:
358
+ full_response += chunk.text
359
+ chat_history[-1]['text'] = full_response
360
+ chat_history[-1]['raw_text'] = full_response
361
+ render_messages.refresh()
362
+ scroll_area.scroll_to(percent=1)
363
+
364
+ except Exception as e:
365
+ ui.notify(f"🤖 Technical Hiccup: {str(e)}", color='negative')
366
+
367
+ text_input.on('keydown.enter', send_message)
368
+ ui.button(icon='send', on_click=send_message).props('flat round dense color=primary').classes('mb-2')
369
+
370
+ # --- 4. INTERFACE STARTUP LOGIC ---
371
+ def start_interface():
372
+ landing_view.set_visibility(False)
373
+ main_chat_view.set_visibility(True)
374
+ drawer.value = True # Triggers the sidebar to slide out smoothly
375
+
376
+ # Wire the button to the function we just defined
377
+ start_btn.on_click(start_interface)
378
+
379
+
380
+ # --- INITIALIZATION (NATIVE DESKTOP MODE) ---
381
+ if __name__ in {"__main__", "__mp_main__"}:
382
+ # Runs the application as a standalone desktop window using PyWebView
383
+ ui.run(
384
+ native=True,
385
+ window_size=(1200, 800),
386
+ title="DACodeX - Academic Core",
387
+ dark=True,
388
+ show=True
389
  )