A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
381 lines
11 KiB
Python
381 lines
11 KiB
Python
"""
|
|
Mindmap Generator - Erstellt Mindmaps aus Quelltexten.
|
|
|
|
Generiert:
|
|
- Hierarchische Struktur aus Text
|
|
- Hauptthema mit Unterthemen
|
|
- Verbindungen und Beziehungen
|
|
"""
|
|
|
|
import logging
|
|
import json
|
|
import re
|
|
from typing import List, Dict, Any, Optional
|
|
from dataclasses import dataclass, field
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class MindmapNode:
|
|
"""Ein Knoten in der Mindmap."""
|
|
id: str
|
|
label: str
|
|
level: int = 0
|
|
children: List['MindmapNode'] = field(default_factory=list)
|
|
color: Optional[str] = None
|
|
icon: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
@dataclass
|
|
class Mindmap:
|
|
"""Eine komplette Mindmap."""
|
|
root: MindmapNode
|
|
title: str
|
|
topic: Optional[str] = None
|
|
total_nodes: int = 0
|
|
|
|
|
|
class MindmapGenerator:
|
|
"""
|
|
Generiert Mindmaps aus Quelltexten.
|
|
|
|
Extrahiert:
|
|
- Hauptthema als Zentrum
|
|
- Unterthemen als Äste
|
|
- Details als Blätter
|
|
"""
|
|
|
|
def __init__(self, llm_client=None):
|
|
"""
|
|
Initialisiert den Generator.
|
|
|
|
Args:
|
|
llm_client: Optional - LLM-Client für intelligente Generierung
|
|
"""
|
|
self.llm_client = llm_client
|
|
logger.info("MindmapGenerator initialized")
|
|
|
|
# Farben für verschiedene Ebenen
|
|
self.level_colors = [
|
|
"#6C1B1B", # Weinrot (Zentrum)
|
|
"#3b82f6", # Blau
|
|
"#22c55e", # Grün
|
|
"#f59e0b", # Orange
|
|
"#8b5cf6", # Violett
|
|
]
|
|
|
|
def generate(
|
|
self,
|
|
source_text: str,
|
|
title: Optional[str] = None,
|
|
max_depth: int = 3,
|
|
topic: Optional[str] = None
|
|
) -> Mindmap:
|
|
"""
|
|
Generiert eine Mindmap aus einem Quelltext.
|
|
|
|
Args:
|
|
source_text: Der Ausgangstext
|
|
title: Optionaler Titel (sonst automatisch ermittelt)
|
|
max_depth: Maximale Tiefe der Hierarchie
|
|
topic: Optionales Thema
|
|
|
|
Returns:
|
|
Mindmap-Objekt
|
|
"""
|
|
logger.info(f"Generating mindmap (max_depth: {max_depth})")
|
|
|
|
if not source_text or len(source_text.strip()) < 50:
|
|
logger.warning("Source text too short")
|
|
return self._empty_mindmap(title or "Mindmap")
|
|
|
|
if self.llm_client:
|
|
return self._generate_with_llm(source_text, title, max_depth, topic)
|
|
else:
|
|
return self._generate_automatic(source_text, title, max_depth, topic)
|
|
|
|
def _generate_with_llm(
|
|
self,
|
|
source_text: str,
|
|
title: Optional[str],
|
|
max_depth: int,
|
|
topic: Optional[str]
|
|
) -> Mindmap:
|
|
"""Generiert Mindmap mit LLM."""
|
|
prompt = f"""
|
|
Erstelle eine Mindmap-Struktur auf Deutsch basierend auf folgendem Text.
|
|
{f'Titel: {title}' if title else 'Ermittle einen passenden Titel.'}
|
|
Maximale Tiefe: {max_depth} Ebenen
|
|
{f'Thema: {topic}' if topic else ''}
|
|
|
|
Text:
|
|
{source_text}
|
|
|
|
Erstelle eine hierarchische Struktur mit:
|
|
- Einem zentralen Hauptthema
|
|
- 3-5 Hauptästen (Unterthemen)
|
|
- Jeweils 2-4 Details pro Ast
|
|
|
|
Antworte im JSON-Format:
|
|
{{
|
|
"title": "Hauptthema",
|
|
"branches": [
|
|
{{
|
|
"label": "Unterthema 1",
|
|
"children": [
|
|
{{"label": "Detail 1.1"}},
|
|
{{"label": "Detail 1.2"}}
|
|
]
|
|
}},
|
|
{{
|
|
"label": "Unterthema 2",
|
|
"children": [
|
|
{{"label": "Detail 2.1"}}
|
|
]
|
|
}}
|
|
]
|
|
}}
|
|
"""
|
|
|
|
try:
|
|
response = self.llm_client.generate(prompt)
|
|
data = json.loads(response)
|
|
return self._create_mindmap_from_llm(data, topic)
|
|
except Exception as e:
|
|
logger.error(f"Error generating with LLM: {e}")
|
|
return self._generate_automatic(source_text, title, max_depth, topic)
|
|
|
|
def _generate_automatic(
|
|
self,
|
|
source_text: str,
|
|
title: Optional[str],
|
|
max_depth: int,
|
|
topic: Optional[str]
|
|
) -> Mindmap:
|
|
"""Generiert Mindmap automatisch ohne LLM."""
|
|
# Extrahiere Struktur aus Text
|
|
sections = self._extract_sections(source_text)
|
|
|
|
# Bestimme Titel
|
|
if not title:
|
|
# Erste Zeile oder erstes Substantiv
|
|
first_line = source_text.split('\n')[0].strip()
|
|
title = first_line[:50] if first_line else "Mindmap"
|
|
|
|
# Erstelle Root-Knoten
|
|
node_counter = [0]
|
|
root = self._create_node(title, 0, node_counter)
|
|
|
|
# Füge Hauptäste hinzu
|
|
for section_title, section_content in sections[:5]: # Max 5 Hauptäste
|
|
branch = self._create_node(section_title, 1, node_counter)
|
|
branch.color = self.level_colors[1 % len(self.level_colors)]
|
|
|
|
# Füge Details hinzu
|
|
details = self._extract_details(section_content)
|
|
for detail in details[:4]: # Max 4 Details pro Ast
|
|
if max_depth >= 2:
|
|
leaf = self._create_node(detail, 2, node_counter)
|
|
leaf.color = self.level_colors[2 % len(self.level_colors)]
|
|
branch.children.append(leaf)
|
|
|
|
root.children.append(branch)
|
|
|
|
return Mindmap(
|
|
root=root,
|
|
title=title,
|
|
topic=topic,
|
|
total_nodes=node_counter[0]
|
|
)
|
|
|
|
def _extract_sections(self, text: str) -> List[tuple]:
|
|
"""Extrahiert Abschnitte aus dem Text."""
|
|
sections = []
|
|
|
|
# Versuche Überschriften zu finden
|
|
lines = text.split('\n')
|
|
current_section = None
|
|
current_content = []
|
|
|
|
for line in lines:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
|
|
# Erkenne potenzielle Überschriften
|
|
if (line.endswith(':') or
|
|
line.isupper() or
|
|
len(line) < 50 and line[0].isupper() and '.' not in line):
|
|
# Speichere vorherige Section
|
|
if current_section:
|
|
sections.append((current_section, '\n'.join(current_content)))
|
|
current_section = line.rstrip(':')
|
|
current_content = []
|
|
else:
|
|
current_content.append(line)
|
|
|
|
# Letzte Section
|
|
if current_section:
|
|
sections.append((current_section, '\n'.join(current_content)))
|
|
|
|
# Falls keine Sections gefunden, erstelle aus Sätzen
|
|
if not sections:
|
|
sentences = re.split(r'[.!?]+', text)
|
|
for i, sentence in enumerate(sentences[:5]):
|
|
sentence = sentence.strip()
|
|
if len(sentence) > 10:
|
|
# Kürze auf max 30 Zeichen für Label
|
|
label = sentence[:30] + '...' if len(sentence) > 30 else sentence
|
|
sections.append((label, sentence))
|
|
|
|
return sections
|
|
|
|
def _extract_details(self, content: str) -> List[str]:
|
|
"""Extrahiert Details aus Abschnittsinhalt."""
|
|
details = []
|
|
|
|
# Aufzählungen
|
|
bullet_pattern = r'[-•*]\s*(.+)'
|
|
bullets = re.findall(bullet_pattern, content)
|
|
details.extend(bullets)
|
|
|
|
# Nummerierte Listen
|
|
num_pattern = r'\d+[.)]\s*(.+)'
|
|
numbered = re.findall(num_pattern, content)
|
|
details.extend(numbered)
|
|
|
|
# Falls keine Listen, nehme Sätze
|
|
if not details:
|
|
sentences = re.split(r'[.!?]+', content)
|
|
for sentence in sentences:
|
|
sentence = sentence.strip()
|
|
if len(sentence) > 5:
|
|
label = sentence[:40] + '...' if len(sentence) > 40 else sentence
|
|
details.append(label)
|
|
|
|
return details
|
|
|
|
def _create_node(
|
|
self,
|
|
label: str,
|
|
level: int,
|
|
counter: List[int]
|
|
) -> MindmapNode:
|
|
"""Erstellt einen neuen Knoten."""
|
|
counter[0] += 1
|
|
return MindmapNode(
|
|
id=f"node_{counter[0]}",
|
|
label=label,
|
|
level=level,
|
|
children=[],
|
|
color=self.level_colors[level % len(self.level_colors)]
|
|
)
|
|
|
|
def _create_mindmap_from_llm(
|
|
self,
|
|
data: Dict[str, Any],
|
|
topic: Optional[str]
|
|
) -> Mindmap:
|
|
"""Erstellt Mindmap aus LLM-Antwort."""
|
|
node_counter = [0]
|
|
title = data.get("title", "Mindmap")
|
|
|
|
root = self._create_node(title, 0, node_counter)
|
|
|
|
for branch_data in data.get("branches", []):
|
|
branch = self._create_node(branch_data.get("label", ""), 1, node_counter)
|
|
branch.color = self.level_colors[1 % len(self.level_colors)]
|
|
|
|
for child_data in branch_data.get("children", []):
|
|
child = self._create_node(child_data.get("label", ""), 2, node_counter)
|
|
child.color = self.level_colors[2 % len(self.level_colors)]
|
|
branch.children.append(child)
|
|
|
|
root.children.append(branch)
|
|
|
|
return Mindmap(
|
|
root=root,
|
|
title=title,
|
|
topic=topic,
|
|
total_nodes=node_counter[0]
|
|
)
|
|
|
|
def _empty_mindmap(self, title: str) -> Mindmap:
|
|
"""Erstellt leere Mindmap bei Fehler."""
|
|
root = MindmapNode(
|
|
id="root",
|
|
label=title,
|
|
level=0,
|
|
color=self.level_colors[0]
|
|
)
|
|
return Mindmap(root=root, title=title, total_nodes=1)
|
|
|
|
def to_dict(self, mindmap: Mindmap) -> Dict[str, Any]:
|
|
"""Konvertiert Mindmap zu Dictionary-Format."""
|
|
def node_to_dict(node: MindmapNode) -> Dict[str, Any]:
|
|
return {
|
|
"id": node.id,
|
|
"label": node.label,
|
|
"level": node.level,
|
|
"color": node.color,
|
|
"icon": node.icon,
|
|
"notes": node.notes,
|
|
"children": [node_to_dict(child) for child in node.children]
|
|
}
|
|
|
|
return {
|
|
"title": mindmap.title,
|
|
"topic": mindmap.topic,
|
|
"total_nodes": mindmap.total_nodes,
|
|
"root": node_to_dict(mindmap.root)
|
|
}
|
|
|
|
def to_mermaid(self, mindmap: Mindmap) -> str:
|
|
"""
|
|
Konvertiert Mindmap zu Mermaid-Format für Visualisierung.
|
|
|
|
Args:
|
|
mindmap: Mindmap-Objekt
|
|
|
|
Returns:
|
|
Mermaid-Diagramm als String
|
|
"""
|
|
lines = ["mindmap"]
|
|
lines.append(f" root(({mindmap.title}))")
|
|
|
|
def add_node(node: MindmapNode, indent: int):
|
|
for child in node.children:
|
|
prefix = " " * (indent + 1)
|
|
if child.children:
|
|
lines.append(f"{prefix}{child.label}")
|
|
else:
|
|
lines.append(f"{prefix}){child.label}(")
|
|
add_node(child, indent + 1)
|
|
|
|
add_node(mindmap.root, 1)
|
|
return "\n".join(lines)
|
|
|
|
def to_json_tree(self, mindmap: Mindmap) -> Dict[str, Any]:
|
|
"""
|
|
Konvertiert Mindmap zu JSON-Tree-Format für JS-Bibliotheken.
|
|
|
|
Args:
|
|
mindmap: Mindmap-Objekt
|
|
|
|
Returns:
|
|
JSON-Tree-Format für d3.js, vis.js etc.
|
|
"""
|
|
def node_to_tree(node: MindmapNode) -> Dict[str, Any]:
|
|
result = {
|
|
"name": node.label,
|
|
"id": node.id,
|
|
"color": node.color
|
|
}
|
|
if node.children:
|
|
result["children"] = [node_to_tree(c) for c in node.children]
|
|
return result
|
|
|
|
return node_to_tree(mindmap.root)
|