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

108
static/background.js Normal file
View File

@@ -0,0 +1,108 @@
// Background animation with Three.js
let scene, camera, renderer, stars = [];
function initBackground() {
// Setup scene
scene = new THREE.Scene();
// Setup camera
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.z = 100;
// Setup renderer
renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 0); // Transparent background
// Append renderer to DOM
const backgroundContainer = document.getElementById('background-container');
if (backgroundContainer) {
backgroundContainer.appendChild(renderer.domElement);
}
// Add stars
for (let i = 0; i < 1000; i++) {
const geometry = new THREE.SphereGeometry(0.2, 8, 8);
const material = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: Math.random() * 0.5 + 0.1 });
const star = new THREE.Mesh(geometry, material);
// Random position
star.position.x = Math.random() * 600 - 300;
star.position.y = Math.random() * 600 - 300;
star.position.z = Math.random() * 600 - 300;
// Store reference to move in animation
star.velocity = Math.random() * 0.02 + 0.005;
stars.push(star);
scene.add(star);
}
// Add large glowing particles
for (let i = 0; i < 15; i++) {
const size = Math.random() * 5 + 2;
const geometry = new THREE.SphereGeometry(size, 16, 16);
// Create a glowing material
const color = new THREE.Color();
color.setHSL(Math.random(), 0.7, 0.5); // Random hue
const material = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.2
});
const particle = new THREE.Mesh(geometry, material);
// Random position but further away
particle.position.x = Math.random() * 1000 - 500;
particle.position.y = Math.random() * 1000 - 500;
particle.position.z = Math.random() * 200 - 400;
// Store reference to move in animation
particle.velocity = Math.random() * 0.01 + 0.002;
stars.push(particle);
scene.add(particle);
}
// Handle window resize
window.addEventListener('resize', onWindowResize);
// Start animation
animate();
}
function animate() {
requestAnimationFrame(animate);
// Move stars
stars.forEach(star => {
star.position.z += star.velocity;
// Reset position if star moves too close
if (star.position.z > 100) {
star.position.z = -300;
}
});
// Rotate the entire scene slightly for a dreamy effect
scene.rotation.y += 0.0003;
scene.rotation.x += 0.0001;
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Initialize background when the DOM is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initBackground);
} else {
initBackground();
}

1
static/background.mp4 Normal file
View File

@@ -0,0 +1 @@
C:\Users\firem\Downloads\background.mp4

104
static/css/assistant.css Normal file
View File

