fix: Restore all files lost during destructive rebase
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>
This commit is contained in:
380
backend/generators/mindmap_generator.py
Normal file
380
backend/generators/mindmap_generator.py
Normal file
@@ -0,0 +1,380 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user