""" AI Processor - Mindmap Generator Generate mindmaps for learning posters. """ from pathlib import Path import json import logging import math import os import requests from ..config import BEREINIGT_DIR, get_openai_api_key logger = logging.getLogger(__name__) def generate_mindmap_data(analysis_path: Path) -> dict: """ Extract technical terms from analysis and group them for a mindmap. Args: analysis_path: Path to *_analyse.json file Returns: Dictionary with mindmap structure: { "topic": "Main topic", "subject": "Subject", "categories": [ { "name": "Category", "color": "#hexcolor", "emoji": "🔬", "terms": [ {"term": "Term", "explanation": "Short explanation"} ] } ] } """ if not analysis_path.exists(): raise FileNotFoundError(f"Analysedatei nicht gefunden: {analysis_path}") try: data = json.loads(analysis_path.read_text(encoding="utf-8")) except json.JSONDecodeError as e: raise RuntimeError(f"Analyse-Datei enthaelt kein gueltiges JSON: {analysis_path}\n{e}") from e title = data.get("title") or "Arbeitsblatt" subject = data.get("subject") or "" canonical_text = data.get("canonical_text") or "" tasks = data.get("tasks", []) or [] # Collect all text for analysis all_text = canonical_text for task in tasks: if task.get("description"): all_text += "\n" + task.get("description") if task.get("text_with_gaps"): all_text += "\n" + task.get("text_with_gaps") if not all_text.strip(): return { "topic": title, "subject": subject, "categories": [] } # AI-based extraction of technical terms api_key = get_openai_api_key() prompt = f"""Analysiere diesen Schultext und extrahiere alle Fachbegriffe fuer eine kindgerechte Lern-Mindmap. THEMA: {title} FACH: {subject} TEXT: {all_text[:3000]} AUFGABE: 1. Identifiziere das Hauptthema (ein einzelnes Wort oder kurzer Begriff) 2. Finde ALLE Fachbegriffe und gruppiere sie in 3-6 sinnvolle Kategorien 3. Gib fuer jeden Begriff eine kurze, kindgerechte Erklaerung (max 10 Woerter) 4. Waehle fuer jede Kategorie ein passendes Emoji und eine Farbe Antworte NUR mit diesem JSON-Format: {{ "topic": "Hauptthema (z.B. 'Das Auge')", "categories": [ {{ "name": "Kategoriename", "emoji": "passendes Emoji", "color": "#Hexfarbe (bunt, kindgerecht)", "terms": [ {{"term": "Fachbegriff", "explanation": "Kurze Erklaerung"}} ] }} ] }} WICHTIG: - Verwende kindgerechte, einfache Sprache - Bunte, froehliche Farben: #FF6B6B, #4ECDC4, #45B7D1, #96CEB4, #FFEAA7, #DDA0DD, #98D8C8 - Passende Emojis fuer jede Kategorie - Mindestens 3 Begriffe pro Kategorie wenn moeglich - Maximal 6 Kategorien""" try: # Try Claude first claude_key = os.environ.get("ANTHROPIC_API_KEY") if claude_key: import anthropic client = anthropic.Anthropic(api_key=claude_key) response = client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=2000, messages=[{"role": "user", "content": prompt}] ) result_text = response.content[0].text else: # Fallback to OpenAI logger.info("Claude Mindmap-Generierung fehlgeschlagen, nutze OpenAI: ANTHROPIC_API_KEY ist nicht gesetzt.") url = "https://api.openai.com/v1/chat/completions" headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} payload = { "model": "gpt-4o-mini", "messages": [ {"role": "system", "content": "Du bist ein Experte fuer kindgerechte Lernmaterialien."}, {"role": "user", "content": prompt} ], "max_tokens": 2000, "temperature": 0.7 } resp = requests.post(url, headers=headers, json=payload, timeout=60) resp.raise_for_status() result_text = resp.json()["choices"][0]["message"]["content"] # Extract JSON result_text = result_text.strip() if result_text.startswith("```"): result_text = result_text.split("```")[1] if result_text.startswith("json"): result_text = result_text[4:] result_text = result_text.strip() mindmap_data = json.loads(result_text) mindmap_data["subject"] = subject return mindmap_data except Exception as e: logger.error(f"Mindmap-Generierung fehlgeschlagen: {e}") return { "topic": title, "subject": subject, "categories": [] } def generate_mindmap_html(mindmap_data: dict, format: str = "a3") -> str: """ Generate a child-friendly HTML/SVG mindmap poster. Args: mindmap_data: Dictionary from generate_mindmap_data() format: "a3" for A3 poster (default) or "a4" for A4 view Returns: HTML string with SVG mindmap """ topic = mindmap_data.get("topic", "Thema") subject = mindmap_data.get("subject", "") categories = mindmap_data.get("categories", []) # Format-specific settings if format.lower() == "a4": page_size = "A4 landscape" svg_width = 1100 svg_height = 780 radius = 250 else: # a3 (default) page_size = "A3 landscape" svg_width = 1400 svg_height = 990 radius = 320 # If no categories, show placeholder if not categories: return f""" Mindmap - {topic}

