CoolFace
Apppublic

ballotay/ProgrammingTutor

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
app.py454 linesDownload Raw Back to root
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    )