Compare commits
8 Commits
f9881b678d
...
fd63810845
| Author | SHA1 | Date | |
|---|---|---|---|
| fd63810845 | |||
| 883973fe7b | |||
| 027e632856 | |||
| 406289e54f | |||
| 71b33e6cec | |||
| c74d3164bb | |||
| 4982cddeef | |||
| 631619ccb4 |
12
Dockerfile
12
Dockerfile
@@ -4,7 +4,7 @@ FROM python:3.11-slim
|
||||
# Arbeitsverzeichnis in Container
|
||||
WORKDIR /app
|
||||
|
||||
# Systemabhängigkeiten (falls erforderlich)
|
||||
# Systemabhängigkeiten installieren und Verzeichnisse anlegen
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
@@ -18,14 +18,16 @@ RUN pip install --upgrade pip && \
|
||||
# Anwendungscode kopieren
|
||||
COPY . .
|
||||
|
||||
# Berechtigungen für database-Ordner
|
||||
RUN chmod -R 777 /app/database
|
||||
|
||||
# Exponiere Port 5000 für Flask
|
||||
EXPOSE 5000
|
||||
|
||||
# Setze Umgebungsvariablen
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_ENV=production
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Wenn eine .env im Arbeitsverzeichnis vorhanden ist, wird sie automatisch von Flask geladen
|
||||
|
||||
# Startkommando
|
||||
# Startkommando mit spezifischen Flags für Produktion
|
||||
CMD ["python", "app.py"]
|
||||
Binary file not shown.
@@ -11,8 +11,7 @@ services:
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- .:/app:ro
|
||||
- db_data:/app/database
|
||||
- ./database:/app/database
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
@@ -2,10 +2,12 @@
|
||||
# Kopiere diese Datei zu .env und passe die Werte an
|
||||
|
||||
# Flask
|
||||
SECRET_KEY=dein-geheimer-schluessel-hier
|
||||
FLASK_APP=app.py
|
||||
FLASK_ENV=development
|
||||
SECRET_KEY=your-secret-key-replace-in-production
|
||||
|
||||
# OpenAI API
|
||||
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
|
||||
# Datenbank
|
||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||
|
||||
51
start.sh
51
start.sh
@@ -1,22 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
#!/usr/bin/env powershell
|
||||
# Windows PowerShell-Version des Start-Skripts
|
||||
# Datum: 01.05.2025
|
||||
|
||||
# Docker-Status prüfen
|
||||
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
|
||||
try {
|
||||
$status = docker ps -q
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
|
||||
Write-Host $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Alte Container stoppen und entfernen
|
||||
if [ $(docker ps -aq --filter "name=systades_app" | wc -l) -gt 0 ]; then
|
||||
docker rm -f systades_app || true
|
||||
fi
|
||||
if [ $(docker ps -aq --filter "name=systades_db" | wc -l) -gt 0 ]; then
|
||||
docker rm -f systades_db || true
|
||||
fi
|
||||
$containerExists = docker ps -a --filter "name=systades_app" -q
|
||||
if ($containerExists) {
|
||||
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
|
||||
docker rm -f systades_app
|
||||
}
|
||||
|
||||
# Alte Images löschen
|
||||
docker rmi -f systades_app:latest || true
|
||||
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
|
||||
docker rmi -f systades_app:latest
|
||||
|
||||
# Stelle sicher, dass das Datenbankverzeichnis existiert
|
||||
if (-not (Test-Path "database")) {
|
||||
New-Item -Path "database" -ItemType Directory -Force
|
||||
}
|
||||
|
||||
# Docker-Compose Setup neu bauen
|
||||
Write-Host "Baue Container neu..." -ForegroundColor Green
|
||||
docker-compose build --no-cache
|
||||
|
||||
# Docker-Compose neu starten
|
||||
Write-Host "Starte Container..." -ForegroundColor Green
|
||||
docker-compose up -d --force-recreate
|
||||
|
||||
# Warte kurz und prüfe, ob der Container läuft
|
||||
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
|
||||
Start-Sleep -Seconds 3
|
||||
docker ps | Select-String "systades_app"
|
||||
|
||||
# Ausgabe
|
||||
echo "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar."
|
||||
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
|
||||
Write-Host "----------------------------------------"
|
||||
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
|
||||
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
|
||||
Write-Host "----------------------------------------"
|
||||
@@ -9,22 +9,12 @@ if (!window.MindMap) {
|
||||
}
|
||||
|
||||
// Registriere den Initialisierer im MindMap-Objekt
|
||||
if (window.MindMap) {
|
||||
window.MindMap.pageInitializers = window.MindMap.pageInitializers || {};
|
||||
window.MindMap.pageInitializers.mindmap = initMindmapPage;
|
||||
}
|
||||
|
||||
// Initialisiere die Mindmap-Seite nur, wenn alle Abhängigkeiten vorhanden sind
|
||||
if (window.MindMap && typeof MindMapVisualization !== 'undefined') {
|
||||
if (document.body && document.body.dataset && document.body.dataset.page === 'mindmap') {
|
||||
window.MindMap.pageInitializers = window.MindMap.pageInitializers || {};
|
||||
window.MindMap.pageInitializers.mindmap = initMindmapPage;
|
||||
initMindmapPage();
|
||||
}
|
||||
}
|
||||
window.MindMap.pageInitializers = window.MindMap.pageInitializers || {};
|
||||
window.MindMap.pageInitializers.mindmap = initMindmapPage;
|
||||
|
||||
// Event-Listener für DOMContentLoaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prüfe, ob wir auf der Mindmap-Seite sind und initialisiere
|
||||
// Prüfe, ob wir auf der Mindmap-Seite sind
|
||||
if (document.body && document.body.dataset && document.body.dataset.page === 'mindmap') {
|
||||
initMindmapPage();
|
||||
}
|
||||
@@ -34,423 +24,415 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
* Initialisiert die Mindmap-Seite
|
||||
*/
|
||||
function initMindmapPage() {
|
||||
console.log('Mindmap-Seite Initialisierung startet...');
|
||||
console.log('D3 Bibliothek verfügbar:', typeof d3 !== 'undefined');
|
||||
console.log('MindMapVisualization verfügbar:', typeof MindMapVisualization !== 'undefined');
|
||||
console.log('Mindmap-Seite wird initialisiert...');
|
||||
|
||||
const mindmapContainer = document.getElementById('cy');
|
||||
const thoughtsContainer = document.getElementById('thoughts-container');
|
||||
const nodeDetailsContainer = document.getElementById('node-details');
|
||||
|
||||
if (!mindmapContainer) {
|
||||
console.error('Mindmap-Container nicht gefunden!');
|
||||
return;
|
||||
}
|
||||
console.log('Mindmap-Container gefunden:', mindmapContainer);
|
||||
|
||||
// Prüfe, ob D3.js geladen ist
|
||||
if (typeof d3 === 'undefined') {
|
||||
console.error('D3.js ist nicht geladen!');
|
||||
if (mindmapContainer) {
|
||||
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>
|
||||
`;
|
||||
}
|
||||
// Hauptcontainer für die Mindmap
|
||||
const cyContainer = document.getElementById('cy');
|
||||
if (!cyContainer) {
|
||||
console.error('Mindmap-Container #cy nicht gefunden!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe, ob MindMapVisualization definiert ist
|
||||
if (typeof MindMapVisualization === 'undefined') {
|
||||
console.error('MindMapVisualization-Klasse ist nicht definiert!');
|
||||
if (mindmapContainer) {
|
||||
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">MindMap-Visualisierung konnte nicht geladen werden. Bitte laden Sie die Seite neu.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Info-Panel für Knotendetails
|
||||
const nodeInfoPanel = document.getElementById('node-info-panel');
|
||||
const nodeDescription = document.getElementById('node-description');
|
||||
const connectedNodes = document.getElementById('connected-nodes');
|
||||
|
||||
// Erstelle die Mindmap-Visualisierung
|
||||
let mindmap;
|
||||
// Toolbar-Buttons
|
||||
const fitButton = document.getElementById('fit-btn');
|
||||
const resetButton = document.getElementById('reset-btn');
|
||||
const toggleLabelsButton = document.getElementById('toggle-labels-btn');
|
||||
|
||||
// Mindmap-Instanz
|
||||
let mindmap = null;
|
||||
|
||||
// Cytoscape.js für die Visualisierung initialisieren
|
||||
try {
|
||||
console.log('Versuche, MindMapVisualization zu erstellen...');
|
||||
mindmap = new MindMapVisualization('#cy', {
|
||||
height: 600,
|
||||
onNodeClick: handleNodeClick
|
||||
});
|
||||
|
||||
// Globale Referenz für die Zoom-Buttons erstellen
|
||||
window.mindmapInstance = mindmap;
|
||||
|
||||
// Lade die Mindmap-Daten
|
||||
mindmap.loadData();
|
||||
console.log('MindMapVisualization erfolgreich erstellt und geladen');
|
||||
|
||||
// Dark Mode Listener registrieren
|
||||
registerDarkModeListener(mindmap);
|
||||
// Cytoscape.js-Bibliothek überprüfen
|
||||
if (typeof cytoscape === 'undefined') {
|
||||
loadExternalScript('https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js')
|
||||
.then(() => {
|
||||
initCytoscape();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden von Cytoscape.js:', error);
|
||||
showErrorMessage(cyContainer, 'Cytoscape.js konnte nicht geladen werden.');
|
||||
});
|
||||
} else {
|
||||
initCytoscape();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der MindMapVisualization:', error);
|
||||
if (mindmapContainer) {
|
||||
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">Fehler beim Erstellen der Mindmap-Visualisierung:</p>
|
||||
<p class="text-md mt-2">${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return;
|
||||
console.error('Fehler bei der Initialisierung der Mindmap:', error);
|
||||
showErrorMessage(cyContainer, 'Die Mindmap konnte nicht initialisiert werden: ' + error.message);
|
||||
}
|
||||
|
||||
// Suchfunktion für die Mindmap
|
||||
const searchInput = document.getElementById('search-mindmap');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
mindmap.filterBySearchTerm(e.target.value);
|
||||
/**
|
||||
* Lädt ein externes Script asynchron
|
||||
*/
|
||||
function loadExternalScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
// UI-Elemente initialisieren
|
||||
initializeMindmapButtons(mindmap);
|
||||
|
||||
/**
|
||||
* Behandelt Klicks auf Mindmap-Knoten
|
||||
* Zeigt eine Fehlermeldung im Container an
|
||||
*/
|
||||
async function handleNodeClick(node) {
|
||||
// Details im Detailbereich anzeigen
|
||||
if (nodeDetailsContainer) {
|
||||
updateNodeDetails(node);
|
||||
}
|
||||
|
||||
if (!thoughtsContainer) return;
|
||||
|
||||
// Zeige Lade-Animation
|
||||
thoughtsContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center p-4">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-500"></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="p-4">
|
||||
<div class="text-red-500 mb-2">
|
||||
<i class="fa-solid fa-triangle-exclamation text-xl"></i>
|
||||
</div>
|
||||
<p class="text-sm">Fehler beim Laden der Gedanken.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Node-Details-Bereich mit Informationen zum ausgewählten Knoten
|
||||
*/
|
||||
function updateNodeDetails(node) {
|
||||
if (!nodeDetailsContainer) return;
|
||||
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
const textClass = isDarkMode ? 'text-gray-300' : 'text-gray-600';
|
||||
|
||||
nodeDetailsContainer.innerHTML = `
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-2 ${isDarkMode ? 'text-white' : 'text-gray-800'}">
|
||||
${node.name}
|
||||
</h3>
|
||||
<div class="mb-3">
|
||||
<span class="px-2 py-1 text-xs rounded-full ${getCategoryBadgeClass(node.category)}">
|
||||
${node.category || 'Keine Kategorie'}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm ${textClass}">
|
||||
${node.description || 'Keine Beschreibung verfügbar.'}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 text-sm">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="${textClass}">Erstellt:</span>
|
||||
<span class="${textClass}">${formatDate(node.created_at || new Date())}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="${textClass}">Verbindungen:</span>
|
||||
<span class="${textClass}">${node.connections || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
function showErrorMessage(container, message) {
|
||||
container.innerHTML = `
|
||||
<div class="p-8 text-center bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||
<i class="fa-solid fa-triangle-exclamation text-4xl text-red-500 mb-4"></i>
|
||||
<p class="text-lg text-red-600 dark:text-red-400">${message}</p>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Bitte laden Sie die Seite neu oder kontaktieren Sie den Support.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert ein Datum in ein lesbares Format
|
||||
* Initialisiert Cytoscape mit den Mindmap-Daten
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert eine CSS-Klasse für die Kategorie-Badge basierend auf der Kategorie
|
||||
*/
|
||||
function getCategoryBadgeClass(category) {
|
||||
if (!category) return 'bg-gray-500 text-white';
|
||||
function initCytoscape() {
|
||||
console.log('Cytoscape.js wird initialisiert...');
|
||||
|
||||
const categories = {
|
||||
'Konzept': 'bg-blue-500 text-white',
|
||||
'Theorie': 'bg-purple-500 text-white',
|
||||
'Methode': 'bg-green-500 text-white',
|
||||
'Person': 'bg-yellow-500 text-black',
|
||||
'Ereignis': 'bg-red-500 text-white',
|
||||
'Referenz': 'bg-indigo-500 text-white'
|
||||
};
|
||||
|
||||
return categories[category] || 'bg-gray-500 text-white';
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Gedanken in den Container
|
||||
*/
|
||||
function renderThoughts(thoughts, nodeName) {
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
|
||||
// Wenn keine Gedanken vorhanden sind
|
||||
if (thoughts.length === 0) {
|
||||
thoughtsContainer.innerHTML = `
|
||||
<div class="text-center py-4">
|
||||
<div class="text-blue-400 mb-2">
|
||||
<i class="fa-solid fa-info-circle text-xl"></i>
|
||||
</div>
|
||||
<p class="text-sm ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}">
|
||||
Keine Gedanken zu "${nodeName}" vorhanden.
|
||||
</p>
|
||||
<button id="add-thought-btn" class="mt-3 px-3 py-1.5 text-xs rounded-lg transition-colors duration-200"
|
||||
${isDarkMode ?
|
||||
'class="bg-primary-600 hover:bg-primary-700 text-white"' :
|
||||
'class="bg-primary-500 hover:bg-primary-600 text-white"'}>
|
||||
<i class="fa-solid fa-plus mr-1"></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-3">
|
||||
<h3 class="text-sm font-semibold ${isDarkMode ? 'text-white' : 'text-gray-800'}">
|
||||
Gedanken zu "${nodeName}"
|
||||
</h3>
|
||||
<button id="add-thought-btn" class="px-2 py-1 text-xs rounded-lg transition-colors duration-200"
|
||||
${isDarkMode ?
|
||||
'class="bg-primary-600 hover:bg-primary-700 text-white"' :
|
||||
'class="bg-primary-500 hover:bg-primary-600 text-white"'}>
|
||||
<i class="fa-solid fa-plus mr-1"></i> Neu
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-3 max-h-80 overflow-y-auto pr-1" 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, isDarkMode);
|
||||
|
||||
// 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, isDarkMode) {
|
||||
const card = document.createElement('div');
|
||||
card.className = `relative transition-all duration-300 opacity-0 translate-y-4 transform rounded-lg overflow-hidden p-3 border-l-4 ${isDarkMode ? 'bg-gray-800/70' : 'bg-white/90'} border-l-[${thought.color_code || '#4080ff'}]`;
|
||||
|
||||
// Karten-Inhalt
|
||||
card.innerHTML = `
|
||||
<div class="flex justify-between items-start mb-1">
|
||||
<h4 class="text-sm font-semibold ${isDarkMode ? 'text-white' : 'text-gray-800'}">${thought.title}</h4>
|
||||
<div class="text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}">${formatDate(thought.timestamp)}</div>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert prose-sm">
|
||||
<p class="text-xs line-clamp-2 ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}">${thought.content}</p>
|
||||
</div>
|
||||
${thought.keywords ? `
|
||||
<div class="flex flex-wrap gap-1 mt-2">
|
||||
${thought.keywords.split(',').slice(0, 2).map(keyword =>
|
||||
`<span class="px-2 py-0.5 text-[10px] rounded-full ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-200 text-gray-700'}">${keyword.trim()}</span>`
|
||||
).join('')}
|
||||
${thought.keywords.split(',').length > 2 ?
|
||||
`<span class="px-2 py-0.5 text-[10px] rounded-full ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-200 text-gray-700'}">+${thought.keywords.split(',').length - 2}</span>` :
|
||||
''}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="mt-2 flex justify-between items-center">
|
||||
<div class="text-xs ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}">
|
||||
<i class="fa-solid fa-user text-xs mr-1"></i> ${thought.author}
|
||||
</div>
|
||||
<button class="viewThought-btn text-xs px-2 py-1 rounded ${isDarkMode ? 'hover:bg-white/10' : 'hover:bg-gray-200'} transition-colors"
|
||||
data-thought-id="${thought.id}">
|
||||
<i class="fa-solid fa-eye mr-1"></i> Ansehen
|
||||
</button>
|
||||
// Zeige Ladeanimation
|
||||
cyContainer.innerHTML = `
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event-Listener für die Detailansicht
|
||||
card.querySelector('.viewThought-btn').addEventListener('click', function() {
|
||||
const thoughtId = this.getAttribute('data-thought-id');
|
||||
showThoughtDetail(thoughtId);
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event-Listener für Dark-Mode-Änderungen registrieren
|
||||
*/
|
||||
function registerDarkModeListener(mindmap) {
|
||||
document.addEventListener('darkModeToggled', function(event) {
|
||||
const isDark = event.detail.isDark;
|
||||
console.log('Dark mode changed to:', isDark);
|
||||
|
||||
// Mindmap-Styling aktualisieren
|
||||
if (mindmap && mindmap.updateColorScheme) {
|
||||
mindmap.updateColorScheme(isDark);
|
||||
}
|
||||
|
||||
// UI-Elemente aktualisieren, falls notwendig
|
||||
if (nodeDetailsContainer) {
|
||||
const selectedNode = mindmap.getSelectedNode();
|
||||
if (selectedNode) {
|
||||
updateNodeDetails(selectedNode);
|
||||
// Lade Daten vom Backend
|
||||
fetch('/api/mindmap')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Netzwerkfehler beim Laden der Mindmap-Daten');
|
||||
}
|
||||
}
|
||||
|
||||
if (thoughtsContainer) {
|
||||
const thoughts = thoughtsContainer.querySelector('#thoughts-grid');
|
||||
if (thoughts) {
|
||||
// Einfache Aktualisierung - im Produktionscode würde man das komplexer machen
|
||||
const selectedNode = mindmap.getSelectedNode();
|
||||
if (selectedNode) {
|
||||
handleNodeClick(selectedNode);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Mindmap-Daten erfolgreich geladen');
|
||||
renderMindmap(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
||||
|
||||
// Verwende Standarddaten als Fallback
|
||||
console.log('Verwende Standarddaten als Fallback...');
|
||||
const defaultData = generateDefaultData();
|
||||
renderMindmap(defaultData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Standarddaten für die Mindmap als Fallback
|
||||
*/
|
||||
function generateDefaultData() {
|
||||
return {
|
||||
nodes: [
|
||||
{ id: 'root', name: 'Wissen', description: 'Zentrale Wissensbasis', category: 'Zentral', color_code: '#4299E1' },
|
||||
{ id: 'philosophy', name: 'Philosophie', description: 'Philosophisches Denken', category: 'Philosophie', color_code: '#9F7AEA', parent_id: 'root' },
|
||||
{ id: 'science', name: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', category: 'Wissenschaft', color_code: '#48BB78', parent_id: 'root' },
|
||||
{ id: 'technology', name: 'Technologie', description: 'Technologische Entwicklungen', category: 'Technologie', color_code: '#ED8936', parent_id: 'root' },
|
||||
{ id: 'arts', name: 'Künste', description: 'Künstlerische Ausdrucksformen', category: 'Künste', color_code: '#ED64A6', parent_id: 'root' },
|
||||
{ id: 'psychology', name: 'Psychologie', description: 'Menschliches Verhalten und Geist', category: 'Psychologie', color_code: '#4299E1', parent_id: 'root' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Mindmap mit Cytoscape.js
|
||||
*/
|
||||
function renderMindmap(data) {
|
||||
console.log('Rendere Mindmap mit Daten:', data);
|
||||
|
||||
// Konvertiere Backend-Daten in Cytoscape-Format
|
||||
const elements = convertToCytoscapeFormat(data);
|
||||
|
||||
// Leere den Container
|
||||
cyContainer.innerHTML = '';
|
||||
|
||||
// Erstelle Cytoscape-Instanz
|
||||
mindmap = cytoscape({
|
||||
container: cyContainer,
|
||||
elements: elements,
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': 'data(color)',
|
||||
'label': 'data(name)',
|
||||
'width': 30,
|
||||
'height': 30,
|
||||
'font-size': 12,
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
'text-margin-y': 8,
|
||||
'color': document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#334155',
|
||||
'text-background-color': document.documentElement.classList.contains('dark') ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)',
|
||||
'text-background-opacity': 0.8,
|
||||
'text-background-padding': '2px',
|
||||
'text-background-shape': 'roundrectangle',
|
||||
'text-wrap': 'ellipsis',
|
||||
'text-max-width': '100px'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': 2,
|
||||
'line-color': document.documentElement.classList.contains('dark') ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
|
||||
'target-arrow-color': document.documentElement.classList.contains('dark') ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
|
||||
'curve-style': 'bezier'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node:selected',
|
||||
style: {
|
||||
'background-color': 'data(color)',
|
||||
'border-width': 3,
|
||||
'border-color': '#8b5cf6',
|
||||
'width': 40,
|
||||
'height': 40,
|
||||
'font-size': 14,
|
||||
'font-weight': 'bold',
|
||||
'text-background-color': '#8b5cf6',
|
||||
'text-background-opacity': 0.9
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 800,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
refresh: 30,
|
||||
randomize: true,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 8000,
|
||||
nodeOverlap: 20,
|
||||
idealEdgeLength: 200,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 1.2,
|
||||
gravity: 80,
|
||||
fit: true,
|
||||
padding: 30
|
||||
}
|
||||
});
|
||||
|
||||
// Event-Listener für Knoteninteraktionen
|
||||
mindmap.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
const nodeData = node.data();
|
||||
|
||||
// Update Info-Panel
|
||||
updateNodeInfoPanel(nodeData);
|
||||
|
||||
// Lade verbundene Knoten
|
||||
updateConnectedNodes(node);
|
||||
});
|
||||
|
||||
// Toolbar-Buttons aktivieren
|
||||
if (fitButton) {
|
||||
fitButton.addEventListener('click', () => {
|
||||
mindmap.fit();
|
||||
mindmap.center();
|
||||
});
|
||||
}
|
||||
|
||||
if (resetButton) {
|
||||
resetButton.addEventListener('click', () => {
|
||||
mindmap.layout({
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
randomize: true,
|
||||
fit: true
|
||||
}).run();
|
||||
});
|
||||
}
|
||||
|
||||
if (toggleLabelsButton) {
|
||||
let labelsVisible = true;
|
||||
toggleLabelsButton.addEventListener('click', () => {
|
||||
labelsVisible = !labelsVisible;
|
||||
|
||||
if (labelsVisible) {
|
||||
mindmap.style()
|
||||
.selector('node')
|
||||
.style('label', 'data(name)')
|
||||
.update();
|
||||
} else {
|
||||
mindmap.style()
|
||||
.selector('node')
|
||||
.style('label', '')
|
||||
.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Dark Mode-Änderungen überwachen
|
||||
document.addEventListener('darkModeToggled', function(event) {
|
||||
updateDarkModeStyles(event.detail.isDark);
|
||||
});
|
||||
|
||||
// Initial fit und center
|
||||
setTimeout(() => {
|
||||
mindmap.fit();
|
||||
mindmap.center();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert die Buttons für die Mindmap
|
||||
* Konvertiert die Backend-Daten ins Cytoscape-Format
|
||||
*/
|
||||
function initializeMindmapButtons(mindmap) {
|
||||
// Zoom-Buttons
|
||||
document.getElementById('zoomIn')?.addEventListener('click', () => mindmap.zoomIn());
|
||||
document.getElementById('zoomOut')?.addEventListener('click', () => mindmap.zoomOut());
|
||||
document.getElementById('zoomReset')?.addEventListener('click', () => mindmap.zoomReset());
|
||||
function convertToCytoscapeFormat(data) {
|
||||
const elements = {
|
||||
nodes: [],
|
||||
edges: []
|
||||
};
|
||||
|
||||
// Layout-Button
|
||||
document.getElementById('reLayout')?.addEventListener('click', () => mindmap.reLayout());
|
||||
// Nodes hinzufügen
|
||||
if (data.nodes && data.nodes.length > 0) {
|
||||
data.nodes.forEach(node => {
|
||||
elements.nodes.push({
|
||||
data: {
|
||||
id: String(node.id),
|
||||
name: node.name,
|
||||
description: node.description || 'Keine Beschreibung verfügbar',
|
||||
category: node.category || 'Allgemein',
|
||||
color: node.color_code || getRandomColor()
|
||||
}
|
||||
});
|
||||
|
||||
// Kante zum Elternknoten hinzufügen (falls vorhanden)
|
||||
if (node.parent_id) {
|
||||
elements.edges.push({
|
||||
data: {
|
||||
id: `edge-${node.parent_id}-${node.id}`,
|
||||
source: String(node.parent_id),
|
||||
target: String(node.id)
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Zusätzliche Kanten zwischen Knoten hinzufügen (falls in den Daten vorhanden)
|
||||
if (data.edges && data.edges.length > 0) {
|
||||
data.edges.forEach(edge => {
|
||||
elements.edges.push({
|
||||
data: {
|
||||
id: `edge-${edge.source}-${edge.target}`,
|
||||
source: String(edge.source),
|
||||
target: String(edge.target)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export-Button
|
||||
document.getElementById('exportMindmap')?.addEventListener('click', () => mindmap.exportMindmap());
|
||||
|
||||
// Bearbeitungs-Buttons
|
||||
document.getElementById('addNode')?.addEventListener('click', () => openAddNodeModal(mindmap));
|
||||
document.getElementById('addEdge')?.addEventListener('click', () => mindmap.startAddEdgeMode());
|
||||
document.getElementById('editNode')?.addEventListener('click', () => {
|
||||
const selectedNode = mindmap.getSelectedNode();
|
||||
if (selectedNode) {
|
||||
openEditNodeModal(selectedNode, mindmap);
|
||||
} else {
|
||||
showNotification('Bitte wählen Sie zuerst einen Knoten aus.', 'warning');
|
||||
}
|
||||
});
|
||||
document.getElementById('deleteNode')?.addEventListener('click', () => {
|
||||
const selectedNode = mindmap.getSelectedNode();
|
||||
if (selectedNode) {
|
||||
confirmDeleteNode(selectedNode, mindmap);
|
||||
} else {
|
||||
showNotification('Bitte wählen Sie zuerst einen Knoten aus.', 'warning');
|
||||
}
|
||||
});
|
||||
document.getElementById('deleteEdge')?.addEventListener('click', () => {
|
||||
const selectedEdge = mindmap.getSelectedEdge();
|
||||
if (selectedEdge) {
|
||||
confirmDeleteEdge(selectedEdge, mindmap);
|
||||
} else {
|
||||
showNotification('Bitte wählen Sie zuerst eine Verbindung aus.', 'warning');
|
||||
}
|
||||
});
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Benachrichtigung an
|
||||
* Aktualisiert das Informations-Panel mit den Knotendaten
|
||||
*/
|
||||
function showNotification(message, type = 'info') {
|
||||
console.log(`Benachrichtigung (${type}): ${message}`);
|
||||
// Implementiere eine Toast-Benachrichtigung
|
||||
// ...
|
||||
function updateNodeInfoPanel(nodeData) {
|
||||
if (nodeInfoPanel && nodeDescription) {
|
||||
// Panel anzeigen
|
||||
nodeInfoPanel.style.display = 'block';
|
||||
|
||||
// Titel und Beschreibung aktualisieren
|
||||
const titleElement = nodeInfoPanel.querySelector('.info-panel-title');
|
||||
if (titleElement) {
|
||||
titleElement.textContent = nodeData.name;
|
||||
}
|
||||
|
||||
if (nodeDescription) {
|
||||
nodeDescription.textContent = nodeData.description || 'Keine Beschreibung verfügbar';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ... Weitere Funktionen wie openAddThoughtModal, openAddNodeModal usw. ...
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das Modal zum Hinzufügen eines Gedankens
|
||||
*/
|
||||
function openAddThoughtModal(nodeName) {
|
||||
// Modal-Implementierung...
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt die Detailansicht eines Gedankens
|
||||
*/
|
||||
function showThoughtDetail(thoughtId) {
|
||||
// Detailansicht-Implementierung...
|
||||
/**
|
||||
* Aktualisiert die Liste der verbundenen Knoten
|
||||
*/
|
||||
function updateConnectedNodes(node) {
|
||||
if (connectedNodes) {
|
||||
// Leere den Container
|
||||
connectedNodes.innerHTML = '';
|
||||
|
||||
// Hole verbundene Knoten
|
||||
const connectedEdges = node.connectedEdges();
|
||||
|
||||
if (connectedEdges.length === 0) {
|
||||
connectedNodes.innerHTML = '<div class="text-sm italic">Keine verbundenen Knoten</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Füge alle verbundenen Knoten hinzu
|
||||
connectedEdges.forEach(edge => {
|
||||
const targetNode = edge.target().id() === node.id() ? edge.source() : edge.target();
|
||||
const targetData = targetNode.data();
|
||||
|
||||
const nodeLink = document.createElement('div');
|
||||
nodeLink.className = 'node-link';
|
||||
nodeLink.innerHTML = `
|
||||
<span class="w-2 h-2 rounded-full" style="background-color: ${targetData.color}"></span>
|
||||
${targetData.name}
|
||||
`;
|
||||
|
||||
// Klick-Event zum Fokussieren des Knotens
|
||||
nodeLink.addEventListener('click', () => {
|
||||
mindmap.center(targetNode);
|
||||
targetNode.select();
|
||||
updateNodeInfoPanel(targetData);
|
||||
updateConnectedNodes(targetNode);
|
||||
});
|
||||
|
||||
connectedNodes.appendChild(nodeLink);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Styles bei Dark Mode-Änderungen
|
||||
*/
|
||||
function updateDarkModeStyles(isDark) {
|
||||
if (!mindmap) return;
|
||||
|
||||
const textColor = isDark ? '#f1f5f9' : '#334155';
|
||||
const textBgColor = isDark ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)';
|
||||
const edgeColor = isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)';
|
||||
|
||||
mindmap.style()
|
||||
.selector('node')
|
||||
.style({
|
||||
'color': textColor,
|
||||
'text-background-color': textBgColor
|
||||
})
|
||||
.selector('edge')
|
||||
.style({
|
||||
'line-color': edgeColor,
|
||||
'target-arrow-color': edgeColor
|
||||
})
|
||||
.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine zufällige Farbe
|
||||
*/
|
||||
function getRandomColor() {
|
||||
const colors = [
|
||||
'#4299E1', // Blau
|
||||
'#9F7AEA', // Lila
|
||||
'#48BB78', // Grün
|
||||
'#ED8936', // Orange
|
||||
'#ED64A6', // Pink
|
||||
'#F56565' // Rot
|
||||
];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -191,6 +191,17 @@
|
||||
showSettingsModal: false,
|
||||
|
||||
init() {
|
||||
this.initDarkMode();
|
||||
},
|
||||
|
||||
initDarkMode() {
|
||||
// Lade zuerst den Wert aus dem localStorage (client-seitig)
|
||||
const storedMode = localStorage.getItem('colorMode');
|
||||
if (storedMode) {
|
||||
this.darkMode = storedMode === 'dark';
|
||||
}
|
||||
|
||||
// Dann hole die Server-Einstellung, die Vorrang hat
|
||||
this.fetchDarkModeFromSession();
|
||||
},
|
||||
|
||||
@@ -200,8 +211,7 @@
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.darkMode = data.darkMode === 'true';
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
document.querySelector('body').classList.toggle('dark', this.darkMode);
|
||||
this.applyDarkMode();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -209,10 +219,15 @@
|
||||
});
|
||||
},
|
||||
|
||||
toggleDarkMode() {
|
||||
this.darkMode = !this.darkMode;
|
||||
applyDarkMode() {
|
||||
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||
document.querySelector('body').classList.toggle('dark', this.darkMode);
|
||||
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
|
||||
},
|
||||
|
||||
toggleDarkMode() {
|
||||
this.darkMode = !this.darkMode;
|
||||
this.applyDarkMode();
|
||||
|
||||
fetch('/api/set_dark_mode', {
|
||||
method: 'POST',
|
||||
@@ -224,7 +239,6 @@
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||
detail: { isDark: this.darkMode }
|
||||
}));
|
||||
@@ -613,37 +627,42 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Dark/Light-Mode persistent und robust -->
|
||||
<!-- Dark/Light-Mode vereinheitlicht -->
|
||||
<script>
|
||||
(function() {
|
||||
function applyMode(mode) {
|
||||
if (mode === 'dark') {
|
||||
// Globaler Zugriff für externe Skripte
|
||||
window.MindMap = window.MindMap || {};
|
||||
|
||||
window.MindMap.toggleDarkMode = function() {
|
||||
// Alpine.js-Instanz benutzen, wenn verfügbar
|
||||
const appEl = document.querySelector('body');
|
||||
if (appEl && appEl.__x) {
|
||||
appEl.__x.$data.toggleDarkMode();
|
||||
} else {
|
||||
// Fallback: Nur classList und localStorage
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.documentElement.classList.toggle('dark', !isDark);
|
||||
document.body.classList.toggle('dark', !isDark);
|
||||
localStorage.setItem('colorMode', !isDark ? 'dark' : 'light');
|
||||
|
||||
// Server aktualisieren
|
||||
fetch('/api/set_dark_mode', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ darkMode: !isDark })
|
||||
}).catch(console.error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fallback für Browser-Präferenz, falls keine Einstellung geladen werden konnte
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!document.body.classList.contains('dark') && !document.documentElement.classList.contains('dark')) {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (prefersDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('colorMode', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('colorMode', 'light');
|
||||
document.body.classList.add('dark');
|
||||
}
|
||||
}
|
||||
// Beim Laden: Präferenz aus localStorage oder System übernehmen
|
||||
const stored = localStorage.getItem('colorMode');
|
||||
if (stored === 'dark' || stored === 'light') {
|
||||
applyMode(stored);
|
||||
} else {
|
||||
// Systempräferenz als Fallback
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
applyMode(prefersDark ? 'dark' : 'light');
|
||||
}
|
||||
// Umschalter für alle Mode-Toggles
|
||||
window.toggleColorMode = function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
applyMode(isDark ? 'light' : 'dark');
|
||||
};
|
||||
// Optional: globales Event für andere Skripte
|
||||
window.addEventListener('storage', function(e) {
|
||||
if (e.key === 'colorMode') applyMode(e.newValue);
|
||||
});
|
||||
})();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -126,162 +126,140 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2 gradient-text">Interaktive Mindmap</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400">Visualisieren und erkunden Sie Wissensnetze und Gedankenkonstrukte</p>
|
||||
</div>
|
||||
|
||||
<div class="mindmap-section">
|
||||
<!-- Linke Spalte: Mindmap -->
|
||||
<div class="mindmap-container">
|
||||
<!-- Suchleiste -->
|
||||
<div class="mindmap-toolbar">
|
||||
<div class="flex-1">
|
||||
<input type="text" id="search-mindmap"
|
||||
class="w-full max-w-md px-3 py-2 rounded-lg"
|
||||
x-bind:class="darkMode ? 'bg-gray-800 border border-gray-700 text-white' : 'bg-white border border-gray-300 text-gray-700'"
|
||||
placeholder="In der Mindmap suchen...">
|
||||
</div>
|
||||
|
||||
<button id="addNode" class="btn"
|
||||
x-bind:class="darkMode ? 'bg-primary-600 hover:bg-primary-700 text-white' : 'bg-primary-500 hover:bg-primary-600 text-white'">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span class="hidden sm:inline">Knoten hinzufügen</span>
|
||||
</button>
|
||||
|
||||
<button id="addEdge" class="btn"
|
||||
x-bind:class="darkMode ? 'bg-primary-600 hover:bg-primary-700 text-white' : 'bg-primary-500 hover:bg-primary-600 text-white'">
|
||||
<i class="fa-solid fa-link"></i>
|
||||
<span class="hidden sm:inline">Verbindung</span>
|
||||
</button>
|
||||
|
||||
<button id="reLayout" class="btn"
|
||||
x-bind:class="darkMode ? 'bg-gray-700 hover:bg-gray-600 text-white' : 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
||||
<i class="fa-solid fa-compass-drafting"></i>
|
||||
<span class="hidden sm:inline">Anordnen</span>
|
||||
</button>
|
||||
|
||||
<div class="relative" x-data="{ open: false }">
|
||||
<button @click="open = !open" class="btn"
|
||||
x-bind:class="darkMode ? 'bg-gray-700 hover:bg-gray-600 text-white' : 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
|
||||
<i class="fa-solid fa-ellipsis-vertical"></i>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex flex-col lg:flex-row gap-8">
|
||||
<!-- Hauptinhalt -->
|
||||
<div class="w-full lg:w-3/4">
|
||||
<!-- Mindmap-Titelbereich -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold mb-2 mystical-glow"
|
||||
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||
Wissenslandkarte
|
||||
</h1>
|
||||
<p class="opacity-80 text-lg"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
Visualisiere die Verbindungen zwischen Gedanken und Konzepten
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mindmap-Container -->
|
||||
<div class="mindmap-container">
|
||||
<!-- Toolbar -->
|
||||
<div class="mindmap-toolbar">
|
||||
<button id="fit-btn" class="mindmap-action-btn">
|
||||
<i class="fa-solid fa-expand"></i>
|
||||
<span>Ansicht anpassen</span>
|
||||
</button>
|
||||
<button id="reset-btn" class="mindmap-action-btn">
|
||||
<i class="fa-solid fa-undo"></i>
|
||||
<span>Zurücksetzen</span>
|
||||
</button>
|
||||
<button id="toggle-labels-btn" class="mindmap-action-btn">
|
||||
<i class="fa-solid fa-tags"></i>
|
||||
<span>Labels ein/aus</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hauptvisualisierung -->
|
||||
<div id="cy"></div>
|
||||
|
||||
<!-- Info-Panel -->
|
||||
<div id="node-info-panel" class="mindmap-info-panel">
|
||||
<h4 class="info-panel-title">Knoteninfo</h4>
|
||||
<p id="node-description" class="info-panel-description">Wählen Sie einen Knoten aus...</p>
|
||||
|
||||
<div x-show="open" @click.away="open = false"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 mt-2 w-48 origin-top-right z-10 rounded-md shadow-lg"
|
||||
x-bind:class="darkMode ? 'bg-gray-800' : 'bg-white'">
|
||||
<div class="py-1">
|
||||
<button id="editNode" class="block w-full text-left px-4 py-2 text-sm hover:bg-gray-700"
|
||||
x-bind:class="darkMode ? 'text-gray-300 hover:bg-gray-700' : 'text-gray-700 hover:bg-gray-100'">
|
||||
<i class="fa-solid fa-edit mr-2"></i>Knoten bearbeiten
|
||||
</button>
|
||||
<button id="deleteNode" class="block w-full text-left px-4 py-2 text-sm hover:bg-red-600/20"
|
||||
x-bind:class="darkMode ? 'text-gray-300 hover:bg-red-600/20' : 'text-gray-700 hover:bg-red-100'">
|
||||
<i class="fa-solid fa-trash-alt mr-2"></i>Knoten löschen
|
||||
</button>
|
||||
<button id="deleteEdge" class="block w-full text-left px-4 py-2 text-sm hover:bg-red-600/20"
|
||||
x-bind:class="darkMode ? 'text-gray-300 hover:bg-red-600/20' : 'text-gray-700 hover:bg-red-100'">
|
||||
<i class="fa-solid fa-unlink mr-2"></i>Verbindung löschen
|
||||
</button>
|
||||
<button id="exportMindmap" class="block w-full text-left px-4 py-2 text-sm"
|
||||
x-bind:class="darkMode ? 'text-gray-300 hover:bg-gray-700' : 'text-gray-700 hover:bg-gray-100'">
|
||||
<i class="fa-solid fa-file-export mr-2"></i>Exportieren
|
||||
</button>
|
||||
<div class="node-navigation">
|
||||
<h5 class="node-navigation-title">Verknüpfte Knoten</h5>
|
||||
<div id="connected-nodes" class="node-links">
|
||||
<!-- Wird dynamisch befüllt -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kategoriefilter -->
|
||||
<div id="category-filters" class="category-filters"></div>
|
||||
|
||||
<!-- Mindmap-Visualisierung -->
|
||||
<div id="cy"></div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte: Detail-Ansicht und Interaktionen -->
|
||||
<div class="space-y-6">
|
||||
<!-- Knoteninformationen -->
|
||||
<div class="card p-5"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/80 border border-gray-700' : 'bg-white/90 border border-gray-200'">
|
||||
<h2 class="text-xl font-semibold mb-3"
|
||||
|
||||
<!-- Seitenleiste -->
|
||||
<div class="w-full lg:w-1/4 space-y-6">
|
||||
<!-- Nutzlänge -->
|
||||
<div class="p-5 rounded-lg overflow-hidden border transition-colors duration-300"
|
||||
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||
<h3 class="text-xl font-semibold mb-3"
|
||||
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||
<i class="fa-solid fa-circle-info mr-2"></i>Information
|
||||
</h2>
|
||||
<div id="node-details">
|
||||
<p class="text-sm"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
Wählen Sie einen Knoten aus der Mindmap aus, um Details anzuzeigen.
|
||||
</p>
|
||||
<i class="fa-solid fa-circle-info text-purple-400 mr-2"></i>Über die Mindmap
|
||||
</h3>
|
||||
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
<p class="mb-2">Die interaktive Wissenslandkarte zeigt Verbindungen zwischen verschiedenen Gedanken und Konzepten.</p>
|
||||
<p class="mb-2">Sie können:</p>
|
||||
<ul class="list-disc pl-5 space-y-1 text-sm">
|
||||
<li>Knoten auswählen, um Details zu sehen</li>
|
||||
<li>Zoomen (Mausrad oder Pinch-Geste)</li>
|
||||
<li>Die Karte verschieben (Drag & Drop)</li>
|
||||
<li>Die Toolbar nutzen für weitere Aktionen</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verbundene Gedanken -->
|
||||
<div class="card p-5"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/80 border border-gray-700' : 'bg-white/90 border border-gray-200'">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h2 class="text-xl font-semibold"
|
||||
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||
<i class="fa-solid fa-lightbulb mr-2"></i>Gedanken
|
||||
</h2>
|
||||
<button class="btn-sm"
|
||||
x-bind:class="darkMode ? 'bg-primary-600 hover:bg-primary-700 text-white' : 'bg-primary-500 hover:bg-primary-600 text-white'">
|
||||
<i class="fa-solid fa-plus mr-1"></i>Neu
|
||||
</button>
|
||||
</div>
|
||||
<div id="thoughts-container">
|
||||
<p class="text-sm"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
Wählen Sie einen Knoten aus, um zugehörige Gedanken zu sehen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schnellhilfe -->
|
||||
<div class="card p-5"
|
||||
x-bind:class="darkMode ? 'bg-gray-800/80 border border-gray-700' : 'bg-white/90 border border-gray-200'">
|
||||
<h2 class="text-xl font-semibold mb-3"
|
||||
|
||||
<!-- Kategorienlegende -->
|
||||
<div class="p-5 rounded-lg overflow-hidden border transition-colors duration-300"
|
||||
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||
<h3 class="text-xl font-semibold mb-3"
|
||||
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||
<i class="fa-solid fa-circle-question mr-2"></i>Schnellhilfe
|
||||
</h2>
|
||||
<ul class="text-sm space-y-2"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
<li><i class="fa-solid fa-mouse mr-2"></i>Klicken Sie auf einen Knoten, um ihn auszuwählen</li>
|
||||
<li><i class="fa-solid fa-arrows-up-down-left-right mr-2"></i>Ziehen Sie die Maus, um die Ansicht zu verschieben</li>
|
||||
<li><i class="fa-solid fa-magnifying-glass-plus mr-2"></i>Mausrad zum Zoomen</li>
|
||||
<li><i class="fa-solid fa-right-click mr-2"></i>Rechtsklick für Kontextmenü</li>
|
||||
</ul>
|
||||
<i class="fa-solid fa-palette text-purple-400 mr-2"></i>Kategorien
|
||||
</h3>
|
||||
<div id="category-legend" class="space-y-2 text-sm"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
<!-- Wird dynamisch befüllt -->
|
||||
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-purple-500 mr-2"></span> Philosophie</div>
|
||||
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-green-500 mr-2"></span> Wissenschaft</div>
|
||||
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-orange-500 mr-2"></span> Technologie</div>
|
||||
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-pink-500 mr-2"></span> Künste</div>
|
||||
<div class="flex items-center"><span class="w-3 h-3 rounded-full bg-blue-500 mr-2"></span> Psychologie</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Meine Mindmaps -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="p-5 rounded-lg overflow-hidden border transition-colors duration-300"
|
||||
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
|
||||
<h3 class="text-xl font-semibold mb-3"
|
||||
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
|
||||
<i class="fa-solid fa-map text-purple-400 mr-2"></i>Meine Mindmaps
|
||||
</h3>
|
||||
<div class="mb-3">
|
||||
<a href="{{ url_for('create_mindmap') }}" class="w-full inline-block py-2 px-4 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-center text-sm font-medium transition-colors">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Neue Mindmap erstellen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 max-h-60 overflow-y-auto"
|
||||
x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||
{% if user_mindmaps %}
|
||||
{% for mindmap in user_mindmaps %}
|
||||
<a href="{{ url_for('user_mindmap', mindmap_id=mindmap.id) }}" class="block p-2 hover:bg-purple-500/20 rounded-lg transition-colors">
|
||||
<div class="text-sm font-medium">{{ mindmap.name }}</div>
|
||||
<div class="text-xs opacity-70">{{ mindmap.nodes|length }} Knoten</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-sm italic">Sie haben noch keine eigenen Mindmaps erstellt.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cytoscape.js für die Mindmap-Visualisierung -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
||||
|
||||
<!-- Socket.IO für Echtzeitaktualisierungen -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Sobald die Seite und die Scripte geladen sind, initialisiere die Mindmap
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Verarbeite Dark-Mode-Umschaltungen für die Mindmap-Visualisierung
|
||||
document.addEventListener('darkModeToggled', function(event) {
|
||||
// Hier könnten wir das Mindmap-Styling basierend auf Dark/Light-Mode aktualisieren
|
||||
// z.B. Hintergrundfarben, Knotenfarben etc.
|
||||
console.log('Dark Mode für Mindmap aktualisiert:', event.detail.isDark);
|
||||
});
|
||||
|
||||
// Weitere Mindmap-spezifische Initialisierung
|
||||
// Sollte mit dem vorhandenen mindmap.js-Modul funktionieren
|
||||
if (window.MindMap && window.MindMap.pageInitializers && window.MindMap.pageInitializers.mindmap) {
|
||||
window.MindMap.pageInitializers.mindmap();
|
||||
} else {
|
||||
console.error('Mindmap-Initialisierung konnte nicht gefunden werden!');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user