-
๐ต Rhythm Pattern: ${pattern}
-
-
+
๐ต Rhythm Pattern: ${currentPatternData.pattern}
+ ${enableVideo ? `
+
+
+
+
+
+
+
+
+ 50%
+
+
+
+
+ 30%
+
+
+ ` : `
+
+
+
+
+
+ 30%
+
+
+ `}
+
+
+
- ${bpmSequence[0]} BPM
+ ${bpmSequence[0] === 0 ? 'PAUSE' : bpmSequence[0]} BPM
Phase ${currentPhase + 1} of ${bpmSequence.length}
-
- ${timeRemaining}s remaining
-
`;
+ // Add timer to sidebar
+ const sidebar = document.getElementById('campaign-sidebar');
+ if (sidebar) {
+ sidebar.style.display = 'block';
+ const mins = Math.floor(timeRemaining / 60);
+ const secs = timeRemaining % 60;
+ sidebar.innerHTML = `
+
+
Time Remaining
+
${mins}:${String(secs).padStart(2, '0')}
+
+ `;
+ }
+
+ // Create metronome sound (simple beep using Web Audio API)
+ let audioContext;
+ if (enableMetronomeSound && (window.AudioContext || window.webkitAudioContext)) {
+ try {
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
+ } catch (err) {
+ console.warn('โ ๏ธ Could not create audio context for metronome:', err);
+ }
+ }
+
+ const playMetronomeTick = () => {
+ if (!audioContext || metronomeVolume === 0) return;
+
+ try {
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ oscillator.frequency.value = 800; // 800 Hz tick sound
+ oscillator.type = 'sine';
+
+ gainNode.gain.setValueAtTime(metronomeVolume, audioContext.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
+
+ oscillator.start(audioContext.currentTime);
+ oscillator.stop(audioContext.currentTime + 0.1);
+ } catch (err) {
+ console.warn('โ ๏ธ Error playing metronome tick:', err);
+ }
+ };
+
+ // Start video (only if enabled)
+ const videoContainer = enableVideo ? container.querySelector('#rhythm-video-container') : null;
+ let currentVideoElement = null;
+
+ const playRandomVideo = async () => {
+ if (videoContainer && window.videoPlayerManager) {
+ try {
+ const availableVideos = window.videoPlayerManager.videoLibrary?.background || [];
+ if (availableVideos.length > 0) {
+ const randomVideo = availableVideos[Math.floor(Math.random() * availableVideos.length)];
+ const videoPath = typeof randomVideo === 'object' ? randomVideo.path : randomVideo;
+ await window.videoPlayerManager.playTaskVideo(videoPath, videoContainer);
+
+ // Get the video element reference
+ currentVideoElement = videoContainer.querySelector('video');
+
+ // Apply current volume
+ if (currentVideoElement) {
+ const volumeSlider = container.querySelector('#rhythm-video-volume');
+ currentVideoElement.volume = (volumeSlider?.value || 50) / 100;
+
+ // Auto-play next video when current ends
+ currentVideoElement.addEventListener('ended', async () => {
+ await playRandomVideo();
+ });
+ }
+ }
+ } catch (err) {
+ console.warn('โ ๏ธ Could not start video for rhythm task:', err);
+ }
+ }
+ };
+
+ if (enableVideo) {
+ await playRandomVideo();
+ }
+
+ // Setup video controls (only if video enabled)
+ const skipVideoBtn = enableVideo ? container.querySelector('#rhythm-skip-video') : null;
+ const videoVolumeSlider = enableVideo ? container.querySelector('#rhythm-video-volume') : null;
+ const videoVolumeDisplay = enableVideo ? container.querySelector('#rhythm-video-volume-display') : null;
+ const metronomeVolumeSlider = container.querySelector('#rhythm-metronome-volume');
+ const metronomeVolumeDisplay = container.querySelector('#rhythm-metronome-volume-display');
+
+ if (skipVideoBtn) {
+ skipVideoBtn.addEventListener('click', async () => {
+ await playRandomVideo();
+ });
+ }
+
+ if (videoVolumeSlider && videoVolumeDisplay) {
+ videoVolumeSlider.addEventListener('input', (e) => {
+ const volume = e.target.value;
+ videoVolumeDisplay.textContent = `${volume}%`;
+
+ if (currentVideoElement) {
+ currentVideoElement.volume = volume / 100;
+ }
+ });
+ }
+
+ if (metronomeVolumeSlider && metronomeVolumeDisplay) {
+ metronomeVolumeSlider.addEventListener('input', (e) => {
+ const volume = e.target.value;
+ metronomeVolumeDisplay.textContent = `${volume}%`;
+ metronomeVolume = volume / 100;
+ });
+ }
+
const metronomeVisual = container.querySelector('#metronome-visual');
const bpmDisplay = container.querySelector('.bpm-value');
- const timerDisplay = container.querySelector('#rhythm-timer');
+ const timerDisplay = document.getElementById('rhythm-timer'); // Now in sidebar
const phaseIndicator = container.querySelector('.phase-indicator');
+ const patternTitle = container.querySelector('#rhythm-pattern-title');
// Start metronome animation
let bpm = bpmSequence[currentPhase];
let beatInterval = (60 / bpm) * 1000;
const beat = () => {
+ // Skip beat if in pause phase (BPM = 0)
+ if (bpm === 0) return;
+
metronomeVisual.classList.add('beat');
setTimeout(() => metronomeVisual.classList.remove('beat'), 100);
+ if (enableMetronomeSound) {
+ playMetronomeTick();
+ }
};
let beatTimer = setInterval(beat, beatInterval);
// Countdown timer and phase progression
- const phaseDuration = duration / bpmSequence.length;
+ let phaseDuration = currentPatternData.duration / bpmSequence.length;
let phaseTime = phaseDuration;
const countdown = setInterval(() => {
timeRemaining--;
+ patternTimeRemaining--;
phaseTime--;
- timerDisplay.textContent = `${timeRemaining}s`;
- if (phaseTime <= 0 && currentPhase < bpmSequence.length - 1) {
+ const mins = Math.floor(timeRemaining / 60);
+ const secs = timeRemaining % 60;
+ timerDisplay.textContent = `${mins}:${String(secs).padStart(2, '0')}`;
+
+ // Check if we need to switch to next pattern in schedule
+ if (multiPattern && patternTimeRemaining <= 0 && currentPatternIndex < patternSchedule.length - 1) {
+ currentPatternIndex++;
+ currentPatternData = patternSchedule[currentPatternIndex];
+ bpmSequence = patterns[currentPatternData.pattern] || patterns['steady'];
+ currentPhase = 0;
+ patternTimeRemaining = currentPatternData.duration;
+ phaseDuration = currentPatternData.duration / bpmSequence.length;
+ phaseTime = phaseDuration;
+
+ bpm = bpmSequence[currentPhase];
+ beatInterval = (60 / bpm) * 1000;
+
+ clearInterval(beatTimer);
+ beatTimer = setInterval(beat, beatInterval);
+
+ bpmDisplay.textContent = bpm === 0 ? 'PAUSE' : bpm;
+ phaseIndicator.textContent = `Phase ${currentPhase + 1} of ${bpmSequence.length}`;
+ patternTitle.textContent = `๐ต Rhythm Pattern: ${currentPatternData.pattern}`;
+
+ console.log(`๐ต Pattern switched to: ${currentPatternData.pattern}`);
+ }
+ // Check phase progression within current pattern
+ else if (phaseTime <= 0 && currentPhase < bpmSequence.length - 1) {
currentPhase++;
phaseTime = phaseDuration;
bpm = bpmSequence[currentPhase];
@@ -4778,7 +4965,7 @@ class InteractiveTaskManager {
clearInterval(beatTimer);
beatTimer = setInterval(beat, beatInterval);
- bpmDisplay.textContent = bpm;
+ bpmDisplay.textContent = bpm === 0 ? 'PAUSE' : bpm;
phaseIndicator.textContent = `Phase ${currentPhase + 1} of ${bpmSequence.length}`;
}
@@ -4798,8 +4985,23 @@ class InteractiveTaskManager {
// Store cleanup
task.cleanup = () => {
+ console.log('๐งน Rhythm task cleanup called');
clearInterval(countdown);
clearInterval(beatTimer);
+ console.log('โ
Cleared intervals');
+
+ // Stop audio context
+ if (audioContext) {
+ console.log('๐ Closing audio context');
+ audioContext.close().catch(err => console.warn('โ ๏ธ Error closing audio context:', err));
+ }
+
+ // Hide sidebar
+ const sidebar = document.getElementById('campaign-sidebar');
+ if (sidebar) {
+ sidebar.style.display = 'none';
+ sidebar.innerHTML = '';
+ }
};
}
@@ -4872,30 +5074,70 @@ class InteractiveTaskManager {
const suggestedTags = task.params?.suggestedTags || [];
const preserveContent = task.params?.preserveContent !== false; // Default true
- // Track tagged files count
- if (!task.taggedFilesCount) {
- task.taggedFilesCount = 0;
+ // Track tagged files count and initial count
+ if (!task.totalTagsCount) {
+ task.totalTagsCount = 0;
}
+ // Track initial total tags count (first time task is created)
+ if (task.initialTagsCount === undefined) {
+ // Count total number of tags across all media
+ try {
+ const tagsData = localStorage.getItem('library-mediaTags');
+ if (tagsData) {
+ const parsed = JSON.parse(tagsData);
+ const mediaTags = parsed.mediaTags || {};
+ // Count total tags (sum of all tag arrays)
+ let totalTags = 0;
+ Object.values(mediaTags).forEach(tags => {
+ if (tags && Array.isArray(tags)) {
+ totalTags += tags.length;
+ }
+ });
+ task.initialTagsCount = totalTags;
+ task.totalTagsCount = totalTags;
+ console.log(`๐ท๏ธ Initial total tags: ${task.initialTagsCount}`);
+ } else {
+ task.initialTagsCount = 0;
+ task.totalTagsCount = 0;
+ }
+ } catch (error) {
+ console.error('โ Error reading initial tag data:', error);
+ task.initialTagsCount = 0;
+ task.totalTagsCount = 0;
+ }
+ }
+
+ const initialCount = task.initialTagsCount;
+
+ // Calculate target based on initial count
+ const targetTags = initialCount + minFiles;
+
const tagUI = `
๐ท๏ธ Tag Your Files
-
Tag at least ${minFiles} files in your library to improve content filtering.
+
Add ${minFiles} tags to your library (you can add multiple tags to the same file).
${directory ? `
Focus on: ${directory}
` : ''}
${suggestedTags.length > 0 ? `
๐ก Suggested tags: ${suggestedTags.map(t => `${t}`).join(' ')}
` : ''}
- ${task.taggedFilesCount || 0} / ${minFiles} files tagged
+ ${task.totalTagsCount || 0} / ${targetTags} tags
+
+ (+${Math.max(0, task.totalTagsCount - task.initialTagsCount)} added this session)
+
+
`;
@@ -4909,58 +5151,77 @@ class InteractiveTaskManager {
}
const btn = container.querySelector('#open-tagging-btn');
- const progressEl = container.querySelector('#files-tagged');
+ const refreshBtn = container.querySelector('#refresh-progress-btn');
+ const progressEl = container.querySelector('#tags-added-count');
const progressBar = container.querySelector('#tag-progress-bar');
const statusArea = container.querySelector('#tag-task-status');
+ const tagsAddedSessionEl = container.querySelector('#tags-added-session');
// Function to update progress
const updateProgress = () => {
- // Count how many media items have tags by checking localStorage directly
+ // Count total tags across all media
try {
// Tags are stored in localStorage under 'library-mediaTags' key
const tagsData = localStorage.getItem('library-mediaTags');
if (tagsData) {
const parsed = JSON.parse(tagsData);
const mediaTags = parsed.mediaTags || {};
- // Count media items that have at least one tag
- const taggedMedia = Object.keys(mediaTags).filter(mediaId => {
- const tags = mediaTags[mediaId];
- return tags && tags.length > 0;
+ // Count total tags (sum of all tag arrays)
+ let totalTags = 0;
+ Object.values(mediaTags).forEach(tags => {
+ if (tags && Array.isArray(tags)) {
+ totalTags += tags.length;
+ }
});
- task.taggedFilesCount = taggedMedia.length;
- console.log(`๐ท๏ธ Progress update: ${task.taggedFilesCount} files tagged`);
+
+ task.totalTagsCount = totalTags;
+ console.log(`๐ท๏ธ Progress update: ${task.totalTagsCount} total tags (started with ${initialCount})`);
} else {
- task.taggedFilesCount = 0;
- console.log('๐ท๏ธ No tag data found in localStorage');
+ task.totalTagsCount = initialCount;
+ console.log('๐ท๏ธ No tag data found in localStorage, keeping initial count');
}
} catch (error) {
console.error('โ Error reading tag data:', error);
- task.taggedFilesCount = 0;
+ task.totalTagsCount = initialCount;
}
- progressEl.textContent = task.taggedFilesCount;
- const percent = Math.min(100, (task.taggedFilesCount / minFiles) * 100);
+ const tagsAdded = Math.max(0, task.totalTagsCount - initialCount);
+ const targetTags = initialCount + minFiles;
+
+ progressEl.textContent = task.totalTagsCount;
+ if (tagsAddedSessionEl) {
+ tagsAddedSessionEl.textContent = tagsAdded;
+ }
+ const percent = Math.min(100, (task.totalTagsCount / targetTags) * 100);
progressBar.style.width = percent + '%';
- if (task.taggedFilesCount >= minFiles) {
+ if (task.totalTagsCount >= targetTags) {
task.completed = true;
btn.disabled = true;
btn.textContent = 'โ
Tagging Complete';
btn.style.background = 'var(--color-success)';
- statusArea.innerHTML = `
โ
You have tagged ${task.taggedFilesCount} files! Task complete.
`;
+ statusArea.innerHTML = `
โ
You have added ${tagsAdded} new tags! Task complete.
`;
const completeBtn = document.getElementById('interactive-complete-btn');
if (completeBtn) {
completeBtn.disabled = false;
}
} else {
- statusArea.innerHTML = `
โน๏ธ Tag ${minFiles - task.taggedFilesCount} more file(s) to complete this task
`;
+ const remaining = targetTags - task.totalTagsCount;
+ statusArea.innerHTML = `
โน๏ธ Add ${remaining} more tag(s) to complete this task
`;
}
};
// Check initial progress
updateProgress();
+ // Manual refresh button
+ refreshBtn.addEventListener('click', () => {
+ console.log('๐ Manual progress refresh triggered');
+ updateProgress();
+ statusArea.innerHTML = '
๐ Progress refreshed!
';
+ });
+
btn.addEventListener('click', async () => {
console.log('๐ท๏ธ Tag Files button clicked');
console.log('๐ท๏ธ electronAPI available:', !!window.electronAPI);
@@ -5773,13 +6034,12 @@ class InteractiveTaskManager {
async createVideoStartTask(task, container) {
const player = task.params?.player || 'task';
const tags = task.params?.tags || [];
- const minDuration = task.params?.minDuration || 60; // Default 60 seconds minimum viewing
+ const minDuration = task.params?.minDuration || task.params?.duration || 60; // Support both minDuration and duration params
container.innerHTML = `
๐ฌ Video Immersion
Prepare to focus on the video content
-
Minimum viewing time: ${minDuration} seconds
${tags.length > 0 ? '
Filtering by tags: ' + tags.map(t => '' + t + '').join(' ') + '
' : ''}
@@ -5790,7 +6050,6 @@ class InteractiveTaskManager {
โญ๏ธ Skip to Next Video
-
`;
@@ -5799,9 +6058,9 @@ class InteractiveTaskManager {
const skipBtn = container.querySelector('#skip-video-btn');
const statusArea = container.querySelector('#video-start-status');
const videoContainer = container.querySelector('#video-player-container');
- const timerDisplay = container.querySelector('#video-timer-display');
let availableVideos = [];
+ let timerInterval = null;
const playRandomVideo = async () => {
if (availableVideos.length === 0) return;
@@ -5848,21 +6107,40 @@ class InteractiveTaskManager {
btn.style.display = 'none';
skipBtn.style.display = 'inline-block';
- timerDisplay.style.display = 'block';
+
+ // Add timer to sidebar
+ const sidebar = document.getElementById('campaign-sidebar');
+ if (sidebar) {
+ sidebar.style.display = 'block';
+ const mins = Math.floor(minDuration / 60);
+ const secs = minDuration % 60;
+ sidebar.innerHTML = `
+
+
Time Remaining
+
${mins}:${String(secs).padStart(2, '0')}
+
+ `;
+ }
// Start countdown timer
let remainingTime = minDuration;
- timerDisplay.innerHTML = `โฑ๏ธ Keep watching... ${remainingTime}s remaining`;
+ const timerDisplay = document.getElementById('video-timer');
- const timerInterval = setInterval(() => {
+ timerInterval = setInterval(() => {
remainingTime--;
if (remainingTime > 0) {
- timerDisplay.innerHTML = `โฑ๏ธ Keep watching... ${remainingTime}s remaining`;
+ const mins = Math.floor(remainingTime / 60);
+ const secs = remainingTime % 60;
+ if (timerDisplay) {
+ timerDisplay.textContent = `${mins}:${String(secs).padStart(2, '0')}`;
+ }
} else {
clearInterval(timerInterval);
- timerDisplay.innerHTML = 'โ
Minimum viewing time complete!';
- timerDisplay.style.color = 'var(--color-success, #4caf50)';
+ if (timerDisplay) {
+ timerDisplay.textContent = 'COMPLETE!';
+ timerDisplay.style.color = 'var(--color-success, #4caf50)';
+ }
statusArea.innerHTML = '
โ
You may now continue when ready
';
task.completed = true;
@@ -5885,6 +6163,21 @@ class InteractiveTaskManager {
}
}
});
+
+ // Store cleanup function
+ task.cleanup = () => {
+ console.log('๐งน Video start task cleanup called');
+ if (timerInterval) {
+ clearInterval(timerInterval);
+ }
+
+ // Hide sidebar
+ const sidebar = document.getElementById('campaign-sidebar');
+ if (sidebar) {
+ sidebar.style.display = 'none';
+ sidebar.innerHTML = '';
+ }
+ };
}
validateVideoStartTask(task) {
@@ -6047,7 +6340,7 @@ class InteractiveTaskManager {
/**
* Hypno Caption Combo - Hypno spiral with timed captions
*/
- createHypnoCaptionComboTask(container, task) {
+ createHypnoCaptionComboTask(task, container) {
const params = task.params || {};
const duration = params.duration || 300;
const captionTexts = params.captions || [
@@ -6136,7 +6429,7 @@ class InteractiveTaskManager {
/**
* Dynamic Captions - Preference-based captions
*/
- createDynamicCaptionsTask(container, task) {
+ createDynamicCaptionsTask(task, container) {
const params = task.params || {};
const duration = params.duration || 300;
@@ -6212,7 +6505,7 @@ class InteractiveTaskManager {
/**
* TTS Hypno Sync - Voice + hypno synchronized
*/
- createTTSHypnoSyncTask(container, task) {
+ createTTSHypnoSyncTask(task, container) {
const params = task.params || {};
const duration = params.duration || 300;
const commands = params.commands || [
@@ -6223,12 +6516,17 @@ class InteractiveTaskManager {
'Obey and edge'
];
+ if (!container) {
+ console.error('โ TTS Hypno Sync: No container provided');
+ return;
+ }
+
container.innerHTML = `
`;
@@ -6240,6 +6538,15 @@ class InteractiveTaskManager {
const commandEl = document.getElementById('tts-command-display');
const timeEl = document.getElementById('tts-time-remaining');
+
+ // Safety check for elements
+ if (!commandEl || !timeEl) {
+ console.error('โ TTS Hypno Sync elements not found in DOM');
+ console.log('Container:', container);
+ console.log('Container innerHTML:', container?.innerHTML);
+ return;
+ }
+
let timeRemaining = duration;
let commandIndex = 0;
@@ -6290,7 +6597,7 @@ class InteractiveTaskManager {
/**
* Sensory Overload - All features combined
*/
- createSensoryOverloadTask(container, task) {
+ createSensoryOverloadTask(task, container) {
const params = task.params || {};
const duration = params.duration || 600;
const edgeTarget = params.edgeCount || 50;
@@ -6382,7 +6689,7 @@ class InteractiveTaskManager {
/**
* Enable Interruptions - Activate interruption system
*/
- createEnableInterruptionsTask(container, task) {
+ createEnableInterruptionsTask(task, container) {
const params = task.params || {};
const types = params.types || ['edge', 'pose', 'mantra', 'stop-stroking'];
const frequency = params.frequency || 'medium';
@@ -6421,7 +6728,7 @@ class InteractiveTaskManager {
/**
* Denial Training - Timed stroking/stopping periods
*/
- createDenialTrainingTask(container, task) {
+ createDenialTrainingTask(task, container) {
const params = task.params || {};
const periods = params.denialPeriods || [
{ allowStroking: 300, instruction: 'Stroke for 5 minutes' },
@@ -6488,7 +6795,7 @@ class InteractiveTaskManager {
/**
* Stop Stroking - Enforced hands-off period
*/
- createStopStrokingTask(container, task) {
+ createStopStrokingTask(task, container) {
const params = task.params || {};
const duration = params.duration || 120;
@@ -6532,7 +6839,7 @@ class InteractiveTaskManager {
/**
* Enable Popups - Activate popup system
*/
- createEnablePopupsTask(container, task) {
+ createEnablePopupsTask(task, container) {
const params = task.params || {};
const frequency = params.frequency || 'medium';
const sources = params.sources || ['tasks', 'consequences'];
@@ -6571,7 +6878,7 @@ class InteractiveTaskManager {
/**
* Popup Image - Display specific popup
*/
- createPopupImageTask(container, task) {
+ createPopupImageTask(task, container) {
const params = task.params || {};
const imagePath = params.image || '';
const displayTime = params.duration || 10;