@@ -0,0 +1,104 @@
/* ChatGPT Assistent Styles */
#chatgpt-assistant {
font-family: 'Inter', sans-serif;
}
#assistant-chat {
transition: max-height 0.3s ease, opacity 0.3s ease;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
border-radius: 0.75rem;
overflow: hidden;
}
#assistant-toggle {
transition: transform 0.3s ease;
}
#assistant-toggle:hover {
transform: scale(1.1);
}
#assistant-history {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
#assistant-history::-webkit-scrollbar {
width: 6px;
}
#assistant-history::-webkit-scrollbar-track {
background: transparent;
}
#assistant-history::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 3px;
}
.dark #assistant-history::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.3);
}
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
.notification-area {
bottom: 5rem;
}
/* Verbesserter Glassmorphism-Effekt */
.glass-morphism {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.dark .glass-morphism {
background: rgba(15, 23, 42, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
}
/* Dunkleres Dark Theme */
.dark {
--tw-bg-opacity: 1;
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
min-height: 100vh;
}
.dark .bg-dark-900 {
--tw-bg-opacity: 1;
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
}
.dark .bg-dark-800 {
--tw-bg-opacity: 1;
background-color: rgba(15, 23, 42, var(--tw-bg-opacity)) !important;
}
.dark .bg-dark-700 {
--tw-bg-opacity: 1;
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
}
/* Footer immer unten */
html, body {
height: 100%;
margin: 0;
min-height: 100vh;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1 0 auto;
}
footer {
flex-shrink: 0;
}

426
static/css/base-styles.css Normal file
View File

@@ -0,0 +1,426 @@
/* Globale Variablen */
:root {
--dark-bg: #0e1220;
--dark-card-bg: rgba(24, 28, 45, 0.8);
--dark-element-bg: rgba(24, 28, 45, 0.8);
--light-bg: #f0f4f8;
--light-card-bg: rgba(255, 255, 255, 0.85);
--accent-color: #b38fff;
--accent-gradient: linear-gradient(135deg, #b38fff, #58a9ff);
--accent-gradient-hover: linear-gradient(135deg, #c7a8ff, #70b5ff);
--blur-amount: 20px;
--border-radius: 28px;
--card-border-radius: 24px;
--button-radius: 18px;
--nav-item-radius: 14px;
}
/* Dark Mode Einstellungen */
html.dark {
color-scheme: dark;
}
/* Base Styles */
html, body {
background-color: var(--dark-bg) !important;
min-height: 100vh;
width: 100%;
color: #ffffff;
margin: 0;
padding: 0;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
overflow-x: hidden;
transition: background-color 0.5s ease, color 0.5s ease;
}
/* Sicherstellen, dass der dunkle Hintergrund die gesamte Seite abdeckt */
#app-container, .container, main, .mx-auto, .py-12, #content-wrapper {
background-color: transparent !important;
width: 100%;
}
/* Light Mode Einstellungen */
html.light, html.light body {
background-color: var(--light-bg) !important;
color: #1a202c;
}
/* Große Headings mit verbesserten Stilen */
h1.hero-heading {
font-size: clamp(2.5rem, 8vw, 5rem);
line-height: 1.1;
font-weight: 800;
letter-spacing: -0.03em;
margin-bottom: 1.5rem;
}
h2.section-heading {
font-size: clamp(1.75rem, 5vw, 3rem);
line-height: 1.2;
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 1.25rem;
}
/* Verbesserte Glasmorphismus-Stile */
.glass-morphism {
background: var(--dark-card-bg);
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: var(--card-border-radius);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
transition: all 0.3s ease;
}
.glass-morphism:hover {
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
transform: translateY(-2px);
}
.glass-morphism-light {
background: var(--light-card-bg);
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: var(--card-border-radius);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
}
.glass-morphism-light:hover {
border: 1px solid rgba(0, 0, 0, 0.15);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
transform: translateY(-2px);
}
/* Verbesserte Navbar-Styles */
.glass-navbar-dark {
background: rgba(14, 18, 32, 0.85);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
border-radius: 0 0 20px 20px;
}
.glass-navbar-light {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-color: rgba(0, 0, 0, 0.05);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
border-radius: 0 0 20px 20px;
}
/* Verbesserte Button-Stile mit besserer Lesbarkeit und stärkeren Farbverläufen */
.btn, button, .button, [type="button"], [type="submit"] {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: var(--button-radius);
padding: 0.75rem 1.5rem;
font-weight: 600;
letter-spacing: 0.4px;
color: rgba(255, 255, 255, 1);
background: linear-gradient(135deg, rgba(99, 102, 241, 0.8), rgba(168, 85, 247, 0.8));
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
position: relative;
overflow: hidden;
outline: none;
}
.btn:hover, button:hover, .button:hover, [type="button"]:hover, [type="submit"]:hover {
background: linear-gradient(135deg, rgba(129, 140, 248, 0.9), rgba(192, 132, 252, 0.9));
transform: translateY(-3px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.25), 0 0 12px rgba(179, 143, 255, 0.35);
color: white;
}
.btn:active, button:active, .button:active, [type="button"]:active, [type="submit"]:active {
transform: translateY(1px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
}
/* Navigation Stile mit verbesserten Farbverläufen */
.nav-link {
transition: all 0.25s ease;
border-radius: var(--nav-item-radius);
padding: 0.625rem 1rem;
font-weight: 500;
letter-spacing: 0.3px;
color: rgba(255, 255, 255, 0.9);
position: relative;
overflow: visible;
}
.nav-link:hover {
background: rgba(179, 143, 255, 0.2);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.nav-link-active {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.3), rgba(139, 92, 246, 0.3));
color: white;
font-weight: 600;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
}
.nav-link-active::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
width: 80%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.7), transparent);
}
/* Light-Mode Navigation Stile */
.nav-link-light {
color: rgba(26, 32, 44, 0.85);
}
.nav-link-light:hover {
background: rgba(179, 143, 255, 0.15);
color: rgba(26, 32, 44, 1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.nav-link-light-active {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2), rgba(139, 92, 246, 0.2));
color: rgba(26, 32, 44, 1);
font-weight: 600;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
.nav-link-light-active::after {
background: linear-gradient(90deg, transparent, rgba(26, 32, 44, 0.5), transparent);
}
/* Entfernung von Gradient-Hintergrund überall */
.gradient-bg, .purple-gradient, .gradient-purple-bg {
background: var(--dark-bg) !important;
background-image: none !important;
}
/* Verbesserte Light-Mode-Stile für Buttons */
html.light .btn, html.light button, html.light .button,
html.light [type="button"], html.light [type="submit"] {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.7), rgba(139, 92, 246, 0.7));
color: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
text-shadow: none;
}
html.light .btn:hover, html.light button:hover, html.light .button:hover,
html.light [type="button"]:hover, html.light [type="submit"]:hover {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.85), rgba(168, 85, 247, 0.85));
color: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.12), 0 0 12px rgba(179, 143, 255, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
/* Verbesserte Buttons mit Glasmorphismus */
.btn-primary {
background: linear-gradient(135deg, rgba(179, 143, 255, 0.8), rgba(88, 169, 255, 0.8));
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: var(--button-radius);
color: white !important;
font-weight: 600;
padding: 0.75rem 1.5rem;
transition: all 0.3s ease;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
background: linear-gradient(135deg, rgba(190, 160, 255, 0.9), rgba(100, 180, 255, 0.9));
}
.btn-secondary {
background: rgba(32, 36, 55, 0.8);
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--button-radius);
color: white;
font-weight: 500;
padding: 0.75rem 1.5rem;
transition: all 0.3s ease;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}
.btn-secondary:hover {
transform: translateY(-3px);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
background: rgba(38, 42, 65, 0.9);
border: 1px solid rgba(255, 255, 255, 0.15);
}
/* Steuerungsbutton-Stil */
.control-btn {
padding: 0.5rem 1rem;
background: rgba(32, 36, 55, 0.8);
color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
}
.control-btn:hover {
background: rgba(38, 42, 65, 0.9);
border: 1px solid rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
}
/* Verbesserter Farbverlauf-Text */
.gradient-text {
background: linear-gradient(135deg, rgba(200, 170, 255, 1), rgba(100, 180, 255, 1));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 700;
letter-spacing: -0.02em;
text-shadow: 0 2px 10px rgba(179, 143, 255, 0.3);
filter: drop-shadow(0 2px 6px rgba(179, 143, 255, 0.3));
}
/* Globaler Hintergrund */
.full-page-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: var(--dark-bg);
z-index: -10;
}
html.light .full-page-bg {
background-color: var(--light-bg);
}
/* Animationen für Hintergrundeffekte */
@keyframes float {
0% { transform: translateY(0); }
50% { transform: translateY(-12px); }
100% { transform: translateY(0); }
}
@keyframes pulse {
0% { opacity: 0.7; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
100% { opacity: 0.7; transform: scale(1); }
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-pulse {
animation: pulse 3s ease-in-out infinite;
}
/* Verbesserter Container für konsistente Layouts */
.page-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
/* Dark Mode Toggle Stile */
.dot {
transform: translateX(0);
transition: transform 0.3s ease-in-out, background-color 0.3s ease;
}
input:checked ~ .dot {
transform: translateX(100%);
background-color: #58a9ff;
}
input:checked ~ .block {
background-color: rgba(88, 169, 255, 0.4);
}
/* Feature Cards mit Glasmorphismus und Farbverlauf */
.feature-card {
border-radius: var(--card-border-radius);
padding: 2rem;
transition: all 0.3s ease;
background: linear-gradient(145deg, rgba(32, 36, 55, 0.7), rgba(24, 28, 45, 0.9));
backdrop-filter: blur(var(--blur-amount));
-webkit-backdrop-filter: blur(var(--blur-amount));
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.15);
background: linear-gradient(145deg, rgba(40, 44, 65, 0.8), rgba(28, 32, 50, 0.95));
}
html.light .feature-card {
background: linear-gradient(145deg, rgba(255, 255, 255, 0.8), rgba(240, 240, 250, 0.9));
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
html.light .feature-card:hover {
background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(245, 245, 255, 0.95));
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
}
.feature-card .icon {
width: 60px;
height: 60px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 1.25rem;
background: linear-gradient(135deg, rgba(124, 58, 237, 0.8), rgba(139, 92, 246, 0.6));
color: white;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
}
.feature-card h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: rgba(255, 255, 255, 0.95);
}
html.light .feature-card h3 {
color: rgba(26, 32, 44, 0.95);
}
.feature-card p {
color: rgba(255, 255, 255, 0.75);
line-height: 1.6;
}
html.light .feature-card p {
color: rgba(26, 32, 44, 0.75);
}

