added audio field enhancements in MockDreams, improved chart handling for chip inputs (EEG, vitals, and movement visualization) using d3, and expanded multimedia support in DreamPage UI components

Signed-off-by: Matthias Puchstein <matthias@puchstein.bayern>
This commit is contained in:
2025-07-17 00:01:44 +02:00
parent 0c3b2bf81d
commit c4bf7c8b20
2 changed files with 377 additions and 17 deletions

View File

@@ -38,12 +38,12 @@ export const mockDreams: Dream[] = [
input: {
inputType: 'audio',
transcript: 'Ich besuchte ein Konzert unter dem Ozean. Die Musiker waren Delfine, die Melodien anklickten, während Korallenpolypen im Takt zum Rhythmus mit Licht pulsierten.',
audio: '02.mp3',
audio: 'in_02.mp3'
} as AudioInput,
ai: {
interpretation: 'Das Unterwasserkonzert kann als Ausdruck von Kreativität und Harmonie gedeutet werden. Delfine symbolisieren Intelligenz und Spielfreude, Korallenlicht verweist auf emotionale Schwingungen und die Verbindung zum Inneren.',
image: '02.png',
audio: '02.mp3',
video: '02.mp4'
}
}),
@@ -64,7 +64,8 @@ export const mockDreams: Dream[] = [
ai: {
interpretation: 'Der Wüstenzug steht für eine Reise durch unbewusste Räume und persönliche Herausforderungen (Dünen). Der violette Himmel deutet auf Spiritualität hin, während die Laternen der Kamele Hoffnung und Wegweisung symbolisieren.',
image: '03.png',
video: '03.mp4'
video: '03.mp4',
audio: '03.mp3'
}
}),
@@ -78,11 +79,12 @@ export const mockDreams: Dream[] = [
input: {
inputType: 'audio',
transcript: 'Ich saß mit Freunden auf einer Wolke zu einer Teeparty. Jede Tasse war mit Sternenstaub gefüllt, und der Himmel um uns herum schimmerte in pastelligen Regenbogenfarben.',
audio: '04.mp3',
audio: 'in_04.mp3',
} as AudioInput,
ai: {
interpretation: 'Diese Szene steht für Geborgenheit und Gemeinschaft auf einer höheren Ebene. Der Sternenstaub in den Tassen symbolisiert geteilte Träume, die pastelligen Regenbögen zeigen eine optimistische Grundstimmung und Leichtigkeit.',
image: '04.png'
image: '04.png',
audio: '04.mp3',
}
}),
@@ -190,7 +192,10 @@ export const mockDreams: Dream[] = [
]
} as ChipInput,
ai: {
interpretation: 'Das neuronale Netzwerk spiegelt dein Interesse an künstlicher Intelligenz und dem menschlichen Bewusstsein wider. Die Fähigkeit, Gedanken zu formen, deutet auf deinen Wunsch nach Kontrolle über deine mentalen Prozesse hin.'
interpretation: 'Das neuronale Netzwerk spiegelt dein Interesse an künstlicher Intelligenz und dem menschlichen Bewusstsein wider. Die Fähigkeit, Gedanken zu formen, deutet auf deinen Wunsch nach Kontrolle über deine mentalen Prozesse hin.',
audio: '10.mp3',
image: '10.png',
video: '10.mp4'
}
}),

View File

@@ -9,14 +9,325 @@ 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';
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">
<button
@@ -64,17 +375,61 @@ export default function DreamPage() {
<div className="flex items-center mb-4">
<span className="font-medium dreamy-text">Traum-Beschreibung</span>
</div>
{(dream.input.inputType === 'image' || dream.input.inputType === 'audio') && (
<div className="flex justify-center mb-1">
{dream.input.inputType === 'audio' && (<audio></audio>)}
{dream.input.inputType === 'image' && (<img alt={dream.input.imgAlt}></img>)}
</div>)}
<p className="leading-relaxed text-base sm:text-lg">
{(dream.input.inputType === 'text' && dream.input.input)
|| (dream.input.inputType === 'audio' && dream.input.transcript)
|| (dream.input.inputType === 'image' && dream.input.description)
|| (dream.input.inputType === 'chip' && dream.input.text)}
</p>
{dream.input.inputType === 'image' && (
<div className="flex flex-col sm:flex-row gap-4 mb-4">
<div className="sm:w-1/3">
<img
src={`/assets/dreams/images/${dream.input.img}`}
alt={dream.input.imgAlt}
className="w-full rounded-lg shadow-lg object-cover"
/>
</div>
<div className="sm:w-2/3">
<p className="leading-relaxed text-base sm:text-lg">
{dream.input.description}
</p>
</div>
</div>
)}
{dream.input.inputType === 'audio' && (
<div className="flex flex-col gap-4 mb-4">
<div className="w-full">
<audio
controls
src={`/assets/dreams/audio/${dream.input.audio}`}
className="w-full"
>
Ihr Browser unterstützt das Audio-Element nicht.
</audio>
</div>
<div className="w-full">
<p className="leading-relaxed text-base sm:text-lg">
{dream.input.transcript}
</p>
</div>
</div>
)}
{dream.input.inputType === 'text' && (
<p className="leading-relaxed text-base sm:text-lg">
{dream.input.input}
</p>
)}
{dream.input.inputType === 'chip' && (
<div className="flex flex-col gap-4">
<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>
</div>
)}
</div>
{dream.ai?.interpretation && dream.ai.interpretation !== '' && (<div