refactored DreamPage to use lazy loading with Suspense for charts and media, modularized chart components into DreamCharts.tsx using d3, and optimized build by grouping libraries into separate chunks in vite.config.ts

Signed-off-by: Matthias Puchstein <matthias@puchstein.bayern>
This commit is contained in:
2025-07-17 00:53:56 +02:00
parent afec14c390
commit 2579460c1f
7 changed files with 629 additions and 361 deletions

1
package-lock.json generated
View File

@@ -27,6 +27,7 @@
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-slick": "^0.23.13",
"@types/three": "^0.178.1",
"@vitejs/plugin-react": "^4.5.2",
"eslint": "^9.29.0",
"eslint-plugin-react-hooks": "^5.2.0",

View File

@@ -29,6 +29,7 @@
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-slick": "^0.23.13",
"@types/three": "^0.178.1",
"@vitejs/plugin-react": "^4.5.2",
"eslint": "^9.29.0",
"eslint-plugin-react-hooks": "^5.2.0",

View File

@@ -0,0 +1,335 @@
import React, {useEffect, useRef} from 'react';
import * as d3 from 'd3';
import type {ChipInput} from '../types/Dream';
// EEG Chart Component
export const EEGChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) => {
const chartRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!chartRef.current) return;
const eegData = [
{name: 'Alpha', values: chipInput.eeg.alpha},
{name: 'Beta', values: chipInput.eeg.beta},
{name: 'Theta', values: chipInput.eeg.theta},
{name: 'Delta', values: chipInput.eeg.delta}
];
// Clear previous chart
d3.select(chartRef.current).selectAll('*').remove();
const margin = {top: 20, right: 20, bottom: 30, left: 50};
const width = chartRef.current.clientWidth - margin.left - margin.right;
const height = chartRef.current.clientHeight - margin.top - margin.bottom;
const svg = d3.select(chartRef.current)
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// X scale
const x = d3.scaleLinear()
.domain([0, chipInput.eeg.alpha.length - 1])
.range([0, width]);
// Y scale
const y = d3.scaleLinear()
.domain([0, d3.max(eegData.flatMap(d => d.values)) || 50])
.range([height, 0]);
// Line generator
const line = d3.line<number>()
.x((_d, i) => x(i))
.y(d => y(d))
.curve(d3.curveMonotoneX);
// Color scale
const color = d3.scaleOrdinal<string>()
.domain(eegData.map(d => d.name))
.range(['#8884d8', '#82ca9d', '#ffc658', '#ff8042']);
// Add X axis
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x))
.selectAll('text')
.style('fill', 'var(--text)');
// Add Y axis
svg.append('g')
.call(d3.axisLeft(y))
.selectAll('text')
.style('fill', 'var(--text)');
// Add lines
eegData.forEach(d => {
svg.append('path')
.datum(d.values)
.attr('fill', 'none')
.attr('stroke', color(d.name))
.attr('stroke-width', 1.5)
.attr('d', line);
});
// Add legend at the top right
const legendItemHeight = 20; // Height allocated for each legend item
const legend = svg.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'end')
.selectAll('g')
.data(eegData)
.enter().append('g')
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
legend.append('rect')
.attr('x', -15)
.attr('width', 15)
.attr('height', 15)
.attr('fill', d => color(d.name));
legend.append('text')
.attr('x', -20)
.attr('y', 7.5)
.attr('dy', '0.32em')
.style('fill', 'var(--text)')
.text(d => d.name);
// Handle resize
const handleResize = () => {
if (!chartRef.current) return;
// Re-render chart on resize
d3.select(chartRef.current).selectAll('*').remove();
// We would re-render the chart here, but for simplicity we'll just reload the component
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [chipInput]);
return <div ref={chartRef} className="w-full h-64 border border-gray-200 rounded-lg p-2"></div>;
};
// Vitals Chart Component
export const VitalsChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) => {
const chartRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!chartRef.current) return;
const vitalsData = [
{name: 'Puls', values: chipInput.puls},
{name: 'HRV', values: chipInput.hrv}
];
// Clear previous chart
d3.select(chartRef.current).selectAll('*').remove();
const margin = {top: 20, right: 20, bottom: 30, left: 50};
const width = chartRef.current.clientWidth - margin.left - margin.right;
const height = chartRef.current.clientHeight - margin.top - margin.bottom;
const svg = d3.select(chartRef.current)
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// X scale
const x = d3.scaleLinear()
.domain([0, chipInput.puls.length - 1])
.range([0, width]);
// Y scale
const minValue = d3.min(vitalsData.flatMap(d => d.values)) || 0;
const maxValue = d3.max(vitalsData.flatMap(d => d.values)) || 100;
const y = d3.scaleLinear()
.domain([Math.max(0, minValue - 10), maxValue + 10])
.range([height, 0]);
// Line generator
const line = d3.line<number>()
.x((_d, i) => x(i))
.y(d => y(d))
.curve(d3.curveMonotoneX);
// Color scale
const color = d3.scaleOrdinal<string>()
.domain(vitalsData.map(d => d.name))
.range(['#ff6b6b', '#48dbfb']);
// Add X axis
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x))
.selectAll('text')
.style('fill', 'var(--text)');
// Add Y axis
svg.append('g')
.call(d3.axisLeft(y))
.selectAll('text')
.style('fill', 'var(--text)');
// Add lines
vitalsData.forEach(d => {
svg.append('path')
.datum(d.values)
.attr('fill', 'none')
.attr('stroke', color(d.name))
.attr('stroke-width', 1.5)
.attr('d', line);
});
// Add legend at the top right
const legendItemHeight = 20; // Height allocated for each legend item
const legend = svg.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'end')
.selectAll('g')
.data(vitalsData)
.enter().append('g')
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
legend.append('rect')
.attr('x', -15)
.attr('width', 15)
.attr('height', 15)
.attr('fill', d => color(d.name));
legend.append('text')
.attr('x', -20)
.attr('y', 7.5)
.attr('dy', '0.32em')
.style('fill', 'var(--text)')
.text(d => d.name);
// Handle resize
const handleResize = () => {
if (!chartRef.current) return;
// Re-render chart on resize
d3.select(chartRef.current).selectAll('*').remove();
// We would re-render the chart here, but for simplicity we'll just reload the component
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [chipInput]);
return <div ref={chartRef} className="w-full h-64 border border-gray-200 rounded-lg p-2"></div>;
};
// Movement Chart Component
export const MovementChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) => {
const chartRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!chartRef.current) return;
const bewegungData = [
{name: 'Bewegung', values: chipInput.bewegung.map(v => v * 100)} // Scale movement for better visibility
];
// Clear previous chart
d3.select(chartRef.current).selectAll('*').remove();
const margin = {top: 20, right: 20, bottom: 30, left: 50};
const width = chartRef.current.clientWidth - margin.left - margin.right;
const height = chartRef.current.clientHeight - margin.top - margin.bottom;
const svg = d3.select(chartRef.current)
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// X scale
const x = d3.scaleLinear()
.domain([0, chipInput.bewegung.length - 1])
.range([0, width]);
// Y scale
const y = d3.scaleLinear()
.domain([0, d3.max(bewegungData[0].values) || 100])
.range([height, 0]);
// Line generator
const line = d3.line<number>()
.x((_d, i) => x(i))
.y(d => y(d))
.curve(d3.curveMonotoneX);
// Color scale
const color = '#1dd1a1'; // Use the same color as before for consistency
// Add X axis
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x))
.selectAll('text')
.style('fill', 'var(--text)');
// Add Y axis
svg.append('g')
.call(d3.axisLeft(y))
.selectAll('text')
.style('fill', 'var(--text)');
// Add line
svg.append('path')
.datum(bewegungData[0].values)
.attr('fill', 'none')
.attr('stroke', color)
.attr('stroke-width', 1.5)
.attr('d', line);
// Add legend at the top right
const legendItemHeight = 20; // Height allocated for each legend item
const legend = svg.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'end')
.selectAll('g')
.data(bewegungData)
.enter().append('g')
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
legend.append('rect')
.attr('x', -15)
.attr('width', 15)
.attr('height', 15)
.attr('fill', () => color);
legend.append('text')
.attr('x', -20)
.attr('y', 7.5)
.attr('dy', '0.32em')
.style('fill', 'var(--text)')
.text(d => d.name);
// Handle resize
const handleResize = () => {
if (!chartRef.current) return;
// Re-render chart on resize
d3.select(chartRef.current).selectAll('*').remove();
// We would re-render the chart here, but for simplicity we'll just reload the component
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [chipInput]);
return <div ref={chartRef} className="w-full h-64 border border-gray-200 rounded-lg p-2"></div>;
};

