SamCChauhan commited on
Commit
c710560
·
unverified ·
1 Parent(s): 51f244f

Add files via upload

Browse files
Files changed (1) hide show
  1. dacodex_server.py +508 -0
dacodex_server.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ 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.")
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-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
+ def get_logo(width=400, height=100):
84
+ return f"""
85
+ <div style="display: flex; justify-content: center; align-items: center; padding: 20px 0;">
86
+ <svg width="{width}" height="{height}" viewBox="0 0 400 100" fill="none" xmlns="http://www.w3.org/2000/svg">
87
+ <defs>
88
+ <filter id="neonRed" x="-20%" y="-20%" width="140%" height="140%">
89
+ <feGaussianBlur stdDeviation="3" result="blur" />
90
+ <feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#dc2626" />
91
+ <feComposite in="SourceGraphic" in2="blur" operator="over" />
92
+ </filter>
93
+ </defs>
94
+ <path d="M40 30L20 50L40 70" stroke="#dc2626" stroke-width="5" stroke-linecap="round" filter="url(#neonRed)"/>
95
+ <path d="M70 30L90 50L70 70" stroke="#dc2626" stroke-width="5" stroke-linecap="round" filter="url(#neonRed)"/>
96
+ <text x="100" y="65" fill="#ffffff" style="font-family:'JetBrains Mono', monospace; font-weight:800; font-size:45px;">DA</text>
97
+ <text x="165" y="65" fill="#dc2626" style="font-family:'JetBrains Mono', monospace; font-weight:800; font-size:45px;" filter="url(#neonRed)">CODE</text>
98
+ <text x="285" y="65" fill="#ffffff" style="font-family:'JetBrains Mono', monospace; font-weight:200; font-size:45px;">X</text>
99
+ <rect x="100" y="75" width="230" height="2" fill="#dc2626" fill-opacity="0.5"/>
100
+ </svg>
101
+ </div>
102
+ """
103
+
104
+ # --- UI COMPONENTS & LOGIC ---
105
+ @ui.page('/')
106
+ def main_page():
107
+ # WEB MODIFICATION: State lists moved inside the page function!
108
+ # This ensures that when User A and User B connect from different computers,
109
+ # they get their own isolated variables and don't share chats.
110
+ chat_history = []
111
+ session_storage = {}
112
+ pending_uploads = []
113
+
114
+ # --- STYLING (RED THEME RESTORED) ---
115
+ ui.add_css("""
116
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;800&display=swap');
117
+ body { background-color: #09090b; color: #e4e4e7; font-family: 'JetBrains Mono', monospace; }
118
+
119
+ @keyframes flicker {
120
+ 0% { opacity: 0.97; } 5% { opacity: 0.9; } 10% { opacity: 0.97; } 100% { opacity: 1; }
121
+ }
122
+ .landing-container {
123
+ height: 100vh;
124
+ background: radial-gradient(circle at center, #1e1b4b 0%, #09090b 100%);
125
+ animation: flicker 0.15s infinite;
126
+ }
127
+ .start-btn {
128
+ border: 1px solid #dc2626 !important;
129
+ box-shadow: 0 0 15px rgba(220, 38, 38, 0.4);
130
+ letter-spacing: 2px;
131
+ transition: all 0.3s ease !important;
132
+ }
133
+ .start-btn:hover {
134
+ box-shadow: 0 0 30px rgba(220, 38, 38, 0.8);
135
+ transform: scale(1.05) !important;
136
+ }
137
+
138
+ /* Message Text Colors (RED) */
139
+ .q-message-text { background-color: #121217 !important; border: 1px solid #27272a; position: relative; }
140
+ .q-message-text--sent { background-color: #dc2626 !important; border: none; }
141
+ .q-message-name { color: #D1D5DB !important; }
142
+
143
+ /* === MARKDOWN SPECIFIC STYLING === */
144
+ .q-message-text-content { color: #ffffff !important; }
145
+ .q-message-text-content p { margin: 0 0 0.5em 0; color: #ffffff !important; }
146
+ .q-message-text-content p:last-child { margin-bottom: 0; }
147
+ .q-message-text-content a { color: #ef4444; font-weight: bold; }
148
+
149
+ /* Lists Fix for Quasar Reset */
150
+ .q-message-text-content ul {
151
+ list-style-type: disc !important;
152
+ padding-left: 1.5em !important;
153
+ margin-top: 0.5em !important;
154
+ margin-bottom: 0.5em !important;
155
+ }
156
+ .q-message-text-content ol {
157
+ list-style-type: decimal !important;
158
+ padding-left: 1.5em !important;
159
+ margin-top: 0.5em !important;
160
+ margin-bottom: 0.5em !important;
161
+ }
162
+ .q-message-text-content li {
163
+ display: list-item !important;
164
+ margin-bottom: 0.25em !important;
165
+ color: #ffffff !important;
166
+ }
167
+
168
+ /* Inline code (e.g., `print()`) */
169
+ .q-message-text-content :not(pre) > code {
170
+ background-color: #27272a;
171
+ color: #ffb3c1;
172
+ padding: 2px 6px;
173
+ border-radius: 4px;
174
+ font-family: 'JetBrains Mono', monospace;
175
+ font-size: 0.9em;
176
+ }
177
+
178
+ /* Code blocks (e.g., ```python ... ```) */
179
+ .q-message-text-content pre {
180
+ position: relative;
181
+ background-color: #09090b !important;
182
+ border: 1px solid #27272a;
183
+ padding: 12px;
184
+ border-radius: 8px;
185
+ overflow-x: auto;
186
+ margin: 0.5em 0;
187
+ }
188
+ .q-message-text-content pre code {
189
+ color: #e4e4e7;
190
+ background-color: transparent;
191
+ padding: 0;
192
+ font-family: 'JetBrains Mono', monospace;
193
+ font-size: 0.9em;
194
+ }
195
+
196
+ /* Copy Button Style */
197
+ .copy-btn {
198
+ position: absolute;
199
+ top: 5px;
200
+ right: 5px;
201
+ padding: 4px 8px;
202
+ background: #27272a;
203
+ color: #e4e4e7;
204
+ border: 1px solid #dc2626;
205
+ border-radius: 4px;
206
+ font-size: 10px;
207
+ cursor: pointer;
208
+ z-index: 10;
209
+ opacity: 0.6;
210
+ transition: opacity 0.2s;
211
+ }
212
+ .copy-btn:hover { opacity: 1; background: #dc2626; }
213
+ /* ================================= */
214
+
215
+ .drawer-bg { background-color: #121217 !important; border-left: 1px solid #27272a; }
216
+ """)
217
+ ui.colors(primary='#dc2626', secondary='#121217', accent='#ef4444')
218
+
219
+ ui.add_head_html("""
220
+ <script>
221
+ function copyCode(btn) {
222
+ const pre = btn.parentElement;
223
+ const code = pre.querySelector('code').innerText;
224
+ navigator.clipboard.writeText(code).then(() => {
225
+ const oldText = btn.innerText;
226
+ btn.innerText = 'COPIED!';
227
+ setTimeout(() => { btn.innerText = oldText; }, 2000);
228
+ });
229
+ }
230
+
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))
251
+ ui.markdown("### // SYSTEM STATUS: ONLINE\n// ACADEMIC CORE: READY").classes('text-center')
252
+ start_btn = ui.button("INITIALIZE INTERFACE").classes('start-btn mt-4 px-8 py-4 text-lg font-bold rounded text-white')
253
+
254
+ # --- 2. SIDEBAR ---
255
+ with ui.right_drawer(value=False).classes('drawer-bg p-4') as drawer:
256
+ ui.html(get_logo(width=200, height=60)).classes('mb-4')
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()
277
+
278
+ mode_select = ui.select(["Socratic", "Direct"], value="Socratic", label="Teaching Protocol").classes('w-full mt-2 text-white')
279
+ 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')
280
+ language_select = ui.select(["Java", "Python", "JavaScript", "C++", "C#", "SQL"], value="Python", label="Target Language").classes('w-full mt-2 text-white')
281
+
282
+ ui.separator().classes('my-4')
283
+ ui.label("Session Archives").classes('text-lg font-bold text-gray-300')
284
+
285
+ history_dropdown = ui.select([], label="Previous Chats").classes('w-full mt-2 text-white')
286
+
287
+ def archive_session():
288
+ if not chat_history: return
289
+ timestamp = datetime.datetime.now().strftime("%H:%M:%S")
290
+ label = f"Session {timestamp} ({len(chat_history)} msgs)"
291
+ session_storage[label] = chat_history.copy()
292
+ history_dropdown.options = list(session_storage.keys())
293
+ history_dropdown.update()
294
+ chat_history.clear()
295
+ render_messages.refresh()
296
+
297
+ ui.button("Archive Current Session", on_click=archive_session).props('outline rounded').classes('w-full mt-2 text-white')
298
+
299
+ def load_session(e):
300
+ if e.value in session_storage:
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')
307
+
308
+ # WEB MODIFICATION: File downloads via browser instead of OS file paths
309
+ def download_transcript():
310
+ if not chat_history:
311
+ ui.notify("No chat history to save.", type="warning")
312
+ return
313
+
314
+ transcript_text = "DACODEX MENTOR SESSION\n" + "="*30 + "\n\n"
315
+ for msg in chat_history:
316
+ prefix = "STUDENT" if msg["role"] == "user" else "MENTOR"
317
+ transcript_text += f"{prefix}:\n{msg['raw_text']}\n\n"
318
+
319
+ filename = f"DACodeX_Transcript_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.txt"
320
+
321
+ # Encodes text into memory and triggers a prompt in the User's Web Browser
322
+ ui.download(transcript_text.encode('utf-8'), filename)
323
+ ui.notify("Download initiated!", type='positive')
324
+
325
+ ui.button("Download Text File", on_click=download_transcript).classes('w-full mt-2 start-btn text-white')
326
+
327
+ # --- 3. MAIN CHAT AREA ---
328
+ with ui.column().classes('w-full h-screen relative') as main_chat_view:
329
+ main_chat_view.set_visibility(False)
330
+
331
+ with ui.row().classes('w-full p-4 border-b border-[#27272a] bg-[#121217] items-center justify-between z-10'):
332
+ ui.label('DACodeX - Coding Assistant').classes('text-xl font-bold ml-2 text-white')
333
+ ui.button(icon='menu', on_click=drawer.toggle).props('flat round dense color=white')
334
+
335
+ with ui.scroll_area().classes('flex-grow w-full p-4 pb-40') as scroll_area:
336
+ @ui.refreshable
337
+ def render_messages():
338
+ for index, msg in enumerate(chat_history):
339
+ with ui.chat_message(name=msg['name'], sent=msg['sent']):
340
+ ui.markdown(msg['text'], extras=['fenced-code-blocks', 'tables', 'cuddled-lists', 'breaks'])
341
+ for img_html in msg.get('images', []):
342
+ ui.html(img_html).classes('max-w-xs rounded mt-2')
343
+
344
+ render_messages()
345
+
346
+ # WEB MODIFICATION: Replaced native OS file picker with a Web Upload Component
347
+ with ui.dialog() as upload_dialog, ui.card().classes('bg-[#121217] border border-[#27272a] text-white'):
348
+ ui.label('Upload Reference Material').classes('text-lg font-bold')
349
+
350
+ def handle_web_upload(e: events.UploadEventArguments):
351
+ try:
352
+ content_bytes = e.content.read()
353
+ filename = e.name
354
+ ext = filename.split('.')[-1].lower() if '.' in filename else ''
355
+
356
+ if ext in ['png', 'jpg', 'jpeg', 'webp', 'gif']:
357
+ img = Image.open(io.BytesIO(content_bytes))
358
+ pending_uploads.append({'type': 'image', 'data': img, 'name': filename})
359
+ ui.notify(f"Attached Image: {filename}", type='positive')
360
+ else:
361
+ text_content = content_bytes.decode('utf-8', errors='ignore')
362
+ pending_uploads.append({'type': 'text', 'data': f"\n\n--- Uploaded File: {filename} ---\n{text_content}", 'name': filename})
363
+ ui.notify(f"Attached File: {filename}", type='positive')
364
+
365
+ render_previews.refresh()
366
+ upload_dialog.close()
367
+ except Exception as ex:
368
+ ui.notify(f"Upload failed: {str(ex)}", color='negative')
369
+
370
+ ui.upload(multiple=True, on_upload=handle_web_upload, auto_upload=True).classes('max-w-full')
371
+ ui.button('Close', on_click=upload_dialog.close).props('outline rounded').classes('w-full mt-2')
372
+
373
+
374
+ # --- 4. UNIFIED CHAT INPUT BOX ---
375
+ with ui.column().classes('absolute bottom-0 w-full p-4 bg-[#09090b] border-t border-[#27272a] z-10'):
376
+
377
+ # Unified Container Box
378
+ with ui.column().classes('w-full bg-[#121217] border border-[#27272a] rounded-xl p-1 gap-0'):
379
+
380
+ # Dynamic Preview Area
381
+ @ui.refreshable
382
+ def render_previews():
383
+ if pending_uploads:
384
+ with ui.row().classes('w-full gap-3 px-3 pt-3 pb-1 overflow-x-auto no-wrap'):
385
+ for idx, item in enumerate(pending_uploads):
386
+ 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'):
387
+ if item['type'] == 'image':
388
+ buffered = io.BytesIO()
389
+ item['data'].save(buffered, format="PNG")
390
+ img_str = base64.b64encode(buffered.getvalue()).decode()
391
+ ui.html(f'<img src="data:image/png;base64,{img_str}" style="width: 100%; height: 100%; object-fit: cover; border-radius: 6px;" />')
392
+ else:
393
+ ui.label('📄').classes('text-2xl')
394
+
395
+ def remove_item(i=idx):
396
+ pending_uploads.pop(i)
397
+ render_previews.refresh()
398
+
399
+ 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')
400
+
401
+ render_previews()
402
+
403
+ # Text Input Row
404
+ with ui.row().classes('w-full items-center no-wrap px-1 pb-1'):
405
+ # Opens the Web Upload dialog instead of a native window
406
+ ui.button(icon='attach_file', on_click=upload_dialog.open).props('flat round dense color=white')
407
+ text_input = ui.input(placeholder="Type your message...").classes('flex-grow px-2').props('borderless dark')
408
+ ui.button(icon='send', on_click=lambda: asyncio.create_task(send_message())).props('flat round dense color=primary')
409
+
410
+ async def send_message():
411
+ user_text = text_input.value.strip()
412
+ if not user_text and not pending_uploads:
413
+ ui.notify("Please provide some text or an image.", color='warning')
414
+ return
415
+
416
+ payload = []
417
+ images_for_ui = []
418
+ raw_text_record = user_text
419
+
420
+ if user_text:
421
+ payload.append(user_text)
422
+
423
+ for item in pending_uploads:
424
+ if item['type'] == 'image':
425
+ payload.append(item['data'])
426
+ raw_text_record += f"\n[Uploaded Image: {item['name']}]"
427
+ buffered = io.BytesIO()
428
+ item['data'].save(buffered, format="PNG")
429
+ img_str = base64.b64encode(buffered.getvalue()).decode()
430
+ images_for_ui.append(f'<img src="data:image/png;base64,{img_str}" />')
431
+ elif item['type'] == 'text':
432
+ payload.append(item['data'])
433
+ raw_text_record += f"\n[Uploaded File: {item['name']}]"
434
+
435
+ chat_history.append({
436
+ 'text': user_text if user_text else "📎 *(Attachments sent)*",
437
+ 'user_input_only': user_text,
438
+ 'name': 'Student',
439
+ 'sent': True,
440
+ 'role': 'user',
441
+ 'raw_text': raw_text_record,
442
+ 'images': images_for_ui
443
+ })
444
+
445
+ text_input.value = ""
446
+ pending_uploads.clear()
447
+ render_previews.refresh()
448
+ render_messages.refresh()
449
+ scroll_area.scroll_to(percent=1)
450
+
451
+ current_instruction = build_system_prompt(mode_select.value, language_select.value, course_select.value)
452
+
453
+ gemini_history = []
454
+ for msg in chat_history[:-1]:
455
+ role = msg['role']
456
+ gemini_history.append(types.Content(role=role, parts=[types.Part.from_text(text=msg['raw_text'])]))
457
+
458
+ try:
459
+ chat = client.aio.chats.create(
460
+ model=MODEL_ID,
461
+ config=types.GenerateContentConfig(
462
+ system_instruction=current_instruction,
463
+ temperature=0.7 if mode_select.value == "Socratic" else 0.2
464
+ ),
465
+ history=gemini_history
466
+ )
467
+
468
+ chat_history.append({'text': '', 'name': 'DACodeX', 'sent': False, 'role': 'model', 'raw_text': ''})
469
+ render_messages.refresh()
470
+ scroll_area.scroll_to(percent=1)
471
+
472
+ response_stream = await chat.send_message_stream(payload)
473
+ full_response = ""
474
+
475
+ async for chunk in response_stream:
476
+ if chunk.text:
477
+ full_response += chunk.text
478
+
479
+ # Update the UI with the natural chunks as they arrive
480
+ chat_history[-1]['text'] = full_response
481
+ chat_history[-1]['raw_text'] = full_response
482
+
483
+ # Refreshing per chunk is much easier on the network
484
+ render_messages.refresh()
485
+ scroll_area.scroll_to(percent=1)
486
+
487
+ except Exception as e:
488
+ ui.notify(f"🤖 Technical Hiccup: {str(e)}", color='negative')
489
+
490
+ text_input.on('keydown.enter', send_message)
491
+
492
+ def start_interface():
493
+ landing_view.set_visibility(False)
494
+ main_chat_view.set_visibility(True)
495
+ drawer.value = True
496
+
497
+ start_btn.on_click(start_interface)
498
+
499
+ # --- SERVER INITIALIZATION ---
500
+ if __name__ in {"__main__", "__mp_main__"}:
501
+ # WEB MODIFICATION: native=False, Host binding to 0.0.0.0, specific port
502
+ ui.run(
503
+ title="DACodeX - Academic Core",
504
+ dark=True,
505
+ host='0.0.0.0', # Listens to external devices on your network
506
+ port=8081, # The port clients will connect to
507
+ reload=False
508
+ )