SamCChauhan commited on
Commit
a471385
Β·
unverified Β·
1 Parent(s): 3169ed9

Refactor app.py for improved functionality and style

Browse files
Files changed (1) hide show
  1. app.py +213 -135
app.py CHANGED
@@ -6,10 +6,10 @@ 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.")
@@ -18,82 +18,68 @@ try:
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.
27
  Your primary goal is to assist students in learning to program by explaining concepts, guiding problem-solving,
28
- and supporting debugging. You are currently tutoring a student in the '{course}' curriculum, focusing on the '{language}' programming language.
29
- """
30
 
31
- PEDAGOGY_SOCRATIC = """
32
- STRATEGY (SOCRATIC MODE):
33
  - Act like a good instructor, not like Stack Overflow.
34
  - Use scaffolded instruction: hints β†’ partial guidance β†’ full solution (only as an absolute last resort).
35
  - Ask guiding questions to encourage student reasoning and productive struggle before revealing answers.
36
- - Never act as a shortcut solution generator.
37
- """
38
 
39
- PEDAGOGY_DIRECT = """
40
- STRATEGY (DIRECT INSTRUCTION MODE):
41
  - Provide direct, clear explanations of concepts and syntax.
42
  - Use very small code snippets (max 3-5 lines) to demonstrate specific rules.
43
  - Explain the 'WHY' behind the code and how the computer handles it.
44
- - Do not write their entire assignment for them; focus on the specific concept they are stuck on.
45
- """
46
 
47
- CODE_AWARENESS = """
48
- CODE & LANGUAGE CAPABILITIES:
49
  - You fully understand the syntax, semantics, and common beginner mistakes of {language}.
50
  - When evaluating {language} code or reviewing screenshots of code, explain what it does, why it fails, and how to fix it.
51
- - Use simple, precise, age-appropriate explanations, avoiding heavy professional jargon.
52
- """
53
 
54
- ERROR_HANDLING = """
55
- ERROR FOCUS & DEBUGGING-FIRST:
56
  - Treat errors as learning opportunities, not failures.
57
  - Interpret compiler errors, runtime errors, and logic errors in plain English.
58
  - Encourage debugging strategies: code tracing, print statements, test cases, and rubber-duck reasoning.
59
- - Sound like a teacher during a test: "I can help you think through the logic, but I can't write the code for you here."
60
- """
61
 
62
- ADAPTABILITY_AND_TONE = """
63
- ADAPTABILITY & TONE (AFFECTIVE COMPUTING):
64
  - Detect the student's level based on their questions and code complexity, adjusting your vocabulary, pace, and depth.
65
  - Challenge advanced students with "What if..." scenarios, optimization prompts, and edge-case analysis.
66
  - Maintain a patient, non-judgmental, calm, and encouraging tone.
67
- - Use phrases like "You're close" or "This is a common mistake." Never shame or ridicule; normalize confusion.
68
- """
69
 
70
- TRANSPARENCY_AND_ASSESSMENT = """
71
- TRANSPARENCY & ASSESSMENT AWARENESS:
72
  - No Black Boxes: Explain why a solution works. Show step-by-step execution, variable state changes, or call stack evolution.
73
  - Encourage mental models, not memorization.
74
  - Understand AP-style coding task verbs: Predict, Trace, Debug, Modify.
75
  - Can simulate Free-Response Questions, output prediction, and code completion.
76
  - Grade and evaluate the student's *thinking* and logic, not just the correctness of the final code.
