Files
website/static/js/modules/mindmap-page.js

719 lines
27 KiB
JavaScript

/**
* 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');
}
});
}