3884
static/css/main.css Normal file

File diff suppressed because it is too large Load Diff

207
static/css/src/input.css Normal file
View File

@@ -0,0 +1,207 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
@apply scroll-smooth;
}
body {
@apply bg-gray-50 text-gray-800 dark:bg-dark-800 dark:text-gray-100 font-sans;
}
h1, h2, h3, h4, h5, h6 {
@apply font-bold text-gray-900 dark:text-white;
}
h1 {
@apply text-4xl md:text-5xl lg:text-6xl;
}
h2 {
@apply text-3xl md:text-4xl;
}
h3 {
@apply text-2xl md:text-3xl;
}
h4 {
@apply text-xl md:text-2xl;
}
a {
@apply text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 transition-colors;
}
input, textarea, select {
@apply bg-white border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-dark-700 dark:border-dark-500 dark:focus:ring-primary-400 dark:focus:border-primary-400;
}
label {
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1;
}
::placeholder {
@apply text-gray-400 dark:text-gray-500;
}
}
@layer components {
.btn {
@apply inline-flex items-center justify-center px-4 py-2 font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 shadow-sm text-base;
}
.btn-primary {
@apply btn bg-primary-600 hover:bg-primary-700 text-white focus:ring-primary-500 hover:shadow-md active:translate-y-0.5 dark:bg-primary-700 dark:hover:bg-primary-600;
}
.btn-secondary {
@apply btn bg-secondary-600 hover:bg-secondary-700 text-white focus:ring-secondary-500 hover:shadow-md active:translate-y-0.5 dark:bg-secondary-700 dark:hover:bg-secondary-600;
}
.btn-outline {
@apply btn border-2 border-gray-300 dark:border-dark-500 bg-white dark:bg-transparent text-gray-700 dark:text-gray-200 hover:bg-gray-50 hover:border-primary-500 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:border-primary-400 dark:hover:text-primary-400 focus:ring-gray-500 active:translate-y-0.5;
}
.card {
@apply bg-white shadow-md dark:bg-dark-700 rounded-lg overflow-hidden border border-gray-200 dark:border-dark-600 hover:shadow-lg transition-shadow duration-300;
}
.gradient-text {
@apply text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-secondary-600 dark:from-primary-500 dark:to-secondary-500 font-bold;
}
.node-tooltip {
@apply max-w-xs p-3 bg-white text-gray-800 dark:bg-dark-800 dark:text-white rounded-lg shadow-lg text-sm z-50 border border-gray-200 dark:border-dark-600;
}
.mindmap-node {
@apply cursor-pointer transition-all duration-200 hover:shadow-lg border-2 border-gray-200 dark:border-dark-600;
}
/* Mindmap-spezifische Stile */
.mindmap-container {
@apply bg-gray-50/80 dark:bg-dark-800/80 rounded-lg p-4 shadow-inner;
}
.mindmap-node-root {
@apply bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100 border-primary-300 dark:border-primary-700;
}
.mindmap-node-branch {
@apply bg-secondary-100 dark:bg-secondary-900 text-secondary-900 dark:text-secondary-100 border-secondary-300 dark:border-secondary-700;
}
.mindmap-node-leaf {
@apply bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-gray-200 border-gray-300 dark:border-dark-500;
}
.mindmap-link {
@apply stroke-gray-400 dark:stroke-gray-500 stroke-[2];
}
.mindmap-link-active {
@apply stroke-primary-500 dark:stroke-primary-400 stroke-[3];
}
.form-group {
@apply mb-4;
}
.form-control {
@apply w-full;
}
.ascii-art {
@apply font-mono text-xs leading-none whitespace-pre tracking-tight select-none text-primary-700 dark:text-primary-400 opacity-80 dark:opacity-60 font-bold;
}
/* Verbesserte Formulareingabefelder */
.form-input,
.form-textarea,
.form-select {
@apply w-full rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-800 shadow-sm
focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50
dark:border-dark-500 dark:bg-dark-700 dark:text-gray-100 dark:focus:border-primary-400 dark:focus:ring-primary-400;
}
.form-input-lg {
@apply py-3 text-lg;
}
.form-input-sm {
@apply py-1 text-sm;
}
.form-checkbox,
.form-radio {
@apply h-5 w-5 rounded border-gray-300 text-primary-600 shadow-sm
focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50
dark:border-dark-500 dark:bg-dark-700 dark:focus:border-primary-400 dark:focus:ring-primary-400;
}
.form-checkbox {
@apply rounded;
}
.form-radio {
@apply rounded-full;
}
.form-error {
@apply mt-1 text-sm text-red-600 dark:text-red-400;
}
}
@layer utilities {
.tech-gradient {
@apply bg-gradient-to-r from-primary-600 to-secondary-500;
}
.glass-effect {
@apply bg-white/95 backdrop-blur-md border border-gray-200 shadow-md dark:bg-dark-800/90 dark:border-dark-700/50 dark:shadow-xl;
}
.focus-ring {
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-primary-400 dark:focus:ring-offset-dark-800;
}
.input-focus {
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400;
}
}
/* Form validation styles */
.is-valid {
@apply border-green-500 dark:border-green-400;
}
.is-invalid {
@apply border-red-500 dark:border-red-400;
}
.is-valid:focus {
@apply ring-green-500/30 border-green-500 dark:ring-green-400/30 dark:border-green-400;
}
.is-invalid:focus {
@apply ring-red-500/30 border-red-500 dark:ring-red-400/30 dark:border-red-400;
}
.form-hint {
@apply text-xs text-gray-500 dark:text-gray-400 mt-1;
}
.form-label {
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1;
}
.input-icon {
@apply absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-500 dark:text-gray-400;
}
.input-with-icon {
@apply pl-10;
}