77
- - Prevent misuse: Never complete graded assignments for the student. Prioritize student learning over speed of answers.
78
- """
79
 
80
  def build_system_prompt(mode, language, course):
81
  lang_label = language if language else "General Programming"
82
  course_label = course if course else "General Computer Science"
83
  prompt_parts = [BASE_PERSONA.format(course=course_label, language=lang_label)]
84
-
85
  if mode == "Socratic":
86
  prompt_parts.append(PEDAGOGY_SOCRATIC)
87
  else:
88
  prompt_parts.append(PEDAGOGY_DIRECT)
89
-
90
  prompt_parts.append(CODE_AWARENESS.format(language=lang_label))
91
  prompt_parts.append(ERROR_HANDLING)
92
  prompt_parts.append(ADAPTABILITY_AND_TONE)
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
@@ -106,24 +92,24 @@ def get_logo(width=400, height=100):
106
  <defs>
107
  <filter id="neonRed" x="-20%" y="-20%" width="140%" height="140%">
108
  <feGaussianBlur stdDeviation="3" result="blur" />
 
109
  <feComposite in="SourceGraphic" in2="blur" operator="over" />
110
  </filter>
111
  </defs>
112
  <path d="M40 30L20 50L40 70" stroke="#dc2626" stroke-width="5" stroke-linecap="round" filter="url(#neonRed)"/>
113
  <path d="M70 30L90 50L70 70" stroke="#dc2626" stroke-width="5" stroke-linecap="round" filter="url(#neonRed)"/>
114
- <text x="100" y="65" fill="white" style="font-family:'JetBrains Mono', monospace; font-weight:800; font-size:45px;">DA</text>
115
  <text x="165" y="65" fill="#dc2626" style="font-family:'JetBrains Mono', monospace; font-weight:800; font-size:45px;" filter="url(#neonRed)">CODE</text>
116
- <text x="285" y="65" fill="white" style="font-family:'JetBrains Mono', monospace; font-weight:200; font-size:45px;">X</text>
117
- <rect x="100" y="75" width="230" height="2" fill="#dc2626" fill-opacity="0.3"/>
118
  </svg>
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; }
@@ -137,18 +123,18 @@ def main_page():
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 */
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-name { color: #D1D5DB !important; }
154
 
@@ -179,37 +165,86 @@ def main_page():
179
 
180
  /* Inline code (e.g., `print()`) */
181
  .q-message-text-content :not(pre) > code {
182
- background-color: #27272a;
183
- color: #ffb3c1;
184
- padding: 2px 6px;
185
- border-radius: 4px;
186
- font-family: 'JetBrains Mono', monospace;
187
  font-size: 0.9em;
188
  }
189
 
190
  /* Code blocks (e.g., ```python ... ```) */
191
  .q-message-text-content pre {
192
- background-color: #09090b !important;
193
- border: 1px solid #27272a;
194
- padding: 12px;
195
- border-radius: 8px;
196
- overflow-x: auto;
 
197
  margin: 0.5em 0;
198
  }
199
  .q-message-text-content pre code {
200
- color: #e4e4e7;
201
- background-color: transparent;
202
- padding: 0;
203
- font-family: 'JetBrains Mono', monospace;
204
  font-size: 0.9em;
205
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  /* ================================= */
207
 
208
  .drawer-bg { background-color: #121217 !important; border-right: 1px solid #27272a; }
209
  """)
210
-
211
  ui.colors(primary='#dc2626', secondary='#121217', accent='#ef4444')
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  # --- 1. LANDING PAGE ---
214
  with ui.column().classes('w-full items-center justify-center landing-container') as landing_view:
215
  ui.html(get_logo(width=600, height=150))
@@ -222,15 +257,20 @@ def main_page():
222
 
223
  with ui.dialog() as info_dialog, ui.card().classes('bg-[#1a1a23] border border-[#dc2626] text-white'):
224
  ui.markdown("""
225
- **<u>Teaching Protocol:</u>**
226
- * **Socratic:** AI hints and asks questions to guide you.
227
- * **Direct:** AI explains concepts and gives examples immediately.
228
- **<u>Upload Images & Code:</u>**
229
- Use the πŸ“Ž icon in the chat bar to upload screenshots of errors, flowcharts, or even raw `.py` files!
230
- **<u>Archive Current Session:</u>**
231
- Saves current chat in 'Previous Chats' and creates a new session.
232
- """)
233
- ui.button('Close', on_click=info_dialog.close)
 
 
 
 
 
234
 
235
  ui.button("ℹ️ Quick Guide", on_click=info_dialog.open).props('outline rounded size=sm').classes('w-full mb-4 text-white')
236
  ui.separator()
@@ -261,7 +301,6 @@ def main_page():
261
  chat_history.clear()
262
  chat_history.extend(session_storage[e.value])
263
  render_messages.refresh()
264
-
265
  history_dropdown.on_value_change(load_session)
266
 
267
  ui.separator().classes('my-4')
@@ -270,7 +309,7 @@ def main_page():
270
  if not chat_history:
271
  ui.notify("No chat history to save.", type="warning")
272
  return
273
-
274
  transcript_text = "DACODEX MENTOR SESSION\n" + "="*30 + "\n\n"
275
  for msg in chat_history:
276
  prefix = "STUDENT" if msg["role"] == "user" else "MENTOR"
@@ -279,10 +318,9 @@ def main_page():
279
  filename = f"DACodeX_Transcript_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.txt"
280
 
281
  try:
282
- # Reliably save locally by targeting the user's Downloads folder
283
  downloads_path = os.path.join(os.path.expanduser('~'), 'Downloads')
284
  if not os.path.exists(downloads_path):
285
- downloads_path = os.getcwd() # Fallback
286
 
287
  full_path = os.path.join(downloads_path, filename)
288
 
@@ -299,60 +337,102 @@ def main_page():
299
  with ui.column().classes('w-full h-screen relative') as main_chat_view:
300
  main_chat_view.set_visibility(False)
301
 
302
- # Header row for drawer toggle
303
  with ui.row().classes('w-full p-4 border-b border-[#27272a] bg-[#121217] items-center z-10'):
