flowify-api / static /script.js
Yuvvan Talreja
Upload fix
422dc44
Raw
History Blame Contribute Delete
82.7 kB
import { pipeline, env } from 'https://unpkg.com/@xenova/transformers@2.16.0/dist/transformers.min.js';
env.allowLocalModels = false;
env.useBrowserCache = true;
// Utility function to check if elements exist and log errors if they don't
function getElements(selectors) {
const elements = {};
let allFound = true;
for (const [key, selector] of Object.entries(selectors)) {
const element = selector.startsWith('.') ?
document.querySelector(selector) :
document.getElementById(selector);
if (!element) {
console.warn(`Element not found: ${selector}`);
allFound = false;
}
elements[key] = element;
}
return { elements, allFound };
}
// Global variables
const { elements } = getElements({
videoPlayer: 'videoPlayer',
fileInput: 'fileInput',
transcribeButton: 'transcribeButton',
analyzeButton: 'analyzeButton',
transcriptionDiv: 'transcription',
topicResultsDiv: 'topicResults',
statusDiv: 'status',
modelSelector: 'modelSelector',
modelStatus: 'modelStatus'
});
const videoPlayer = elements.videoPlayer;
const fileInput = elements.fileInput;
const transcribeButton = elements.transcribeButton;
const analyzeButton = elements.analyzeButton;
const transcriptionDiv = elements.transcriptionDiv;
const topicResultsDiv = elements.topicResultsDiv;
const statusDiv = elements.statusDiv;
const modelSelector = elements.modelSelector;
const modelStatus = elements.modelStatus;
let whisperPipeline = null;
let transcriptText = '';
let mindmap;
// Original transcribeAudio function definition
async function transcribeAudio(audioData) {
try {
statusDiv.textContent = 'Transcribing audio...';
transcriptionDiv.textContent = '';
// Make sure we're using the correct progress element
const progressBarFill = document.querySelector('.progress-bar-fill');
if (progressBarFill) {
progressBarFill.style.width = '0%';
}
// Check if server-side transcription is possible
const selectedModel = modelSelector.value;
if (selectedModel !== 'browser') {
// Convert audio data to base64
const base64Audio = arrayBufferToBase64(audioData.buffer);
// Send to server as JSON
const response = await fetch('/transcribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
audio: base64Audio,
model: selectedModel
})
});
if (!response.ok) {
throw new Error('Server transcription failed');
}
const result = await response.json();
transcriptText = result.transcript || result.text || '';
// Process the transcript to create segments for display
if (transcriptText) {
const segments = [];
// Extract timestamps and text using regex
const timestampPattern = /\[(\d+)\](.*?)(?=\[\d+\]|$)/g;
let match;
while ((match = timestampPattern.exec(transcriptText)) !== null) {
segments.push({
start: parseInt(match[1]),
text: match[2].trim()
});
}
// If no timestamps were found, fallback to displaying the whole text
if (segments.length === 0 && transcriptText.trim()) {
segments.push({
start: 0,
text: transcriptText.trim()
});
}
// Display with timestamps
console.log("Displaying transcription with segments:", segments);
displayTranscription(segments);
}
statusDiv.textContent = 'Transcription complete!';
analyzeButton.disabled = false;
if (progressBarFill) {
progressBarFill.style.width = '100%';
}
return;
}
// Browser-side transcription using Whisper
if (!whisperPipeline) {
statusDiv.textContent = 'Loading Whisper model...';
// Initialize the pipeline
whisperPipeline = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en');
statusDiv.textContent = 'Model loaded. Transcribing...';
}
// Processing with progress updates
const transcriptionResult = await whisperPipeline(audioData, {
chunk_length_s: 30,
stride_length_s: 5,
language: 'english',
task: 'transcribe',
return_timestamps: true,
callback_function: function(progressData) {
const percentage = Math.round(progressData.progress * 100);
statusDiv.textContent = `Transcribing: ${percentage}%`;
const progressBarFill = document.querySelector('.progress-bar-fill');
if (progressBarFill) {
progressBarFill.style.width = `${percentage}%`;
}
}
});
// Save and display transcription
transcriptText = transcriptionResult.text;
displayTranscription(transcriptionResult.chunks || []);
statusDiv.textContent = 'Transcription complete!';
analyzeButton.disabled = false;
if (progressBarFill) {
progressBarFill.style.width = '100%';
}
} catch (error) {
console.error('Transcription error:', error);
statusDiv.textContent = `Error: ${error.message}`;
throw error;
}
}
// Helper function to display transcription with timestamps
function displayTranscription(segments) {
console.log("displayTranscription called with segments:", segments);
// Clear the transcription div of all content, including the empty state
if (transcriptionDiv) {
transcriptionDiv.innerHTML = '';
} else {
console.error("transcriptionDiv element not found");
return;
}
if (!segments || segments.length === 0) {
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<i class="fas fa-file-alt"></i>
<h3 class="empty-state-title">No transcription yet</h3>
<p class="empty-state-description">Upload a video and click "Transcribe Video" to see the transcription here</p>
`;
transcriptionDiv.appendChild(emptyState);
return;
}
// Create a container for the transcription content
const transcriptionContent = document.createElement('div');
transcriptionContent.className = 'transcription-content';
segments.forEach(segment => {
const paragraphElement = document.createElement('p');
// Format and add timestamp prefix
let timestamp = '?';
let text = '';
if (Array.isArray(segment) && segment.length >= 2) {
// For array format [start, end, text]
timestamp = Math.floor(segment[0]);
text = segment[2] || '';
} else if (typeof segment === 'object' && segment.start !== undefined) {
// For object format {start, end, text}
timestamp = Math.floor(segment.start);
text = segment.text || '';
} else {
// Fallback
text = segment.toString();
}
paragraphElement.textContent = `[${timestamp}] ${text}`;
// Add click handler to jump to timestamp
paragraphElement.addEventListener('click', () => {
videoPlayer.currentTime = timestamp;
videoPlayer.play().catch(e => console.error('Could not play video:', e));
});
paragraphElement.style.cursor = 'pointer';
transcriptionContent.appendChild(paragraphElement);
});
transcriptionDiv.appendChild(transcriptionContent);
}
// Store original function references
const originalTranscribeAudio = transcribeAudio;
const originalAnalyzeTopics = analyzeTopics;
// Override with enhanced versions
transcribeAudio = async function(audioData) {
// Set progress step - use the global function safely
try {
if (typeof window.setActiveStep === 'function') {
window.setActiveStep('transcribe');
}
} catch (err) {
console.warn('Could not update progress steps:', err);
}
try {
await originalTranscribeAudio(audioData);
// If successful, make analyze button fancy with pulse animation
analyzeButton.classList.add('animate-pulse');
setTimeout(() => analyzeButton.classList.remove('animate-pulse'), 3000);
} catch (error) {
console.error('Transcription error:', error);
throw error;
}
};
analyzeTopics = async function() {
// Set progress step - use the global function safely
try {
if (typeof window.setActiveStep === 'function') {
window.setActiveStep('analyze');
}
} catch (err) {
console.warn('Could not update progress steps:', err);
}
// Remove pulse animation if still there
analyzeButton.classList.remove('animate-pulse');
try {
await originalAnalyzeTopics();
// If successful, we move to visualization
try {
if (typeof window.setActiveStep === 'function') {
window.setActiveStep('visualize');
}
} catch (err) {
console.warn('Could not update progress steps:', err);
}
} catch (error) {
console.error('Analysis error:', error);
throw error;
}
};
// Fixed file input change handler - properly handle video file selection
fileInput.addEventListener('change', (e) => {
console.log("File input change event fired", e.target.files);
const file = e.target.files[0];
if (file) {
console.log("Processing selected file:", file.name, file.type);
// Only process video files
if (!file.type.startsWith('video/')) {
alert('Please select a video file');
return;
}
try {
const videoURL = URL.createObjectURL(file);
console.log("Created video URL:", videoURL);
videoPlayer.src = videoURL;
videoPlayer.load(); // Force reload
// Update UI
transcribeButton.disabled = false;
analyzeButton.disabled = true;
transcriptionDiv.innerHTML = '';
topicResultsDiv.innerHTML = '';
transcriptText = '';
statusDiv.textContent = 'Video loaded. Ready to transcribe.';
// Reset progress
const progressBarFill = document.querySelector('.progress-bar-fill');
if (progressBarFill) {
progressBarFill.style.width = '0%';
}
// Set active step
if (typeof window.setActiveStep === 'function') {
window.setActiveStep('upload');
}
// Show the video element
videoPlayer.style.display = 'block';
// Add a success message
console.log("Video file successfully loaded");
} catch (error) {
console.error("Error processing video file:", error);
alert('Error loading video: ' + error.message);
}
}
});
// Audio Processing Functions
function convertAudioBuffer(audioBuffer) {
const audioData = audioBuffer.getChannelData(0);
const targetSampleRate = 16000;
const resamplingRatio = targetSampleRate / audioBuffer.sampleRate;
const resampledLength = Math.floor(audioData.length * resamplingRatio);
const resampledData = new Float32Array(resampledLength);
for (let i = 0; i < resampledLength; i++) {
const originalIndex = Math.floor(i / resamplingRatio);
resampledData[i] = audioData[originalIndex];
}
return resampledData;
}
async function extractAudio(videoFile) {
try {
console.log('Starting audio extraction');
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const arrayBuffer = await videoFile.arrayBuffer();
console.log('Video file loaded into buffer');
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
console.log('Audio decoded successfully');
return convertAudioBuffer(audioBuffer);
} catch (error) {
console.error('Error extracting audio:', error);
throw new Error(`Failed to extract audio: ${error.message}`);
}
}
// Define setActiveStep globally before any other functions use it
window.setActiveStep = function(step) {
// Will be initialized properly later, but create empty function to prevent errors
console.log(`Progress step: ${step}`);
};
// Helper function to convert ArrayBuffer to base64
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
function formatTimestamp(seconds) {
// Round down to nearest second and return as integer
return `[${Math.floor(seconds)}]`;
}
async function analyzeTopics() {
try {
statusDiv.textContent = 'Analyzing topics...';
// Make sure we're using the correct progress element
const progressBarFill = document.querySelector('.progress-bar-fill');
if (progressBarFill) {
progressBarFill.style.width = '0%';
}
const response = await fetch('/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ transcript: transcriptText })
});
if (!response.ok) {
throw new Error('Failed to analyze topics');
}
const data = await response.json();
displayTopicResults(data.segments);
statusDiv.textContent = 'Topic analysis complete!';
if (progressBarFill) {
progressBarFill.style.width = '100%';
}
} catch (error) {
console.error('Topic analysis error:', error);
statusDiv.textContent = `Error: ${error.message}`;
}
}
function displayTopicResults(segments) {
// Clear previous results
topicResultsDiv.innerHTML = '';
// Display topics in the sidebar
segments.forEach(segment => {
const topicElement = document.createElement('div');
topicElement.className = 'topic';
const headerElement = document.createElement('div');
headerElement.className = 'topic-header';
headerElement.textContent = segment.topic_name;
const contentElement = document.createElement('div');
contentElement.className = 'topic-content';
// Handle content that could be either an array or a string
if (Array.isArray(segment.content)) {
contentElement.textContent = segment.content.join('\n');
} else {
contentElement.textContent = segment.content;
}
topicElement.appendChild(headerElement);
topicElement.appendChild(contentElement);
// If this is a parent topic with children, display them as nested elements
if (segment.is_parent && segment.children && segment.children.length > 0) {
topicElement.classList.add('parent-topic');
const childrenContainer = document.createElement('div');
childrenContainer.className = 'subtopics';
segment.children.forEach(child => {
const childElement = document.createElement('div');
childElement.className = 'subtopic';
const childHeader = document.createElement('div');
childHeader.className = 'subtopic-header';
childHeader.textContent = child.topic_name;
const childContent = document.createElement('div');
childContent.className = 'subtopic-content';
// Handle content that could be either an array or a string
if (Array.isArray(child.content)) {
childContent.textContent = child.content.join('\n');
} else {
childContent.textContent = child.content;
}
childElement.appendChild(childHeader);
childElement.appendChild(childContent);
childrenContainer.appendChild(childElement);
});
topicElement.appendChild(childrenContainer);
}
topicResultsDiv.appendChild(topicElement);
});
// Update mindmap and show fullscreen overlay
mindmap.update(segments);
}
// Event Listeners
transcribeButton.addEventListener('click', async () => {
try {
transcribeButton.disabled = true;
modelSelector.disabled = true;
const videoFile = fileInput.files[0];
if (!videoFile) {
throw new Error('No video file selected');
}
const videoPreview = document.querySelector('.video-preview');
const videoHeight = videoPlayer.offsetHeight;
if (videoHeight > 0) {
videoPreview.style.height = `${videoHeight}px`;
}
statusDiv.textContent = 'Extracting audio...';
const audioData = await extractAudio(videoFile);
await transcribeAudio(audioData);
modelSelector.disabled = false;
} catch (error) {
console.error('Processing error:', error);
statusDiv.textContent = `Error: ${error.message}`;
transcribeButton.disabled = false;
modelSelector.disabled = false;
}
});
analyzeButton.addEventListener('click', analyzeTopics);
modelSelector.addEventListener('change', async (e) => {
const selectedModel = e.target.value;
transcribeButton.disabled = false;
analyzeButton.disabled = true;
await initWhisper(selectedModel);
if (fileInput.files.length > 0) {
transcribeButton.disabled = false;
}
});
// Initialize with the selected model when the page loads
window.addEventListener('DOMContentLoaded', () => {
// Initialize all UI components with error handling for each
try {
initThemeToggle();
} catch (error) {
console.error('Failed to initialize theme toggle:', error);
}
try {
initFAB();
} catch (error) {
console.error('Failed to initialize FAB:', error);
}
try {
initGallery();
} catch (error) {
console.error('Failed to initialize gallery:', error);
}
try {
initCustomVideoPlayer();
} catch (error) {
console.error('Failed to initialize custom video player:', error);
}
try {
initUploadParticles();
} catch (error) {
console.error('Failed to initialize upload particles:', error);
}
try {
initProgressSteps();
} catch (error) {
console.error('Failed to initialize progress steps:', error);
}
// Initialize mindmap system
try {
mindmap = new TopicMindmap();
} catch (error) {
console.error('Failed to initialize mindmap:', error);
}
// Check for model selector and initialize whisper if available
const modelSelector = document.getElementById('modelSelector');
if (modelSelector) {
initWhisper(modelSelector.value).catch(error => {
console.error('Failed to initialize Whisper:', error);
const statusDiv = document.getElementById('status');
if (statusDiv) {
statusDiv.textContent = 'Failed to initialize the transcription model';
}
});
}
});
// Theme toggle functionality
function initThemeToggle() {
const themeToggle = document.getElementById('themeToggle');
const htmlElement = document.documentElement;
// Check for saved theme preference
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
htmlElement.setAttribute('data-theme', savedTheme);
}
themeToggle.addEventListener('click', () => {
const currentTheme = htmlElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
htmlElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
}
// FAB functionality
function initFAB() {
const fabContainer = document.getElementById('fabContainer');
const mainFab = document.getElementById('mainFab');
const exportPdfBtn = document.getElementById('exportPdfBtn');
const exportPngBtn = document.getElementById('exportPngBtn');
const exportSvgBtn = document.getElementById('exportSvgBtn');
const shareMindmapBtn = document.getElementById('shareMindmapBtn');
// Toggle FAB options
mainFab.addEventListener('click', () => {
fabContainer.classList.toggle('active');
});
// Close FAB when clicking outside
document.addEventListener('click', (e) => {
if (!fabContainer.contains(e.target) && fabContainer.classList.contains('active')) {
fabContainer.classList.remove('active');
}
});
// Export as PDF
exportPdfBtn.addEventListener('click', () => {
exportMindmap('pdf');
});
// Export as PNG
exportPngBtn.addEventListener('click', () => {
exportMindmap('png');
});
// Export as SVG
exportSvgBtn.addEventListener('click', () => {
exportMindmap('svg');
});
// Share mindmap
shareMindmapBtn.addEventListener('click', () => {
shareMindmap();
});
}
// Gallery functionality
function initGallery() {
const galleryContainer = document.getElementById('resultsGallery');
const galleryClose = document.getElementById('galleryClose');
const galleryContent = document.getElementById('galleryContent');
// Exit early if required elements don't exist
if (!galleryContainer || !galleryClose || !galleryContent) {
console.warn('Gallery elements not found, skipping gallery initialization');
return;
}
// Add "Recent Mindmaps" option to FAB
const fabContainer = document.getElementById('fabContainer');
if (!fabContainer) {
console.warn('FAB container not found, skipping recent mindmaps button');
return;
}
const fabOptions = fabContainer.querySelector('.fab-options');
if (!fabOptions) {
console.warn('FAB options not found, skipping recent mindmaps button');
return;
}
const recentMindmapsBtn = document.createElement('button');
recentMindmapsBtn.className = 'fab-option';
recentMindmapsBtn.setAttribute('title', 'Recent Mindmaps');
recentMindmapsBtn.innerHTML = '<i class="fas fa-history"></i>';
fabOptions.appendChild(recentMindmapsBtn);
// Show gallery
recentMindmapsBtn.addEventListener('click', () => {
showGallery();
fabContainer.classList.remove('active');
});
// Close gallery
galleryClose.addEventListener('click', () => {
galleryContainer.classList.remove('active');
});
// Close gallery with escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && galleryContainer.classList.contains('active')) {
galleryContainer.classList.remove('active');
}
});
}
// Show gallery and populate with saved mindmaps
function showGallery() {
const galleryContainer = document.getElementById('resultsGallery');
const galleryContent = document.getElementById('galleryContent');
// Clear existing content
galleryContent.innerHTML = '';
// Get saved mindmaps from localStorage
const savedMindmaps = getSavedMindmaps();
if (savedMindmaps.length === 0) {
// Show empty state
galleryContent.innerHTML = `
<div class="gallery-empty">
<i class="fas fa-file-alt" style="font-size: 3rem; color: var(--text-secondary); margin-bottom: 1rem;"></i>
<p>No saved mindmaps yet</p>
<p>Your analyzed videos will appear here</p>
</div>
`;
} else {
// Create gallery items for each saved mindmap
savedMindmaps.forEach(mindmap => {
createGalleryItem(mindmap);
});
}
// Show gallery
galleryContainer.classList.add('active');
}
// Create a gallery item for a saved mindmap
function createGalleryItem(mindmapData) {
const galleryContent = document.getElementById('galleryContent');
const item = document.createElement('div');
item.className = 'gallery-item';
// Use a random color from our palette for preview
const colors = ['#3498db', '#2ecc71', '#e74c3c', '#9b59b6', '#e67e22', '#f1c40f', '#1abc9c'];
const randomColor = colors[Math.floor(Math.random() * colors.length)];
item.innerHTML = `
<div class="gallery-item-preview" style="background-color: ${randomColor};">
<div style="height: 100%; display: flex; align-items: center; justify-content: center; color: white;">
<i class="fas fa-project-diagram" style="font-size: 2rem;"></i>
</div>
</div>
<div class="gallery-item-info">
<div class="gallery-item-title">${mindmapData.title || 'Untitled Mindmap'}</div>
<div class="gallery-item-date">${new Date(mindmapData.date).toLocaleDateString()}</div>
</div>
`;
// Load mindmap when clicked
item.addEventListener('click', () => {
loadMindmap(mindmapData);
document.getElementById('resultsGallery').classList.remove('active');
});
galleryContent.appendChild(item);
}
// Export mindmap in various formats
function exportMindmap(format) {
if (!mindmap || !mindmap.mindmap) {
alert('No mindmap to export. Please analyze a video first.');
return;
}
try {
switch (format) {
case 'pdf':
exportAsPDF();
break;
case 'png':
exportAsPNG();
break;
case 'svg':
exportAsSVG();
break;
}
} catch (error) {
console.error(`Error exporting as ${format}:`, error);
alert(`Failed to export mindmap as ${format.toUpperCase()}`);
}
}
// Export as PDF using html2pdf
function exportAsPDF() {
// We'll use html2pdf.js for PDF export
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js';
document.head.appendChild(script);
script.onload = () => {
const element = document.getElementById('mindmap');
const opt = {
margin: 1,
filename: 'flowify-mindmap.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'in', format: 'letter', orientation: 'landscape' }
};
// Use html2pdf library to generate PDF
html2pdf().set(opt).from(element).save();
};
}
// Export as PNG
function exportAsPNG() {
// We'll use html2canvas for PNG export
const script = document.createElement('script');
script.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js';
document.head.appendChild(script);
script.onload = () => {
html2canvas(document.getElementById('mindmap'), {
scale: 2,
backgroundColor: null
}).then(canvas => {
const link = document.createElement('a');
link.download = 'flowify-mindmap.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
};
}
// Export as SVG
function exportAsSVG() {
alert('SVG export functionality is under development. This feature will be available in a future update.');
}
// Share mindmap (placeholder)
function shareMindmap() {
// Save current mindmap to localStorage
if (mindmap && mindmap.mindmap) {
const data = mindmap.mindmap.get_data();
const mindmapData = {
id: Date.now(),
title: data.data.topic || 'Video Topics',
data: data,
date: new Date().toISOString()
};
saveMindmap(mindmapData);
alert('Mindmap saved! You can access it from the Recent Mindmaps gallery.');
} else {
alert('No mindmap to share. Please analyze a video first.');
}
}
// Save mindmap to localStorage
function saveMindmap(mindmapData) {
let savedMindmaps = getSavedMindmaps();
savedMindmaps.push(mindmapData);
// Keep only the most recent 20 mindmaps
if (savedMindmaps.length > 20) {
savedMindmaps = savedMindmaps.slice(-20);
}
localStorage.setItem('flowify-mindmaps', JSON.stringify(savedMindmaps));
}
// Get saved mindmaps from localStorage
function getSavedMindmaps() {
const savedData = localStorage.getItem('flowify-mindmaps');
return savedData ? JSON.parse(savedData) : [];
}
// Load a mindmap from saved data
function loadMindmap(mindmapData) {
if (mindmap && mindmap.mindmap && mindmapData.data) {
mindmap.mindmap.show(mindmapData.data);
mindmap.mindmapOverlay.classList.add('active');
}
}
class TopicMindmap {
constructor() {
this.mindmap = null;
this.container = 'mindmap';
this.zoomScale = 1;
this.isPanning = false;
this.panX = 0;
this.panY = 0;
this.isEditing = false;
this.segmentData = new Map();
// Get DOM elements
this.mainVideo = document.getElementById('videoPlayer');
this.overlayVideo = document.getElementById('overlayVideo');
this.videoSection = document.getElementById('videoSection');
this.videoInfo = document.getElementById('videoInfo');
this.toolbar = document.querySelector('.mindmap-toolbar');
this.mindmapOverlay = document.getElementById('mindmapOverlay');
this.videoCloseButton = document.getElementById('videoCloseButton');
// Context menu elements
this.contextMenu = document.getElementById('nodeContextMenu');
this.colorPicker = document.getElementById('colorPicker');
this.currentNodeId = null;
// Initially hide video section
if (this.videoSection) {
this.videoSection.style.display = 'none';
}
this.initialize();
this.setupToolbar();
this.setupNodeInteraction();
this.setupVideoCloseButton();
this.setupContextMenu();
}
initialize() {
const options = {
container: this.container,
theme: 'primary',
editable: true,
support_html: true,
view: {
draggable: true,
hmargin: 300,
vmargin: 200,
line_width: 2,
line_color: '#95a5a6',
node_spacing: 100
},
layout: {
hspace: 150,
vspace: 80,
pspace: 30
}
};
try {
// Add a style element for immediate expander fixes
this.addExpanderFixStyles();
this.mindmap = new jsMind(options);
const emptyMind = this.createEmptyMindMap();
this.mindmap.show(emptyMind);
const container = document.querySelector('.fullscreen-mindmap');
container.style.transformOrigin = '0 0';
container.style.position = 'absolute';
container.style.width = '100%';
container.style.height = '100%';
const mindmapWrapper = document.getElementById('mindmap');
mindmapWrapper.style.position = 'absolute';
mindmapWrapper.style.width = '100%';
mindmapWrapper.style.height = '100%';
mindmapWrapper.style.overflow = 'visible';
// Apply custom classes to expanders after rendering
this.applyCustomExpanderClasses();
this.setupNodeInteraction();
this.setupPanning();
} catch (error) {
console.error('Error initializing mindmap:', error);
}
}
addExpanderFixStyles() {
// Add a style element to ensure expanders don't show any text content
const style = document.createElement('style');
style.textContent = `
jmexpander * {
display: none !important;
}
jmexpander::before, jmexpander::after {
display: none !important;
}
`;
document.head.appendChild(style);
}
setupNodeInteraction() {
// Remove any existing event listeners
const container = document.getElementById(this.container);
if (!container) return;
container.addEventListener('click', (e) => {
const target = e.target;
if (target.tagName.toLowerCase() === 'jmnode') {
const nodeId = target.getAttribute('nodeid');
if (nodeId) {
this.handleNodeClick(nodeId);
}
}
});
}
handleNodeClick(nodeId) {
console.log('Node clicked:', nodeId);
const segmentInfo = this.segmentData.get(nodeId);
console.log('Segment info:', segmentInfo);
if (segmentInfo && segmentInfo.timestamp !== undefined) {
// Show video section
if (this.videoSection) {
this.videoSection.style.display = 'block';
console.log('Video section displayed');
}
// Adjust layout
if (this.toolbar) {
this.toolbar.classList.add('with-video');
}
// Setup video
if (this.mainVideo && this.mainVideo.src && this.overlayVideo) {
console.log('Setting up video playback at time:', segmentInfo.timestamp);
// Set video source if needed
if (this.overlayVideo.src !== this.mainVideo.src) {
this.overlayVideo.src = this.mainVideo.src;
}
// Get the timestamp as a number
let timeInSeconds = segmentInfo.timestamp;
// If it's still a string (with brackets), parse it
if (typeof timeInSeconds === 'string') {
if (timeInSeconds.startsWith('[') && timeInSeconds.endsWith(']')) {
timeInSeconds = parseFloat(timeInSeconds.slice(1, -1));
} else {
timeInSeconds = parseFloat(timeInSeconds);
}
}
// Ensure we have a valid number
if (!isNaN(timeInSeconds)) {
console.log('Seeking to time:', timeInSeconds);
this.overlayVideo.currentTime = timeInSeconds;
this.overlayVideo.play().catch(error => {
console.error('Error playing video:', error);
});
// Update info
if (this.videoInfo) {
const timestamp = this.formatTimestamp(timeInSeconds);
const content = segmentInfo.content.replace(/\[\d+(?:\.\d+)?\]\s*/, '');
this.videoInfo.textContent = `${timestamp}: ${content}`;
}
} else {
console.error('Invalid timestamp:', segmentInfo.timestamp);
}
}
// Update layout
this.updateLayout();
}
}
updateLayout() {
// Adjust mindmap section width when video is shown
const mindmapSection = document.querySelector('.mindmap-section');
const videoWidth = '40%';
const mindmapWidth = '60%';
if (this.videoSection && this.videoSection.style.display === 'block') {
this.videoSection.style.width = videoWidth;
if (mindmapSection) {
mindmapSection.style.width = mindmapWidth;
}
}
// Force mindmap refresh
if (this.mindmap) {
this.mindmap.resize();
}
}
setupNodeEditing() {
// Add double-click event listener for editing nodes
const container = document.getElementById(this.container);
container.addEventListener('dblclick', (e) => {
const target = e.target;
if (target.tagName.toLowerCase() === 'jmnode') {
this.editNode(target);
}
});
// Add node drag and drop functionality
container.addEventListener('mousedown', (e) => {
if (!this.isPanning && e.target.tagName.toLowerCase() === 'jmnode') {
this.startNodeDrag(e);
}
});
}
editNode(nodeElement) {
if (this.isPanning) return;
const nodeId = nodeElement.getAttribute('nodeid');
const currentText = this.mindmap.get_node(nodeId).topic;
// Create and style the input element
const input = document.createElement('input');
input.type = 'text';
input.value = currentText;
input.style.width = Math.max(nodeElement.offsetWidth, 100) + 'px';
input.style.height = nodeElement.offsetHeight + 'px';
input.style.position = 'absolute';
input.style.left = nodeElement.offsetLeft + 'px';
input.style.top = nodeElement.offsetTop + 'px';
input.style.zIndex = '1000';
input.style.padding = '4px';
input.style.border = '2px solid #3498db';
input.style.borderRadius = '4px';
input.style.backgroundColor = 'white';
// Replace node with input
nodeElement.style.visibility = 'hidden';
nodeElement.parentNode.appendChild(input);
input.focus();
input.select();
const finishEditing = () => {
const newText = input.value.trim();
if (newText) {
this.mindmap.update_node(nodeId, newText);
}
nodeElement.style.visibility = 'visible';
input.remove();
};
input.addEventListener('blur', finishEditing);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
finishEditing();
} else if (e.key === 'Escape') {
nodeElement.style.visibility = 'visible';
input.remove();
}
});
}
startNodeDrag(e) {
if (this.isPanning) return;
const nodeElement = e.target;
const nodeId = nodeElement.getAttribute('nodeid');
if (!nodeId) return;
const startX = e.clientX;
const startY = e.clientY;
const originalLeft = nodeElement.offsetLeft;
const originalTop = nodeElement.offsetTop;
const mouseMoveHandler = (e) => {
const dx = (e.clientX - startX) / this.zoomScale;
const dy = (e.clientY - startY) / this.zoomScale;
nodeElement.style.left = (originalLeft + dx) / this.zoomScale + 'px';
nodeElement.style.top = (originalTop + dy) / this.zoomScale + 'px';
// Update connected lines
this.mindmap.layout.layout();
};
const mouseUpHandler = () => {
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
// Ensure the mindmap updates its internal positions
this.mindmap.layout.layout();
};
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
}
setupPanning() {
const container = document.querySelector('.fullscreen-mindmap');
const mindmapWrapper = document.getElementById('mindmap');
const panButton = document.getElementById('panButton');
panButton.addEventListener('click', () => {
this.isPanning = !this.isPanning;
panButton.classList.toggle('active');
container.style.cursor = this.isPanning ? 'grab' : 'default';
});
container.addEventListener('wheel', (e) => {
if (e.ctrlKey) {
e.preventDefault();
const delta = e.deltaY * -0.01;
const newScale = Math.min(Math.max(this.zoomScale + delta, 0.1), 3);
// Get mouse position relative to container
const rect = container.getBoundingClientRect();
const x = (e.clientX - rect.left) / this.zoomScale;
const y = (e.clientY - rect.top) / this.zoomScale;
// Calculate new pan position to zoom towards mouse
const scale = newScale / this.zoomScale;
this.panX = x - (x - this.panX) * scale;
this.panY = y - (y - this.panY) * scale;
this.zoomScale = newScale;
this.applyTransform();
}
});
container.addEventListener('mousedown', (e) => {
if (!this.isPanning) return;
container.style.cursor = 'grabbing';
const startX = e.clientX - this.panX * this.zoomScale;
const startY = e.clientY - this.panY * this.zoomScale;
const mouseMoveHandler = (e) => {
if (!this.isPanning) return;
this.panX = (e.clientX - startX) / this.zoomScale;
this.panY = (e.clientY - startY) / this.zoomScale;
this.applyTransform();
};
const mouseUpHandler = () => {
container.style.cursor = this.isPanning ? 'grab' : 'default';
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
});
}
applyTransform() {
const container = document.querySelector('.fullscreen-mindmap');
const matrix = `matrix(${this.zoomScale}, 0, 0, ${this.zoomScale}, ${this.panX * this.zoomScale}, ${this.panY * this.zoomScale})`;
container.style.transform = matrix;
}
resetView() {
this.zoomScale = 1;
this.panX = 0;
this.panY = 0;
this.applyTransform();
const panButton = document.getElementById('panButton');
this.isPanning = false;
panButton.classList.remove('active');
document.querySelector('.fullscreen-mindmap').style.cursor = 'default';
if (this.mindmap) {
this.mindmap.resize();
// Center the mindmap after reset
const container = document.querySelector('.fullscreen-mindmap');
const mindmapEl = document.getElementById(this.container);
if (container && mindmapEl) {
const containerRect = container.getBoundingClientRect();
const mindmapRect = mindmapEl.getBoundingClientRect();
this.panX = (containerRect.width - mindmapRect.width) / 2;
this.panY = (containerRect.height - mindmapRect.height) / 2;
this.applyTransform();
}
}
}
setupToolbar() {
const backButton = document.getElementById('backButton');
const editButton = document.getElementById('editButton');
const zoomInButton = document.getElementById('zoomInButton');
const zoomOutButton = document.getElementById('zoomOutButton');
const resetButton = document.getElementById('resetButton');
const panButton = document.getElementById('panButton');
if (backButton) {
backButton.addEventListener('click', () => {
// Hide video section when closing
this.videoSection.classList.remove('active');
this.toolbar.classList.remove('with-video');
// Stop video playback
if (this.overlayVideo.src) {
this.overlayVideo.pause();
}
document.getElementById('mindmapOverlay').classList.remove('active');
});
}
if (editButton) {
editButton.addEventListener('click', () => {
this.isEditing = !this.isEditing;
editButton.classList.toggle('active');
if (this.isEditing) {
this.mindmap.enable_edit();
editButton.innerHTML = '<i class="fas fa-edit"></i> Done';
} else {
this.mindmap.disable_edit();
editButton.innerHTML = '<i class="fas fa-edit"></i> Edit';
}
});
}
if (zoomInButton) {
zoomInButton.addEventListener('click', () => this.zoomIn());
}
if (zoomOutButton) {
zoomOutButton.addEventListener('click', () => this.zoomOut());
}
if (resetButton) {
resetButton.addEventListener('click', () => this.resetView());
}
}
createEmptyMindMap() {
return {
meta: {
name: 'Topics',
author: 'Video Analyzer',
version: '1.0'
},
format: 'node_tree',
data: {
id: 'root',
topic: 'Video Topics',
direction: 'center',
children: []
}
};
}
zoomIn() {
this.zoomScale = Math.min(this.zoomScale + 0.1, 2);
this.applyZoom();
}
zoomOut() {
this.zoomScale = Math.max(this.zoomScale - 0.1, 0.5);
this.applyZoom();
}
resetView() {
this.zoomScale = 1;
this.applyZoom();
}
applyZoom() {
const container = document.querySelector('.fullscreen-mindmap');
if (container) {
container.style.transform = `scale(${this.zoomScale})`;
container.style.transformOrigin = 'center center';
}
}
createMindMapData(segments) {
if (!segments || segments.length === 0) {
return this.createEmptyMindMap();
}
const mind = {
meta: {
name: 'Topics',
author: 'Video Analyzer',
version: '1.0'
},
format: 'node_tree',
data: {
id: 'root',
topic: 'Video Topics',
direction: 'center',
expanded: true,
children: []
}
};
// Clear previous segment data
this.segmentData.clear();
// Process each segment
let nodeId = 1;
segments.forEach(segment => {
// Extract a cleaner topic name without repeating terms
const topicName = segment.topic_name.split(':')[1]?.trim() || segment.topic_name.trim();
// Create the main topic node
const topicNode = {
id: `topic_${nodeId}`,
topic: topicName,
direction: this.getDirection(nodeId, segments.length),
expanded: false,
children: []
};
// Store the timestamp for the main node
this.segmentData.set(`topic_${nodeId}`, {
timestamp: segment.timestamp || 0,
content: Array.isArray(segment.content) ? segment.content.join('\n') : segment.content
});
// If this is a parent topic with children, add them as subnodes
if (segment.is_parent && segment.children && segment.children.length > 0) {
segment.children.forEach((child, childIndex) => {
const childName = child.topic_name.split(':')[1]?.trim() || child.topic_name.trim();
const childContent = Array.isArray(child.content) ? child.content.join('\n') : child.content;
// Extract a meaningful snippet from the content
let contentSnippet = '';
const cleanContent = childContent.replace(/\[\d+(?:\.\d+)?\]\s*/g, '');
contentSnippet = cleanContent.split('.')[0];
if (contentSnippet.length > 50) {
contentSnippet = contentSnippet.slice(0, 50) + '...';
} else {
contentSnippet += '...';
}
const childNodeId = `content_${nodeId}_${childIndex}`;
topicNode.children.push({
id: childNodeId,
topic: childName,
direction: topicNode.direction,
expanded: false
});
// Store the timestamp and content for this child node
this.segmentData.set(childNodeId, {
timestamp: child.timestamp || 0,
content: childContent
});
});
}
// For regular segments or parents without children, add their content as subnodes
else {
// Get the content as an array
const contentArray = Array.isArray(segment.content) ? segment.content : [segment.content];
// If there's a lot of content, create content summary nodes
if (contentArray.length > 5) {
// Create just one summary node for all content
const contentNodeId = `content_${nodeId}_0`;
// Create a content summary
const cleanContent = contentArray.join(' ').replace(/\[\d+(?:\.\d+)?\]\s*/g, '');
let contentSnippet = cleanContent.split('.')[0];
if (contentSnippet.length > 50) {
contentSnippet = contentSnippet.slice(0, 50) + '...';
} else {
contentSnippet += '...';
}
topicNode.children.push({
id: contentNodeId,
topic: contentSnippet,
direction: topicNode.direction,
expanded: false
});
// Store the timestamp and full content
this.segmentData.set(contentNodeId, {
timestamp: segment.timestamp || 0,
content: contentArray.join('\n')
});
}
// For small segments, include separate nodes for each part
else {
contentArray.forEach((content, index) => {
if (typeof content === 'string' && content.trim()) {
const contentNodeId = `content_${nodeId}_${index}`;
// Clean up the content for display
const cleanContent = content.replace(/\[\d+(?:\.\d+)?\]\s*/g, '');
let contentSnippet = cleanContent.split('.')[0];
if (contentSnippet.length > 50) {
contentSnippet = contentSnippet.slice(0, 50) + '...';
} else {
contentSnippet += '...';
}
topicNode.children.push({
id: contentNodeId,
topic: contentSnippet,
direction: topicNode.direction,
expanded: false
});
// Store the timestamp and full content
this.segmentData.set(contentNodeId, {
timestamp: segment.timestamp || 0,
content: content
});
}
});
}
}
mind.data.children.push(topicNode);
nodeId++;
});
return mind;
}
extractTimestamp(text) {
// Look for timestamps in the format [X.X] or [X]
const matches = text.match(/\[(\d+(?:\.\d+)?)\]/g);
if (matches && matches.length > 0) {
// Extract the first timestamp found
const firstTimestamp = matches[0].replace('[', '').replace(']', '');
// Round down to the nearest second (floor)
const seconds = Math.floor(parseFloat(firstTimestamp));
console.log('Extracted timestamp:', seconds, 'from text:', text);
return seconds;
}
return 0;
}
getDirection(index, total) {
const halfTotal = Math.ceil(total / 2);
if (index <= halfTotal) {
return 'right';
} else {
return 'left';
}
}
update(segments) {
if (!this.mindmap) {
console.error('JSMind not initialized');
return;
}
try {
// Store any current node colors before updating
const nodeColors = this.saveNodeColors();
// Clear previous data
this.segmentData.clear();
// Create and show new mindmap
const mindData = this.createMindMapData(segments);
this.mindmap.show(mindData);
// Save the mindmap to gallery
this.saveMindmapToGallery(mindData);
// Restore node colors
this.restoreNodeColors(nodeColors);
// Apply custom classes to expanders
this.applyCustomExpanderClasses();
// Reset view and show overlay
this.resetView();
if (this.mindmapOverlay) {
this.mindmapOverlay.classList.add('active');
}
// Hide video section initially
if (this.videoSection) {
this.videoSection.style.display = 'none';
}
// Reset mindmap section to full width
const mindmapSection = document.querySelector('.mindmap-section');
if (mindmapSection) {
mindmapSection.style.width = '100%';
}
// Remove video-related classes
if (this.toolbar) {
this.toolbar.classList.remove('with-video');
}
// Reinitialize node interaction
this.setupNodeInteraction();
} catch (error) {
console.error('Error updating mindmap:', error);
}
}
saveMindmapToGallery(mindData) {
// Create mindmap data object
const mindmapData = {
id: Date.now(),
title: mindData.data.topic || 'Video Topics',
data: mindData,
date: new Date().toISOString()
};
// Save to localStorage
saveMindmap(mindmapData);
}
saveNodeColors() {
// Save current node colors
const nodeColors = {};
if (this.mindmap) {
const nodes = document.querySelectorAll('jmnode');
nodes.forEach(node => {
const nodeId = node.getAttribute('nodeid');
if (nodeId) {
const colorClasses = [
'node-blue', 'node-green', 'node-red', 'node-purple',
'node-orange', 'node-yellow', 'node-teal', 'node-gray'
];
colorClasses.forEach(cls => {
if (node.classList.contains(cls)) {
nodeColors[nodeId] = cls.replace('node-', '');
}
});
}
});
}
return nodeColors;
}
restoreNodeColors(nodeColors) {
// Restore saved node colors
if (nodeColors && Object.keys(nodeColors).length > 0) {
setTimeout(() => {
for (const [nodeId, color] of Object.entries(nodeColors)) {
this.changeNodeColor(nodeId, color);
}
}, 100);
}
}
setupVideoCloseButton() {
if (this.videoCloseButton) {
this.videoCloseButton.addEventListener('click', () => {
this.closeVideoOverlay();
});
}
}
closeVideoOverlay() {
// Hide video section
if (this.videoSection) {
this.videoSection.style.display = 'none';
// Stop video playback
if (this.overlayVideo && this.overlayVideo.src) {
this.overlayVideo.pause();
}
}
// Remove active class from toolbar
if (this.toolbar) {
this.toolbar.classList.remove('with-video');
}
// Reset mindmap section to full width
const mindmapSection = document.querySelector('.mindmap-section');
if (mindmapSection) {
mindmapSection.style.width = '100%';
}
// Force mindmap refresh
if (this.mindmap) {
this.mindmap.resize();
}
}
applyCustomExpanderClasses() {
// Add custom classes to expand/collapse buttons
setTimeout(() => {
this.processExpanders();
// Set up a mutation observer to handle future changes
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
this.processExpanders();
}
});
});
observer.observe(document.getElementById(this.container), {
childList: true,
subtree: true,
characterData: true
});
}, 100);
}
processExpanders() {
const expanders = document.querySelectorAll('jmexpander');
expanders.forEach(expander => {
// Check if it's an expand or collapse button
if (expander.textContent.includes('+')) {
expander.classList.add('expand');
// Clear content more aggressively
expander.textContent = '';
expander.innerText = '';
expander.innerHTML = '';
} else if (expander.textContent.includes('-')) {
expander.classList.add('collapse');
// Clear content more aggressively
expander.textContent = '';
expander.innerText = '';
expander.innerHTML = '';
}
});
}
setupContextMenu() {
// Hide context menu when clicking elsewhere
document.addEventListener('click', (e) => {
if (!this.contextMenu.contains(e.target)) {
this.hideContextMenu();
}
});
// Hide context menu on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.hideContextMenu();
}
});
// Hide color picker by default and setup action handlers
this.colorPicker.style.display = 'none';
this.setupContextMenuActions();
// Add event listeners for right-click context menu
const container = document.getElementById(this.container);
if (!container) return;
container.addEventListener('contextmenu', (e) => {
const target = e.target;
if (target.tagName.toLowerCase() === 'jmnode') {
e.preventDefault();
const nodeId = target.getAttribute('nodeid');
if (nodeId) {
this.currentNodeId = nodeId;
this.showContextMenu(e.clientX, e.clientY);
// Update expand/collapse text based on node state
const expandAction = document.getElementById('expandCollapseAction');
const node = this.mindmap.get_node(nodeId);
const isExpanded = this.mindmap.get_node_expanded(node);
if (expandAction) {
if (isExpanded) {
expandAction.innerHTML = '<i class="fas fa-angle-double-up"></i> Collapse all children';
} else {
expandAction.innerHTML = '<i class="fas fa-angle-double-down"></i> Expand all children';
}
}
// Disable parent addition for root node
const addParentAction = document.getElementById('addParentAction');
if (addParentAction) {
if (nodeId === 'root') {
addParentAction.classList.add('disabled');
addParentAction.style.opacity = '0.5';
addParentAction.style.cursor = 'not-allowed';
} else {
addParentAction.classList.remove('disabled');
addParentAction.style.opacity = '1';
addParentAction.style.cursor = 'pointer';
}
}
}
}
});
}
setupContextMenuActions() {
// Edit node action
document.getElementById('editNodeAction').addEventListener('click', () => {
this.editNodeText(this.currentNodeId);
this.hideContextMenu();
});
// Add child node action
document.getElementById('addChildAction').addEventListener('click', () => {
this.addChildNode(this.currentNodeId);
this.hideContextMenu();
});
// Add parent node action
document.getElementById('addParentAction').addEventListener('click', () => {
if (this.currentNodeId !== 'root') {
this.addParentNode(this.currentNodeId);
}
this.hideContextMenu();
});
// Focus on branch action
document.getElementById('focusNodeAction').addEventListener('click', () => {
this.focusOnNode(this.currentNodeId);
this.hideContextMenu();
});
// Expand/collapse all children action
document.getElementById('expandCollapseAction').addEventListener('click', () => {
this.toggleExpandCollapseAll(this.currentNodeId);
this.hideContextMenu();
});
// Open color picker
document.getElementById('nodeColorAction').addEventListener('click', (e) => {
e.stopPropagation();
this.toggleColorPicker();
});
// Delete node action
document.getElementById('deleteNodeAction').addEventListener('click', () => {
this.deleteNode(this.currentNodeId);
this.hideContextMenu();
});
// Color picker options
const colorOptions = document.querySelectorAll('.color-option');
colorOptions.forEach(option => {
option.addEventListener('click', (e) => {
const color = e.target.getAttribute('data-color');
this.changeNodeColor(this.currentNodeId, color);
this.hideContextMenu();
});
});
}
showContextMenu(x, y) {
// Reset color picker visibility
this.colorPicker.style.display = 'none';
// Position the context menu
this.contextMenu.style.left = `${x}px`;
this.contextMenu.style.top = `${y}px`;
// Show the context menu
this.contextMenu.classList.add('visible');
}
hideContextMenu() {
// Hide the context menu
this.contextMenu.classList.remove('visible');
this.colorPicker.style.display = 'none';
}
toggleColorPicker() {
// Toggle color picker visibility
this.colorPicker.style.display = this.colorPicker.style.display === 'none' ? 'block' : 'none';
}
editNodeText(nodeId) {
// Get the node element and its position
const node = this.mindmap.get_node(nodeId);
const nodeElement = document.querySelector(`jmnode[nodeid="${nodeId}"]`);
if (!node || !nodeElement) return;
// Get node position
const rect = nodeElement.getBoundingClientRect();
// Create input element
const input = document.createElement('input');
input.type = 'text';
input.className = 'node-edit-input';
input.value = node.topic;
// Position input over the node
input.style.left = `${rect.left}px`;
input.style.top = `${rect.top}px`;
input.style.width = `${Math.max(rect.width, 150)}px`;
// Add input to DOM
document.body.appendChild(input);
// Focus and select text
input.focus();
input.select();
// Handle completion of editing
const completeEdit = () => {
const newText = input.value.trim();
if (newText && newText !== node.topic) {
this.mindmap.update_node(nodeId, newText);
}
document.body.removeChild(input);
};
// Event listeners for input
input.addEventListener('blur', completeEdit);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
completeEdit();
} else if (e.key === 'Escape') {
document.body.removeChild(input);
}
});
}
addChildNode(parentId) {
// Generate a unique ID for the new node
const newNodeId = `node_${Date.now()}`;
// Add a new child node
this.mindmap.add_node(parentId, newNodeId, 'New Node');
// Ensure the parent is expanded
this.mindmap.expand_node(parentId);
// After a small delay, edit the new node
setTimeout(() => {
this.editNodeText(newNodeId);
}, 100);
}
addParentNode(childId) {
// This is a more complex operation as jsMind doesn't have direct support
const child = this.mindmap.get_node(childId);
if (!child || childId === 'root') return;
const originalParentId = child.parent.id;
// Generate a unique ID for the new parent node
const newParentId = `node_${Date.now()}`;
// Add the new parent to the original parent
this.mindmap.add_node(originalParentId, newParentId, 'New Parent');
// Move the child node to be under the new parent
this.mindmap.move_node(childId, newParentId);
// Ensure nodes are expanded
this.mindmap.expand_node(originalParentId);
this.mindmap.expand_node(newParentId);
// After a small delay, edit the new node
setTimeout(() => {
this.editNodeText(newParentId);
}, 100);
}
focusOnNode(nodeId) {
// First, ensure the node and all its ancestors are expanded
let node = this.mindmap.get_node(nodeId);
while (node && node.id !== 'root') {
this.mindmap.expand_node(node.parent.id);
node = node.parent;
}
// Get the node element
const nodeElement = document.querySelector(`jmnode[nodeid="${nodeId}"]`);
if (!nodeElement) return;
// Get node position and center the view on it
const rect = nodeElement.getBoundingClientRect();
const containerRect = document.querySelector('.fullscreen-mindmap').getBoundingClientRect();
// Calculate the center position
const centerX = containerRect.width / 2;
const centerY = containerRect.height / 2;
// Calculate the target position to center the node
this.panX = centerX - rect.left - rect.width / 2;
this.panY = centerY - rect.top - rect.height / 2;
// Apply the transformation
this.applyTransform();
}
toggleExpandCollapseAll(nodeId) {
const node = this.mindmap.get_node(nodeId);
if (!node) return;
const isExpanded = this.mindmap.get_node_expanded(node);
if (isExpanded) {
// Collapse all descendants
this.collapseAllChildren(nodeId);
} else {
// Expand all descendants
this.expandAllChildren(nodeId);
}
}
expandAllChildren(nodeId) {
// Recursively expand all nodes
const expandRecursively = (id) => {
const node = this.mindmap.get_node(id);
if (!node) return;
this.mindmap.expand_node(id);
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
expandRecursively(child.id);
});
}
};
expandRecursively(nodeId);
}
collapseAllChildren(nodeId) {
// Recursively collapse all nodes
const collapseRecursively = (id) => {
const node = this.mindmap.get_node(id);
if (!node) return;
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
collapseRecursively(child.id);
});
this.mindmap.collapse_node(id);
}
};
// We don't want to collapse the starting node itself
const node = this.mindmap.get_node(nodeId);
if (node && node.children && node.children.length > 0) {
node.children.forEach(child => {
collapseRecursively(child.id);
});
}
}
changeNodeColor(nodeId, color) {
if (!nodeId || !color) return;
// Get the node element
const nodeElement = document.querySelector(`jmnode[nodeid="${nodeId}"]`);
if (!nodeElement) return;
// Remove existing color classes
const colorClasses = [
'node-blue', 'node-green', 'node-red', 'node-purple',
'node-orange', 'node-yellow', 'node-teal', 'node-gray'
];
colorClasses.forEach(cls => {
nodeElement.classList.remove(cls);
});
// Add the new color class
nodeElement.classList.add(`node-${color}`);
// Store the color in the node data (if needed for persistence)
const node = this.mindmap.get_node(nodeId);
if (node) {
node.data = node.data || {};
node.data.color = color;
}
}
deleteNode(nodeId) {
// Can't delete the root node
if (nodeId === 'root') return;
// Confirm deletion
if (confirm('Are you sure you want to delete this node and all its children?')) {
this.mindmap.remove_node(nodeId);
}
}
}
// Custom Video Player
function initCustomVideoPlayer() {
const videoPlayer = document.getElementById('videoPlayer');
const videoControls = document.querySelector('.video-controls');
const playPauseBtn = document.getElementById('playPauseBtn');
const progressBar = document.querySelector('.video-progress-bar');
const progress = document.querySelector('.video-progress');
const currentTimeDisplay = document.getElementById('currentTime');
const durationDisplay = document.getElementById('duration');
const muteBtn = document.getElementById('muteBtn');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const videoContainer = document.querySelector('.video-preview');
if (!videoPlayer || !videoControls) return;
// Hide default controls (but keep them accessible for mobile/compatibility)
videoPlayer.controls = false;
// Play/Pause
playPauseBtn.addEventListener('click', () => {
if (videoPlayer.paused) {
videoPlayer.play();
videoContainer.classList.add('playing');
} else {
videoPlayer.pause();
videoContainer.classList.remove('playing');
}
});
// Update progress bar
videoPlayer.addEventListener('timeupdate', () => {
const percent = (videoPlayer.currentTime / videoPlayer.duration) * 100;
progressBar.style.width = `${percent}%`;
// Update time display
currentTimeDisplay.textContent = formatTime(videoPlayer.currentTime);
});
// Click on progress bar to seek
progress.addEventListener('click', (e) => {
const rect = progress.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
videoPlayer.currentTime = pos * videoPlayer.duration;
});
// Load metadata (duration, etc.)
videoPlayer.addEventListener('loadedmetadata', () => {
durationDisplay.textContent = formatTime(videoPlayer.duration);
});
// Mute/Unmute
muteBtn.addEventListener('click', () => {
videoPlayer.muted = !videoPlayer.muted;
muteBtn.innerHTML = videoPlayer.muted ?
'<i class="fas fa-volume-mute"></i>' :
'<i class="fas fa-volume-up"></i>';
});
// Fullscreen
fullscreenBtn.addEventListener('click', () => {
if (videoContainer.requestFullscreen) {
videoContainer.requestFullscreen();
} else if (videoContainer.webkitRequestFullscreen) {
videoContainer.webkitRequestFullscreen();
} else if (videoContainer.msRequestFullscreen) {
videoContainer.msRequestFullscreen();
}
});
// Update UI on play/pause
videoPlayer.addEventListener('play', () => {
videoContainer.classList.add('playing');
});
videoPlayer.addEventListener('pause', () => {
videoContainer.classList.remove('playing');
});
// Format time to MM:SS
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
seconds = Math.floor(seconds % 60);
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
}
}
// Upload Particles Animation
function initUploadParticles() {
const uploadContainer = document.querySelector('.upload-container');
const particles = document.querySelector('.particles');
const fileInput = document.getElementById('fileInput');
if (!uploadContainer || !particles || !fileInput) {
console.warn('Upload elements not found, skipping upload particles initialization');
return;
}
// Create particles
for (let i = 0; i < 15; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
// Random position
const posX = Math.random() * 100;
particle.style.left = `${posX}%`;
particle.style.bottom = '0';
// Random size
const size = Math.random() * 4 + 2;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
// Random animation delay and duration
const delay = Math.random() * 2;
const duration = Math.random() * 2 + 1;
particle.style.animation = `particleAnimation ${duration}s ${delay}s infinite ease-out`;
// Add to container
particles.appendChild(particle);
}
// Add click event to trigger file input - PROPERLY FIXED VERSION
uploadContainer.addEventListener('click', (e) => {
// Don't prevent default on the click event here
console.log("Upload container clicked");
// Directly trigger the file input click
fileInput.click();
});
// Also add click handlers to the specific elements inside the container
const uploadIcon = uploadContainer.querySelector('.upload-icon-container');
const uploadText = uploadContainer.querySelector('.upload-text');
const uploadSubtext = uploadContainer.querySelector('.upload-subtext');
if (uploadIcon) {
uploadIcon.addEventListener('click', (e) => {
console.log("Upload icon clicked");
fileInput.click();
});
}
if (uploadText) {
uploadText.addEventListener('click', (e) => {
console.log("Upload text clicked");
fileInput.click();
});
}
if (uploadSubtext) {
uploadSubtext.addEventListener('click', (e) => {
console.log("Upload subtext clicked");
fileInput.click();
});
}
// Drag and drop functionality - keep these preventDefault calls
uploadContainer.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
uploadContainer.classList.add('active');
});
uploadContainer.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
uploadContainer.classList.remove('active');
});
uploadContainer.addEventListener('drop', (e) => {
console.log("File dropped");
e.preventDefault();
e.stopPropagation();
uploadContainer.classList.remove('active');
if (e.dataTransfer.files.length) {
console.log("Processing dropped file");
fileInput.files = e.dataTransfer.files;
// Create and dispatch a proper change event
const event = new Event('change', { bubbles: true });
fileInput.dispatchEvent(event);
}
});
}
// Progress Steps Visualization
function initProgressSteps() {
const uploadStep = document.getElementById('uploadStep');
const transcribeStep = document.getElementById('transcribeStep');
const analyzeStep = document.getElementById('analyzeStep');
const visualizeStep = document.getElementById('visualizeStep');
const progressBarFill = document.querySelector('.progress-bar-fill');
if (!uploadStep || !progressBarFill) return;
// Set initial state
setActiveStep('upload');
// Update steps based on current operation
fileInput.addEventListener('change', () => {
setActiveStep('upload');
});
transcribeButton.addEventListener('click', () => {
setTimeout(() => setActiveStep('transcribe'), 100);
});
analyzeButton.addEventListener('click', () => {
setTimeout(() => setActiveStep('analyze'), 100);
});
// Redefine the global setActiveStep function with the proper implementation
window.setActiveStep = function(step) {
// Skip if elements aren't available yet
if (!uploadStep || !progressBarFill) return;
// Reset all steps
uploadStep.className = 'progress-step';
transcribeStep.className = 'progress-step';
analyzeStep.className = 'progress-step';
visualizeStep.className = 'progress-step';
// Set progress level
let progressWidth = '0%';
switch(step) {
case 'upload':
uploadStep.className = 'progress-step active';
progressWidth = '25%';
break;
case 'transcribe':
uploadStep.className = 'progress-step completed';
transcribeStep.className = 'progress-step active';
progressWidth = '50%';
break;
case 'analyze':
uploadStep.className = 'progress-step completed';
transcribeStep.className = 'progress-step completed';
analyzeStep.className = 'progress-step active';
progressWidth = '75%';
break;
case 'visualize':
uploadStep.className = 'progress-step completed';
transcribeStep.className = 'progress-step completed';
analyzeStep.className = 'progress-step completed';
visualizeStep.className = 'progress-step active';
progressWidth = '100%';
break;
}
// Animate progress bar
progressBarFill.style.width = progressWidth;
};
}