1444
static/css/style.css Normal file

File diff suppressed because it is too large Load Diff

456
static/d3-extensions.js vendored Normal file
View File

@@ -0,0 +1,456 @@
/**
* D3.js Erweiterungen für verbesserte Mindmap-Funktionalität
* Diese Datei enthält zusätzliche Hilfsfunktionen und Erweiterungen für D3.js
*/
class D3Extensions {
/**
* Erstellt einen verbesserten radialen Farbverlauf
* @param {Object} defs - Das D3 defs Element
* @param {string} id - ID für den Gradienten
* @param {string} baseColor - Grundfarbe in hexadezimal oder RGB
* @returns {Object} - Das erstellte Gradient-Element
*/
static createEnhancedRadialGradient(defs, id, baseColor) {
// Farben berechnen
const d3Color = d3.color(baseColor);
const lightColor = d3Color.brighter(0.7);
const darkColor = d3Color.darker(0.3);
const midColor = d3Color;
// Gradient erstellen
const gradient = defs.append('radialGradient')
.attr('id', id)
.attr('cx', '30%')
.attr('cy', '30%')
.attr('r', '70%');
// Farbstops hinzufügen für realistischeren Verlauf
gradient.append('stop')
.attr('offset', '0%')
.attr('stop-color', lightColor.formatHex());
gradient.append('stop')
.attr('offset', '50%')
.attr('stop-color', midColor.formatHex());
gradient.append('stop')
.attr('offset', '100%')
.attr('stop-color', darkColor.formatHex());
return gradient;
}
/**
* Erstellt einen Glüheffekt-Filter
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @param {String} color - Farbe des Glüheffekts (Hex-Code)
* @param {Number} strength - Stärke des Glüheffekts
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createGlowFilter(defs, id, color = '#b38fff', strength = 5) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Unschärfe-Effekt
filter.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', strength)
.attr('result', 'blur');
// Farbverstärkung für den Glüheffekt
filter.append('feColorMatrix')
.attr('in', 'blur')
.attr('type', 'matrix')
.attr('values', '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 -7')
.attr('result', 'glow');
// Farbflut mit der angegebenen Farbe
filter.append('feFlood')
.attr('flood-color', color)
.attr('flood-opacity', '0.7')
.attr('result', 'color');
// Zusammensetzen des Glüheffekts mit der Farbe
filter.append('feComposite')
.attr('in', 'color')
.attr('in2', 'glow')
.attr('operator', 'in')
.attr('result', 'glow-color');
// Zusammenfügen aller Ebenen
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'glow-color');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Berechnet eine konsistente Farbe aus einem String
* @param {string} str - Eingabestring
* @returns {string} - Generierte Farbe als Hex-String
*/
static stringToColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
// Basis-Farbpalette für konsistente Farben
const colorPalette = [
"#4299E1", // Blau
"#9F7AEA", // Lila
"#ED64A6", // Pink
"#48BB78", // Grün
"#ECC94B", // Gelb
"#F56565", // Rot
"#38B2AC", // Türkis
"#ED8936", // Orange
"#667EEA", // Indigo
];
// Farbe aus der Palette wählen basierend auf dem Hash
const colorIndex = Math.abs(hash) % colorPalette.length;
return colorPalette[colorIndex];
}
/**
* Erstellt einen Schatteneffekt-Filter
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createShadowFilter(defs, id) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Einfacher Schlagschatten
filter.append('feDropShadow')
.attr('dx', 0)
.attr('dy', 4)
.attr('stdDeviation', 4)
.attr('flood-color', 'rgba(0, 0, 0, 0.3)');
return filter;
}
/**
* Erstellt einen Glasmorphismus-Effekt-Filter
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createGlassMorphismFilter(defs, id) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Hintergrund-Unschärfe für den Glaseffekt
filter.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', 8)
.attr('result', 'blur');
// Hellere Farbe für den Glaseffekt
filter.append('feColorMatrix')
.attr('in', 'blur')
.attr('type', 'matrix')
.attr('values', '1 0 0 0 0.1 0 1 0 0 0.1 0 0 1 0 0.1 0 0 0 0.6 0')
.attr('result', 'glass');
// Überlagerung mit dem Original
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'glass');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Erstellt einen verstärkten Glasmorphismus-Effekt mit Farbverlauf
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @param {String} color1 - Erste Farbe des Verlaufs (Hex-Code)
* @param {String} color2 - Zweite Farbe des Verlaufs (Hex-Code)
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static createEnhancedGlassMorphismFilter(defs, id, color1 = '#b38fff', color2 = '#58a9ff') {
// Farbverlauf für den Glaseffekt definieren
const gradientId = `gradient-${id}`;
const gradient = defs.append('linearGradient')
.attr('id', gradientId)
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '100%');
gradient.append('stop')
.attr('offset', '0%')
.attr('stop-color', color1)
.attr('stop-opacity', '0.3');
gradient.append('stop')
.attr('offset', '100%')
.attr('stop-color', color2)
.attr('stop-opacity', '0.3');
// Filter erstellen
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Hintergrund-Unschärfe
filter.append('feGaussianBlur')
.attr('in', 'SourceGraphic')
.attr('stdDeviation', 6)
.attr('result', 'blur');
// Farbverlauf einfügen
const feImage = filter.append('feImage')
.attr('xlink:href', `#${gradientId}`)
.attr('result', 'gradient')
.attr('x', '0%')
.attr('y', '0%')
.attr('width', '100%')
.attr('height', '100%')
.attr('preserveAspectRatio', 'none');
// Zusammenfügen aller Ebenen
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'blur');
feMerge.append('feMergeNode')
.attr('in', 'gradient');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Erstellt einen 3D-Glaseffekt mit verbesserter Tiefe und Reflexionen
* @param {Object} defs - D3-Referenz auf den defs-Bereich
* @param {String} id - ID des Filters
* @returns {Object} D3-Referenz auf den erstellten Filter
*/
static create3DGlassEffect(defs, id) {
const filter = defs.append('filter')
.attr('id', id)
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
// Farbmatrix für Transparenz
filter.append('feColorMatrix')
.attr('type', 'matrix')
.attr('in', 'SourceGraphic')
.attr('values', '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.7 0')
.attr('result', 'transparent');
// Hintergrund-Unschärfe für Tiefe
filter.append('feGaussianBlur')
.attr('in', 'transparent')
.attr('stdDeviation', '4')
.attr('result', 'blurred');
// Lichtquelle und Schattierung hinzufügen
const lightSource = filter.append('feSpecularLighting')
.attr('in', 'blurred')
.attr('surfaceScale', '6')
.attr('specularConstant', '1')
.attr('specularExponent', '30')
.attr('lighting-color', '#ffffff')
.attr('result', 'specular');
lightSource.append('fePointLight')
.attr('x', '100')
.attr('y', '100')
.attr('z', '200');
// Lichtreflexion verstärken
filter.append('feComposite')
.attr('in', 'specular')
.attr('in2', 'SourceGraphic')
.attr('operator', 'in')
.attr('result', 'specularHighlight');
// Inneren Schatten erzeugen
const innerShadow = filter.append('feOffset')
.attr('in', 'SourceAlpha')
.attr('dx', '0')
.attr('dy', '1')
.attr('result', 'offsetblur');
innerShadow.append('feGaussianBlur')
.attr('in', 'offsetblur')
.attr('stdDeviation', '2')
.attr('result', 'innerShadow');
filter.append('feComposite')
.attr('in', 'innerShadow')
.attr('in2', 'SourceGraphic')
.attr('operator', 'out')
.attr('result', 'innerShadowEffect');
// Schichten kombinieren
const feMerge = filter.append('feMerge');
feMerge.append('feMergeNode')
.attr('in', 'blurred');
feMerge.append('feMergeNode')
.attr('in', 'innerShadowEffect');
feMerge.append('feMergeNode')
.attr('in', 'specularHighlight');
feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
return filter;
}
/**
* Fügt einen Partikelsystem-Effekt für interaktive Knoten hinzu
* @param {Object} parent - Das übergeordnete SVG-Element
* @param {number} x - X-Koordinate des Zentrums
* @param {number} y - Y-Koordinate des Zentrums
* @param {string} color - Partikelfarbe (Hex-Code)
* @param {number} count - Anzahl der Partikel
*/
static createParticleEffect(parent, x, y, color = '#b38fff', count = 5) {
const particles = [];
for (let i = 0; i < count; i++) {
const particle = parent.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', 0)
.attr('fill', color)
.style('opacity', 0.8);
particles.push(particle);
// Partikel animieren
animateParticle(particle);
}
function animateParticle(particle) {
// Zufällige Richtung und Geschwindigkeit
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 2;
const distance = 20 + Math.random() * 30;
// Zielposition berechnen
const targetX = x + Math.cos(angle) * distance;
const targetY = y + Math.sin(angle) * distance;
// Animation mit zufälliger Dauer
const duration = 1000 + Math.random() * 500;
particle
.attr('r', 0)
.style('opacity', 0.8)
.transition()
.duration(duration)
.attr('cx', targetX)
.attr('cy', targetY)
.attr('r', 2 + Math.random() * 3)
.style('opacity', 0)
.on('end', function() {
// Partikel entfernen
particle.remove();
});
}
}
/**
* Führt eine Pulsanimation auf einem Knoten durch
* @param {Object} node - D3-Knoten-Selektion
* @returns {void}
*/
static pulseAnimation(node) {
if (!node) return;
const circle = node.select('circle');
const originalRadius = parseFloat(circle.attr('r'));
const originalFill = circle.attr('fill');
// Pulsanimation
circle
.transition()
.duration(400)
.attr('r', originalRadius * 1.3)
.attr('fill', '#b38fff')
.transition()
.duration(400)
.attr('r', originalRadius)
.attr('fill', originalFill);
}
/**
* Berechnet eine adaptive Schriftgröße basierend auf der Textlänge
* @param {string} text - Der anzuzeigende Text
* @param {number} maxSize - Maximale Schriftgröße in Pixel
* @param {number} minSize - Minimale Schriftgröße in Pixel
* @returns {number} - Die berechnete Schriftgröße
*/
static getAdaptiveFontSize(text, maxSize = 14, minSize = 10) {
if (!text) return maxSize;
// Linear die Schriftgröße basierend auf der Textlänge anpassen
const length = text.length;
if (length <= 6) return maxSize;
if (length >= 20) return minSize;
// Lineare Interpolation
const factor = (length - 6) / (20 - 6);
return maxSize - factor * (maxSize - minSize);
}
/**
* Fügt einen Pulsierenden Effekt zu einer Selektion hinzu
* @param {Object} selection - D3-Selektion
* @param {number} duration - Dauer eines Puls-Zyklus in ms
* @param {number} minOpacity - Minimale Opazität
* @param {number} maxOpacity - Maximale Opazität
*/
static addPulseEffect(selection, duration = 1500, minOpacity = 0.4, maxOpacity = 0.9) {
function pulse() {
selection
.transition()
.duration(duration / 2)
.style('opacity', minOpacity)
.transition()
.duration(duration / 2)
.style('opacity', maxOpacity)
.on('end', pulse);
}
// Initialen Stil setzen
selection.style('opacity', maxOpacity);
// Pulsanimation starten
pulse();
}
}
// Globale Verfügbarkeit sicherstellen
window.D3Extensions = D3Extensions;