304
  ui.button(icon='menu', on_click=drawer.toggle).props('flat round dense color=white')
305
  ui.label('DACodeX - Academic Core').classes('text-xl font-bold ml-2 text-white')
306
 
307
- # Chat Messages Area
308
- with ui.scroll_area().classes('flex-grow w-full p-4 pb-32') as scroll_area:
309
  @ui.refreshable
310
  def render_messages():
311
- for msg in chat_history:
312
- # Note: We NO LONGER pass text=msg['text'] here.
313
- # We pass the text into ui.markdown() inside the context manager instead!
314
  with ui.chat_message(name=msg['name'], sent=msg['sent']):
315
- # Renders the text as rich Markdown with support for breaks and cuddled lists
316
  ui.markdown(msg['text'], extras=['fenced-code-blocks', 'tables', 'cuddled-lists', 'breaks'])
317
-
318
- # Render images if attached
319
  for img_html in msg.get('images', []):
320
  ui.html(img_html).classes('max-w-xs rounded mt-2')
321
-
322
  render_messages()
323
 
324
- # Input Area (Pinned to bottom)
325
- with ui.row().classes('absolute bottom-0 w-full p-4 bg-[#09090b] border-t border-[#27272a] items-end z-10'):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
- def handle_upload(e: events.UploadEventArguments):
328
- filename = e.name
329
- ext = filename.split('.')[-1].lower()
330
- content = e.content.read()
331
 
332
- if ext in ['png', 'jpg', 'jpeg', 'webp', 'gif']:
333
- try:
334
- img = Image.open(io.BytesIO(content))
335
- pending_uploads.append({'type': 'image', 'data': img, 'name': filename})
336
- ui.notify(f"Attached Image: {filename}", type='positive')
337
- except Exception as ex:
338
- ui.notify(f"Error loading image: {ex}", color='negative')
339
- else:
340
- try:
341
- text_content = content.decode('utf-8', errors='ignore')
342
- pending_uploads.append({'type': 'text', 'data': f"\n\n--- Uploaded File: {filename} ---\n{text_content}", 'name': filename})
343
- ui.notify(f"Attached File: {filename}", type='positive')
344
- except Exception as ex:
345
- ui.notify(f"Could not read file {filename}: {ex}", color='negative')
 
 
 
 
 
 
 
346
 
347
- upload_element.reset()
348
-
349
- # The invisible uploader component
350
- 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')
351
-
352
- # The visible icon button that triggers the hidden uploader's file dialog via JS
353
- 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')
354
-
355
- text_input = ui.input(placeholder="Type your message...").classes('flex-grow mx-2').props('outlined dark rounded')
356
 
357
  async def send_message():
358
  user_text = text_input.value.strip()
@@ -360,7 +440,6 @@ def main_page():
360
  ui.notify("Please provide some text or an image.", color='warning')
361
  return
362
 
363
- # 1. Build Payload and UI Message
364
  payload = []
365
  images_for_ui = []
366
  raw_text_record = user_text
@@ -372,8 +451,6 @@ def main_page():
372
  if item['type'] == 'image':
373
  payload.append(item['data'])
374
  raw_text_record += f"\n[Uploaded Image: {item['name']}]"
375
- # For UI display, convert PIL to base64
376
- import base64
377
  buffered = io.BytesIO()
378
  item['data'].save(buffered, format="PNG")
379
  img_str = base64.b64encode(buffered.getvalue()).decode()
@@ -382,9 +459,10 @@ def main_page():
382
  payload.append(item['data'])
383
  raw_text_record += f"\n[Uploaded File: {item['name']}]"
384
 
