Remove deprecated files and templates: Delete unused files including deployment scripts, environment configurations, and various HTML templates to streamline the project structure. This cleanup enhances maintainability and reduces clutter in the codebase.

This commit is contained in:
2025-04-27 14:50:20 +02:00
parent d117978005
commit edf3049e42
77 changed files with 110 additions and 552 deletions

229
static/js/main.js Normal file
View File

@@ -0,0 +1,229 @@
/**
* MindMap - Hauptdatei für globale JavaScript-Funktionen
*/
// Import des ChatGPT-Assistenten
import ChatGPTAssistant from './modules/chatgpt-assistant.js';
/**
* Hauptmodul für die MindMap-Anwendung
* Verwaltet die globale Anwendungslogik
*/
document.addEventListener('DOMContentLoaded', function() {
// Initialisiere die Anwendung
MindMap.init();
// Wende Dunkel-/Hellmodus an
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
document.documentElement.classList.toggle('dark', isDarkMode);
});
/**
* Hauptobjekt der MindMap-Anwendung
*/
const MindMap = {
// App-Status
initialized: false,
darkMode: document.documentElement.classList.contains('dark'),
pageInitializers: {},
currentPage: document.body.dataset.page,
/**
* Initialisiert die MindMap-Anwendung
*/
init() {
if (this.initialized) return;
console.log('MindMap-Anwendung wird initialisiert...');
// Initialisiere den ChatGPT-Assistenten
const assistant = new ChatGPTAssistant();
assistant.init();
// Speichere als Teil von MindMap
this.assistant = assistant;
// Seiten-spezifische Initialisierer aufrufen
if (this.currentPage && this.pageInitializers[this.currentPage]) {
this.pageInitializers[this.currentPage]();
}
// Event-Listener einrichten
this.setupEventListeners();
// Dunkel-/Hellmodus aus LocalStorage wiederherstellen
if (localStorage.getItem('darkMode') === 'dark') {
document.documentElement.classList.add('dark');
this.darkMode = true;
}
// Mindmap initialisieren, falls auf der richtigen Seite
this.initializeMindmap();
this.initialized = true;
},
/**
* Initialisiert die D3.js Mindmap-Visualisierung
*/
initializeMindmap() {
// Prüfe, ob wir auf der Mindmap-Seite sind
const mindmapContainer = document.getElementById('mindmap-container');
if (!mindmapContainer) return;
try {
console.log('Initialisiere Mindmap...');
// Initialisiere die Mindmap
const mindmap = new MindMapVisualization('#mindmap-container', {
height: mindmapContainer.clientHeight || 600,
nodeRadius: 18,
selectedNodeRadius: 24,
linkDistance: 150,
onNodeClick: this.handleNodeClick.bind(this)
});
// Globale Referenz für andere Module
window.mindmapInstance = mindmap;
// Event-Listener für Zoom-Buttons
const zoomInBtn = document.getElementById('zoom-in-btn');
if (zoomInBtn) {
zoomInBtn.addEventListener('click', () => {
const svg = d3.select('#mindmap-container svg');
const currentZoom = d3.zoomTransform(svg.node());
const newScale = currentZoom.k * 1.3;
svg.transition().duration(300).call(
d3.zoom().transform,
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
);
});
}
const zoomOutBtn = document.getElementById('zoom-out-btn');
if (zoomOutBtn) {
zoomOutBtn.addEventListener('click', () => {
const svg = d3.select('#mindmap-container svg');
const currentZoom = d3.zoomTransform(svg.node());
const newScale = currentZoom.k / 1.3;
svg.transition().duration(300).call(
d3.zoom().transform,
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
);
});
}
const centerBtn = document.getElementById('center-btn');
if (centerBtn) {
centerBtn.addEventListener('click', () => {
const svg = d3.select('#mindmap-container svg');
svg.transition().duration(500).call(
d3.zoom().transform,
d3.zoomIdentity.scale(1)
);
});
}
// Event-Listener für Add-Thought-Button
const addThoughtBtn = document.getElementById('add-thought-btn');
if (addThoughtBtn) {
addThoughtBtn.addEventListener('click', () => {
this.showAddThoughtDialog();
});
}
// Event-Listener für Connect-Button
const connectBtn = document.getElementById('connect-btn');
if (connectBtn) {
connectBtn.addEventListener('click', () => {
this.showConnectDialog();
});
}
} catch (error) {
console.error('Fehler bei der Initialisierung der Mindmap:', error);
}
},
/**
* Handler für Klick auf einen Knoten in der Mindmap
* @param {Object} node - Der angeklickte Knoten
*/
handleNodeClick(node) {
console.log('Knoten wurde angeklickt:', node);
// Hier könnte man Logik hinzufügen, um Detailinformationen anzuzeigen
// oder den ausgewählten Knoten hervorzuheben
const detailsContainer = document.getElementById('node-details');
if (detailsContainer) {
detailsContainer.innerHTML = `
<div class="p-4">
<h3 class="text-xl font-bold mb-2">${node.name}</h3>
<p class="text-gray-300 mb-4">${node.description || 'Keine Beschreibung verfügbar.'}</p>
<div class="flex items-center justify-between">
<span class="text-sm">
<i class="fas fa-brain mr-1"></i> ${node.thought_count || 0} Gedanken
</span>
<button class="px-3 py-1 bg-purple-600 bg-opacity-30 rounded-lg text-sm">
<i class="fas fa-plus mr-1"></i> Gedanke hinzufügen
</button>
</div>
</div>
`;
// Button zum Hinzufügen eines Gedankens
const addThoughtBtn = detailsContainer.querySelector('button');
addThoughtBtn.addEventListener('click', () => {
this.showAddThoughtDialog(node);
});
}
},
/**
* Dialog zum Hinzufügen eines neuen Knotens
*/
showAddNodeDialog() {
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
},
/**
* Dialog zum Hinzufügen eines neuen Gedankens zu einem Knoten
*/
showAddThoughtDialog(node) {
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
},
/**
* Dialog zum Verbinden von Knoten
*/
showConnectDialog() {
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
alert('Diese Funktion steht bald zur Verfügung!');
},
/**
* Richtet Event-Listener für die Benutzeroberfläche ein
*/
setupEventListeners() {
// Event-Listener für Dark Mode-Wechsel
document.addEventListener('darkModeToggled', (event) => {
this.darkMode = event.detail.isDark;
});
// Responsive Anpassungen bei Fenstergröße
window.addEventListener('resize', () => {
if (window.mindmapInstance) {
const container = document.getElementById('mindmap-container');
if (container) {
window.mindmapInstance.width = container.clientWidth;
window.mindmapInstance.height = container.clientHeight;
}
}
});
}
};
// Globale Export für andere Module
window.MindMap = MindMap;

