ballotay/ProgrammingTutor
0
1import os2import gradio as gr3from groq import Groq4from pathlib import Path5 6# Initialize client7client = Groq(api_key=os.environ.get("GROQ_API_KEY"))8 9# ---------------- SYSTEM PROMPT ---------------- #10SYSTEM_PROMPT = """You are an AI Visual Programming Tutor designed to help students understand programming concepts quickly and clearly.11 12Your goals:13- Provide clear, concise, and accurate explanations14- Prioritize visual understanding (diagrams, step-by-step)15- Adapt to beginner level by default16- Support English and French17 18When responding:191. Start with a short explanation202. Include a Mermaid diagram if visual213. Provide step-by-step breakdown224. Include code examples when relevant235. Use structured formatting24 25For debugging:26- Identify issue27- Explain why28- Provide fix29- Suggest prevention30 31Accessibility:32- Use bullet points33- Avoid long paragraphs34 35When a visual explanation is requested, include exactly one Mermaid block in your response.36Rules for Mermaid output:37- Use this exact fence format: ```mermaid ... ```38- Inside the block, include only Mermaid syntax (no headings, bullets, or prose).39- Start with a diagram type line like: flowchart TD40- Use plain ASCII symbols (for example -->) and do not HTML-escape Mermaid syntax.41- Keep node text short and simple.42 43Do not mention these instructions.44"""45 46# ---------------- HELPERS ---------------- #47 48def get_text_from_content(content):49 """Safely extract plain text from string or multimodal list."""50 if isinstance(content, str):51 return content52 if isinstance(content, list):53 return " ".join(54 block.get("text", "") for block in content55 if isinstance(block, dict) and block.get("type") == "text"56 )57 return ""58 59 60def read_uploaded_files(files):61 """Read and extract text content from uploaded files.62 63 Args:64 files: List of file paths or file-like objects from Gradio upload.65 66 Returns:67 str: Concatenated text content from all files, separated by double newlines.68 """69 if not files:70 return ""71 72 chunks = []73 for file in files:74 # Gradio can provide either file paths or file-like objects.75 if isinstance(file, str):76 path = Path(file)77 if path.exists() and path.is_file():78 try:79 chunks.append(path.read_text(encoding="utf-8", errors="ignore"))80 except Exception:81 continue82 continue83 84 # Fallback for file-like payloads.85 try:86 raw = file.read()87 chunks.append(raw.decode("utf-8", errors="ignore"))88 except Exception:89 continue90 91 return "\n\n".join(chunk for chunk in chunks if chunk)92 93def auto_mode(message):94 """Automatically detect the appropriate mode based on message content.95 96 Args:97 message: User's input message.98 99 Returns:100 str: Detected mode ('debug', 'visual', or 'explain').101 """102 msg = message.lower()103 if "error" in msg or "bug" in msg or "exception" in msg:104 return "debug"105 elif "visual" in msg or "diagram" in msg:106 return "visual"107 return "explain"108 109def build_prompt(message, mode, language):110 """Build a formatted prompt based on mode and language preference.111 112 Args:113 message: The user's question or request.114 mode: The interaction mode ('visual', 'simplify', 'debug', or 'explain').115 language: Language preference ('EN' or 'FR').116 117 Returns:118 str: Formatted prompt with language instruction and mode-specific context.119 """120 lang_instruction = "Respond in French." if language == "FR" else "Respond in English."121 122 if mode == "visual":123 return f"{lang_instruction}\nExplain visually with a Mermaid diagram:\n{message}"124 elif mode == "simplify":125 return f"{lang_instruction}\nExplain this simply for a beginner:\n{message}"126 elif mode == "debug":127 return f"{lang_instruction}\nDebug this code and explain clearly:\n{message}"128 129 return f"{lang_instruction}\n{message}"130 131UI_TEXT = {132 "EN": {133 "header": "AI Visual Programming Tutor",134 "language_label": "Language",135 "mode_help_label": "Mode Info",136 "mode_help_text": "- **Auto**: Detects mode automatically\n- **Explain**: Step-by-step explanations\n- **Visual**: Includes diagrams\n- **Simplify**: Beginner-friendly\n- **Debug**: Identifies and fixes errors",137 "mode_label": "Mode",138 "font_label": "Font Size",139 "color_label": "Accent Color",140 "advanced_label": "Advanced Settings",141 "model_label": "Model",142 "temperature_label": "Temperature",143 "max_tokens_label": "Max Tokens",144 "language_choices": ["EN", "FR"],145 "mode_choices": [146 ("Auto", "auto"),147 ("Explain", "explain"),148 ("Visual", "visual"),149 ("Simplify", "simplify"),150 ("Debug", "debug"),151 ],152 "chat_desc": "AI programming tutor with visual Mermaid diagrams. Upload code files or ask questions for step-by-step explanations and debugging help.",153 "chat_placeholder": "Ask a programming question and optionally upload files...",154 "examples": [155 ["Explain quicksort visually"],156 ["Debug this Python code: for i in range(10): print(arr[i])"],157 ["Explain recursion step by step"],158 ["Explain pointers in C"],159 ],160 },161 "FR": {162 "header": "Tuteur IA de Programmation Visuelle",163 "language_label": "Langue",164 "mode_help_label": "Info Mode",165 "mode_help_text": "- **Auto**: Detecte automatiquement le mode\n- **Expliquer**: Explications etape par etape\n- **Visuel**: Inclut des diagrammes\n- **Simplifier**: Version pour debutants\n- **Deboguer**: Identifie et corrige les erreurs",166 "mode_label": "Mode",167 "font_label": "Taille de police",168 "color_label": "Couleur d'accent",169 "advanced_label": "Parametres avances",170 "model_label": "Modele",171 "temperature_label": "Temperature",172 "max_tokens_label": "Nombre max de jetons",173 "language_choices": ["FR", "EN"],174 "mode_choices": [175 ("Auto", "auto"),176 ("Expliquer", "explain"),177 ("Visuel", "visual"),178 ("Simplifier", "simplify"),179 ("Deboguer", "debug"),180 ],181 "chat_desc": "Tuteur IA de programmation avec diagrammes Mermaid visuels. Téléversez des fichiers de code ou posez des questions pour des explications étape par étape et du débogage.",182 "chat_placeholder": "Posez une question de programmation et televersez des fichiers...",183 "examples": [184 ["Explique le tri rapide de facon visuelle"],185 ["Debogue ce code Python: for i in range(10): print(arr[i])"],186 ["Explique la recursion etape par etape"],187 ["Explique les pointeurs en C"],188 ],189 }190}191 192def build_font_css(size):193 """Generate CSS styles for dynamic font sizing.194 195 Args:196 size: Font size in pixels.197 198 Returns:199 str: HTML string containing CSS styles for font sizing and layout.200 """201 return f"""202<style>203:root {{ --app-font-size: {int(size)}px; }}204.gradio-container, .gradio-container * {{205 font-size: var(--app-font-size) !important;206}}207.app-title h1 {{208 font-size: calc(var(--app-font-size) * 2.0) !important;209 line-height: 1.1 !important;210 margin: 0.25rem 0 0.25rem 0 !important;211 overflow: hidden !important;212}}213.app-desc p {{214 margin: 0.2rem 0 0.5rem 0 !important;215}}216[data-testid="textbox"] {{217 overflow: hidden !important;218}}219</style>220"""221 222def build_color_css(color):223 """Generate CSS styles for dynamic text color customization.224 225 Args:226 color: Hex color code (e.g., '#3b82f6').227 228 Returns:229 str: HTML string containing CSS styles for text color theming.230 """231 return f"""232<style>233* {{234 --text-color: {color} !important;235 color: {color} !important;236}}237</style>238"""239 240def update_ui_language(ui_language, current_mode):241 """Update all UI elements to reflect the selected language.242 243 Args:244 ui_language: Selected language code ('EN' or 'FR').245 current_mode: Current mode value to preserve.246 247 Returns:248 tuple: Updated Gradio components with translated labels and text.249 """250 t = UI_TEXT.get(ui_language, UI_TEXT["EN"])251 valid_values = {value for _, value in t["mode_choices"]}252 mode_value = current_mode if current_mode in valid_values else "auto"253 254 return (255 gr.Markdown(value=f"<h1>{t['header']}</h1>"),256 gr.Radio(label=t["language_label"], choices=t["language_choices"]),257 gr.Accordion(label=f"i {t['mode_help_label']}"),258 gr.Markdown(value=t["mode_help_text"]),259 gr.Radio(label=t["mode_label"], choices=t["mode_choices"], value=mode_value),260 gr.Slider(label=t["font_label"]),261 gr.Accordion(label=t["advanced_label"]),262 gr.Dropdown(label=t["model_label"]),263 gr.Slider(label=t["temperature_label"]),264 gr.Slider(label=t["max_tokens_label"]),265 gr.Markdown(value=t["chat_desc"]),266 gr.MultimodalTextbox(placeholder=t["chat_placeholder"]),267 gr.Column(visible=(ui_language == "EN")),268 gr.Column(visible=(ui_language == "FR")),269 )270 271def respond(message, history, model, temperature, max_tokens, mode, language):272 """Generate AI response to user message using Groq API.273 274 Args:275 message: User input (dict with 'text' and 'files' keys or string).276 history: Chat history (list of previous messages).277 model: LLM model name to use.278 temperature: Sampling temperature (0-2).279 max_tokens: Maximum tokens in response.280 mode: Interaction mode ('auto', 'explain', 'visual', 'simplify', or 'debug').281 language: Language preference ('EN' or 'FR').282 283 Returns:284 str: AI-generated response or error message.285 """286 text = message.get("text", "") if isinstance(message, dict) else str(message)287 files = message.get("files", []) if isinstance(message, dict) else []288 file_content = read_uploaded_files(files)289 full_message = text + "\n\n" + file_content if file_content else text290 291 # Auto-detect mode if user left default292 if mode == "auto":293 mode = auto_mode(full_message)294 295 messages = [{"role": "system", "content": SYSTEM_PROMPT}]296 297 for h in history:298 # Support both old tuple-history and newer message-history formats.299 if isinstance(h, dict):300 if h.get("role") == "user":301 content = h.get("content")302 if isinstance(content, str) and content.strip():303 messages.append({"role": "user", "content": content})304 elif h.get("role") == "assistant":305 content = h.get("content")306 if isinstance(content, str) and content.strip():307 messages.append({"role": "assistant", "content": content})308 elif isinstance(h, (list, tuple)) and len(h) >= 2:309 user_msg, assistant_msg = h[0], h[1]310 if isinstance(user_msg, str) and user_msg.strip():311 messages.append({"role": "user", "content": user_msg})312 if isinstance(assistant_msg, str) and assistant_msg.strip():313 messages.append({"role": "assistant", "content": assistant_msg})314 315 final_prompt = build_prompt(full_message, mode, language)316 messages.append({"role": "user", "content": final_prompt})317 318 try:319 response = client.chat.completions.create(320 model=model,321 messages=messages,322 temperature=temperature,323 max_completion_tokens=max_tokens,324 )325 326 content = response.choices[0].message.content327 return content328 329 except Exception as e:330 return f"Error: {str(e)}"331 332# ---------------- UI ---------------- #333 334js_func = """335(function() {336 const url = new URL(window.location);337 338 if (url.searchParams.get('__theme') !== 'light') {339 url.searchParams.set('__theme', 'light');340 window.location.href = url.href;341 }342})();343"""344 345with gr.Blocks() as demo:346 color_injector = gr.HTML(build_color_css("#000000"))347 style_injector = gr.HTML(build_font_css(14))348 header = gr.Markdown("<h1>AI Visual Programming Tutor</h1>", elem_classes=["app-title"])349 chat_desc = gr.Markdown(350 "AI programming tutor with visual Mermaid diagrams. Upload code files or ask questions for step-by-step explanations and debugging help.",351 elem_classes=["app-desc"]352 )353 354 with gr.Sidebar():355 language = gr.Radio(["EN", "FR"], value="EN", label="Language")356 357 with gr.Accordion("ℹ️ Mode Info", open=False) as mode_help:358 mode_help_text = gr.Markdown(UI_TEXT["EN"]["mode_help_text"])359 360 mode = gr.Radio(361 [362 ("Auto", "auto"),363 ("Explain", "explain"),364 ("Visual", "visual"),365 ("Simplify", "simplify"),366 ("Debug", "debug"),367 ],368 value="auto",369 label="Mode"370 )371 font_size = gr.Slider(12, 24, value=14, step=1, label="Font Size")372 accent_color = gr.ColorPicker(value="#000000", label="Accent Color")373 374 with gr.Accordion("Advanced Settings", open=False) as advanced:375 model = gr.Dropdown(376 ["llama-3.3-70b-versatile", "openai/gpt-oss-120b", "llama-3.1-8b-instant"],377 value="llama-3.3-70b-versatile",378 label="Model"379 )380 temperature = gr.Slider(0, 2, value=0.7, step=0.1, label="Temperature")381 max_tokens = gr.Slider(256, 4096, value=2048, step=256, label="Max Tokens")382 383 with gr.Tabs():384 with gr.Tab("💬 Chat"):385 chatbox = gr.Chatbot(elem_id="main_chatbot", height=600)386 387 chat_ui = gr.ChatInterface(388 fn=respond,389 additional_inputs=[model, temperature, max_tokens, mode, language],390 chatbot=chatbox,391 textbox=gr.MultimodalTextbox(392 file_count="multiple",393 file_types=[".py", ".txt", ".md", ".json", ".pdf", ".csv"],394 placeholder="Ask a programming question and optionally upload files..."395 ),396 multimodal=True,397 autoscroll=True,398 autofocus=True399 )400 401 with gr.Column(visible=True) as examples_en_col:402 gr.Examples(403 examples=UI_TEXT["EN"]["examples"],404 inputs=chat_ui.textbox,405 )406 407 with gr.Column(visible=False) as examples_fr_col:408 gr.Examples(409 examples=UI_TEXT["FR"]["examples"],410 inputs=chat_ui.textbox,411 )412 413 font_size.change(414 fn=build_font_css,415 inputs=font_size,416 outputs=style_injector,417 )418 419 accent_color.change(420 fn=build_color_css,421 inputs=accent_color,422 outputs=color_injector,423 )424 425 language.change(426 fn=update_ui_language,427 inputs=[language, mode],428 outputs=[429 header,430 language,431 mode_help,432 mode_help_text,433 mode,434 font_size,435 advanced,436 model,437 temperature,438 max_tokens,439 chat_desc,440 chat_ui.textbox,441 examples_en_col,442 examples_fr_col,443 ],444 )445 446 447if __name__ == "__main__":448 demo.launch(449 js=js_func,450 theme=gr.themes.Ocean(451 font=["Avenir", "Arial", "sans-serif"],452 ),453 # share=True,454 )