import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Dialog } from "@headlessui/react"; import { Cog6ToothIcon } from "@heroicons/react/24/outline"; import { Exercise, CheckResult } from './types'; import { exercises as structExercises } from './data/exercises'; import { programmingExercises } from './data/programming_exercises'; 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'; // --- GEMINI API SETUP & HELPERS --- // (ai instance will be created in App component) const extractJson = (text: string): any => { let jsonString = text.trim(); const fenceRegex = /^```(?:json)?\s*\n?(.*?)\n?\s*```$/s; const match = jsonString.match(fenceRegex); if (match && match[1]) { jsonString = match[1].trim(); } 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); return JSON.parse(finalJsonString); }; const evaluateWithGemini = async ( ai: GoogleGenAI, userCode: string, solutionCode: string, prompt: string ): Promise<{ correct: boolean, explanation: string }> => { const systemInstruction = `You are an expert C programming tutor. Your task is to evaluate a user's code against a model solution for a specific exercise. The user's code might be functionally equivalent even if it looks different. Check for semantic correctness. For example, the order of members in a struct definition does not matter. Variable names can be different as long as they are used consistently and correctly. For programming tasks, focus on whether the user's code fulfills all requirements of the prompt. The exercise part is: "${prompt}" Analyze the user's C code and compare it to the provided solution. Respond ONLY with a single JSON object in the format: { "correct": , "explanation": "" } - "correct": true if the user's code is a valid and correct solution, false otherwise. - "explanation": If correct, provide a brief confirmation. If incorrect, provide a clear, concise explanation of what is wrong. Use markdown for formatting, especially for code blocks. **Your entire explanation must be in German.**`; const contents = `Here is the user's code:\n\`\`\`c\n${userCode}\n\`\`\`\n\nHere is the model solution for reference:\n\`\`\`c\n${solutionCode}\n\`\`\`\n\nPlease evaluate the user's code and provide your response in the specified JSON format.`; const response: GenerateContentResponse = await ai.models.generateContent({ model: "gemini-2.5-flash", 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', }} />
); }; interface CodeDiffViewerProps { oldCode: string; newCode: string; } const CodeDiffViewer: React.FC = ({ oldCode, newCode }) => { const differences = Diff.diffLines(oldCode.trim(), newCode.trim()); return (
            
                {differences.map((part, index) => {
                    const className = part.added 
                        ? 'bg-green-500/20' 
                        : part.removed 
                        ? 'bg-red-500/20' 
                        : '';
                    const prefix = part.added ? '+' : part.removed ? '-' : ' ';
                    
                    const lines = part.value.replace(/\n$/, '').split('\n');

                    return (
                        
                            {lines.map((line, i) => (
                                
{prefix} {line}
))}
); })}
); }; interface ExerciseSheetProps { exercise: Exercise; onNext: () => void; ai: GoogleGenAI | null; } const ExerciseSheet: React.FC = ({ exercise, onNext, ai }) => { const [inputs, setInputs] = useState>({}); const [results, setResults] = useState(null); const [isLoading, setIsLoading] = useState(false); useEffect(() => { setInputs({}); setResults(null); setIsLoading(false); }, [exercise]); const handleInputChange = (partId: string, value: string) => { setInputs(prev => ({ ...prev, [partId]: value })); }; const handleSubmit = async () => { setIsLoading(true); setResults(null); const partsToEvaluate = exercise.parts.map(part => ({ ...part, userInput: inputs[part.id] || '', })); const evaluationPromises = partsToEvaluate.map(part => { if (!part.userInput.trim()) { return Promise.resolve({ partId: part.id, isCorrect: false, explanation: "Keine Eingabe gemacht." }); } if (!ai || typeof ai.models !== "object" || !ai.models.generateContent) { return Promise.resolve({ partId: part.id, isCorrect: false, explanation: "API-Key fehlt oder ungültig, oder Gemini SDK nicht korrekt geladen.", error: "Gemini API nicht verfügbar" }); } return evaluateWithGemini(ai, part.userInput, part.solution, part.prompt) .then(evalResult => ({ partId: part.id, isCorrect: evalResult.correct, explanation: evalResult.explanation, })) .catch(e => ({ partId: part.id, isCorrect: false, explanation: '', error: e instanceof Error ? e.message : 'An unknown error occurred.' })); }); const finalResults = await Promise.all(evaluationPromises); setResults(finalResults); setIsLoading(false); }; const handleReset = () => { setInputs({}); setResults(null); setIsLoading(false); } const handleNextExercise = () => { onNext(); } const hasEvaluationErrors = results?.some(r => r.error); return (
{isLoading && (

Evaluating your code with Gemini...

)}

{exercise.title}

Prüfung Programmieren - C

{!results ? ( // --- EXERCISE VIEW ---
{exercise.parts.map(part => (

Teil {part.id}) ({part.points}P)

{part.prompt}
handleInputChange(part.id, value)} placeholder={`// Code für Teil ${part.id}) hier eingeben...`} />
))}
) : ( // --- RESULT VIEW ---

Auswertung

{results.map(result => { const part = exercise.parts.find(p => p.id === result.partId)!; const userInput = inputs[part.id] || ''; if (result.error) { return (

Teil {part.id}) - Evaluation Failed

Error: {result.error}

Ihre Eingabe:

{userInput || '(Keine Eingabe)'}
); } return (
{result.isCorrect ? : }

Teil {part.id})

{result.isCorrect ? "Korrekt" : "Fehlerhaft"}
{result.explanation}
{!result.isCorrect && userInput.trim() && (

Vergleich Ihrer Eingabe mit der Musterlösung:

- Ihre Eingabe + Korrekte Lösung
)}
); })} {hasEvaluationErrors && (

Bei einigen Teilen ist ein Fehler bei der Auswertung aufgetreten. Sie können es erneut versuchen.

)}
{exercise.explanation}
{hasEvaluationErrors && ( )}
)}
); }; // --- MAIN APP COMPONENT --- type Category = 'structs' | 'programming'; const exerciseData: Record = { structs: structExercises, programming: programmingExercises }; export default function App() { const [exerciseSets, setExerciseSets] = useState>({structs: [], programming: []}); const [category, setCategory] = useState('structs'); const [currentIndex, setCurrentIndex] = useState(0); // --- API KEY STATE --- const [apiKey, setApiKey] = useState(''); const [showSettings, setShowSettings] = useState(false); const [inputKey, setInputKey] = useState(''); // On mount, load API key from localStorage or env useEffect(() => { const stored = localStorage.getItem('gemini_api_key'); if (stored) { setApiKey(stored); } else if (process.env.API_KEY) { setApiKey(process.env.API_KEY); } }, []); // Gemini API instance (re-created if key changes) const ai = useMemo(() => apiKey ? new GoogleGenAI({ apiKey }) : null, [apiKey]); // Save API key to localStorage and state const handleSaveKey = () => { localStorage.setItem('gemini_api_key', inputKey); setApiKey(inputKey); setShowSettings(false); }; // UI logic useEffect(() => { if (showSettings) setInputKey(apiKey || ''); }, [showSettings, apiKey]); useEffect(() => { const shuffledStructs = [...exerciseData.structs].sort(() => Math.random() - 0.5); const shuffledProgramming = [...exerciseData.programming].sort(() => Math.random() - 0.5); setExerciseSets({ structs: shuffledStructs, programming: shuffledProgramming }); }, []); const handleCategoryChange = (newCategory: Category) => { if (newCategory !== category) { setCategory(newCategory); setCurrentIndex(0); } } const exercisesForCategory = useMemo(() => exerciseSets[category], [exerciseSets, category]); const handleNext = useCallback(() => { setCurrentIndex(prev => (prev + 1) % (exercisesForCategory.length || 1)); }, [exercisesForCategory.length]); const currentExercise = useMemo(() => exercisesForCategory[currentIndex], [exercisesForCategory, currentIndex]); if (exercisesForCategory.length === 0 || !currentExercise) { return
Lade Übungen...
; } const getTabClass = (tabCategory: Category) => { const isActive = category === tabCategory; return `px-4 py-3 text-sm md:text-base font-medium transition-all duration-200 border-b-2 focus:outline-none ${ isActive ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800 hover:border-slate-300' }`; } const getExerciseButtonTitle = (ex: Exercise) => { const parts = ex.title.split(/ - |: /); return (parts.pop() || ex.title).trim(); } return (
{/* Settings Button */} {/* Settings Modal */} setShowSettings(false)} className="relative z-50">
); }