🧠 Mindmap: {topic}

Noch keine Daten vorhanden. Bitte zuerst das Arbeitsblatt analysieren.

""" num_categories = len(categories) center_x = svg_width // 2 center_y = svg_height // 2 # Calculate positions of categories in a circle category_positions = [] for i, cat in enumerate(categories): angle = (2 * math.pi * i / num_categories) - (math.pi / 2) # Start at top x = center_x + radius * math.cos(angle) y = center_y + radius * math.sin(angle) category_positions.append({ "x": x, "y": y, "angle": angle, "data": cat }) html = _get_mindmap_html_header(topic, subject, page_size, svg_width, svg_height) # Draw connection lines for pos in category_positions: color = pos["data"].get("color", "#4ECDC4") html += f""" """ # Center (main topic) html += f""" 🌟 {topic} """ # Draw categories with their terms for i, pos in enumerate(category_positions): cat = pos["data"] cat_x = pos["x"] cat_y = pos["y"] color = cat.get("color", "#4ECDC4") emoji = cat.get("emoji", "📚") name = cat.get("name", "Kategorie") terms = cat.get("terms", []) # Category bubble html += f""" {emoji} {name} """ # Terms around the category term_radius = 110 num_terms = len(terms) for j, term_data in enumerate(terms[:8]): # Max 8 terms per category term = term_data.get("term", "") # Calculate position relative to category base_angle = pos["angle"] spread = math.pi * 0.8 # 80% of a half circle if num_terms > 1: term_angle = base_angle - spread/2 + (spread * j / (num_terms - 1)) else: term_angle = base_angle term_x = term_radius * math.cos(term_angle - base_angle) term_y = term_radius * math.sin(term_angle - base_angle) # Small connection line html += f""" """ # Term bubble bubble_width = max(70, len(term) * 8 + 20) html += f""" {term} """ html += " \n" # Legend with explanations (bottom) html += f""" 📖 Begriffe zum Lernen: """ legend_x = 0 for i, pos in enumerate(category_positions): cat = pos["data"] color = cat.get("color", "#4ECDC4") emoji = cat.get("emoji", "📚") name = cat.get("name", "") terms = cat.get("terms", []) terms_text = ", ".join([t.get("term", "") for t in terms[:3]]) if len(terms) > 3: terms_text += "..." html += f""" {emoji} {name}: {terms_text} """ legend_x += 220 html += """ """ return html def save_mindmap_for_worksheet(analysis_path: Path, mindmap_data: dict = None) -> Path: """ Save a mindmap for a worksheet. Args: analysis_path: Path to *_analyse.json file mindmap_data: Optional - already generated mindmap data. If not provided, it will be generated. Returns: Path to saved *_mindmap.json file """ if mindmap_data is None: mindmap_data = generate_mindmap_data(analysis_path) # Save JSON out_name = analysis_path.stem.replace("_analyse", "") + "_mindmap.json" out_path = BEREINIGT_DIR / out_name out_path.write_text(json.dumps(mindmap_data, ensure_ascii=False, indent=2), encoding="utf-8") logger.info(f"Mindmap-Daten gespeichert: {out_path.name}") return out_path def _get_mindmap_html_header(topic: str, subject: str, page_size: str, svg_width: int, svg_height: int) -> str: """Get HTML header for mindmap.""" return f""" Lernposter - {topic}
🧠 Lernposter: {topic}
{subject}
"""