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

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

View File

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