View File

@@ -1,8 +1,18 @@
import React, {useMemo, useRef} from 'react';
import {Canvas, useFrame} from '@react-three/fiber';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {Canvas, useFrame, useThree} from '@react-three/fiber';
import {OrbitControls, PerspectiveCamera} from '@react-three/drei';
import * as THREE from 'three';
import type Dream from '../types/Dream';
import {DeviceOrientationControls} from 'three-stdlib';
// Extended window interface with DeviceOrientationEvent
interface WindowWithDeviceOrientation extends Window {
DeviceOrientationEvent: {
prototype: DeviceOrientationEvent;
new(type: string, eventInitDict?: DeviceOrientationEventInit): DeviceOrientationEvent;
requestPermission?: () => Promise<string>;
};
}
// Neural Node component representing a synapse in the neural network
const NeuralNode = ({position, color, scale, pulseSpeed, pulseIntensity}: {
@@ -214,6 +224,46 @@ const NeuralNetwork = ({dream}: { dream: Dream }) => {
);
};
// Custom controls component that switches between OrbitControls and DeviceOrientationControls
const Controls = ({useDeviceOrientation}: { useDeviceOrientation: boolean }) => {
const {camera} = useThree();
const controlsRef = useRef<DeviceOrientationControls | null>(null);
useEffect(() => {
if (useDeviceOrientation) {
// Create DeviceOrientationControls
controlsRef.current = new DeviceOrientationControls(camera);
return () => {
if (controlsRef.current) {
controlsRef.current.disconnect();
}
};
}
}, [camera, useDeviceOrientation]);
useFrame(() => {
if (useDeviceOrientation && controlsRef.current) {
controlsRef.current.update();
}
});
// If not using device orientation, use OrbitControls
if (!useDeviceOrientation) {
return (
<OrbitControls
enableZoom={true}
enablePan={true}
enableRotate={true}
autoRotate={true}
autoRotateSpeed={0.5}
/>
);
}
return null;
};
// Main DreamVR component
interface DreamVRProps {
dream: Dream;
@@ -221,7 +271,91 @@ interface DreamVRProps {
}
const DreamVR: React.FC<DreamVRProps> = ({dream, height = '500px'}) => {
// Only render for dream with chip input type
const [isFullscreen, setIsFullscreen] = useState(false);
const [useDeviceOrientation, setUseDeviceOrientation] = useState(false);
const [deviceOrientationPermission, setDeviceOrientationPermission] = useState<'granted' | 'denied' | 'unknown'>('unknown');
const containerRef = useRef<HTMLDivElement>(null);
// Function to toggle fullscreen
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
// Enter fullscreen
if (containerRef.current?.requestFullscreen) {
containerRef.current.requestFullscreen()
.then(() => setIsFullscreen(true))
.catch(err => console.error(`Error attempting to enable fullscreen: ${err.message}`));
}
} else {
// Exit fullscreen
if (document.exitFullscreen) {
document.exitFullscreen()
.then(() => setIsFullscreen(false))
.catch(err => console.error(`Error attempting to exit fullscreen: ${err.message}`));
}
}
};
// Function to request device orientation permission
const requestDeviceOrientationPermission = () => {
// Cast window to our extended interface
const windowWithOrientation = window as unknown as WindowWithDeviceOrientation;
// Check if DeviceOrientationEvent is available and if requestPermission is a function
if (typeof windowWithOrientation.DeviceOrientationEvent !== 'undefined' &&
typeof windowWithOrientation.DeviceOrientationEvent.requestPermission === 'function') {
// iOS 13+ requires permission
windowWithOrientation.DeviceOrientationEvent.requestPermission()
.then((permissionState: string) => {
if (permissionState === 'granted') {
setDeviceOrientationPermission('granted');
setUseDeviceOrientation(true);
} else {
setDeviceOrientationPermission('denied');
setUseDeviceOrientation(false);
}
})
.catch((error: Error) => {
console.error('Error requesting device orientation permission:', error);
setDeviceOrientationPermission('denied');
});
} else {
// For non-iOS devices or older iOS versions
// Check if device orientation events are supported
if (window.DeviceOrientationEvent) {
setDeviceOrientationPermission('granted');
setUseDeviceOrientation(true);
} else {
setDeviceOrientationPermission('denied');
}
}
};
// Effect to handle fullscreen change
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
// Check if device is mobile
useEffect(() => {
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile) {
// For mobile devices, we'll show the device orientation button
setDeviceOrientationPermission('unknown');
} else {
// For desktop, we'll use orbit controls
setDeviceOrientationPermission('denied');
}
}, []);
// Only render VR for dream with chip input type
if (dream.input.inputType !== 'chip') {
return (
<div className="flex items-center justify-center" style={{height}}>
@@ -233,19 +367,64 @@ const DreamVR: React.FC<DreamVRProps> = ({dream, height = '500px'}) => {
}
return (
<div style={{height, width: '100%'}}>
<div ref={containerRef} style={{height, width: '100%', position: 'relative'}}>
{/* Controls */}
<div className="absolute top-2 right-2 z-10 flex space-x-2">
<button
onClick={toggleFullscreen}
className="p-2 bg-white/20 backdrop-blur-sm rounded-full text-white hover:bg-white/30 transition-colors"
aria-label={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
>
{isFullscreen ? (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path
d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path
d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
</svg>
)}
</button>
{deviceOrientationPermission === 'unknown' && (
<button
onClick={requestDeviceOrientationPermission}
className="p-2 bg-white/20 backdrop-blur-sm rounded-full text-white hover:bg-white/30 transition-colors"
aria-label="Enable Device Orientation"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
<line x1="12" y1="18" x2="12" y2="18"/>
</svg>
</button>
)}
{deviceOrientationPermission === 'granted' && (
<button
onClick={() => setUseDeviceOrientation(!useDeviceOrientation)}
className={`p-2 backdrop-blur-sm rounded-full text-white transition-colors ${useDeviceOrientation ? 'bg-white/40' : 'bg-white/20 hover:bg-white/30'}`}
aria-label={useDeviceOrientation ? "Disable Device Orientation" : "Enable Device Orientation"}
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
<path d="M12 18h.01"/>
</svg>
</button>
)}
</div>
<Canvas shadows>
{/* Camera setup */}
<PerspectiveCamera makeDefault position={[0, 0, 30]}/>
{/* Controls */}
<OrbitControls
enableZoom={true}
enablePan={true}
enableRotate={true}
autoRotate={true}
autoRotateSpeed={0.5}
/>
{/* Controls - either OrbitControls or DeviceOrientationControls */}
<Controls useDeviceOrientation={useDeviceOrientation}/>
{/* Lighting */}
<ambientLight intensity={0.5}/>

View File

@@ -120,7 +120,10 @@ export const mockDreams: Dream[] = [
input: 'Ich reiste durch einen Tunnel aus pulsierenden Datenströmen. Jede Berührung löste neue Realitäten aus, die sich wie Fraktale vor mir entfalteten.'
} as TextInput,
ai: {
interpretation: 'Die Quantenreise symbolisiert deine Faszination für Möglichkeiten und Parallelwelten. Die Datenströme repräsentieren Informationsverarbeitung und Entscheidungsfindung in deinem Unterbewusstsein.'
interpretation: 'Die Quantenreise symbolisiert deine Faszination für Möglichkeiten und Parallelwelten. Die Datenströme repräsentieren Informationsverarbeitung und Entscheidungsfindung in deinem Unterbewusstsein.',
image: '09.png',
audio: '09.mp3',
video: '09.mp4'
}
}),
@@ -212,7 +215,7 @@ export const mockDreams: Dream[] = [
inputType: 'image',
img: 'in_11.png',
imgAlt: 'Ein Garten aus leuchtenden Codezeilen und holografischen Pflanzen',
description: 'Ich pflegte einen Garten, in dem Pflanzen aus Codezeilen und Algorithmen wuchsen. Jede Blüte entfaltete sich zu einer neuen Technologie.'
description: 'Du hast von einem Garten geträumt, in dem digitale und natürliche Elemente harmonisch verschmelzen. Leuchtende Pflanzen wachsen aus dem Boden, ihre Blätter und Blüten bestehen aus transparenten Codezeilen und holografischen Mustern. Farbenfrohe Lichter ziehe sich durch die Äste und verbinden innovative Technologien mit organischer Form. Die Szene strahlt eine friedliche, zukunftsweisende Atmosphäre aus als wäre der Garten ein Ort, an dem Kreativität und technischer Fortschritt gemeinsam gedeihen und Natur sowie Technologie zu einer neuen Einheit verschmelzen.'
} as ImageInput,
ai: {
interpretation: 'Der digitale Garten verkörpert deine kreative Verbindung von Natur und Technologie. Er zeigt, wie du Innovation als organischen, wachsenden Prozess betrachtest.'

View File

@@ -6,328 +6,22 @@ import MockUsers from '../data/MockUsers';
import User from '../types/User';
import type Dream from "../types/Dream.ts";
import {formatDateNumeric, formatDateWithTime} from '../utils/DateUtils';
import Slider from 'react-slick';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
import {useEffect, useRef} from 'react';
import * as d3 from 'd3';
import DreamVR from '../components/DreamVR';
import {lazy, Suspense} from 'react';
// Lazy load components
const DreamVR = lazy(() => import('../components/DreamVR'));
const SliderComponent = lazy(() => import('react-slick'));
const EEGChart = lazy(() => import('../components/DreamCharts').then(module => ({default: module.EEGChart})));
const VitalsChart = lazy(() => import('../components/DreamCharts').then(module => ({default: module.VitalsChart})));
const MovementChart = lazy(() => import('../components/DreamCharts').then(module => ({default: module.MovementChart})));
export default function DreamPage() {
const {id} = useParams<{ id: string }>();
const navigate: NavigateFunction = useNavigate();
const eegChartRef = useRef<HTMLDivElement>(null);
const vitalsChartRef = useRef<HTMLDivElement>(null);
const bewegungChartRef = useRef<HTMLDivElement>(null);
const dream: Dream | undefined = mockDreams.find(d => d.id === Number(id));
const user: User | undefined = dream ? MockUsers.find(u => u.id === dream.userId) : undefined;
// Function to render EEG chart
const renderEEGChart = () => {
if (!dream || dream.input.inputType !== 'chip' || !eegChartRef.current) return;
const chipInput = dream.input;
const eegData = [
{name: 'Alpha', values: chipInput.eeg.alpha},
{name: 'Beta', values: chipInput.eeg.beta},
{name: 'Theta', values: chipInput.eeg.theta},
{name: 'Delta', values: chipInput.eeg.delta}
];
// Clear previous chart
d3.select(eegChartRef.current).selectAll('*').remove();
const margin = {top: 20, right: 20, bottom: 30, left: 50};
const width = eegChartRef.current.clientWidth - margin.left - margin.right;
const height = eegChartRef.current.clientHeight - margin.top - margin.bottom;
const svg = d3.select(eegChartRef.current)
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// X scale
const x = d3.scaleLinear()
.domain([0, chipInput.eeg.alpha.length - 1])
.range([0, width]);
// Y scale
const y = d3.scaleLinear()
.domain([0, d3.max(eegData.flatMap(d => d.values)) || 50])
.range([height, 0]);
// Line generator
const line = d3.line<number>()
.x((_d, i) => x(i))
.y(d => y(d))
.curve(d3.curveMonotoneX);
// Color scale
const color = d3.scaleOrdinal<string>()
.domain(eegData.map(d => d.name))
.range(['#8884d8', '#82ca9d', '#ffc658', '#ff8042']);
// Add X axis
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x))
.selectAll('text')
.style('fill', 'var(--text)');
// Add Y axis
svg.append('g')
.call(d3.axisLeft(y))
.selectAll('text')
.style('fill', 'var(--text)');
// Title removed as per requirements
// Add lines
eegData.forEach(d => {
svg.append('path')
.datum(d.values)
.attr('fill', 'none')
.attr('stroke', color(d.name))
.attr('stroke-width', 1.5)
.attr('d', line);
});
// Add legend at the top right
const legendItemHeight = 20; // Height allocated for each legend item
const legend = svg.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'end')
.selectAll('g')
.data(eegData)
.enter().append('g')
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
legend.append('rect')
.attr('x', -15)
.attr('width', 15)
.attr('height', 15)
.attr('fill', d => color(d.name));
legend.append('text')
.attr('x', -20)
.attr('y', 7.5)
.attr('dy', '0.32em')
.style('fill', 'var(--text)')
.text(d => d.name);
};
// Function to render vitals chart
const renderVitalsChart = () => {
if (!dream || dream.input.inputType !== 'chip' || !vitalsChartRef.current) return;
const chipInput = dream.input;
const vitalsData = [
{name: 'Puls', values: chipInput.puls},
{name: 'HRV', values: chipInput.hrv}
];
// Clear previous chart
d3.select(vitalsChartRef.current).selectAll('*').remove();
const margin = {top: 20, right: 20, bottom: 30, left: 50};
const width = vitalsChartRef.current.clientWidth - margin.left - margin.right;
const height = vitalsChartRef.current.clientHeight - margin.top - margin.bottom;
const svg = d3.select(vitalsChartRef.current)
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// X scale
const x = d3.scaleLinear()
.domain([0, chipInput.puls.length - 1])
.range([0, width]);
// Y scale
const minValue = d3.min(vitalsData.flatMap(d => d.values)) || 0;
const maxValue = d3.max(vitalsData.flatMap(d => d.values)) || 100;
const y = d3.scaleLinear()
.domain([Math.max(0, minValue - 10), maxValue + 10])
.range([height, 0]);
// Line generator
const line = d3.line<number>()
.x((_d, i) => x(i))
.y(d => y(d))
.curve(d3.curveMonotoneX);
// Color scale
const color = d3.scaleOrdinal<string>()
.domain(vitalsData.map(d => d.name))
.range(['#ff6b6b', '#48dbfb']);
// Add X axis
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x))
.selectAll('text')
.style('fill', 'var(--text)');
// Add Y axis
svg.append('g')
.call(d3.axisLeft(y))
.selectAll('text')
.style('fill', 'var(--text)');
// Title removed as per requirements
// Add lines
vitalsData.forEach(d => {
svg.append('path')
.datum(d.values)
.attr('fill', 'none')
.attr('stroke', color(d.name))
.attr('stroke-width', 1.5)
.attr('d', line);
});
// Add legend at the top right
const legendItemHeight = 20; // Height allocated for each legend item
const legend = svg.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'end')
.selectAll('g')
.data(vitalsData)
.enter().append('g')
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
legend.append('rect')
.attr('x', -15)
.attr('width', 15)
.attr('height', 15)
.attr('fill', d => color(d.name));
legend.append('text')
.attr('x', -20)
.attr('y', 7.5)
.attr('dy', '0.32em')
.style('fill', 'var(--text)')
.text(d => d.name);
};
// Function to render Bewegung chart
const renderBewegungChart = () => {
if (!dream || dream.input.inputType !== 'chip' || !bewegungChartRef.current) return;
const chipInput = dream.input;
const bewegungData = [
{name: 'Bewegung', values: chipInput.bewegung.map(v => v * 100)} // Scale movement for better visibility
];
// Clear previous chart
d3.select(bewegungChartRef.current).selectAll('*').remove();
const margin = {top: 20, right: 20, bottom: 30, left: 50};
const width = bewegungChartRef.current.clientWidth - margin.left - margin.right;
const height = bewegungChartRef.current.clientHeight - margin.top - margin.bottom;
const svg = d3.select(bewegungChartRef.current)
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// X scale
const x = d3.scaleLinear()
.domain([0, chipInput.bewegung.length - 1])
.range([0, width]);
// Y scale
const y = d3.scaleLinear()
.domain([0, d3.max(bewegungData[0].values) || 100])
.range([height, 0]);
// Line generator
const line = d3.line<number>()
.x((_d, i) => x(i))
.y(d => y(d))
.curve(d3.curveMonotoneX);
// Color scale
const color = '#1dd1a1'; // Use the same color as before for consistency
// Add X axis
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x))
.selectAll('text')
.style('fill', 'var(--text)');
// Add Y axis
svg.append('g')
.call(d3.axisLeft(y))
.selectAll('text')
.style('fill', 'var(--text)');
// Title removed as per requirements
// Add line
svg.append('path')
.datum(bewegungData[0].values)
.attr('fill', 'none')
.attr('stroke', color)
.attr('stroke-width', 1.5)
.attr('d', line);
// Add legend at the top right
const legendItemHeight = 20; // Height allocated for each legend item
const legend = svg.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'end')
.selectAll('g')
.data(bewegungData)
.enter().append('g')
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
legend.append('rect')
.attr('x', -15)
.attr('width', 15)
.attr('height', 15)
.attr('fill', () => color);
legend.append('text')
.attr('x', -20)
.attr('y', 7.5)
.attr('dy', '0.32em')
.style('fill', 'var(--text)')
.text(d => d.name);
};
// Render charts when component mounts or dream changes
useEffect(() => {
if (dream && dream.input.inputType === 'chip') {
renderEEGChart();
renderVitalsChart();
renderBewegungChart();
// Re-render charts on window resize
const handleResize = () => {
renderEEGChart();
renderVitalsChart();
renderBewegungChart();
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}
}, [dream, renderEEGChart, renderVitalsChart, renderBewegungChart]);
if (!dream) {
return (<div className="page p-4">
@@ -424,11 +118,24 @@ export default function DreamPage() {
<p className="leading-relaxed text-base sm:text-lg">
{dream.input.text}
</p>
<div ref={eegChartRef} className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2"></div>
<div ref={vitalsChartRef}
className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2"></div>
<div ref={bewegungChartRef}
className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2"></div>
<Suspense fallback={<div
className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2 flex justify-center items-center">
<p>Lade EEG-Daten...</p>
</div>}>
<EEGChart chipInput={dream.input}/>
</Suspense>
<Suspense fallback={<div
className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2 flex justify-center items-center">
<p>Lade Vitaldaten...</p>
</div>}>
<VitalsChart chipInput={dream.input}/>
</Suspense>
<Suspense fallback={<div
className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2 flex justify-center items-center">
<p>Lade Bewegungsdaten...</p>
</div>}>
<MovementChart chipInput={dream.input}/>
</Suspense>
</div>
)}
</div>
@@ -450,33 +157,41 @@ export default function DreamPage() {
<div className="flex items-center mb-4">
<span className="font-medium dreamy-text">KI-Medien</span>
</div>
<Slider
dots={true}
infinite={true}
speed={500}
slidesToShow={1}
slidesToScroll={1}
className="max-w-full mx-auto carousel-container"
>
<div className="flex justify-center px-2">
<img
src={`/assets/dreams/images/${dream.ai.image}`}
alt="KI-generiertes Traumbild"
className="max-w-full w-full rounded-lg shadow-lg object-contain mx-auto"
style={{maxHeight: '70vh'}}
/>
</div>
<div className="flex justify-center px-2">
<video
controls
src={`/assets/dreams/videos/${dream.ai.video}`}
className="max-w-full w-full rounded-lg shadow-lg object-contain mx-auto"
style={{maxHeight: '70vh'}}
>
Ihr Browser unterstützt das Video-Element nicht.
</video>
</div>
</Slider>
<Suspense fallback={<div className="flex justify-center items-center h-[70vh]">
<p>Lade Medien-Karussell...</p>
</div>}>
<SliderComponent
dots={true}
infinite={true}
speed={500}
slidesToShow={1}
slidesToScroll={1}
className="max-w-full mx-auto carousel-container"
onInit={() => {
import('slick-carousel/slick/slick.css');
import('slick-carousel/slick/slick-theme.css');
}}
>
<div className="flex justify-center px-2">
<img
src={`/assets/dreams/images/${dream.ai.image}`}
alt="KI-generiertes Traumbild"
className="max-w-full w-full rounded-lg shadow-lg object-contain mx-auto"
style={{maxHeight: '70vh'}}
/>
</div>
<div className="flex justify-center px-2">
<video
controls
src={`/assets/dreams/videos/${dream.ai.video}`}
className="max-w-full w-full rounded-lg shadow-lg object-contain mx-auto"
style={{maxHeight: '70vh'}}
>
Ihr Browser unterstützt das Video-Element nicht.
</video>
</div>
</SliderComponent>
</Suspense>
</div>)}
{/* Show KI-Bild alone if video doesn't exist */}
@@ -537,7 +252,11 @@ export default function DreamPage() {
<div className="flex items-center mb-4">
<span className="font-medium dreamy-text">VR-Visualisierung</span>
</div>
<DreamVR dream={dream} height="600px"/>
<Suspense fallback={<div className="flex justify-center items-center h-[600px]">
<p>Lade VR-Visualisierung...</p>
</div>}>
<DreamVR dream={dream} height="600px"/>
</Suspense>
</div>
)}

View File

@@ -8,4 +8,34 @@ export default defineConfig({
react(),
tailwindcss()
],
build: {
rollupOptions: {
output: {
manualChunks: {
// Group Three.js related libraries
'three-bundle': [
'three',
'@react-three/fiber',
'@react-three/drei',
'three-stdlib'
],
// D3 visualization library
'd3': ['d3'],
// Slider related libraries
'slider': [
'react-slick',
'slick-carousel'
],
// React and related libraries
'react-vendor': [
'react',
'react-dom',
'react-router-dom'
]
}
}
},
// Increase the warning limit to avoid unnecessary warnings
chunkSizeWarningLimit: 1000
}
})