View File

@@ -0,0 +1,280 @@
/**
* ChatGPT Assistent Modul
* Verwaltet die Interaktion mit der OpenAI API und die Benutzeroberfläche des Assistenten
*/
class ChatGPTAssistant {
constructor() {
this.messages = [];
this.isOpen = false;
this.isLoading = false;
this.container = null;
this.chatHistory = null;
this.inputField = null;
}
/**
* Initialisiert den Assistenten und fügt die UI zum DOM hinzu
*/
init() {
// Assistent-Container erstellen
this.createAssistantUI();
// Event-Listener hinzufügen
this.setupEventListeners();
// Ersten Willkommensnachricht anzeigen
this.addMessage("assistant", "Frage den KI-Assistenten");
}
/**
* Erstellt die UI-Elemente für den Assistenten
*/
createAssistantUI() {
// Hauptcontainer erstellen
this.container = document.createElement('div');
this.container.id = 'chatgpt-assistant';
this.container.className = 'fixed bottom-4 right-4 z-50 flex flex-col';
// Button zum Öffnen/Schließen des Assistenten
const toggleButton = document.createElement('button');
toggleButton.id = 'assistant-toggle';
toggleButton.className = 'ml-auto bg-primary-600 hover:bg-primary-700 text-white rounded-full p-3 shadow-lg transition-all duration-300 mb-2';
toggleButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
// Chat-Container
const chatContainer = document.createElement('div');
chatContainer.id = 'assistant-chat';
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 sm:w-96 max-h-0 opacity-0';
// Chat-Header
const header = document.createElement('div');
header.className = 'bg-primary-600 text-white p-3 flex items-center justify-between';
header.innerHTML = `
<div class="flex items-center">
<i class="fas fa-robot mr-2"></i>
<span>KI-Assistent</span>
</div>
<button id="assistant-close" class="text-white hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
`;
// Chat-Verlauf
this.chatHistory = document.createElement('div');
this.chatHistory.id = 'assistant-history';
this.chatHistory.className = 'p-3 overflow-y-auto max-h-80 space-y-3';
// Chat-Eingabe
const inputContainer = document.createElement('div');
inputContainer.className = 'border-t border-gray-200 dark:border-dark-600 p-3 flex items-center';
this.inputField = document.createElement('input');
this.inputField.type = 'text';
this.inputField.placeholder = 'Frage den KI-Assistenten';
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
const sendButton = document.createElement('button');
sendButton.id = 'assistant-send';
sendButton.className = 'bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-r-lg';
sendButton.innerHTML = '<i class="fas fa-paper-plane"></i>';
// Elemente zusammenfügen
inputContainer.appendChild(this.inputField);
inputContainer.appendChild(sendButton);
chatContainer.appendChild(header);
chatContainer.appendChild(this.chatHistory);
chatContainer.appendChild(inputContainer);
this.container.appendChild(toggleButton);
this.container.appendChild(chatContainer);
// Zum DOM hinzufügen
document.body.appendChild(this.container);
}
/**
* Richtet Event-Listener für die Benutzeroberfläche ein
*/
setupEventListeners() {
// Toggle-Button
const toggleButton = document.getElementById('assistant-toggle');
toggleButton.addEventListener('click', () => this.toggleAssistant());
// Schließen-Button
const closeButton = document.getElementById('assistant-close');
closeButton.addEventListener('click', () => this.toggleAssistant(false));
// Senden-Button
const sendButton = document.getElementById('assistant-send');
sendButton.addEventListener('click', () => this.sendMessage());
// Enter-Taste im Eingabefeld
this.inputField.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
}
/**
* Öffnet oder schließt den Assistenten
* @param {boolean} state - Optional: erzwingt einen bestimmten Zustand
*/
toggleAssistant(state = null) {
const chatContainer = document.getElementById('assistant-chat');
this.isOpen = state !== null ? state : !this.isOpen;
if (this.isOpen) {
chatContainer.classList.remove('max-h-0', 'opacity-0');
chatContainer.classList.add('max-h-96', 'opacity-100');
this.inputField.focus();
} else {
chatContainer.classList.remove('max-h-96', 'opacity-100');
chatContainer.classList.add('max-h-0', 'opacity-0');
}
}
/**
* Fügt eine Nachricht zum Chat-Verlauf hinzu
* @param {string} sender - 'user' oder 'assistant'
* @param {string} text - Nachrichtentext
*/
addMessage(sender, text) {
// Nachricht zum Verlauf hinzufügen
this.messages.push({ role: sender, content: text });
// DOM-Element erstellen
const messageEl = document.createElement('div');
messageEl.className = `flex ${sender === 'user' ? 'justify-end' : 'justify-start'}`;
const bubble = document.createElement('div');
bubble.className = sender === 'user'
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
bubble.textContent = text;
messageEl.appendChild(bubble);
this.chatHistory.appendChild(messageEl);
// Scroll zum Ende des Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
}
/**
* Sendet die Benutzernachricht an den Server und zeigt die Antwort an
*/
async sendMessage() {
const userInput = this.inputField.value.trim();
if (!userInput || this.isLoading) return;
// Benutzernachricht anzeigen
this.addMessage('user', userInput);
// Eingabefeld zurücksetzen
this.inputField.value = '';
// Ladeindikator anzeigen
this.isLoading = true;
this.showLoadingIndicator();
try {
// Anfrage an den Server senden
const response = await fetch('/api/assistant', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: this.messages
}),
});
if (!response.ok) {
throw new Error('Netzwerkfehler oder Serverproblem');
}
const data = await response.json();
// Ladeindikator entfernen
this.removeLoadingIndicator();
// Antwort anzeigen
this.addMessage('assistant', data.response);
} catch (error) {
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
// Ladeindikator entfernen
this.removeLoadingIndicator();
// Fehlermeldung anzeigen
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
} finally {
this.isLoading = false;
}
}
/**
* Zeigt einen Ladeindikator im Chat an
*/
showLoadingIndicator() {
const loadingEl = document.createElement('div');
loadingEl.id = 'assistant-loading';
loadingEl.className = 'flex justify-start';
const bubble = document.createElement('div');
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
loadingEl.appendChild(bubble);
this.chatHistory.appendChild(loadingEl);
// Scroll zum Ende des Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
// Stil für den Typing-Indikator
const style = document.createElement('style');
style.textContent = `
.typing-indicator {
display: flex;
align-items: center;
}
.typing-indicator span {
height: 8px;
width: 8px;
background-color: #888;
border-radius: 50%;
display: inline-block;
margin: 0 2px;
opacity: 0.4;
animation: typing 1.5s infinite ease-in-out;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0% { transform: translateY(0); }
50% { transform: translateY(-5px); }
100% { transform: translateY(0); }
}
`;
document.head.appendChild(style);
}
/**
* Entfernt den Ladeindikator aus dem Chat
*/
removeLoadingIndicator() {
const loadingEl = document.getElementById('assistant-loading');
if (loadingEl) {
loadingEl.remove();
}
}
}
// Exportiere die Klasse für die Verwendung in anderen Modulen
export default ChatGPTAssistant;