25
static/img/favicon-gen.py Normal file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from PIL import Image
import cairosvg
# Pfad zum SVG-Favicon
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
# Ausgabepfad für das PNG
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
# Ausgabepfad für das ICO
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
# SVG zu PNG konvertieren
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
# PNG zu ICO konvertieren
img = Image.open(png_path)
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
print(f"Favicon erfolgreich erstellt: {ico_path}")
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
# os.remove(png_path)

21
static/img/favicon.svg Normal file
View File

@@ -0,0 +1,21 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" rx="128" fill="url(#paint0_linear)" />
<path d="M143.5 384V128H180.5L256.5 270L332.5 128H369.5V384H328.5V196L256.5 332H256L183.5 196V384H143.5Z" fill="white"/>
<circle cx="143.5" cy="128" r="20" fill="#a040ff" />
<circle cx="256.5" cy="270" r="25" fill="#a040ff" />
<circle cx="369.5" cy="128" r="20" fill="#a040ff" />
<circle cx="143.5" cy="384" r="20" fill="#4080ff" />
<circle cx="369.5" cy="384" r="20" fill="#4080ff" />
<path d="M143.5 128L183.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
<path d="M256.5 270L183.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
<path d="M256.5 270L328.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
<path d="M369.5 128L328.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
<path d="M183.5 196L143.5 384" stroke="white" stroke-width="4" stroke-linecap="round" />
<path d="M328.5 196L369.5 384" stroke="white" stroke-width="4" stroke-linecap="round" />
<defs>
<linearGradient id="paint0_linear" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
<stop stop-color="#205cf5" />
<stop offset="1" stop-color="#8020f5" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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

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

View File

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

View File

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

View File

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

1834
static/mindmap.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
// Network Animation Effect
document.addEventListener('DOMContentLoaded', function() {
// Check if we're on the mindmap page
const mindmapContainer = document.getElementById('mindmap-container');
if (!mindmapContainer) return;
// Add enhanced animations for links and nodes
setTimeout(function() {
// Get all SVG links (connections between nodes)
const links = document.querySelectorAll('.link');
const nodes = document.querySelectorAll('.node');
// Add animation to links
links.forEach(link => {
// Create random animation duration between 15 and 30 seconds
const duration = 15 + Math.random() * 15;
link.style.animation = `dash ${duration}s linear infinite`;
link.style.strokeDasharray = '5, 5';
// Add pulse effect on hover
link.addEventListener('mouseover', function() {
this.classList.add('highlighted');
this.style.animation = 'dash 5s linear infinite';
});
link.addEventListener('mouseout', function() {
this.classList.remove('highlighted');
this.style.animation = `dash ${duration}s linear infinite`;
});
});
// Add effects to nodes
nodes.forEach(node => {
node.addEventListener('mouseover', function() {
this.querySelector('circle').style.filter = 'drop-shadow(0 0 15px rgba(179, 143, 255, 0.8))';
// Highlight connected links
const nodeId = this.getAttribute('data-id') || this.id;
links.forEach(link => {
const source = link.getAttribute('data-source');
const target = link.getAttribute('data-target');
if (source === nodeId || target === nodeId) {
link.classList.add('highlighted');
link.style.animation = 'dash 5s linear infinite';
}
});
});
node.addEventListener('mouseout', function() {
this.querySelector('circle').style.filter = 'drop-shadow(0 0 8px rgba(179, 143, 255, 0.5))';
// Remove highlight from connected links
const nodeId = this.getAttribute('data-id') || this.id;
links.forEach(link => {
const source = link.getAttribute('data-source');
const target = link.getAttribute('data-target');
if (source === nodeId || target === nodeId) {
link.classList.remove('highlighted');
const duration = 15 + Math.random() * 15;
link.style.animation = `dash ${duration}s linear infinite`;
}
});
});
});
}, 1000); // Wait for the mindmap to be fully loaded
// Add network background effect
const networkBackground = document.createElement('div');
networkBackground.className = 'network-background';
networkBackground.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(179, 143, 255, 0.05);
background-size: cover;
background-position: center;
opacity: 0.15;
z-index: -1;
pointer-events: none;
animation: pulse 10s ease-in-out infinite alternate;
`;
mindmapContainer.appendChild(networkBackground);
});

View File

@@ -0,0 +1,232 @@
// Animated Network Background
let canvas, ctx, networkImage;
let isImageLoaded = false;
let animationSpeed = 0.0003; // Reduzierte Geschwindigkeit für sanftere Rotation
let scaleSpeed = 0.0001; // Reduzierte Geschwindigkeit für sanftere Skalierung
let opacitySpeed = 0.0002; // Reduzierte Geschwindigkeit für sanftere Opazitätsänderung
let rotation = 0;
let scale = 1;
let opacity = 0.7; // Höhere Basisopazität für bessere Sichtbarkeit
let scaleDirection = 1;
let opacityDirection = 1;
let animationFrameId = null;
let isDarkMode = document.documentElement.classList.contains('dark');
let loadAttempts = 0;
const MAX_LOAD_ATTEMPTS = 2;
// Initialize the canvas and load the image
function initNetworkBackground() {
// Create canvas element if it doesn't exist
if (!document.getElementById('network-background')) {
canvas = document.createElement('canvas');
canvas.id = 'network-background';
canvas.style.position = 'fixed';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.zIndex = '-5'; // Höher als -10 für den full-page-bg
canvas.style.pointerEvents = 'none'; // Stellt sicher, dass der Canvas keine Mausinteraktionen blockiert
document.body.appendChild(canvas);
} else {
canvas = document.getElementById('network-background');
}
// Set canvas size to window size with pixel ratio consideration
resizeCanvas();
// Get context with alpha enabled
ctx = canvas.getContext('2d', { alpha: true });
// Load the network image - versuche zuerst die SVG-Version
networkImage = new Image();
networkImage.crossOrigin = "anonymous"; // Vermeidet CORS-Probleme
// Keine Bilder laden, direkt Fallback-Hintergrund verwenden
console.log("Verwende einfachen Hintergrund ohne Bilddateien");
isImageLoaded = true; // Animation ohne Hintergrundbild starten
startAnimation();
// Handle window resize
window.addEventListener('resize', debounce(resizeCanvas, 250));
// Überwache Dark Mode-Änderungen
document.addEventListener('darkModeToggled', function(event) {
isDarkMode = event.detail.isDark;
});
}
// Hilfsfunktion zur Reduzierung der Resize-Event-Aufrufe
function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}
// Resize canvas to match window size with proper pixel ratio
function resizeCanvas() {
if (!canvas) return;
const pixelRatio = window.devicePixelRatio || 1;
const width = window.innerWidth;
const height = window.innerHeight;
// Set display size (css pixels)
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
// Set actual size in memory (scaled for pixel ratio)
canvas.width = width * pixelRatio;
canvas.height = height * pixelRatio;
// Scale context to match pixel ratio
if (ctx) {
ctx.scale(pixelRatio, pixelRatio);
}
// Wenn Animation läuft und Bild geladen, zeichne erneut
if (isImageLoaded && animationFrameId) {
drawNetworkImage();
}
}
// Start animation
function startAnimation() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
// Start animation loop
animate();
}
// Draw network image
function drawNetworkImage() {
if (!ctx) return;
// Clear canvas with proper clear method
ctx.clearRect(0, 0, canvas.width / (window.devicePixelRatio || 1), canvas.height / (window.devicePixelRatio || 1));
// Save context state
ctx.save();
// Move to center of canvas
ctx.translate(canvas.width / (2 * (window.devicePixelRatio || 1)), canvas.height / (2 * (window.devicePixelRatio || 1)));
// Rotate
ctx.rotate(rotation);
// Scale
ctx.scale(scale, scale);
// Set global opacity, angepasst für Dark Mode
ctx.globalAlpha = isDarkMode ? opacity : opacity * 0.8;
if (isImageLoaded && networkImage.complete) {
// Bildgröße berechnen, um den Bildschirm abzudecken
const imgAspect = networkImage.width / networkImage.height;
const canvasAspect = canvas.width / canvas.height;
let drawWidth, drawHeight;
if (canvasAspect > imgAspect) {
drawWidth = canvas.width / (window.devicePixelRatio || 1);
drawHeight = drawWidth / imgAspect;
} else {
drawHeight = canvas.height / (window.devicePixelRatio || 1);
drawWidth = drawHeight * imgAspect;
}
// Draw image centered
ctx.drawImage(
networkImage,
-drawWidth / 2,
-drawHeight / 2,
drawWidth,
drawHeight
);
} else {
// Fallback: Zeichne einen einfachen Hintergrund mit Punkten
drawFallbackBackground();
}
// Restore context state
ctx.restore();
}
// Fallback-Hintergrund mit Punkten und Linien
function drawFallbackBackground() {
const width = canvas.width / (window.devicePixelRatio || 1);
const height = canvas.height / (window.devicePixelRatio || 1);
// Zeichne einige zufällige Punkte
ctx.fillStyle = isDarkMode ? 'rgba(139, 92, 246, 0.2)' : 'rgba(139, 92, 246, 0.1)';
for (let i = 0; i < 50; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const radius = Math.random() * 3 + 1;
ctx.beginPath();
ctx.arc(x - width/2, y - height/2, radius, 0, Math.PI * 2);
ctx.fill();
}
}
// Animation loop
function animate() {
// Update animation parameters
rotation += animationSpeed;
// Update scale with oscillation
scale += scaleSpeed * scaleDirection;
if (scale > 1.05) { // Kleinerer Skalierungsbereich für weniger starke Größenänderung
scaleDirection = -1;
} else if (scale < 0.95) {
scaleDirection = 1;
}
// Update opacity with oscillation
opacity += opacitySpeed * opacityDirection;
if (opacity > 0.75) { // Kleinerer Opazitätsbereich für subtilere Änderungen
opacityDirection = -1;
} else if (opacity < 0.65) {
opacityDirection = 1;
}
// Draw the image
drawNetworkImage();
// Request next frame
animationFrameId = requestAnimationFrame(animate);
}
// Cleanup Funktion für Speicherbereinigung
function cleanupNetworkBackground() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (canvas && canvas.parentNode) {
canvas.parentNode.removeChild(canvas);
}
window.removeEventListener('resize', resizeCanvas);
}
// Führe Initialisierung aus, wenn DOM geladen ist
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initNetworkBackground);
} else {
initNetworkBackground();
}
// Führe Cleanup durch, wenn das Fenster geschlossen wird
window.addEventListener('beforeunload', cleanupNetworkBackground);

0
static/three.min.js vendored Normal file
View File