import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Dialog } from "@headlessui/react"; import { Cog6ToothIcon, ChatBubbleLeftRightIcon } from "@heroicons/react/24/outline"; import * as Sentry from "@sentry/react"; import { Exercise, CheckResult, ExercisePartType } from './types'; import { swaExercises1 } from './data/swa_exercises.1'; import { swaExercises2 } from './data/swa_exercises.2'; import { swaExercises3 } from './data/swa_exercises.3'; import { swaExercises4 } from './data/swa_exercises.4'; import { swaExercises5 } from './data/swa_exercises.5'; import { swaExercises6 } from './data/swa_exercises.6'; import { swaExercises7 } from './data/swa_exercises.7'; import { swaExercisesExamples } from './data/swa_exercises.examples'; import { GoogleGenAI, GenerateContentResponse } from "@google/genai"; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import * as Diff from 'diff'; import Editor from 'react-simple-code-editor'; import Prism from 'prismjs/components/prism-core'; import 'prismjs/components/prism-clike'; import 'prismjs/components/prism-c'; import mermaid from 'mermaid'; // --- GEMINI API SETUP & HELPERS --- // (ai instance will be created in App component) const extractJson = (text: string): any => { let jsonString = text.trim(); // Remove BOM and other invisible characters jsonString = jsonString.replace(/^\uFEFF/, ''); // Remove BOM jsonString = jsonString.replace(/^[\u200B-\u200D\uFEFF]/g, ''); // Remove zero-width characters const fenceRegex = /^```(?:json)?\s*\n?(.*?)\n?\s*```$/s; const match = jsonString.match(fenceRegex); if (match && match[1]) { jsonString = match[1].trim(); } // Try to parse directly first (most common case) try { return JSON.parse(jsonString); } catch (directParseError) { // If direct parsing fails, try the bracket extraction method const lastBracket = jsonString.lastIndexOf('}'); if (lastBracket === -1) { throw new Error("No closing brace '}' found in API response."); } let openBraceCount = 0; let firstBracket = -1; for (let i = lastBracket; i >= 0; i--) { if (jsonString[i] === '}') { openBraceCount++; } else if (jsonString[i] === '{') { openBraceCount--; } if (openBraceCount === 0) { firstBracket = i; break; } } if (firstBracket === -1) { throw new Error("Could not find matching opening brace '{' in API response."); } const finalJsonString = jsonString.substring(firstBracket, lastBracket + 1); try { return JSON.parse(finalJsonString); } catch (finalParseError) { throw new Error(`JSON parsing failed: ${finalParseError.message}. Raw text: ${jsonString.substring(0, 100)}...`); } } }; const evaluateWithGemini = async ( ai: GoogleGenAI, userAnswer: string, solution: string, prompt: string ): Promise<{ correct: boolean, explanation: string }> => { const systemInstruction = `You are an expert Software Architecture (SWA) tutor. Your task is to evaluate a user's answer against a model solution for a specific exercise. The user's answer might be phrased differently but contain the correct key concepts. Focus on semantic correctness, not just literal string matching. For Mermaid diagrams, check if the user's diagram correctly represents the required structures and relationships.\n\nThe exercise is: "${prompt}"\n\nAnalyze the user's answer and compare it to the provided model solution.\n\nRespond ONLY with a single JSON object in the format:\n{\n "correct": ,\n "explanation": ""\n}\n\n- "correct": true if the user's answer is a valid and correct solution, false otherwise.\n- "explanation": If correct, provide a brief confirmation and elaborate on why the answer is good. If incorrect, provide a clear, concise explanation of what is wrong or missing. Use markdown for formatting. **Your entire explanation must be in German.**`; const contents = `Here is the user's answer: \`\`\` ${userAnswer} \`\`\` Here is the model solution for reference: \`\`\` ${solution} \`\`\` Please evaluate the user's answer and provide your response in the specified JSON format.`; const response: GenerateContentResponse = await ai.models.generateContent({ model: "gemini-2.5-flash-preview-04-17", contents: contents, config: { systemInstruction: systemInstruction, responseMimeType: "application/json", }, }); try { const jsonResponse = extractJson(response.text); if (typeof jsonResponse.correct !== 'boolean' || typeof jsonResponse.explanation !== 'string') { throw new Error("Invalid JSON structure from API."); } return jsonResponse; } catch (e) { console.error("Failed to parse Gemini response:", e); console.error("Raw response text:", response.text); throw new Error(`Could not parse the evaluation response.`); } } // --- UI ICONS --- const CheckIcon: React.FC<{ className?: string }> = ({ className }) => ( ); const CrossIcon: React.FC<{ className?: string }> = ({ className }) => ( ); const WarningIcon: React.FC<{ className?: string }> = ({ className }) => ( ); // --- SUB-COMPONENTS --- const CodeEditor: React.FC<{ value: string; onChange: (value: string) => void; placeholder?: string; }> = ({ value, onChange, placeholder }) => { return (
Prism.highlight(code, Prism.languages.c, 'c')} padding={12} placeholder={placeholder} textareaClassName="code-editor-textarea" className="caret-white" style={{ fontFamily: '"Roboto Mono", monospace', fontSize: 14, lineHeight: 1.5, minHeight: '12rem', color: '#f8f8f2', }} />
); }; const TextAreaEditor: React.FC<{ value: string; onChange: (value: string) => void; placeholder?: string; }> = ({ value, onChange, placeholder }) => { return (