Update to v1.0.5: Redesigned Guided Relaxation (Body Scan, Visualization, Grounding)

This commit is contained in:
Gemini AI
2025-12-07 00:06:37 +04:00
Unverified
parent e3d487b0b0
commit 8ffb1cb106
6 changed files with 436 additions and 73 deletions

View File

@@ -1222,72 +1222,119 @@ function quickRelax() {
startGuidedRelaxation();
}
// --- Guided Relaxation (5-4-3-2-1 Grounding) ---
// --- Guided Relaxation (Mindfulness System) ---
let relaxationState = {
mode: null, // 'grounding', 'body_scan', 'visualization'
step: 0,
isActive: false,
steps: [] // Will be populated dynamically based on language
isPaused: false,
steps: [],
timer: null,
duration: 0
};
function getRelaxationSteps() {
// Data Providers
function getGroundingSteps() {
return [
{
title: t('guided_sight_title'),
instruction: t('guided_sight_instruction'),
sub: t('guided_sight_sub'),
count: 5,
icon: "👁️",
color: "#64B5F6"
},
{
title: t('guided_touch_title'),
instruction: t('guided_touch_instruction'),
sub: t('guided_touch_sub'),
count: 4,
icon: "✋",
color: "#81C784"
},
{
title: t('guided_sound_title'),
instruction: t('guided_sound_instruction'),
sub: t('guided_sound_sub'),
count: 3,
icon: "👂",
color: "#FFB74D"
},
{
title: t('guided_smell_title'),
instruction: t('guided_smell_instruction'),
sub: t('guided_smell_sub'),
count: 2,
icon: "👃",
color: "#BA68C8"
},
{
title: t('guided_taste_title'),
instruction: t('guided_taste_instruction'),
sub: t('guided_taste_sub'),
count: 1,
icon: "👅",
color: "#E57373"
}
{ title: t('guided_sight_title'), instruction: t('guided_sight_instruction'), sub: t('guided_sight_sub'), count: 5, icon: "👁️", color: "#64B5F6" },
{ title: t('guided_touch_title'), instruction: t('guided_touch_instruction'), sub: t('guided_touch_sub'), count: 4, icon: "✋", color: "#81C784" },
{ title: t('guided_sound_title'), instruction: t('guided_sound_instruction'), sub: t('guided_sound_sub'), count: 3, icon: "👂", color: "#FFB74D" },
{ title: t('guided_smell_title'), instruction: t('guided_smell_instruction'), sub: t('guided_smell_sub'), count: 2, icon: "👃", color: "#BA68C8" },
{ title: t('guided_taste_title'), instruction: t('guided_taste_instruction'), sub: t('guided_taste_sub'), count: 1, icon: "👅", color: "#E57373" }
];
}
function getBodyScanSteps() {
return [
{ instruction: t('scan_intro'), duration: 5000 },
{ instruction: t('scan_feet'), duration: 8000 },
{ instruction: t('scan_legs'), duration: 8000 },
{ instruction: t('scan_stomach'), duration: 8000 },
{ instruction: t('scan_chest'), duration: 8000 },
{ instruction: t('scan_shoulders'), duration: 8000 },
{ instruction: t('scan_face'), duration: 8000 },
{ instruction: t('scan_outro'), duration: 6000 }
];
}
function getVisualizationSteps() {
return [
{ instruction: t('vis_intro'), duration: 6000 },
{ instruction: t('vis_step1'), duration: 10000 },
{ instruction: t('vis_step2'), duration: 10000 },
{ instruction: t('vis_step3'), duration: 10000 },
{ instruction: t('vis_step4'), duration: 10000 },
{ instruction: t('vis_outro'), duration: 8000 }
];
}
// Entry Point
function startGuidedRelaxation() {
relaxationState.step = 0;
relaxationState.isActive = true;
relaxationState.steps = getRelaxationSteps();
relaxationState = { mode: null, step: 0, isActive: true, isPaused: false, steps: [], timer: null, duration: 0 };
const overlay = document.createElement('div');
overlay.id = 'guided-relaxation-overlay';
overlay.className = 'guided-overlay';
document.body.appendChild(overlay);
renderRelaxationStep();
// Add styles dynamically if not present (using the styles we wrote to guided-styles.css but injecting here for simplicity in single file context if needed, but best to rely on CSS file)
// Assuming guided-styles.css is linked or merged. For safety, we rely on the styles.css update.
renderModeSelection();
}
function renderRelaxationStep() {
function renderModeSelection() {
const overlay = document.getElementById('guided-relaxation-overlay');
if (!overlay) return;
overlay.innerHTML = `
<div class="guided-controls">
<button class="icon-btn" onclick="closeGuidedRelaxation()">
<span class="material-icons">close</span>
</button>
</div>
<h2 class="guided-title-large">${t('guided_select_mode')}</h2>
<div class="guided-mode-grid">
<div class="guided-mode-card" onclick="startSession('grounding')">
<span class="guided-mode-icon">🌿</span>
<span class="guided-mode-title">${t('mode_grounding')}</span>
</div>
<div class="guided-mode-card" onclick="startSession('body_scan')">
<span class="guided-mode-icon">🧘</span>
<span class="guided-mode-title">${t('mode_body_scan')}</span>
</div>
<div class="guided-mode-card" onclick="startSession('visualization')">
<span class="guided-mode-icon">🌄</span>
<span class="guided-mode-title">${t('mode_visualization')}</span>
</div>
</div>
`;
}
function startSession(mode) {
relaxationState.mode = mode;
relaxationState.step = 0;
relaxationState.isActive = true;
const overlay = document.getElementById('guided-relaxation-overlay');
if (overlay) overlay.className = `guided-overlay mode-${mode}`;
if (mode === 'grounding') {
relaxationState.steps = getGroundingSteps();
renderGroundingStep();
} else if (mode === 'body_scan') {
relaxationState.steps = getBodyScanSteps();
runAutoSession();
} else if (mode === 'visualization') {
relaxationState.steps = getVisualizationSteps();
runAutoSession();
}
}
// --- Grounding Logic (Interactive) ---
function renderGroundingStep() {
const overlay = document.getElementById('guided-relaxation-overlay');
if (!overlay || !relaxationState.isActive) return;
@@ -1312,24 +1359,20 @@ function renderRelaxationStep() {
${progressDots}
</div>
<button class="guided-action-btn" onclick="nextRelaxationSubStep()">
<button class="guided-action-btn" onclick="nextGroundingSubStep()">
${t('guided_found_btn')}
</button>
`;
// Speak instruction
speakText(`${currentStep.instruction} ${currentStep.sub}`);
}
let currentDotIndex = 0;
function nextRelaxationSubStep() {
function nextGroundingSubStep() {
const dots = document.querySelectorAll('.progress-dot');
if (currentDotIndex < dots.length) {
dots[currentDotIndex].classList.add('active');
// Haptic feedback
if (navigator.vibrate) navigator.vibrate(50);
// Sound feedback
soundManager.playTone(400 + (currentDotIndex * 50), 'sine', 0.1, 0.1);
currentDotIndex++;
@@ -1339,16 +1382,75 @@ function nextRelaxationSubStep() {
currentDotIndex = 0;
relaxationState.step++;
if (relaxationState.step < relaxationState.steps.length) {
renderRelaxationStep();
renderGroundingStep();
} else {
finishGuidedRelaxation();
finishSession();
}
}, 1000);
}
}
}
function finishGuidedRelaxation() {
// --- Auto Session Logic (Body Scan & Visualization) ---
function runAutoSession() {
const overlay = document.getElementById('guided-relaxation-overlay');
if (!overlay || !relaxationState.isActive) return;
const step = relaxationState.steps[relaxationState.step];
const totalSteps = relaxationState.steps.length;
const progress = ((relaxationState.step) / totalSteps) * 100;
// Visuals based on mode
let visualHTML = '';
if (relaxationState.mode === 'body_scan') {
visualHTML = `<div class="body-scan-pulse"></div><div class="guided-step-icon">🧘</div>`;
} else {
visualHTML = `<div class="guided-step-icon">🌄</div>`;
}
overlay.innerHTML = `
<div class="guided-controls">
<button class="icon-btn" onclick="closeGuidedRelaxation()">
<span class="material-icons">close</span>
</button>
</div>
${visualHTML}
<div class="guided-instruction">${step.instruction}</div>
<div class="guided-progress-bar-container">
<div class="guided-progress-bar" style="width: ${progress}%"></div>
</div>
<div class="guided-bottom-controls">
<!-- Could add Play/Pause here if needed -->
</div>
`;
// Speak and advance
speakText(step.instruction, () => {
// On end of speech, wait remainder of duration
if (!relaxationState.isActive) return;
relaxationState.timer = setTimeout(() => {
relaxationState.step++;
if (relaxationState.step < relaxationState.steps.length) {
runAutoSession();
} else {
finishSession();
}
}, 2000); // Short pause after speech
});
// Animate progress bar
setTimeout(() => {
const bar = document.querySelector('.guided-progress-bar');
if (bar) bar.style.width = `${((relaxationState.step + 1) / totalSteps) * 100}%`;
}, 100);
}
function finishSession() {
const overlay = document.getElementById('guided-relaxation-overlay');
if (overlay) {
overlay.innerHTML = `
@@ -1361,11 +1463,17 @@ function finishGuidedRelaxation() {
`;
speakText(`${t('guided_complete_title')} ${t('guided_complete_sub')}`);
triggerSuccessPing();
// Log stats
const duration = relaxationState.mode === 'grounding' ? 180 :
relaxationState.mode === 'body_scan' ? 300 : 240;
exerciseAPI.logSession(relaxationState.mode, duration).then(() => updateProgress());
}
}
function closeGuidedRelaxation() {
relaxationState.isActive = false;
if (relaxationState.timer) clearTimeout(relaxationState.timer);
currentDotIndex = 0;
const overlay = document.getElementById('guided-relaxation-overlay');
if (overlay) overlay.remove();
@@ -1373,29 +1481,27 @@ function closeGuidedRelaxation() {
if (window.speechSynthesis) {
window.speechSynthesis.cancel();
}
// Log session
if (relaxationState.step >= relaxationState.steps.length) {
exerciseAPI.logSession('grounding', 180).then(() => updateProgress());
}
}
function speakText(text) {
// Enhanced TTS Wrapper
function speakText(text, onEndCallback) {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel(); // Stop previous
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 0.9;
utterance.rate = 0.85; // Slightly slower for relaxation
utterance.pitch = 1.0;
// Map language codes to TTS locales
const langMap = {
'en': 'en-US',
'ru': 'ru-RU',
'he': 'he-IL'
};
const langMap = { 'en': 'en-US', 'ru': 'ru-RU', 'he': 'he-IL' };
utterance.lang = langMap[currentLang] || 'en-US';
if (onEndCallback) {
utterance.onend = onEndCallback;
}
window.speechSynthesis.speak(utterance);
} else {
// Fallback if no TTS
if (onEndCallback) setTimeout(onEndCallback, 3000);
}
}