View File

@@ -0,0 +1,719 @@
/**
* Mindmap-Seite JavaScript
* Spezifische Funktionen für die Mindmap-Seite
*/
document.addEventListener('DOMContentLoaded', function() {
// Registriere den Initialisierer im MindMap-Objekt
if (window.MindMap) {
window.MindMap.pageInitializers.mindmap = initMindmapPage;
}
// Prüfe, ob wir auf der Mindmap-Seite sind und initialisiere
if (document.body.dataset.page === 'mindmap') {
initMindmapPage();
}
});
/**
* Initialisiert die Mindmap-Seite
*/
function initMindmapPage() {
const mindmapContainer = document.getElementById('mindmap-container');
const thoughtsContainer = document.getElementById('thoughts-container');
if (!mindmapContainer) {
console.error('Mindmap-Container nicht gefunden!');
return;
}
// Prüfe, ob D3.js geladen ist
if (typeof d3 === 'undefined') {
console.error('D3.js ist nicht geladen!');
mindmapContainer.innerHTML = `
<div class="glass-effect p-6 text-center">
<div class="text-red-500 mb-4">
<i class="fa-solid fa-triangle-exclamation text-4xl"></i>
</div>
<p class="text-xl">D3.js konnte nicht geladen werden. Bitte laden Sie die Seite neu.</p>
</div>
`;
return;
}
// Erstelle die Mindmap-Visualisierung
const mindmap = new MindMapVisualization('#mindmap-container', {
height: 600,
onNodeClick: handleNodeClick
});
// Globale Referenz für die Zoom-Buttons erstellen
window.mindmapInstance = mindmap;
// Lade die Mindmap-Daten
mindmap.loadData();
// Suchfunktion für die Mindmap
const searchInput = document.getElementById('mindmap-search');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
mindmap.filterBySearchTerm(e.target.value);
});
}
/**
* Behandelt Klicks auf Mindmap-Knoten
*/
async function handleNodeClick(node) {
if (!thoughtsContainer) return;
// Zeige Lade-Animation
thoughtsContainer.innerHTML = `
<div class="flex justify-center items-center p-12">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-400"></div>
</div>
`;
try {
// Lade Gedanken für den ausgewählten Knoten
const response = await fetch(`/api/nodes/${node.id}/thoughts`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const thoughts = await response.json();
// Gedanken anzeigen
renderThoughts(thoughts, node.name);
} catch (error) {
console.error('Fehler beim Laden der Gedanken:', error);
thoughtsContainer.innerHTML = `
<div class="glass-effect p-6 text-center">
<div class="text-red-500 mb-4">
<i class="fa-solid fa-triangle-exclamation text-4xl"></i>
</div>
<p class="text-xl">Fehler beim Laden der Gedanken.</p>
<p class="text-gray-300">Bitte versuchen Sie es später erneut.</p>
</div>
`;
}
}
/**
* Rendert die Gedanken in den Container
*/
function renderThoughts(thoughts, nodeName) {
// Wenn keine Gedanken vorhanden sind
if (thoughts.length === 0) {
thoughtsContainer.innerHTML = `
<div class="glass-effect p-6 text-center">
<div class="text-blue-400 mb-4">
<i class="fa-solid fa-info-circle text-4xl"></i>
</div>
<p class="text-xl">Keine Gedanken für "${nodeName}" vorhanden.</p>
<button id="add-thought-btn" class="btn-primary mt-4">
<i class="fa-solid fa-plus mr-2"></i> Gedanke hinzufügen
</button>
</div>
`;
// Event-Listener für den Button
document.getElementById('add-thought-btn').addEventListener('click', () => {
openAddThoughtModal(nodeName);
});
return;
}
// Gedanken anzeigen
thoughtsContainer.innerHTML = `
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold text-white">Gedanken zu "${nodeName}"</h2>
<button id="add-thought-btn" class="btn-primary">
<i class="fa-solid fa-plus mr-2"></i> Neuer Gedanke
</button>
</div>
<div class="grid grid-cols-1 gap-4" id="thoughts-grid"></div>
`;
// Button-Event-Listener
document.getElementById('add-thought-btn').addEventListener('click', () => {
openAddThoughtModal(nodeName);
});
// Gedanken-Karten rendern
const thoughtsGrid = document.getElementById('thoughts-grid');
thoughts.forEach((thought, index) => {
const card = createThoughtCard(thought);
// Animation verzögern für gestaffeltes Erscheinen
setTimeout(() => {
card.classList.add('opacity-100');
card.classList.remove('opacity-0', 'translate-y-4');
}, index * 100);
thoughtsGrid.appendChild(card);
});
}
/**
* Erstellt eine Gedanken-Karte
*/
function createThoughtCard(thought) {
const card = document.createElement('div');
card.className = 'card transition-all duration-300 opacity-0 translate-y-4 transform hover:shadow-lg border-l-4';
card.style.borderLeftColor = thought.color_code || '#4080ff';
// Karten-Inhalt
card.innerHTML = `
<div class="p-4">
<div class="flex justify-between items-start">
<h3 class="text-lg font-bold text-white">${thought.title}</h3>
<div class="text-sm text-gray-400">${thought.timestamp}</div>
</div>
<div class="prose dark:prose-invert mt-2">
<p>${thought.content}</p>
</div>
${thought.keywords ? `
<div class="flex flex-wrap gap-1 mt-3">
${thought.keywords.split(',').map(keyword =>
`<span class="px-2 py-1 text-xs rounded-full bg-secondary-700 text-white">${keyword.trim()}</span>`
).join('')}
</div>
` : ''}
<div class="mt-4 flex justify-between items-center">
<div class="text-sm text-gray-400">
<i class="fa-solid fa-user mr-1"></i> ${thought.author}
</div>
<div class="flex space-x-2">
<button class="text-sm px-2 py-1 rounded hover:bg-white/10 transition-colors"
onclick="showComments(${thought.id})">
<i class="fa-solid fa-comments mr-1"></i> Kommentare
</button>
<button class="text-sm px-2 py-1 rounded hover:bg-white/10 transition-colors"
onclick="showRelations(${thought.id})">
<i class="fa-solid fa-diagram-project mr-1"></i> Beziehungen
</button>
</div>
</div>
</div>
`;
return card;
}
/**
* Öffnet das Modal zum Hinzufügen eines neuen Gedankens
*/
function openAddThoughtModal(nodeName) {
// Node-Information extrahieren
let nodeId, nodeTitle;
if (typeof nodeName === 'string') {
// Wenn nur ein String übergeben wurde
nodeTitle = nodeName;
// Versuche nodeId aus der Mindmap zu finden
const nodeElement = d3.selectAll('.node-group').filter(d => d.name === nodeName);
if (nodeElement.size() > 0) {
nodeId = nodeElement.datum().id;
}
} else if (typeof nodeName === 'object') {
// Wenn ein Node-Objekt übergeben wurde
nodeId = nodeName.id;
nodeTitle = nodeName.name;
} else {
console.error('Ungültiger Node-Parameter', nodeName);
return;
}
// Modal-Struktur erstellen
const modal = document.createElement('div');
modal.className = 'fixed inset-0 z-50 flex items-center justify-center p-4';
modal.innerHTML = `
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" id="modal-backdrop"></div>
<div class="glass-effect relative rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto z-10 transform transition-all">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white flex items-center">
<span class="w-3 h-3 rounded-full bg-primary-400 mr-2"></span>
Neuer Gedanke zu "${nodeTitle}"
</h3>
<button id="close-modal-btn" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<form id="add-thought-form" class="space-y-4">
<input type="hidden" id="node_id" name="node_id" value="${nodeId || ''}">
<div>
<label for="title" class="block text-sm font-medium text-gray-300">Titel</label>
<input type="text" id="title" name="title" required
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
<div>
<label for="content" class="block text-sm font-medium text-gray-300">Inhalt</label>
<textarea id="content" name="content" rows="5" required
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent"></textarea>
</div>
<div>
<label for="keywords" class="block text-sm font-medium text-gray-300">Schlüsselwörter (kommagetrennt)</label>
<input type="text" id="keywords" name="keywords"
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent">
</div>
<div>
<label for="abstract" class="block text-sm font-medium text-gray-300">Zusammenfassung (optional)</label>
<textarea id="abstract" name="abstract" rows="2"
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent"></textarea>
</div>
<div>
<label for="color_code" class="block text-sm font-medium text-gray-300">Farbcode</label>
<div class="flex space-x-2 mt-1">
<input type="color" id="color_code" name="color_code" value="#4080ff"
class="h-10 w-10 rounded bg-dark-700 border border-dark-500">
<select id="predefined_colors"
class="block flex-grow rounded-md bg-dark-700 border border-dark-500 text-white p-2.5">
<option value="#4080ff">Blau</option>
<option value="#a040ff">Lila</option>
<option value="#40bf80">Grün</option>
<option value="#ff4080">Rot</option>
<option value="#ffaa00">Orange</option>
<option value="#00ccff">Türkis</option>
</select>
</div>
</div>
<div class="flex justify-between pt-4">
<div class="flex items-center">
<div class="relative">
<button type="button" id="open-relation-btn" class="btn-outline text-sm pl-3 pr-9">
<i class="fa-solid fa-diagram-project mr-2"></i> Verbindung
<i class="fa-solid fa-chevron-down absolute right-3 top-1/2 transform -translate-y-1/2"></i>
</button>
<div id="relation-menu" class="absolute left-0 mt-2 w-60 rounded-md shadow-lg bg-dark-800 ring-1 ring-black ring-opacity-5 z-10 hidden">
<div class="py-1">
<div class="px-3 py-2 text-xs font-semibold text-gray-400 border-b border-dark-600">BEZIEHUNGSTYPEN</div>
<div class="max-h-48 overflow-y-auto">
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="supports">
<i class="fa-solid fa-circle-arrow-up text-green-400 mr-2"></i> Stützt
</button>
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="contradicts">
<i class="fa-solid fa-circle-arrow-down text-red-400 mr-2"></i> Widerspricht
</button>
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="builds_upon">
<i class="fa-solid fa-arrow-right text-blue-400 mr-2"></i> Baut auf auf
</button>
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="generalizes">
<i class="fa-solid fa-arrow-up-wide-short text-purple-400 mr-2"></i> Verallgemeinert
</button>
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="specifies">
<i class="fa-solid fa-arrow-down-wide-short text-yellow-400 mr-2"></i> Spezifiziert
</button>
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="inspires">
<i class="fa-solid fa-lightbulb text-amber-400 mr-2"></i> Inspiriert
</button>
</div>
</div>
</div>
</div>
<input type="hidden" id="relation_type" name="relation_type" value="">
<input type="hidden" id="relation_target" name="relation_target" value="">
</div>
<div class="flex space-x-3">
<button type="button" id="cancel-btn" class="btn-outline">Abbrechen</button>
<button type="submit" class="btn-primary">
<i class="fa-solid fa-save mr-2"></i> Speichern
</button>
</div>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
// Focus auf das erste Feld setzen
setTimeout(() => {
modal.querySelector('#title').focus();
}, 100);
// Event-Listener hinzufügen
modal.querySelector('#modal-backdrop').addEventListener('click', closeModal);
modal.querySelector('#close-modal-btn').addEventListener('click', closeModal);
modal.querySelector('#cancel-btn').addEventListener('click', closeModal);
// Farbauswahl-Event-Listener
const colorInput = modal.querySelector('#color_code');
const predefinedColors = modal.querySelector('#predefined_colors');
predefinedColors.addEventListener('change', function() {
colorInput.value = this.value;
});
// Beziehungsmenü-Funktionalität
const relationBtn = modal.querySelector('#open-relation-btn');
const relationMenu = modal.querySelector('#relation-menu');
relationBtn.addEventListener('click', function() {
relationMenu.classList.toggle('hidden');
});
// Klick außerhalb des Menüs schließt es
document.addEventListener('click', function(event) {
if (!relationBtn.contains(event.target) && !relationMenu.contains(event.target)) {
relationMenu.classList.add('hidden');
}
});
// Beziehungstyp-Auswahl
const relationTypeBtns = modal.querySelectorAll('.relation-type-btn');
const relationTypeInput = modal.querySelector('#relation_type');
relationTypeBtns.forEach(btn => {
btn.addEventListener('click', function() {
const relationType = this.dataset.type;
relationTypeInput.value = relationType;
// Sichtbare Anzeige aktualisieren
relationBtn.innerHTML = `
<i class="fa-solid fa-diagram-project mr-2"></i>
${this.innerText.trim()}
<i class="fa-solid fa-chevron-down absolute right-3 top-1/2 transform -translate-y-1/2"></i>
`;
// Menü schließen
relationMenu.classList.add('hidden');
});
});
// Form-Submit-Handler
const form = modal.querySelector('#add-thought-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const thoughtData = {
node_id: formData.get('node_id'),
title: formData.get('title'),
content: formData.get('content'),
keywords: formData.get('keywords'),
abstract: formData.get('abstract'),
color_code: formData.get('color_code'),
relation_type: formData.get('relation_type'),
relation_target: formData.get('relation_target')
};
try {
const response = await fetch('/api/thoughts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(thoughtData)
});
if (!response.ok) {
throw new Error('Fehler beim Speichern des Gedankens.');
}
// Modal schließen
closeModal();
// Gedanken neu laden
if (nodeId) {
handleNodeClick({ id: nodeId, name: nodeTitle });
}
// Erfolgsbenachrichtigung
if (window.MindMap && window.MindMap.showNotification) {
MindMap.showNotification('Gedanke erfolgreich gespeichert.', 'success');
}
} catch (error) {
console.error('Fehler beim Speichern:', error);
if (window.MindMap && window.MindMap.showNotification) {
MindMap.showNotification('Fehler beim Speichern des Gedankens.', 'error');
}
}
});
// Modal schließen
function closeModal() {
modal.classList.add('opacity-0');
setTimeout(() => {
modal.remove();
}, 300);
}
}
}
/**
* Zeigt die Kommentare zu einem Gedanken an
*/
window.showComments = async function(thoughtId) {
try {
// Lade-Animation erstellen
const modal = createModalWithLoading('Kommentare werden geladen...');
document.body.appendChild(modal);
// Kommentare laden
const response = await fetch(`/api/comments/${thoughtId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const comments = await response.json();
// Modal mit Kommentaren aktualisieren
updateModalWithComments(modal, comments, thoughtId);
} catch (error) {
console.error('Fehler beim Laden der Kommentare:', error);
MindMap.showNotification('Fehler beim Laden der Kommentare.', 'error');
}
};
/**
* Zeigt die Beziehungen eines Gedankens an
*/
window.showRelations = async function(thoughtId) {
try {
// Lade-Animation erstellen
const modal = createModalWithLoading('Beziehungen werden geladen...');
document.body.appendChild(modal);
// Beziehungen laden
const response = await fetch(`/api/thoughts/${thoughtId}/relations`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const relations = await response.json();
// Modal mit Beziehungen aktualisieren
updateModalWithRelations(modal, relations, thoughtId);
} catch (error) {
console.error('Fehler beim Laden der Beziehungen:', error);
MindMap.showNotification('Fehler beim Laden der Beziehungen.', 'error');
}
};
/**
* Erstellt ein Modal mit Lade-Animation
*/
function createModalWithLoading(loadingText) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 z-50 flex items-center justify-center p-4';
modal.innerHTML = `
<div class="absolute inset-0 bg-black/50" id="modal-backdrop"></div>
<div class="glass-effect relative rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto z-10">
<div class="p-6 text-center">
<div class="flex justify-center mb-4">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-400"></div>
</div>
<p class="text-lg text-white">${loadingText}</p>
</div>
</div>
`;
// Event-Listener zum Schließen
modal.querySelector('#modal-backdrop').addEventListener('click', () => {
modal.remove();
});
return modal;
}
/**
* Aktualisiert das Modal mit Kommentaren
*/
function updateModalWithComments(modal, comments, thoughtId) {
const modalContent = modal.querySelector('.glass-effect');
modalContent.innerHTML = `
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white">Kommentare</h3>
<button id="close-modal-btn" class="text-gray-400 hover:text-white">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="comments-list mb-6 space-y-4">
${comments.length === 0 ?
'<div class="text-center text-gray-400 py-4">Keine Kommentare vorhanden.</div>' :
comments.map(comment => `
<div class="glass-effect p-3 rounded">
<div class="flex justify-between items-start">
<div class="font-medium text-white">${comment.author}</div>
<div class="text-xs text-gray-400">${comment.timestamp}</div>
</div>
<p class="mt-2 text-gray-200">${comment.content}</p>
</div>
`).join('')
}
</div>
<form id="comment-form" class="space-y-3">
<div>
<label for="comment-content" class="block text-sm font-medium text-gray-300">Neuer Kommentar</label>
<textarea id="comment-content" name="content" rows="3" required
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white"></textarea>
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="btn-primary">
<i class="fa-solid fa-paper-plane mr-2"></i> Senden
</button>
</div>
</form>
</div>
`;
// Event-Listener hinzufügen
modalContent.querySelector('#close-modal-btn').addEventListener('click', () => {
modal.remove();
});
// Kommentar-Formular
const form = modalContent.querySelector('#comment-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const content = form.elements.content.value;
try {
const response = await fetch('/api/comments', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
thought_id: thoughtId,
content: content
})
});
if (!response.ok) {
throw new Error('Fehler beim Speichern des Kommentars.');
}
// Modal schließen
modal.remove();
// Erfolgsbenachrichtigung
MindMap.showNotification('Kommentar erfolgreich gespeichert.', 'success');
} catch (error) {
console.error('Fehler beim Speichern des Kommentars:', error);
MindMap.showNotification('Fehler beim Speichern des Kommentars.', 'error');
}
});
}
/**
* Aktualisiert das Modal mit Beziehungen
*/
function updateModalWithRelations(modal, relations, thoughtId) {
const modalContent = modal.querySelector('.glass-effect');
modalContent.innerHTML = `
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white">Beziehungen</h3>
<button id="close-modal-btn" class="text-gray-400 hover:text-white">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="relations-list mb-6 space-y-4">
${relations.length === 0 ?
'<div class="text-center text-gray-400 py-4">Keine Beziehungen vorhanden.</div>' :
relations.map(relation => `
<div class="glass-effect p-3 rounded">
<div class="flex items-center">
<span class="inline-block px-2 py-1 rounded-full text-xs font-medium bg-primary-600 text-white">
${relation.relation_type}
</span>
<div class="ml-3">
<div class="text-white">Ziel: Gedanke #${relation.target_id}</div>
<div class="text-xs text-gray-400">Erstellt von ${relation.created_by} am ${relation.created_at}</div>
</div>
</div>
</div>
`).join('')
}
</div>
<form id="relation-form" class="space-y-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="target_id" class="block text-sm font-medium text-gray-300">Ziel-Gedanke ID</label>
<input type="number" id="target_id" name="target_id" required
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white">
</div>
<div>
<label for="relation_type" class="block text-sm font-medium text-gray-300">Beziehungstyp</label>
<select id="relation_type" name="relation_type" required
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white">
<option value="SUPPORTS">Stützt</option>
<option value="CONTRADICTS">Widerspricht</option>
<option value="BUILDS_UPON">Baut auf auf</option>
<option value="GENERALIZES">Verallgemeinert</option>
<option value="SPECIFIES">Spezifiziert</option>
<option value="INSPIRES">Inspiriert</option>
</select>
</div>
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="btn-primary">
<i class="fa-solid fa-plus mr-2"></i> Beziehung erstellen
</button>
</div>
</form>
</div>
`;
// Event-Listener hinzufügen
modalContent.querySelector('#close-modal-btn').addEventListener('click', () => {
modal.remove();
});
// Beziehungs-Formular
const form = modalContent.querySelector('#relation-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
source_id: thoughtId,
target_id: parseInt(form.elements.target_id.value),
relation_type: form.elements.relation_type.value
};
try {
const response = await fetch('/api/relations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
throw new Error('Fehler beim Erstellen der Beziehung.');
}
// Modal schließen
modal.remove();
// Erfolgsbenachrichtigung
MindMap.showNotification('Beziehung erfolgreich erstellt.', 'success');
} catch (error) {
console.error('Fehler beim Erstellen der Beziehung:', error);
MindMap.showNotification('Fehler beim Erstellen der Beziehung.', 'error');
}
});
}

