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:
229
static/js/main.js
Normal file
229
static/js/main.js
Normal 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;
|
||||
280
static/js/modules/chatgpt-assistant.js
Normal file
280
static/js/modules/chatgpt-assistant.js
Normal 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;
|
||||
719
static/js/modules/mindmap-page.js
Normal file
719
static/js/modules/mindmap-page.js
Normal 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');
|
||||
}
|
||||
});
|
||||
}
|
||||
777
static/js/modules/mindmap.js
Normal file
777
static/js/modules/mindmap.js
Normal 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;
|
||||
Reference in New Issue
Block a user