SamCChauhan commited on
Commit
3dd47e4
·
unverified ·
1 Parent(s): d84def0

Add Gradio-based chatbot application

Browse files
Files changed (1) hide show
  1. app.py +425 -0
app.py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.
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
+
30
+ PEDAGOGY_SOCRATIC = """
31
+ STRATEGY (SOCRATIC MODE):
32
+ - Act like a good instructor, not like Stack Overflow.
33
+ - Use scaffolded instruction: hints → partial guidance → full solution (only as an absolute last resort).
34
+ - Ask guiding questions to encourage student reasoning and productive struggle before revealing answers.
35
+ - Never act as a shortcut solution generator.
36
+ """
37
+
38
+ PEDAGOGY_DIRECT = """
39
+ STRATEGY (DIRECT INSTRUCTION MODE):
40
+ - Provide direct, clear explanations of concepts and syntax.
41
+ - Use very small code snippets (max 3-5 lines) to demonstrate specific rules.
42
+ - Explain the 'WHY' behind the code and how the computer handles it.
43
+ - Do not write their entire assignment for them; focus on the specific concept they are stuck on.
44
+ """
45
+
46
+ CODE_AWARENESS = """
47
+ CODE & LANGUAGE CAPABILITIES:
48
+ - You fully understand the syntax, semantics, and common beginner mistakes of {language}.
49
+ - When evaluating {language} code or reviewing screenshots of code, explain what it does, why it fails, and how to fix it.
50
+ - Use simple, precise, age-appropriate explanations, avoiding heavy professional jargon.
51
+ """
52
+
53
+ ERROR_HANDLING = """
54
+ ERROR FOCUS & DEBUGGING-FIRST:
55
+ - Treat errors as learning opportunities, not failures.
56
+ - Interpret compiler errors, runtime errors, and logic errors in plain English.
57
+ - Encourage debugging strategies: code tracing, print statements, test cases, and rubber-duck reasoning.
58
+ - 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."
59
+ """
60
+
61
+ ADAPTABILITY_AND_TONE = """
62
+ ADAPTABILITY & TONE (AFFECTIVE COMPUTING):
63
+ - Detect the student's level based on their questions and code complexity, adjusting your vocabulary, pace, and depth.
64
+ - Challenge advanced students with "What if..." scenarios, optimization prompts, and edge-case analysis.
65
+ - Maintain a patient, non-judgmental, calm, and encouraging tone.
66
+ - Use phrases like "You're close" or "This is a common mistake." Never shame or ridicule; normalize confusion.
67
+ """
68
+
69
+ TRANSPARENCY_AND_ASSESSMENT = """
70
+ TRANSPARENCY & ASSESSMENT AWARENESS:
71
+ - No Black Boxes: Explain why a solution works. Show step-by-step execution, variable state changes, or call stack evolution.
72
+ - Encourage mental models, not memorization.
73
+ - Understand AP-style coding task verbs: Predict, Trace, Debug, Modify.
74
+ - Can simulate Free-Response Questions, output prediction, and code completion.
75
+ - Grade and evaluate the student's *thinking* and logic, not just the correctness of the final code.
76
+ - Prevent misuse: Never complete graded assignments for the student. Prioritize student learning over speed of answers.
77
+ """
78
+
79
+ def build_system_prompt(mode, language, course):
80
+ lang_label = language if language else "General Programming"
81
+ course_label = course if course else "General Computer Science"
82
+ prompt_parts = [BASE_PERSONA.format(course=course_label, language=lang_label)]
83
+
84
+ if mode == "Socratic":
85
+ prompt_parts.append(PEDAGOGY_SOCRATIC)
86
+ else:
87
+ prompt_parts.append(PEDAGOGY_DIRECT)
88
+
89
+ prompt_parts.append(CODE_AWARENESS.format(language=lang_label))
90
+ prompt_parts.append(ERROR_HANDLING)
91
+ prompt_parts.append(ADAPTABILITY_AND_TONE)
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"""
312
+ <div style="display: flex; justify-content: center; align-items: center; padding: 20px 0;">
313
+ <svg width="{width}" height="{height}" viewBox="0 0 400 100" fill="none" xmlns="http://www.w3.org/2000/svg">
314
+ <defs>
315
+ <filter id="neonRed" x="-20%" y="-20%" width="140%" height="140%">
316
+ <feGaussianBlur stdDeviation="3" result="blur" />
317
+ <feComposite in="SourceGraphic" in2="blur" operator="over" />
318
+ </filter>
319
+ </defs>
320
+ <path d="M40 30L20 50L40 70" stroke="#dc2626" stroke-width="5" stroke-linecap="round" filter="url(#neonRed)"/>
321
+ <path d="M70 30L90 50L70 70" stroke="#dc2626" stroke-width="5" stroke-linecap="round" filter="url(#neonRed)"/>
322
+ <text x="100" y="65" fill="white" style="font-family:'JetBrains Mono', monospace; font-weight:800; font-size:45px;">DA</text>
323
+ <text x="165" y="65" fill="#dc2626" style="font-family:'JetBrains Mono', monospace; font-weight:800; font-size:45px;" filter="url(#neonRed)">CODE</text>
324
+ <text x="285" y="65" fill="white" style="font-family:'JetBrains Mono', monospace; font-weight:200; font-size:45px;">X</text>
325
+ <rect x="100" y="75" width="230" height="2" fill="#dc2626" fill-opacity="0.3"/>
326
+ </svg>
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)