View File

@@ -0,0 +1,777 @@
/**
* MindMap D3.js Modul
* Visualisiert die Mindmap mit D3.js
*/
class MindMapVisualization {
constructor(containerSelector, options = {}) {
this.containerSelector = containerSelector;
this.container = d3.select(containerSelector);
this.width = options.width || this.container.node().clientWidth || 800;
this.height = options.height || 600;
this.nodeRadius = options.nodeRadius || 14;
this.selectedNodeRadius = options.selectedNodeRadius || 20;
this.linkDistance = options.linkDistance || 150;
this.chargeStrength = options.chargeStrength || -900;
this.centerForce = options.centerForce || 0.15;
this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node));
this.nodes = [];
this.links = [];
this.simulation = null;
this.svg = null;
this.linkElements = null;
this.nodeElements = null;
this.textElements = null;
this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true;
this.mouseoverNode = null;
this.selectedNode = null;
this.zoomFactor = 1;
this.tooltipDiv = null;
this.isLoading = true;
// Lade die gemerkten Knoten
this.bookmarkedNodes = this.loadBookmarkedNodes();
// Sicherstellen, dass der Container bereit ist
if (this.container.node()) {
this.init();
this.setupDefaultNodes();
// Sofortige Datenladung
window.setTimeout(() => {
this.loadData();
}, 100);
} else {
console.error('Mindmap-Container nicht gefunden:', containerSelector);
}
}
// Standardknoten als Fallback einrichten, falls die API nicht reagiert
setupDefaultNodes() {
// Basis-Mindmap mit Hauptthemen
const defaultNodes = [
{ id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 0 },
{ id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 0 },
{ id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 0 },
{ id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 0 },
{ id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 0 }
];
const defaultLinks = [
{ source: "root", target: "philosophy" },
{ source: "root", target: "science" },
{ source: "root", target: "technology" },
{ source: "root", target: "arts" }
];
// Als Fallback verwenden, falls die API fehlschlägt
this.defaultNodes = defaultNodes;
this.defaultLinks = defaultLinks;
}
init() {
// SVG erstellen, wenn noch nicht vorhanden
if (!this.svg) {
// Container zuerst leeren
this.container.html('');
this.svg = this.container
.append('svg')
.attr('width', '100%')
.attr('height', this.height)
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
.attr('class', 'mindmap-svg')
.call(
d3.zoom()
.scaleExtent([0.1, 5])
.on('zoom', (event) => {
this.handleZoom(event.transform);
})
);
// Hauptgruppe für alles, was zoom-transformierbar ist
this.g = this.svg.append('g');
// Tooltip initialisieren
if (!d3.select('body').select('.node-tooltip').size()) {
this.tooltipDiv = d3.select('body')
.append('div')
.attr('class', 'node-tooltip')
.style('opacity', 0)
.style('position', 'absolute')
.style('pointer-events', 'none')
.style('background', 'rgba(20, 20, 40, 0.9)')
.style('color', '#ffffff')
.style('border', '1px solid rgba(160, 80, 255, 0.2)')
.style('border-radius', '6px')
.style('padding', '8px 12px')
.style('font-size', '14px')
.style('max-width', '250px')
.style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
} else {
this.tooltipDiv = d3.select('body').select('.node-tooltip');
}
}
// Force-Simulation initialisieren
this.simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance))
.force('charge', d3.forceManyBody().strength(this.chargeStrength))
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
// Globale Mindmap-Instanz für externe Zugriffe setzen
window.mindmapInstance = this;
}
handleZoom(transform) {
this.g.attr('transform', transform);
this.zoomFactor = transform.k;
// Knotengröße anpassen, um bei Zoom lesbar zu bleiben
if (this.nodeElements) {
this.nodeElements
.attr('r', d => (d === this.selectedNode ? this.selectedNodeRadius : this.nodeRadius) / Math.sqrt(transform.k));
}
// Textgröße anpassen
if (this.textElements) {
this.textElements
.style('font-size', `${12 / Math.sqrt(transform.k)}px`);
}
}
async loadData() {
try {
// Ladeindikator anzeigen
this.showLoading();
// Verwende sofort die Standarddaten für eine schnelle erste Anzeige
this.nodes = [...this.defaultNodes];
this.links = [...this.defaultLinks];
// Visualisierung sofort aktualisieren
this.isLoading = false;
this.updateVisualization();
// Status auf bereit setzen - don't wait for API
this.container.attr('data-status', 'ready');
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout - reduced from 10
const response = await fetch('/api/mindmap', {
signal: controller.signal,
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
clearTimeout(timeoutId);
if (!response.ok) {
console.warn(`HTTP Fehler: ${response.status}, verwende Standarddaten`);
return; // Keep using default data
}
const data = await response.json();
if (!data || !data.nodes || data.nodes.length === 0) {
console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
return; // Keep using default data
}
// Flache Liste von Knoten und Verbindungen erstellen
this.nodes = [];
this.links = [];
this.processHierarchicalData(data.nodes);
// Visualisierung aktualisieren mit den tatsächlichen Daten
this.updateVisualization();
} catch (error) {
console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
// Already using default data, no action needed
}
} catch (error) {
console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
this.container.attr('data-status', 'error');
}
}
showLoading() {
// Element nur leeren, wenn es noch kein SVG enthält
if (!this.container.select('svg').size()) {
this.container.html(`
<div class="flex justify-center items-center h-full">
<div class="text-center">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
<p class="text-lg text-white">Mindmap wird geladen...</p>
</div>
</div>
`);
}
}
processHierarchicalData(hierarchicalNodes, parentId = null) {
hierarchicalNodes.forEach(node => {
// Knoten hinzufügen, wenn noch nicht vorhanden
if (!this.nodes.find(n => n.id === node.id)) {
this.nodes.push({
id: node.id,
name: node.name,
description: node.description || '',
thought_count: node.thought_count || 0,
color: this.generateColorFromString(node.name),
});
}
// Verbindung zum Elternknoten hinzufügen
if (parentId !== null) {
this.links.push({
source: parentId,
target: node.id
});
}
// Rekursiv für Kindknoten aufrufen
if (node.children && node.children.length > 0) {
this.processHierarchicalData(node.children, node.id);
}
});
}
generateColorFromString(str) {
// Erzeugt eine deterministische Farbe basierend auf dem String
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
// Verwende deterministische Farbe aus unserem Farbschema
const colors = [
'#4080ff', // primary-400
'#a040ff', // secondary-400
'#205cf5', // primary-500
'#8020f5', // secondary-500
'#1040e0', // primary-600
'#6010e0', // secondary-600
];
return colors[Math.abs(hash) % colors.length];
}
updateVisualization() {
// Starte die Visualisierung nur, wenn nicht mehr im Ladezustand
if (this.isLoading) return;
// Container leeren, wenn Diagramm neu erstellt wird
if (!this.svg) {
this.container.html('');
this.init();
}
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
const useTransitions = false;
// Links (Edges) erstellen
this.linkElements = this.g.selectAll('.link')
.data(this.links)
.join(
enter => enter.append('line')
.attr('class', 'link')
.attr('stroke', '#ffffff30')
.attr('stroke-width', 2)
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
update => update
.attr('stroke', '#ffffff30')
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
exit => exit.remove()
);
// Pfeilspitze für gerichtete Beziehungen hinzufügen (falls noch nicht vorhanden)
if (!this.svg.select('defs').node()) {
const defs = this.svg.append('defs');
defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 20)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#ffffff50');
}
// Simplified Effekte definieren, falls noch nicht vorhanden
if (!this.svg.select('#glow').node()) {
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
// Glow-Effekt für Knoten
const filter = defs.append('filter')
.attr('id', 'glow')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
filter.append('feGaussianBlur')
.attr('stdDeviation', '1')
.attr('result', 'blur');
filter.append('feComposite')
.attr('in', 'SourceGraphic')
.attr('in2', 'blur')
.attr('operator', 'over');
// Blur-Effekt für Schatten
const blurFilter = defs.append('filter')
.attr('id', 'blur')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
blurFilter.append('feGaussianBlur')
.attr('stdDeviation', '1');
}
// Knoten-Gruppe erstellen/aktualisieren
const nodeGroups = this.g.selectAll('.node-group')
.data(this.nodes)
.join(
enter => {
const group = enter.append('g')
.attr('class', 'node-group')
.call(d3.drag()
.on('start', (event, d) => this.dragStarted(event, d))
.on('drag', (event, d) => this.dragged(event, d))
.on('end', (event, d) => this.dragEnded(event, d)));
// Hintergrundschatten für besseren Kontrast
group.append('circle')
.attr('class', 'node-shadow')
.attr('r', d => this.nodeRadius * 1.2)
.attr('fill', 'rgba(0, 0, 0, 0.3)')
.attr('filter', 'url(#blur)');
// Kreis für jeden Knoten
group.append('circle')
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
.attr('r', this.nodeRadius)
.attr('fill', d => d.color || this.generateColorFromString(d.name))
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2)
.attr('filter', 'url(#glow)');
// Text-Label mit besserem Kontrast
group.append('text')
.attr('class', 'node-label')
.attr('dy', '0.35em')
.attr('text-anchor', 'middle')
.attr('fill', '#ffffff')
.attr('stroke', 'rgba(0, 0, 0, 0.4)')
.attr('stroke-width', '0.7px')
.attr('paint-order', 'stroke')
.style('font-size', '12px')
.style('font-weight', '500')
.style('pointer-events', 'none')
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
// Interaktivität hinzufügen
group
.on('mouseover', (event, d) => this.nodeMouseover(event, d))
.on('mouseout', (event, d) => this.nodeMouseout(event, d))
.on('click', (event, d) => this.nodeClicked(event, d));
return group;
},
update => {
// Knoten aktualisieren
update.select('.node')
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
.attr('fill', d => d.color || this.generateColorFromString(d.name))
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2);
// Text aktualisieren
update.select('.node-label')
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
return update;
},
exit => exit.remove()
);
// Einzelne Elemente für direkten Zugriff speichern
this.nodeElements = this.g.selectAll('.node');
this.textElements = this.g.selectAll('.node-label');
// Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
this.simulation
.nodes(this.nodes)
.on('tick', () => this.ticked())
.alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
.alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
this.simulation.force('link')
.links(this.links);
// Simulation neu starten
this.simulation.restart();
// Update connection counts
this.updateConnectionCounts();
}
ticked() {
// Linienpositionen aktualisieren
this.linkElements
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
// Knotenpositionen aktualisieren
this.g.selectAll('.node-group')
.attr('transform', d => `translate(${d.x}, ${d.y})`);
}
dragStarted(event, d) {
if (!event.active) this.simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
dragEnded(event, d) {
if (!event.active) this.simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
nodeMouseover(event, d) {
this.mouseoverNode = d;
// Tooltip anzeigen
if (this.tooltipEnabled) {
const isBookmarked = this.isNodeBookmarked(d.id);
const tooltipContent = `
<div class="p-2">
<strong>${d.name}</strong>
${d.description ? `<p class="text-sm text-gray-200 mt-1">${d.description}</p>` : ''}
<div class="text-xs text-gray-300 mt-1">
Gedanken: ${d.thought_count}
</div>
<div class="mt-2">
<button id="bookmark-button" class="px-2 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-white"
data-nodeid="${d.id}">
${isBookmarked ? '<i class="fas fa-bookmark mr-1"></i> Gemerkt' : '<i class="far fa-bookmark mr-1"></i> Merken'}
</button>
</div>
</div>
`;
this.tooltipDiv
.html(tooltipContent)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px')
.transition()
.duration(200)
.style('opacity', 1);
// Event-Listener für den Bookmark-Button hinzufügen
document.getElementById('bookmark-button').addEventListener('click', (e) => {
e.stopPropagation();
const nodeId = e.currentTarget.getAttribute('data-nodeid');
const isNowBookmarked = this.toggleBookmark(nodeId);
// Button-Text aktualisieren
if (isNowBookmarked) {
e.currentTarget.innerHTML = '<i class="fas fa-bookmark mr-1"></i> Gemerkt';
} else {
e.currentTarget.innerHTML = '<i class="far fa-bookmark mr-1"></i> Merken';
}
});
}
// Knoten visuell hervorheben
d3.select(event.currentTarget).select('circle')
.transition()
.duration(200)
.attr('r', this.nodeRadius * 1.2)
.attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff');
}
nodeMouseout(event, d) {
this.mouseoverNode = null;
// Tooltip ausblenden
if (this.tooltipEnabled) {
this.tooltipDiv
.transition()
.duration(200)
.style('opacity', 0);
}
// Knoten-Stil zurücksetzen, wenn nicht ausgewählt
const nodeElement = d3.select(event.currentTarget).select('circle');
if (d !== this.selectedNode) {
const isBookmarked = this.isNodeBookmarked(d.id);
nodeElement
.transition()
.duration(200)
.attr('r', this.nodeRadius)
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
.attr('stroke-width', isBookmarked ? 3 : 2);
}
}
nodeClicked(event, d) {
// Frühere Auswahl zurücksetzen
if (this.selectedNode && this.selectedNode !== d) {
this.g.selectAll('.node')
.filter(n => n === this.selectedNode)
.transition()
.duration(200)
.attr('r', this.nodeRadius)
.attr('stroke', '#ffffff50');
}
// Neue Auswahl hervorheben
if (this.selectedNode !== d) {
this.selectedNode = d;
d3.select(event.currentTarget).select('circle')
.transition()
.duration(200)
.attr('r', this.selectedNodeRadius)
.attr('stroke', '#ffffff');
}
// Callback mit Node-Daten aufrufen
this.onNodeClick(d);
}
showError(message) {
this.container.html(`
<div class="w-full text-center p-6">
<div class="mb-4 text-red-500">
<i class="fas fa-exclamation-triangle text-4xl"></i>
</div>
<p class="text-lg text-gray-200">${message}</p>
</div>
`);
}
// Fokussiert die Ansicht auf einen bestimmten Knoten
focusNode(nodeId) {
const node = this.nodes.find(n => n.id === nodeId);
if (!node) return;
// Simuliere einen Klick auf den Knoten
const nodeElement = this.g.selectAll('.node-group')
.filter(d => d.id === nodeId);
nodeElement.dispatch('click');
// Zentriere den Knoten in der Ansicht
const transform = d3.zoomIdentity
.translate(this.width / 2, this.height / 2)
.scale(1.2)
.translate(-node.x, -node.y);
this.svg.transition()
.duration(750)
.call(
d3.zoom().transform,
transform
);
}
// Filtert die Mindmap basierend auf einem Suchbegriff
filterBySearchTerm(searchTerm) {
if (!searchTerm || searchTerm.trim() === '') {
// Alle Knoten anzeigen
this.g.selectAll('.node-group')
.style('opacity', 1)
.style('pointer-events', 'all');
this.g.selectAll('.link')
.style('opacity', 1);
return;
}
const searchLower = searchTerm.toLowerCase();
const matchingNodes = this.nodes.filter(node =>
node.name.toLowerCase().includes(searchLower) ||
(node.description && node.description.toLowerCase().includes(searchLower))
);
const matchingNodeIds = new Set(matchingNodes.map(n => n.id));
// Passende Knoten hervorheben, andere ausblenden
this.g.selectAll('.node-group')
.style('opacity', d => matchingNodeIds.has(d.id) ? 1 : 0.2)
.style('pointer-events', d => matchingNodeIds.has(d.id) ? 'all' : 'none');
// Verbindungen zwischen passenden Knoten hervorheben
this.g.selectAll('.link')
.style('opacity', d =>
matchingNodeIds.has(d.source.id) && matchingNodeIds.has(d.target.id) ? 1 : 0.1
);
// Auf den ersten passenden Knoten fokussieren, wenn vorhanden
if (matchingNodes.length > 0) {
this.focusNode(matchingNodes[0].id);
}
}
/**
* Updates the thought_count property for each node based on existing connections
*/
updateConnectionCounts() {
// Reset all counts first
this.nodes.forEach(node => {
// Initialize thought_count if it doesn't exist
if (typeof node.thought_count !== 'number') {
node.thought_count = 0;
}
// Count connections for this node
const connectedNodes = this.getConnectedNodes(node);
node.thought_count = connectedNodes.length;
});
// Update UI to show counts
this.updateNodeLabels();
}
/**
* Updates the visual representation of node labels to include connection counts
*/
updateNodeLabels() {
if (!this.textElements) return;
this.textElements.text(d => {
if (d.thought_count > 0) {
return `${d.name} (${d.thought_count})`;
}
return d.name;
});
}
/**
* Adds a new connection between nodes and updates the counts
*/
addConnection(sourceNode, targetNode) {
if (!sourceNode || !targetNode) return false;
// Check if connection already exists
if (this.isConnected(sourceNode, targetNode)) return false;
// Add new connection
this.links.push({
source: sourceNode.id,
target: targetNode.id
});
// Update counts
this.updateConnectionCounts();
// Update visualization
this.updateVisualization();
return true;
}
// Lädt gemerkete Knoten aus dem LocalStorage
loadBookmarkedNodes() {
try {
const bookmarked = localStorage.getItem('bookmarkedNodes');
return bookmarked ? JSON.parse(bookmarked) : [];
} catch (error) {
console.error('Fehler beim Laden der gemerkten Knoten:', error);
return [];
}
}
// Speichert gemerkete Knoten im LocalStorage
saveBookmarkedNodes() {
try {
localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes));
} catch (error) {
console.error('Fehler beim Speichern der gemerkten Knoten:', error);
}
}
// Prüft, ob ein Knoten gemerkt ist
isNodeBookmarked(nodeId) {
return this.bookmarkedNodes.includes(nodeId);
}
// Merkt einen Knoten oder hebt die Markierung auf
toggleBookmark(nodeId) {
const index = this.bookmarkedNodes.indexOf(nodeId);
if (index === -1) {
// Node hinzufügen
this.bookmarkedNodes.push(nodeId);
this.updateNodeAppearance(nodeId, true);
} else {
// Node entfernen
this.bookmarkedNodes.splice(index, 1);
this.updateNodeAppearance(nodeId, false);
}
// Änderungen speichern
this.saveBookmarkedNodes();
// Event auslösen für andere Komponenten
const event = new CustomEvent('nodeBookmarkToggled', {
detail: {
nodeId: nodeId,
isBookmarked: index === -1
}
});
document.dispatchEvent(event);
return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben
}
// Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status
updateNodeAppearance(nodeId, isBookmarked) {
this.g.selectAll('.node-group')
.filter(d => d.id === nodeId)
.select('.node')
.classed('bookmarked', isBookmarked)
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
.attr('stroke-width', isBookmarked ? 3 : 2);
}
// Aktualisiert das Aussehen aller gemerkten Knoten
updateAllBookmarkedNodes() {
this.g.selectAll('.node-group')
.each((d) => {
const isBookmarked = this.isNodeBookmarked(d.id);
this.updateNodeAppearance(d.id, isBookmarked);
});
}
}
// Exportiere die Klasse für die Verwendung in anderen Modulen
window.MindMapVisualization = MindMapVisualization;