385
- # 2. Add User Message to UI
386
  chat_history.append({
387
  'text': user_text if user_text else "πŸ“Ž *(Attachments sent)*",
 
388
  'name': 'Student',
389
  'sent': True,
390
  'role': 'user',
@@ -394,20 +472,18 @@ def main_page():
394
 
395
  text_input.value = ""
396
  pending_uploads.clear()
 
397
  render_messages.refresh()
398
  scroll_area.scroll_to(percent=1)
399
 
400
- # 3. Setup GenAI API Call Structure
401
  current_instruction = build_system_prompt(mode_select.value, language_select.value, course_select.value)
402
 
403
  gemini_history = []
404
- for msg in chat_history[:-1]: # Exclude the message we just added (it goes in payload)
405
  role = msg['role']
406
- # Simplified history recreation for API
407
  gemini_history.append(types.Content(role=role, parts=[types.Part.from_text(text=msg['raw_text'])]))
408
 
409
  try:
410
- # 4. Async API Call to prevent GUI Freezing
411
  chat = client.aio.chats.create(
412
  model=MODEL_ID,
413
  config=types.GenerateContentConfig(
@@ -417,45 +493,47 @@ def main_page():
417
  history=gemini_history
418
  )
419
 
420
- # Add empty AI message to UI
421
  chat_history.append({'text': '', 'name': 'DACodeX', 'sent': False, 'role': 'model', 'raw_text': ''})
422
  render_messages.refresh()
423
  scroll_area.scroll_to(percent=1)
424
 
425
  response_stream = await chat.send_message_stream(payload)
426
  full_response = ""
 
427
 
428
  async for chunk in response_stream:
429
  if chunk.text:
430
  full_response += chunk.text
431
- chat_history[-1]['text'] = full_response
432
- chat_history[-1]['raw_text'] = full_response
433
- render_messages.refresh()
434
- scroll_area.scroll_to(percent=1)
435
-
 
 
 
 
 
 
436
  except Exception as e:
437
  ui.notify(f"πŸ€– Technical Hiccup: {str(e)}", color='negative')
438
-
439
  text_input.on('keydown.enter', send_message)
440
- ui.button(icon='send', on_click=send_message).props('flat round dense color=primary').classes('mb-2')
441
 
442
- # --- 4. INTERFACE STARTUP LOGIC ---
443
  def start_interface():
444
  landing_view.set_visibility(False)
445
  main_chat_view.set_visibility(True)
446
- drawer.value = True # Triggers the sidebar to slide out smoothly
447
-
448
- # Wire the button to the function we just defined
449
  start_btn.on_click(start_interface)
450
 
451
-
452
- # --- INITIALIZATION (NATIVE DESKTOP MODE) ---
453
  if __name__ in {"__main__", "__mp_main__"}:
454
- # Runs the application as a standalone desktop window using PyWebView
455
  ui.run(
456
- native=True,
457
- window_size=(1200, 800),
458
  title="DACodeX - Academic Core",
459
  dark=True,
460
- show=True
 
 
461
  )
 
6
  import os
7
  import asyncio
8
  import io
9
+ import base64
10
 
11
  # --- 1. SETUP & MODULAR PROMPT LIBRARY ---
12
  API_KEY = os.environ.get('GOOGLE_API_KEY')
 
13
  try:
14
  if not API_KEY:
15
  print("⚠️ Warning: GOOGLE_API_KEY not found. Set it in your environment variables.")
 
18
  except Exception as e:
19
  print(f"❌ Initialization Error: {e}")
20
 
21
+ MODEL_ID = "gemini-2.5-flash-lite"
22
 
23
  # --- SYSTEM PROMPT FRAGMENTS (PRESERVED) ---
24
+ BASE_PERSONA = """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.
26
  Your primary goal is to assist students in learning to program by explaining concepts, guiding problem-solving,
27
+ and supporting debugging. You are currently tutoring a student in the '{course}' curriculum, focusing on the '{language}' programming language."""
 
28
 
29
+ PEDAGOGY_SOCRATIC = """STRATEGY (SOCRATIC MODE):
 
30
  - Act like a good instructor, not like Stack Overflow.
31
  - Use scaffolded instruction: hints β†’ partial guidance β†’ full solution (only as an absolute last resort).
32
  - Ask guiding questions to encourage student reasoning and productive struggle before revealing answers.
33
+ - Never act as a shortcut solution generator."""
 
34
 
35
+ PEDAGOGY_DIRECT = """STRATEGY (DIRECT INSTRUCTION MODE):
 
36
  - Provide direct, clear explanations of concepts and syntax.
37
  - Use very small code snippets (max 3-5 lines) to demonstrate specific rules.
38
  - Explain the 'WHY' behind the code and how the computer handles it.
39
+ - Do not write their entire assignment for them; focus on the specific concept they are stuck on."""
 
40
 
41
+ CODE_AWARENESS = """CODE & LANGUAGE CAPABILITIES:
 
42
  - You fully understand the syntax, semantics, and common beginner mistakes of {language}.
43
  - When evaluating {language} code or reviewing screenshots of code, explain what it does, why it fails, and how to fix it.
44
+ - Use simple, precise, age-appropriate explanations, avoiding heavy professional jargon."""
 
45
 
46
+ ERROR_HANDLING = """ERROR FOCUS & DEBUGGING-FIRST:
 
47
  - Treat errors as learning opportunities, not failures.
48
  - Interpret compiler errors, runtime errors, and logic errors in plain English.
49
  - Encourage debugging strategies: code tracing, print statements, test cases, and rubber-duck reasoning.
50
+ - Sound like a teacher during a test: "I can help you think through the logic, but I can't write the code for you here." """
 
51
 
52
+ ADAPTABILITY_AND_TONE = """ADAPTABILITY & TONE (AFFECTIVE COMPUTING):
 
53
  - Detect the student's level based on their questions and code complexity, adjusting your vocabulary, pace, and depth.
54
  - Challenge advanced students with "What if..." scenarios, optimization prompts, and edge-case analysis.
55
  - Maintain a patient, non-judgmental, calm, and encouraging tone.
56
+ - Use phrases like "You're close" or "This is a common mistake." Never shame or ridicule; normalize confusion."""
 
57
 
58
+ TRANSPARENCY_AND_ASSESSMENT = """TRANSPARENCY & ASSESSMENT AWARENESS:
 
59
  - No Black Boxes: Explain why a solution works. Show step-by-step execution, variable state changes, or call stack evolution.
60
  - Encourage mental models, not memorization.
61
  - Understand AP-style coding task verbs: Predict, Trace, Debug, Modify.
62
  - Can simulate Free-Response Questions, output prediction, and code completion.
63
  - Grade and evaluate the student's *thinking* and logic, not just the correctness of the final code.
64
+ - Prevent misuse: Never complete graded assignments for the student. Prioritize student learning over speed of answers."""
 
65
 
66
  def build_system_prompt(mode, language, course):
67
  lang_label = language if language else "General Programming"
68
  course_label = course if course else "General Computer Science"
69
  prompt_parts = [BASE_PERSONA.format(course=course_label, language=lang_label)]
70
+
71
  if mode == "Socratic":
72
  prompt_parts.append(PEDAGOGY_SOCRATIC)
73
  else:
74
  prompt_parts.append(PEDAGOGY_DIRECT)
75
+
76
  prompt_parts.append(CODE_AWARENESS.format(language=lang_label))
77
  prompt_parts.append(ERROR_HANDLING)
78
  prompt_parts.append(ADAPTABILITY_AND_TONE)
79
  prompt_parts.append(TRANSPARENCY_AND_ASSESSMENT)
80
+
81
  return "\n\n".join(prompt_parts)
82
 
 
83
  # --- STATE MANAGEMENT ---
84
  chat_history = [] # Stores UI messages
85
  session_storage = {} # Stores archived sessions
 
92
  <defs>
93
  <filter id="neonRed" x="-20%" y="-20%" width="140%" height="140%">
94
  <feGaussianBlur stdDeviation="3" result="blur" />
95
+ <feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#dc2626" />
96
  <feComposite in="SourceGraphic" in2="blur" operator="over" />
97
  </filter>
98
  </defs>
99
  <path d="M40 30L20 50L40 70" stroke="#dc2626" stroke-width="5" stroke-linecap="round" filter="url(#neonRed)"/>
100
  <path d="M70 30L90 50L70 70" stroke="#dc2626" stroke-width="5" stroke-linecap="round" filter="url(#neonRed)"/>
101
+ <text x="100" y="65" fill="#ffffff" style="font-family:'JetBrains Mono', monospace; font-weight:800; font-size:45px;">DA</text>
102
  <text x="165" y="65" fill="#dc2626" style="font-family:'JetBrains Mono', monospace; font-weight:800; font-size:45px;" filter="url(#neonRed)">CODE</text>
103
+ <text x="285" y="65" fill="#ffffff" style="font-family:'JetBrains Mono', monospace; font-weight:200; font-size:45px;">X</text>
104
+ <rect x="100" y="75" width="230" height="2" fill="#dc2626" fill-opacity="0.5"/>
105
  </svg>
106
  </div>
107
  """
108
 
109
  # --- UI COMPONENTS & LOGIC ---
 
110
  @ui.page('/')
111
  def main_page():
112
+ # --- STYLING (RED THEME RESTORED) ---
113
  ui.add_css("""
114
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;800&display=swap');
115
  body { background-color: #09090b; color: #e4e4e7; font-family: 'JetBrains Mono', monospace; }
 
123
  animation: flicker 0.15s infinite;
124
  }
125
  .start-btn {
126
+ border: 1px solid #dc2626 !important;
127
  box-shadow: 0 0 15px rgba(220, 38, 38, 0.4);
128
  letter-spacing: 2px;
129
  transition: all 0.3s ease !important;
130
  }
131
  .start-btn:hover {
132
+ box-shadow: 0 0 30px rgba(220, 38, 38, 0.8);
133
  transform: scale(1.05) !important;
134
  }
135
 
136
+ /* Message Text Colors (RED) */
137
+ .q-message-text { background-color: #121217 !important; border: 1px solid #27272a; position: relative; }
138
  .q-message-text--sent { background-color: #dc2626 !important; border: none; }
139
  .q-message-name { color: #D1D5DB !important; }
140
 
 
165
 
166
  /* Inline code (e.g., `print()`) */
167
  .q-message-text-content :not(pre) > code {
168
+ background-color: #27272a;
169
+ color: #ffb3c1;
170
+ padding: 2px 6px;
171
+ border-radius: 4px;
172
+ font-family: 'JetBrains Mono', monospace;
173
  font-size: 0.9em;
174
  }
175
 
176
  /* Code blocks (e.g., ```python ... ```) */
177
  .q-message-text-content pre {
178
+ position: relative;
179
+ background-color: #09090b !important;
180
+ border: 1px solid #27272a;
181
+ padding: 12px;
182
+ border-radius: 8px;
183
+ overflow-x: auto;
184
  margin: 0.5em 0;
185
  }
186
  .q-message-text-content pre code {
187
+ color: #e4e4e7;
188
+ background-color: transparent;
189
+ padding: 0;
190
+ font-family: 'JetBrains Mono', monospace;
191
  font-size: 0.9em;
192
  }
193
+
194
+ /* Copy Button Style */
195
+ .copy-btn {
196
+ position: absolute;
197
+ top: 5px;
198
+ right: 5px;
199
+ padding: 4px 8px;
200
+ background: #27272a;
201
+ color: #e4e4e7;
202
+ border: 1px solid #dc2626;
203
+ border-radius: 4px;
204
+ font-size: 10px;
205
+ cursor: pointer;
206
+ z-index: 10;
207
+ opacity: 0.6;
208
+ transition: opacity 0.2s;
209
+ }
210
+ .copy-btn:hover { opacity: 1; background: #dc2626; }
211
  /* ================================= */
212
 
213
  .drawer-bg { background-color: #121217 !important; border-right: 1px solid #27272a; }
214
  """)
 
215
  ui.colors(primary='#dc2626', secondary='#121217', accent='#ef4444')
216
 
217
+ # Injecting JavaScript to handle the copy functionality dynamically
218
+ ui.add_head_html("""
219
+ <script>
220
+ function copyCode(btn) {
221
+ const pre = btn.parentElement;
222
+ const code = pre.querySelector('code').innerText;
223
+ navigator.clipboard.writeText(code).then(() => {
224
+ const oldText = btn.innerText;
225
+ btn.innerText = 'COPIED!';
226
+ setTimeout(() => { btn.innerText = oldText; }, 2000);
227
+ });
228
+ }
229
+
230
+ // Observer to add buttons to new code blocks as they appear
231
+ const observer = new MutationObserver((mutations) => {
232
+ document.querySelectorAll('pre:not(.has-copy-btn)').forEach((pre) => {
233
+ pre.classList.add('has-copy-btn');
234
+ const btn = document.createElement('button');
235
+ btn.className = 'copy-btn';
236
+ btn.innerText = 'COPY';
237
+ btn.onclick = function() { copyCode(this); };
238
+ pre.appendChild(btn);
239
+ });
240
+ });
241
+
242
+ document.addEventListener('DOMContentLoaded', () => {
243
+ observer.observe(document.body, { childList: true, subtree: true });
244
+ });
245
+ </script>
246
+ """)
247
+
248
  # --- 1. LANDING PAGE ---
249
  with ui.column().classes('w-full items-center justify-center landing-container') as landing_view:
250
  ui.html(get_logo(width=600, height=150))
 
257
 
258
  with ui.dialog() as info_dialog, ui.card().classes('bg-[#1a1a23] border border-[#dc2626] text-white'):
259
  ui.markdown("""
260
+ **<u>Teaching Protocol:</u>**
261
+
262
+ * **Socratic:** AI hints and asks questions to guide you.
263
+ * **Direct:** AI explains concepts and gives examples immediately.
264
+
265
+ **<u>Upload Images & Code:</u>**
266
+
267
+ * Use the πŸ“Ž icon in the chat bar to upload screenshots of errors, flowcharts, or even raw `.py` files!
268
+
269
+ **<u>Archive Current Session:</u>**
270
+
271
+ * Saves current chat in 'Previous Chats' and creates a new session.
272
+ """).classes('p-4')
273
+ ui.button('Close', on_click=info_dialog.close).classes('mt-2')
274
 
275
  ui.button("ℹ️ Quick Guide", on_click=info_dialog.open).props('outline rounded size=sm').classes('w-full mb-4 text-white')
276
  ui.separator()
 
301
  chat_history.clear()
302
  chat_history.extend(session_storage[e.value])
303
  render_messages.refresh()
 
304
  history_dropdown.on_value_change(load_session)
305
 
306
  ui.separator().classes('my-4')
 
309
  if not chat_history:
310
  ui.notify("No chat history to save.", type="warning")
311
  return
312
+
313
  transcript_text = "DACODEX MENTOR SESSION\n" + "="*30 + "\n\n"
314
  for msg in chat_history:
315
  prefix = "STUDENT" if msg["role"] == "user" else "MENTOR"
 
318
  filename = f"DACodeX_Transcript_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.txt"
319
 
320
  try:
 
321
  downloads_path = os.path.join(os.path.expanduser('~'), 'Downloads')
322
  if not os.path.exists(downloads_path):
323
+ downloads_path = os.getcwd()
324
 
325
  full_path = os.path.join(downloads_path, filename)
326
 
 
337
  with ui.column().classes('w-full h-screen relative') as main_chat_view:
338
  main_chat_view.set_visibility(False)
339
 
 
340
  with ui.row().classes('w-full p-4 border-b border-[#27272a] bg-[#121217] items-center z-10'):
341
  ui.button(icon='menu', on_click=drawer.toggle).props('flat round dense color=white')
342
  ui.label('DACodeX - Academic Core').classes('text-xl font-bold ml-2 text-white')
343
 
344
+ with ui.scroll_area().classes('flex-grow w-full p-4 pb-40') as scroll_area:
 
345
  @ui.refreshable
346
  def render_messages():
347
+ for index, msg in enumerate(chat_history):
348
+ # STUDENT IS ON THE RIGHT (sent=True), MENTOR IS ON THE LEFT (sent=False)
 
349
  with ui.chat_message(name=msg['name'], sent=msg['sent']):
 
350
  ui.markdown(msg['text'], extras=['fenced-code-blocks', 'tables', 'cuddled-lists', 'breaks'])
 
 
351
  for img_html in msg.get('images', []):
352
  ui.html(img_html).classes('max-w-xs rounded mt-2')
353
+
354
  render_messages()
355
 
356
+ # --- 4. UNIFIED CHAT INPUT BOX (Gemini Style) ---
357
+ with ui.column().classes('absolute bottom-0 w-full p-4 bg-[#09090b] border-t border-[#27272a] z-10'):
358
+
359
+ async def handle_native_upload():
360
+ """Opens the native file dialog safely using PyWebView."""
361
+ try:
362
+ if not app.native.main_window:
363
+ ui.notify("Window not fully loaded yet.", color="warning")
364
+ return
365
+
366
+ # By explicitly passing integer '10' (which evaluates to OPEN_DIALOG inside pywebview),
367
+ # we completely bypass all deprecation warnings and cross-process PicklingErrors!
368
+ file_paths = await app.native.main_window.create_file_dialog(
369
+ dialog_type=10,
370
+ allow_multiple=True,
371
+ file_types=('Supported Files (*.png;*.jpg;*.jpeg;*.gif;*.webp;*.py;*.txt;*.md;*.js;*.html;*.css)', 'All Files (*.*)')
372
+ )
373
+
374
+ if not file_paths:
375
+ return # User canceled the dialog
376
+
377
+ for filepath in file_paths:
378
+ if not os.path.exists(filepath):
379
+ continue
380
+
381
+ filename = os.path.basename(filepath)
382
+ ext = filename.split('.')[-1].lower() if '.' in filename else ''
383
+
384
+ try:
385
+ with open(filepath, 'rb') as f:
386
+ content_bytes = f.read()
387
+
388
+ if ext in ['png', 'jpg', 'jpeg', 'webp', 'gif']:
389
+ img = Image.open(io.BytesIO(content_bytes))
390
+ pending_uploads.append({'type': 'image', 'data': img, 'name': filename})
391
+ ui.notify(f"Attached Image: {filename}", type='positive')
392
+ else:
393
+ text_content = content_bytes.decode('utf-8', errors='ignore')
394
+ pending_uploads.append({'type': 'text', 'data': f"\n\n--- Uploaded File: {filename} ---\n{text_content}", 'name': filename})
395
+ ui.notify(f"Attached File: {filename}", type='positive')
396
+ except Exception as ex:
397
+ ui.notify(f"Could not read file {filename}: {ex}", color='negative')
398
+
399
+ render_previews.refresh() # Update the preview thumbnails
400
+ except Exception as e:
401
+ ui.notify(f"Upload failed: {e}", color="negative")
402
 
403
+ # Unified Container Box
404
+ with ui.column().classes('w-full bg-[#121217] border border-[#27272a] rounded-xl p-1 gap-0'):
 
 
405
 
406
+ # Dynamic Preview Area (Renders directly above the text input)
407
+ @ui.refreshable
408
+ def render_previews():
409
+ if pending_uploads:
410
+ with ui.row().classes('w-full gap-3 px-3 pt-3 pb-1 overflow-x-auto no-wrap'):
411
+ for idx, item in enumerate(pending_uploads):
412
+ with ui.card().classes('w-16 h-16 p-0 bg-[#09090b] border border-[#3f3f46] rounded-lg relative shadow-none flex-shrink-0 flex items-center justify-center'):
413
+ if item['type'] == 'image':
414
+ buffered = io.BytesIO()
415
+ item['data'].save(buffered, format="PNG")
416
+ img_str = base64.b64encode(buffered.getvalue()).decode()
417
+ ui.html(f'<img src="data:image/png;base64,{img_str}" style="width: 100%; height: 100%; object-fit: cover; border-radius: 6px;" />')
418
+ else:
419
+ ui.label('πŸ“„').classes('text-2xl')
420
+
421
+ # Cross button to remove attachment
422
+ def remove_item(i=idx):
423
+ pending_uploads.pop(i)
424
+ render_previews.refresh()
425
+
426
+ ui.button(icon='close', on_click=remove_item).props('flat round dense size=xs color=white').classes('absolute -top-2 -right-2 bg-[#dc2626] rounded-full z-10 w-5 h-5 min-h-0 min-w-0 p-0 shadow')
427
 
428
+ render_previews()
429
+
430
+ # Text Input Row
431
+ with ui.row().classes('w-full items-center no-wrap px-1 pb-1'):
432
+ # This now hooks directly into PyWebView's thread-safe dialog using our integer bypass!
433
+ ui.button(icon='attach_file', on_click=handle_native_upload).props('flat round dense color=white')
434
+ text_input = ui.input(placeholder="Type your message...").classes('flex-grow px-2').props('borderless dark')
435
+ ui.button(icon='send', on_click=lambda: asyncio.create_task(send_message())).props('flat round dense color=primary')
 
436
 
437
  async def send_message():
438
  user_text = text_input.value.strip()
 
440
  ui.notify("Please provide some text or an image.", color='warning')
441
  return
442
 
 
443
  payload = []
444
  images_for_ui = []
445
  raw_text_record = user_text
 
451
  if item['type'] == 'image':
452
  payload.append(item['data'])
453
  raw_text_record += f"\n[Uploaded Image: {item['name']}]"
 
 
454
  buffered = io.BytesIO()
455
  item['data'].save(buffered, format="PNG")
456
  img_str = base64.b64encode(buffered.getvalue()).decode()
 
459
  payload.append(item['data'])
460
  raw_text_record += f"\n[Uploaded File: {item['name']}]"
461
 
462
+ # STUDENT ON RIGHT (sent=True)
463
  chat_history.append({
464
  'text': user_text if user_text else "πŸ“Ž *(Attachments sent)*",
465
+ 'user_input_only': user_text, # Save for context
466
  'name': 'Student',
467
  'sent': True,
468
  'role': 'user',
 
472
 
473
  text_input.value = ""
474
  pending_uploads.clear()
475
+ render_previews.refresh() # Clear previews from UI box
476
  render_messages.refresh()
477
  scroll_area.scroll_to(percent=1)
478
 
 
479
  current_instruction = build_system_prompt(mode_select.value, language_select.value, course_select.value)
480
 
481
  gemini_history = []
482
+ for msg in chat_history[:-1]:
483
  role = msg['role']
 
484
  gemini_history.append(types.Content(role=role, parts=[types.Part.from_text(text=msg['raw_text'])]))
485
 
486
  try:
 
487
  chat = client.aio.chats.create(
488
  model=MODEL_ID,
489
  config=types.GenerateContentConfig(
 
493
  history=gemini_history
494
  )
495
 
496
+ # MENTOR ON LEFT (sent=False)
497
  chat_history.append({'text': '', 'name': 'DACodeX', 'sent': False, 'role': 'model', 'raw_text': ''})
498
  render_messages.refresh()
499
  scroll_area.scroll_to(percent=1)
500
 
501
  response_stream = await chat.send_message_stream(payload)
502
  full_response = ""
503
+ displayed_text = ""
504
 
505
  async for chunk in response_stream:
506
  if chunk.text:
507
  full_response += chunk.text
508
+ while len(displayed_text) < len(full_response):
509
+ chars_to_add = min(len(full_response) - len(displayed_text), 5)
510
+ displayed_text += full_response[len(displayed_text):len(displayed_text) + chars_to_add]
511
+
512
+ chat_history[-1]['text'] = displayed_text
513
+ chat_history[-1]['raw_text'] = full_response
514
+ render_messages.refresh()
515
+ scroll_area.scroll_to(percent=1)
516
+
517
+ await asyncio.sleep(0.02)
518
+
519
  except Exception as e:
520
  ui.notify(f"πŸ€– Technical Hiccup: {str(e)}", color='negative')
521
+
522
  text_input.on('keydown.enter', send_message)
 
523
 
 
524
  def start_interface():
525
  landing_view.set_visibility(False)
526
  main_chat_view.set_visibility(True)
527
+ drawer.value = True
528
+
 
529
  start_btn.on_click(start_interface)
530
 
531
+ # --- INITIALIZATION ---
 
532
  if __name__ in {"__main__", "__mp_main__"}:
 
533
  ui.run(
 
 
534
  title="DACodeX - Academic Core",
535
  dark=True,
536
+ native=True, # <--- THIS IS THE KEY: It opens in a window
537
+ window_size=(1200, 800), # Optional: Set your preferred start size
538
+ reload=False
539
  )