Yuvvan Talreja commited on
Commit
12089c6
·
1 Parent(s): 39f82ec

Initial Deployment

Browse files
Files changed (8) hide show
  1. Dockerfile +23 -0
  2. app.py +7 -0
  3. app_fastapi.py +1 -0
  4. requirements.txt +17 -0
  5. static/script.js +2173 -0
  6. static/styles.css +1889 -0
  7. templates/index.html +315 -0
  8. topic_segmenter.py +519 -0
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies including FFmpeg
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ ffmpeg \
8
+ build-essential \
9
+ && apt-get clean \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements first for better caching
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copy the rest of the application
17
+ COPY . .
18
+
19
+ # Expose the port
20
+ EXPOSE 5000
21
+
22
+ # Run using Gunicorn
23
+ CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
app.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from app_fastapi import app
2
+
3
+ # This file is used for Hugging Face Spaces deployment
4
+ # Spaces will automatically detect this app.py file and serve the FastAPI app
5
+
6
+ # For local development, you can run:
7
+ # uvicorn app_fastapi:app --host 0.0.0.0 --port 8000
app_fastapi.py ADDED
@@ -0,0 +1 @@
 
 
1
+
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask>=2.0.0
2
+ gunicorn>=20.0.0
3
+ numpy>=1.19.0
4
+ scikit-learn>=1.0.0
5
+ nltk>=3.6.0
6
+ torch>=1.9.0
7
+ transformers>=4.0.0
8
+ keybert>=0.5.0
9
+ spacy>=3.0.0
10
+ librosa>=0.8.0
11
+ soundfile>=0.10.0
12
+ thinc>=8.0.0
13
+ fastapi>=0.68.0
14
+ uvicorn>=0.15.0
15
+ jinja2>=3.0.0
16
+ python-multipart>=0.0.5
17
+ pydantic>=1.8.2
static/script.js ADDED
@@ -0,0 +1,2173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { pipeline, env } from 'https://unpkg.com/@xenova/transformers@2.16.0/dist/transformers.min.js';
2
+
3
+
4
+ env.allowLocalModels = false;
5
+ env.useBrowserCache = true;
6
+
7
+
8
+ const videoPlayer = document.getElementById('videoPlayer');
9
+ const fileInput = document.getElementById('fileInput');
10
+ const transcribeButton = document.getElementById('transcribeButton');
11
+ const analyzeButton = document.getElementById('analyzeButton');
12
+ const transcriptionDiv = document.getElementById('transcription');
13
+ const topicResultsDiv = document.getElementById('topicResults');
14
+ const statusDiv = document.getElementById('status');
15
+ const modelSelector = document.getElementById('modelSelector');
16
+ const modelStatus = document.getElementById('modelStatus');
17
+
18
+ let whisperPipeline = null;
19
+ let transcriptText = '';
20
+ let mindmap;
21
+
22
+ // Original transcribeAudio function definition
23
+ async function transcribeAudio(audioData) {
24
+ try {
25
+ statusDiv.textContent = 'Transcribing audio...';
26
+ transcriptionDiv.textContent = '';
27
+
28
+ // Make sure we're using the correct progress element
29
+ const progressBarFill = document.querySelector('.progress-bar-fill');
30
+ if (progressBarFill) {
31
+ progressBarFill.style.width = '0%';
32
+ }
33
+
34
+ // Check if server-side transcription is possible
35
+ const selectedModel = modelSelector.value;
36
+ if (selectedModel !== 'browser') {
37
+ // Convert audio data to base64
38
+ const base64Audio = arrayBufferToBase64(audioData.buffer);
39
+
40
+ // Send to server as JSON
41
+ const response = await fetch('/transcribe', {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/json'
45
+ },
46
+ body: JSON.stringify({
47
+ audio: base64Audio,
48
+ model: selectedModel
49
+ })
50
+ });
51
+
52
+ if (!response.ok) {
53
+ throw new Error('Server transcription failed');
54
+ }
55
+
56
+ const result = await response.json();
57
+ transcriptText = result.transcript || result.text || '';
58
+
59
+ // Process the transcript to create segments for display
60
+ if (transcriptText) {
61
+ const segments = [];
62
+ // Extract timestamps and text using regex
63
+ const timestampPattern = /\[(\d+)\](.*?)(?=\[\d+\]|$)/g;
64
+ let match;
65
+
66
+ while ((match = timestampPattern.exec(transcriptText)) !== null) {
67
+ segments.push({
68
+ start: parseInt(match[1]),
69
+ text: match[2].trim()
70
+ });
71
+ }
72
+
73
+ // If no timestamps were found, fallback to displaying the whole text
74
+ if (segments.length === 0 && transcriptText.trim()) {
75
+ segments.push({
76
+ start: 0,
77
+ text: transcriptText.trim()
78
+ });
79
+ }
80
+
81
+ // Display with timestamps
82
+ console.log("Displaying transcription with segments:", segments);
83
+ displayTranscription(segments);
84
+ }
85
+
86
+ statusDiv.textContent = 'Transcription complete!';
87
+ analyzeButton.disabled = false;
88
+ if (progressBarFill) {
89
+ progressBarFill.style.width = '100%';
90
+ }
91
+ return;
92
+ }
93
+
94
+ // Browser-side transcription using Whisper
95
+ if (!whisperPipeline) {
96
+ statusDiv.textContent = 'Loading Whisper model...';
97
+
98
+ // Initialize the pipeline
99
+ whisperPipeline = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en');
100
+ statusDiv.textContent = 'Model loaded. Transcribing...';
101
+ }
102
+
103
+ // Processing with progress updates
104
+ const transcriptionResult = await whisperPipeline(audioData, {
105
+ chunk_length_s: 30,
106
+ stride_length_s: 5,
107
+ language: 'english',
108
+ task: 'transcribe',
109
+ return_timestamps: true,
110
+ callback_function: function(progressData) {
111
+ const percentage = Math.round(progressData.progress * 100);
112
+ statusDiv.textContent = `Transcribing: ${percentage}%`;
113
+
114
+ const progressBarFill = document.querySelector('.progress-bar-fill');
115
+ if (progressBarFill) {
116
+ progressBarFill.style.width = `${percentage}%`;
117
+ }
118
+ }
119
+ });
120
+
121
+ // Save and display transcription
122
+ transcriptText = transcriptionResult.text;
123
+ displayTranscription(transcriptionResult.chunks || []);
124
+
125
+ statusDiv.textContent = 'Transcription complete!';
126
+ analyzeButton.disabled = false;
127
+ if (progressBarFill) {
128
+ progressBarFill.style.width = '100%';
129
+ }
130
+
131
+ } catch (error) {
132
+ console.error('Transcription error:', error);
133
+ statusDiv.textContent = `Error: ${error.message}`;
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ // Helper function to display transcription with timestamps
139
+ function displayTranscription(segments) {
140
+ console.log("displayTranscription called with segments:", segments);
141
+
142
+ // Clear the transcription div of all content, including the empty state
143
+ if (transcriptionDiv) {
144
+ transcriptionDiv.innerHTML = '';
145
+ } else {
146
+ console.error("transcriptionDiv element not found");
147
+ return;
148
+ }
149
+
150
+ if (!segments || segments.length === 0) {
151
+ const emptyState = document.createElement('div');
152
+ emptyState.className = 'empty-state';
153
+ emptyState.innerHTML = `
154
+ <i class="fas fa-file-alt"></i>
155
+ <h3 class="empty-state-title">No transcription yet</h3>
156
+ <p class="empty-state-description">Upload a video and click "Transcribe Video" to see the transcription here</p>
157
+ `;
158
+ transcriptionDiv.appendChild(emptyState);
159
+ return;
160
+ }
161
+
162
+ // Create a container for the transcription content
163
+ const transcriptionContent = document.createElement('div');
164
+ transcriptionContent.className = 'transcription-content';
165
+
166
+ segments.forEach(segment => {
167
+ const paragraphElement = document.createElement('p');
168
+
169
+ // Format and add timestamp prefix
170
+ let timestamp = '?';
171
+ let text = '';
172
+
173
+ if (Array.isArray(segment) && segment.length >= 2) {
174
+ // For array format [start, end, text]
175
+ timestamp = Math.floor(segment[0]);
176
+ text = segment[2] || '';
177
+ } else if (typeof segment === 'object' && segment.start !== undefined) {
178
+ // For object format {start, end, text}
179
+ timestamp = Math.floor(segment.start);
180
+ text = segment.text || '';
181
+ } else {
182
+ // Fallback
183
+ text = segment.toString();
184
+ }
185
+
186
+ paragraphElement.textContent = `[${timestamp}] ${text}`;
187
+
188
+ // Add click handler to jump to timestamp
189
+ paragraphElement.addEventListener('click', () => {
190
+ videoPlayer.currentTime = timestamp;
191
+ videoPlayer.play().catch(e => console.error('Could not play video:', e));
192
+ });
193
+
194
+ paragraphElement.style.cursor = 'pointer';
195
+ transcriptionContent.appendChild(paragraphElement);
196
+ });
197
+
198
+ transcriptionDiv.appendChild(transcriptionContent);
199
+ }
200
+
201
+ // Store original function references
202
+ const originalTranscribeAudio = transcribeAudio;
203
+ const originalAnalyzeTopics = analyzeTopics;
204
+
205
+ // Override with enhanced versions
206
+ transcribeAudio = async function(audioData) {
207
+ // Set progress step - use the global function safely
208
+ try {
209
+ if (typeof window.setActiveStep === 'function') {
210
+ window.setActiveStep('transcribe');
211
+ }
212
+ } catch (err) {
213
+ console.warn('Could not update progress steps:', err);
214
+ }
215
+
216
+ try {
217
+ await originalTranscribeAudio(audioData);
218
+ // If successful, make analyze button fancy with pulse animation
219
+ analyzeButton.classList.add('animate-pulse');
220
+ setTimeout(() => analyzeButton.classList.remove('animate-pulse'), 3000);
221
+ } catch (error) {
222
+ console.error('Transcription error:', error);
223
+ throw error;
224
+ }
225
+ };
226
+
227
+ analyzeTopics = async function() {
228
+ // Set progress step - use the global function safely
229
+ try {
230
+ if (typeof window.setActiveStep === 'function') {
231
+ window.setActiveStep('analyze');
232
+ }
233
+ } catch (err) {
234
+ console.warn('Could not update progress steps:', err);
235
+ }
236
+
237
+ // Remove pulse animation if still there
238
+ analyzeButton.classList.remove('animate-pulse');
239
+
240
+ try {
241
+ await originalAnalyzeTopics();
242
+ // If successful, we move to visualization
243
+ try {
244
+ if (typeof window.setActiveStep === 'function') {
245
+ window.setActiveStep('visualize');
246
+ }
247
+ } catch (err) {
248
+ console.warn('Could not update progress steps:', err);
249
+ }
250
+ } catch (error) {
251
+ console.error('Analysis error:', error);
252
+ throw error;
253
+ }
254
+ };
255
+
256
+ fileInput.addEventListener('change', (e) => {
257
+ const file = e.target.files[0];
258
+ if (file) {
259
+ const videoURL = URL.createObjectURL(file);
260
+ videoPlayer.src = videoURL;
261
+ transcribeButton.disabled = false;
262
+ analyzeButton.disabled = true;
263
+ transcriptionDiv.textContent = '';
264
+ topicResultsDiv.textContent = '';
265
+ transcriptText = '';
266
+ statusDiv.textContent = 'Video loaded. Ready to transcribe.';
267
+
268
+ const progressBarFill = document.querySelector('.progress-bar-fill');
269
+ if (progressBarFill) {
270
+ progressBarFill.style.width = '0%';
271
+ }
272
+ }
273
+ });
274
+
275
+ // Audio Processing Functions
276
+ function convertAudioBuffer(audioBuffer) {
277
+ const audioData = audioBuffer.getChannelData(0);
278
+ const targetSampleRate = 16000;
279
+ const resamplingRatio = targetSampleRate / audioBuffer.sampleRate;
280
+ const resampledLength = Math.floor(audioData.length * resamplingRatio);
281
+ const resampledData = new Float32Array(resampledLength);
282
+
283
+ for (let i = 0; i < resampledLength; i++) {
284
+ const originalIndex = Math.floor(i / resamplingRatio);
285
+ resampledData[i] = audioData[originalIndex];
286
+ }
287
+
288
+ return resampledData;
289
+ }
290
+
291
+ async function extractAudio(videoFile) {
292
+ try {
293
+ console.log('Starting audio extraction');
294
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
295
+ const arrayBuffer = await videoFile.arrayBuffer();
296
+ console.log('Video file loaded into buffer');
297
+
298
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
299
+ console.log('Audio decoded successfully');
300
+
301
+ return convertAudioBuffer(audioBuffer);
302
+ } catch (error) {
303
+ console.error('Error extracting audio:', error);
304
+ throw new Error(`Failed to extract audio: ${error.message}`);
305
+ }
306
+ }
307
+
308
+ // Define setActiveStep globally before any other functions use it
309
+ window.setActiveStep = function(step) {
310
+ // Will be initialized properly later, but create empty function to prevent errors
311
+ console.log(`Progress step: ${step}`);
312
+ };
313
+
314
+ // Helper function to convert ArrayBuffer to base64
315
+ function arrayBufferToBase64(buffer) {
316
+ let binary = '';
317
+ const bytes = new Uint8Array(buffer);
318
+ const len = bytes.byteLength;
319
+ for (let i = 0; i < len; i++) {
320
+ binary += String.fromCharCode(bytes[i]);
321
+ }
322
+ return window.btoa(binary);
323
+ }
324
+
325
+ function formatTimestamp(seconds) {
326
+ // Round down to nearest second and return as integer
327
+ return `[${Math.floor(seconds)}]`;
328
+ }
329
+
330
+ async function analyzeTopics() {
331
+ try {
332
+ statusDiv.textContent = 'Analyzing topics...';
333
+
334
+ // Make sure we're using the correct progress element
335
+ const progressBarFill = document.querySelector('.progress-bar-fill');
336
+ if (progressBarFill) {
337
+ progressBarFill.style.width = '0%';
338
+ }
339
+
340
+ const response = await fetch('/analyze', {
341
+ method: 'POST',
342
+ headers: {
343
+ 'Content-Type': 'application/json'
344
+ },
345
+ body: JSON.stringify({ transcript: transcriptText })
346
+ });
347
+
348
+ if (!response.ok) {
349
+ throw new Error('Failed to analyze topics');
350
+ }
351
+
352
+ const data = await response.json();
353
+ displayTopicResults(data.segments);
354
+ statusDiv.textContent = 'Topic analysis complete!';
355
+
356
+ if (progressBarFill) {
357
+ progressBarFill.style.width = '100%';
358
+ }
359
+
360
+ } catch (error) {
361
+ console.error('Topic analysis error:', error);
362
+ statusDiv.textContent = `Error: ${error.message}`;
363
+ }
364
+ }
365
+
366
+ function displayTopicResults(segments) {
367
+ // Clear previous results
368
+ topicResultsDiv.innerHTML = '';
369
+
370
+ // Display topics in the sidebar
371
+ segments.forEach(segment => {
372
+ const topicElement = document.createElement('div');
373
+ topicElement.className = 'topic';
374
+
375
+ const headerElement = document.createElement('div');
376
+ headerElement.className = 'topic-header';
377
+ headerElement.textContent = segment.topic_name;
378
+
379
+ const contentElement = document.createElement('div');
380
+ contentElement.className = 'topic-content';
381
+
382
+ // Handle content that could be either an array or a string
383
+ if (Array.isArray(segment.content)) {
384
+ contentElement.textContent = segment.content.join('\n');
385
+ } else {
386
+ contentElement.textContent = segment.content;
387
+ }
388
+
389
+ topicElement.appendChild(headerElement);
390
+ topicElement.appendChild(contentElement);
391
+
392
+ // If this is a parent topic with children, display them as nested elements
393
+ if (segment.is_parent && segment.children && segment.children.length > 0) {
394
+ topicElement.classList.add('parent-topic');
395
+
396
+ const childrenContainer = document.createElement('div');
397
+ childrenContainer.className = 'subtopics';
398
+
399
+ segment.children.forEach(child => {
400
+ const childElement = document.createElement('div');
401
+ childElement.className = 'subtopic';
402
+
403
+ const childHeader = document.createElement('div');
404
+ childHeader.className = 'subtopic-header';
405
+ childHeader.textContent = child.topic_name;
406
+
407
+ const childContent = document.createElement('div');
408
+ childContent.className = 'subtopic-content';
409
+
410
+ // Handle content that could be either an array or a string
411
+ if (Array.isArray(child.content)) {
412
+ childContent.textContent = child.content.join('\n');
413
+ } else {
414
+ childContent.textContent = child.content;
415
+ }
416
+
417
+ childElement.appendChild(childHeader);
418
+ childElement.appendChild(childContent);
419
+ childrenContainer.appendChild(childElement);
420
+ });
421
+
422
+ topicElement.appendChild(childrenContainer);
423
+ }
424
+
425
+ topicResultsDiv.appendChild(topicElement);
426
+ });
427
+
428
+ // Update mindmap and show fullscreen overlay
429
+ mindmap.update(segments);
430
+ }
431
+
432
+ // Event Listeners
433
+ transcribeButton.addEventListener('click', async () => {
434
+ try {
435
+ transcribeButton.disabled = true;
436
+ modelSelector.disabled = true;
437
+ const videoFile = fileInput.files[0];
438
+
439
+ if (!videoFile) {
440
+ throw new Error('No video file selected');
441
+ }
442
+
443
+ const videoPreview = document.querySelector('.video-preview');
444
+ const videoHeight = videoPlayer.offsetHeight;
445
+ if (videoHeight > 0) {
446
+ videoPreview.style.height = `${videoHeight}px`;
447
+ }
448
+
449
+ statusDiv.textContent = 'Extracting audio...';
450
+ const audioData = await extractAudio(videoFile);
451
+ await transcribeAudio(audioData);
452
+ modelSelector.disabled = false;
453
+
454
+ } catch (error) {
455
+ console.error('Processing error:', error);
456
+ statusDiv.textContent = `Error: ${error.message}`;
457
+ transcribeButton.disabled = false;
458
+ modelSelector.disabled = false;
459
+ }
460
+ });
461
+
462
+ analyzeButton.addEventListener('click', analyzeTopics);
463
+
464
+ modelSelector.addEventListener('change', async (e) => {
465
+ const selectedModel = e.target.value;
466
+ transcribeButton.disabled = false;
467
+ analyzeButton.disabled = true;
468
+ await initWhisper(selectedModel);
469
+ if (fileInput.files.length > 0) {
470
+ transcribeButton.disabled = false;
471
+ }
472
+ });
473
+
474
+ // Initialize with the selected model when the page loads
475
+ window.addEventListener('DOMContentLoaded', () => {
476
+ // Initialize all UI components
477
+ initThemeToggle();
478
+ initFAB();
479
+ initGallery();
480
+ initCustomVideoPlayer();
481
+ initUploadParticles();
482
+ initProgressSteps();
483
+
484
+ // Original initialization
485
+ mindmap = new TopicMindmap();
486
+ initWhisper(modelSelector.value).catch(error => {
487
+ console.error('Failed to initialize Whisper:', error);
488
+ statusDiv.textContent = 'Failed to initialize the transcription model';
489
+ });
490
+ });
491
+
492
+ // Theme toggle functionality
493
+ function initThemeToggle() {
494
+ const themeToggle = document.getElementById('themeToggle');
495
+ const htmlElement = document.documentElement;
496
+
497
+ // Check for saved theme preference
498
+ const savedTheme = localStorage.getItem('theme');
499
+ if (savedTheme) {
500
+ htmlElement.setAttribute('data-theme', savedTheme);
501
+ }
502
+
503
+ themeToggle.addEventListener('click', () => {
504
+ const currentTheme = htmlElement.getAttribute('data-theme');
505
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
506
+
507
+ htmlElement.setAttribute('data-theme', newTheme);
508
+ localStorage.setItem('theme', newTheme);
509
+ });
510
+ }
511
+
512
+ // FAB functionality
513
+ function initFAB() {
514
+ const fabContainer = document.getElementById('fabContainer');
515
+ const mainFab = document.getElementById('mainFab');
516
+ const exportPdfBtn = document.getElementById('exportPdfBtn');
517
+ const exportPngBtn = document.getElementById('exportPngBtn');
518
+ const exportSvgBtn = document.getElementById('exportSvgBtn');
519
+ const shareMindmapBtn = document.getElementById('shareMindmapBtn');
520
+
521
+ // Toggle FAB options
522
+ mainFab.addEventListener('click', () => {
523
+ fabContainer.classList.toggle('active');
524
+ });
525
+
526
+ // Close FAB when clicking outside
527
+ document.addEventListener('click', (e) => {
528
+ if (!fabContainer.contains(e.target) && fabContainer.classList.contains('active')) {
529
+ fabContainer.classList.remove('active');
530
+ }
531
+ });
532
+
533
+ // Export as PDF
534
+ exportPdfBtn.addEventListener('click', () => {
535
+ exportMindmap('pdf');
536
+ });
537
+
538
+ // Export as PNG
539
+ exportPngBtn.addEventListener('click', () => {
540
+ exportMindmap('png');
541
+ });
542
+
543
+ // Export as SVG
544
+ exportSvgBtn.addEventListener('click', () => {
545
+ exportMindmap('svg');
546
+ });
547
+
548
+ // Share mindmap
549
+ shareMindmapBtn.addEventListener('click', () => {
550
+ shareMindmap();
551
+ });
552
+ }
553
+
554
+ // Gallery functionality
555
+ function initGallery() {
556
+ const galleryContainer = document.getElementById('resultsGallery');
557
+ const galleryClose = document.getElementById('galleryClose');
558
+ const galleryContent = document.getElementById('galleryContent');
559
+
560
+ // Add "Recent Mindmaps" option to FAB
561
+ const fabContainer = document.getElementById('fabContainer');
562
+ const recentMindmapsBtn = document.createElement('button');
563
+ recentMindmapsBtn.className = 'fab-option';
564
+ recentMindmapsBtn.setAttribute('title', 'Recent Mindmaps');
565
+ recentMindmapsBtn.innerHTML = '<i class="fas fa-history"></i>';
566
+ fabContainer.querySelector('.fab-options').appendChild(recentMindmapsBtn);
567
+
568
+ // Show gallery
569
+ recentMindmapsBtn.addEventListener('click', () => {
570
+ showGallery();
571
+ fabContainer.classList.remove('active');
572
+ });
573
+
574
+ // Close gallery
575
+ galleryClose.addEventListener('click', () => {
576
+ galleryContainer.classList.remove('active');
577
+ });
578
+
579
+ // Close gallery with escape key
580
+ document.addEventListener('keydown', (e) => {
581
+ if (e.key === 'Escape' && galleryContainer.classList.contains('active')) {
582
+ galleryContainer.classList.remove('active');
583
+ }
584
+ });
585
+ }
586
+
587
+ // Show gallery and populate with saved mindmaps
588
+ function showGallery() {
589
+ const galleryContainer = document.getElementById('resultsGallery');
590
+ const galleryContent = document.getElementById('galleryContent');
591
+
592
+ // Clear existing content
593
+ galleryContent.innerHTML = '';
594
+
595
+ // Get saved mindmaps from localStorage
596
+ const savedMindmaps = getSavedMindmaps();
597
+
598
+ if (savedMindmaps.length === 0) {
599
+ // Show empty state
600
+ galleryContent.innerHTML = `
601
+ <div class="gallery-empty">
602
+ <i class="fas fa-file-alt" style="font-size: 3rem; color: var(--text-secondary); margin-bottom: 1rem;"></i>
603
+ <p>No saved mindmaps yet</p>
604
+ <p>Your analyzed videos will appear here</p>
605
+ </div>
606
+ `;
607
+ } else {
608
+ // Create gallery items for each saved mindmap
609
+ savedMindmaps.forEach(mindmap => {
610
+ createGalleryItem(mindmap);
611
+ });
612
+ }
613
+
614
+ // Show gallery
615
+ galleryContainer.classList.add('active');
616
+ }
617
+
618
+ // Create a gallery item for a saved mindmap
619
+ function createGalleryItem(mindmapData) {
620
+ const galleryContent = document.getElementById('galleryContent');
621
+
622
+ const item = document.createElement('div');
623
+ item.className = 'gallery-item';
624
+
625
+ // Use a random color from our palette for preview
626
+ const colors = ['#3498db', '#2ecc71', '#e74c3c', '#9b59b6', '#e67e22', '#f1c40f', '#1abc9c'];
627
+ const randomColor = colors[Math.floor(Math.random() * colors.length)];
628
+
629
+ item.innerHTML = `
630
+ <div class="gallery-item-preview" style="background-color: ${randomColor};">
631
+ <div style="height: 100%; display: flex; align-items: center; justify-content: center; color: white;">
632
+ <i class="fas fa-project-diagram" style="font-size: 2rem;"></i>
633
+ </div>
634
+ </div>
635
+ <div class="gallery-item-info">
636
+ <div class="gallery-item-title">${mindmapData.title || 'Untitled Mindmap'}</div>
637
+ <div class="gallery-item-date">${new Date(mindmapData.date).toLocaleDateString()}</div>
638
+ </div>
639
+ `;
640
+
641
+ // Load mindmap when clicked
642
+ item.addEventListener('click', () => {
643
+ loadMindmap(mindmapData);
644
+ document.getElementById('resultsGallery').classList.remove('active');
645
+ });
646
+
647
+ galleryContent.appendChild(item);
648
+ }
649
+
650
+ // Export mindmap in various formats
651
+ function exportMindmap(format) {
652
+ if (!mindmap || !mindmap.mindmap) {
653
+ alert('No mindmap to export. Please analyze a video first.');
654
+ return;
655
+ }
656
+
657
+ try {
658
+ switch (format) {
659
+ case 'pdf':
660
+ exportAsPDF();
661
+ break;
662
+ case 'png':
663
+ exportAsPNG();
664
+ break;
665
+ case 'svg':
666
+ exportAsSVG();
667
+ break;
668
+ }
669
+ } catch (error) {
670
+ console.error(`Error exporting as ${format}:`, error);
671
+ alert(`Failed to export mindmap as ${format.toUpperCase()}`);
672
+ }
673
+ }
674
+
675
+ // Export as PDF using html2pdf
676
+ function exportAsPDF() {
677
+ // We'll use html2pdf.js for PDF export
678
+ const script = document.createElement('script');
679
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js';
680
+ document.head.appendChild(script);
681
+
682
+ script.onload = () => {
683
+ const element = document.getElementById('mindmap');
684
+ const opt = {
685
+ margin: 1,
686
+ filename: 'flowify-mindmap.pdf',
687
+ image: { type: 'jpeg', quality: 0.98 },
688
+ html2canvas: { scale: 2 },
689
+ jsPDF: { unit: 'in', format: 'letter', orientation: 'landscape' }
690
+ };
691
+
692
+ // Use html2pdf library to generate PDF
693
+ html2pdf().set(opt).from(element).save();
694
+ };
695
+ }
696
+
697
+ // Export as PNG
698
+ function exportAsPNG() {
699
+ // We'll use html2canvas for PNG export
700
+ const script = document.createElement('script');
701
+ script.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js';
702
+ document.head.appendChild(script);
703
+
704
+ script.onload = () => {
705
+ html2canvas(document.getElementById('mindmap'), {
706
+ scale: 2,
707
+ backgroundColor: null
708
+ }).then(canvas => {
709
+ const link = document.createElement('a');
710
+ link.download = 'flowify-mindmap.png';
711
+ link.href = canvas.toDataURL('image/png');
712
+ link.click();
713
+ });
714
+ };
715
+ }
716
+
717
+ // Export as SVG
718
+ function exportAsSVG() {
719
+ alert('SVG export functionality is under development. This feature will be available in a future update.');
720
+ }
721
+
722
+ // Share mindmap (placeholder)
723
+ function shareMindmap() {
724
+ // Save current mindmap to localStorage
725
+ if (mindmap && mindmap.mindmap) {
726
+ const data = mindmap.mindmap.get_data();
727
+ const mindmapData = {
728
+ id: Date.now(),
729
+ title: data.data.topic || 'Video Topics',
730
+ data: data,
731
+ date: new Date().toISOString()
732
+ };
733
+
734
+ saveMindmap(mindmapData);
735
+
736
+ alert('Mindmap saved! You can access it from the Recent Mindmaps gallery.');
737
+ } else {
738
+ alert('No mindmap to share. Please analyze a video first.');
739
+ }
740
+ }
741
+
742
+ // Save mindmap to localStorage
743
+ function saveMindmap(mindmapData) {
744
+ let savedMindmaps = getSavedMindmaps();
745
+ savedMindmaps.push(mindmapData);
746
+
747
+ // Keep only the most recent 20 mindmaps
748
+ if (savedMindmaps.length > 20) {
749
+ savedMindmaps = savedMindmaps.slice(-20);
750
+ }
751
+
752
+ localStorage.setItem('flowify-mindmaps', JSON.stringify(savedMindmaps));
753
+ }
754
+
755
+ // Get saved mindmaps from localStorage
756
+ function getSavedMindmaps() {
757
+ const savedData = localStorage.getItem('flowify-mindmaps');
758
+ return savedData ? JSON.parse(savedData) : [];
759
+ }
760
+
761
+ // Load a mindmap from saved data
762
+ function loadMindmap(mindmapData) {
763
+ if (mindmap && mindmap.mindmap && mindmapData.data) {
764
+ mindmap.mindmap.show(mindmapData.data);
765
+ mindmap.mindmapOverlay.classList.add('active');
766
+ }
767
+ }
768
+
769
+ class TopicMindmap {
770
+ constructor() {
771
+ this.mindmap = null;
772
+ this.container = 'mindmap';
773
+ this.zoomScale = 1;
774
+ this.isPanning = false;
775
+ this.panX = 0;
776
+ this.panY = 0;
777
+ this.isEditing = false;
778
+ this.segmentData = new Map();
779
+
780
+ // Get DOM elements
781
+ this.mainVideo = document.getElementById('videoPlayer');
782
+ this.overlayVideo = document.getElementById('overlayVideo');
783
+ this.videoSection = document.getElementById('videoSection');
784
+ this.videoInfo = document.getElementById('videoInfo');
785
+ this.toolbar = document.querySelector('.mindmap-toolbar');
786
+ this.mindmapOverlay = document.getElementById('mindmapOverlay');
787
+ this.videoCloseButton = document.getElementById('videoCloseButton');
788
+
789
+ // Context menu elements
790
+ this.contextMenu = document.getElementById('nodeContextMenu');
791
+ this.colorPicker = document.getElementById('colorPicker');
792
+ this.currentNodeId = null;
793
+
794
+ // Initially hide video section
795
+ if (this.videoSection) {
796
+ this.videoSection.style.display = 'none';
797
+ }
798
+
799
+ this.initialize();
800
+ this.setupToolbar();
801
+ this.setupNodeInteraction();
802
+ this.setupVideoCloseButton();
803
+ this.setupContextMenu();
804
+ }
805
+
806
+ initialize() {
807
+ const options = {
808
+ container: this.container,
809
+ theme: 'primary',
810
+ editable: true,
811
+ support_html: true,
812
+ view: {
813
+ draggable: true,
814
+ hmargin: 300,
815
+ vmargin: 200,
816
+ line_width: 2,
817
+ line_color: '#95a5a6',
818
+ node_spacing: 100
819
+ },
820
+ layout: {
821
+ hspace: 150,
822
+ vspace: 80,
823
+ pspace: 30
824
+ }
825
+ };
826
+
827
+ try {
828
+ // Add a style element for immediate expander fixes
829
+ this.addExpanderFixStyles();
830
+
831
+ this.mindmap = new jsMind(options);
832
+ const emptyMind = this.createEmptyMindMap();
833
+ this.mindmap.show(emptyMind);
834
+
835
+ const container = document.querySelector('.fullscreen-mindmap');
836
+ container.style.transformOrigin = '0 0';
837
+ container.style.position = 'absolute';
838
+ container.style.width = '100%';
839
+ container.style.height = '100%';
840
+
841
+ const mindmapWrapper = document.getElementById('mindmap');
842
+ mindmapWrapper.style.position = 'absolute';
843
+ mindmapWrapper.style.width = '100%';
844
+ mindmapWrapper.style.height = '100%';
845
+ mindmapWrapper.style.overflow = 'visible';
846
+
847
+ // Apply custom classes to expanders after rendering
848
+ this.applyCustomExpanderClasses();
849
+
850
+ this.setupNodeInteraction();
851
+ this.setupPanning();
852
+ } catch (error) {
853
+ console.error('Error initializing mindmap:', error);
854
+ }
855
+ }
856
+
857
+ addExpanderFixStyles() {
858
+ // Add a style element to ensure expanders don't show any text content
859
+ const style = document.createElement('style');
860
+ style.textContent = `
861
+ jmexpander * {
862
+ display: none !important;
863
+ }
864
+ jmexpander::before, jmexpander::after {
865
+ display: none !important;
866
+ }
867
+ `;
868
+ document.head.appendChild(style);
869
+ }
870
+
871
+ setupNodeInteraction() {
872
+ // Remove any existing event listeners
873
+ const container = document.getElementById(this.container);
874
+ if (!container) return;
875
+
876
+ container.addEventListener('click', (e) => {
877
+ const target = e.target;
878
+ if (target.tagName.toLowerCase() === 'jmnode') {
879
+ const nodeId = target.getAttribute('nodeid');
880
+ if (nodeId) {
881
+ this.handleNodeClick(nodeId);
882
+ }
883
+ }
884
+ });
885
+ }
886
+
887
+ handleNodeClick(nodeId) {
888
+ console.log('Node clicked:', nodeId);
889
+ const segmentInfo = this.segmentData.get(nodeId);
890
+ console.log('Segment info:', segmentInfo);
891
+
892
+ if (segmentInfo && segmentInfo.timestamp !== undefined) {
893
+ // Show video section
894
+ if (this.videoSection) {
895
+ this.videoSection.style.display = 'block';
896
+ console.log('Video section displayed');
897
+ }
898
+
899
+ // Adjust layout
900
+ if (this.toolbar) {
901
+ this.toolbar.classList.add('with-video');
902
+ }
903
+
904
+ // Setup video
905
+ if (this.mainVideo && this.mainVideo.src && this.overlayVideo) {
906
+ console.log('Setting up video playback at time:', segmentInfo.timestamp);
907
+
908
+ // Set video source if needed
909
+ if (this.overlayVideo.src !== this.mainVideo.src) {
910
+ this.overlayVideo.src = this.mainVideo.src;
911
+ }
912
+
913
+ // Get the timestamp as a number
914
+ let timeInSeconds = segmentInfo.timestamp;
915
+
916
+ // If it's still a string (with brackets), parse it
917
+ if (typeof timeInSeconds === 'string') {
918
+ if (timeInSeconds.startsWith('[') && timeInSeconds.endsWith(']')) {
919
+ timeInSeconds = parseFloat(timeInSeconds.slice(1, -1));
920
+ } else {
921
+ timeInSeconds = parseFloat(timeInSeconds);
922
+ }
923
+ }
924
+
925
+ // Ensure we have a valid number
926
+ if (!isNaN(timeInSeconds)) {
927
+ console.log('Seeking to time:', timeInSeconds);
928
+ this.overlayVideo.currentTime = timeInSeconds;
929
+ this.overlayVideo.play().catch(error => {
930
+ console.error('Error playing video:', error);
931
+ });
932
+
933
+ // Update info
934
+ if (this.videoInfo) {
935
+ const timestamp = this.formatTimestamp(timeInSeconds);
936
+ const content = segmentInfo.content.replace(/\[\d+(?:\.\d+)?\]\s*/, '');
937
+ this.videoInfo.textContent = `${timestamp}: ${content}`;
938
+ }
939
+ } else {
940
+ console.error('Invalid timestamp:', segmentInfo.timestamp);
941
+ }
942
+ }
943
+
944
+ // Update layout
945
+ this.updateLayout();
946
+ }
947
+ }
948
+
949
+ updateLayout() {
950
+ // Adjust mindmap section width when video is shown
951
+ const mindmapSection = document.querySelector('.mindmap-section');
952
+ const videoWidth = '40%';
953
+ const mindmapWidth = '60%';
954
+
955
+ if (this.videoSection && this.videoSection.style.display === 'block') {
956
+ this.videoSection.style.width = videoWidth;
957
+ if (mindmapSection) {
958
+ mindmapSection.style.width = mindmapWidth;
959
+ }
960
+ }
961
+
962
+ // Force mindmap refresh
963
+ if (this.mindmap) {
964
+ this.mindmap.resize();
965
+ }
966
+ }
967
+
968
+ setupNodeEditing() {
969
+ // Add double-click event listener for editing nodes
970
+ const container = document.getElementById(this.container);
971
+ container.addEventListener('dblclick', (e) => {
972
+ const target = e.target;
973
+ if (target.tagName.toLowerCase() === 'jmnode') {
974
+ this.editNode(target);
975
+ }
976
+ });
977
+
978
+ // Add node drag and drop functionality
979
+ container.addEventListener('mousedown', (e) => {
980
+ if (!this.isPanning && e.target.tagName.toLowerCase() === 'jmnode') {
981
+ this.startNodeDrag(e);
982
+ }
983
+ });
984
+ }
985
+
986
+ editNode(nodeElement) {
987
+ if (this.isPanning) return;
988
+
989
+ const nodeId = nodeElement.getAttribute('nodeid');
990
+ const currentText = this.mindmap.get_node(nodeId).topic;
991
+
992
+ // Create and style the input element
993
+ const input = document.createElement('input');
994
+ input.type = 'text';
995
+ input.value = currentText;
996
+ input.style.width = Math.max(nodeElement.offsetWidth, 100) + 'px';
997
+ input.style.height = nodeElement.offsetHeight + 'px';
998
+ input.style.position = 'absolute';
999
+ input.style.left = nodeElement.offsetLeft + 'px';
1000
+ input.style.top = nodeElement.offsetTop + 'px';
1001
+ input.style.zIndex = '1000';
1002
+ input.style.padding = '4px';
1003
+ input.style.border = '2px solid #3498db';
1004
+ input.style.borderRadius = '4px';
1005
+ input.style.backgroundColor = 'white';
1006
+
1007
+ // Replace node with input
1008
+ nodeElement.style.visibility = 'hidden';
1009
+ nodeElement.parentNode.appendChild(input);
1010
+ input.focus();
1011
+ input.select();
1012
+
1013
+ const finishEditing = () => {
1014
+ const newText = input.value.trim();
1015
+ if (newText) {
1016
+ this.mindmap.update_node(nodeId, newText);
1017
+ }
1018
+ nodeElement.style.visibility = 'visible';
1019
+ input.remove();
1020
+ };
1021
+
1022
+ input.addEventListener('blur', finishEditing);
1023
+ input.addEventListener('keydown', (e) => {
1024
+ if (e.key === 'Enter') {
1025
+ finishEditing();
1026
+ } else if (e.key === 'Escape') {
1027
+ nodeElement.style.visibility = 'visible';
1028
+ input.remove();
1029
+ }
1030
+ });
1031
+ }
1032
+
1033
+ startNodeDrag(e) {
1034
+ if (this.isPanning) return;
1035
+
1036
+ const nodeElement = e.target;
1037
+ const nodeId = nodeElement.getAttribute('nodeid');
1038
+ if (!nodeId) return;
1039
+
1040
+ const startX = e.clientX;
1041
+ const startY = e.clientY;
1042
+ const originalLeft = nodeElement.offsetLeft;
1043
+ const originalTop = nodeElement.offsetTop;
1044
+
1045
+ const mouseMoveHandler = (e) => {
1046
+ const dx = (e.clientX - startX) / this.zoomScale;
1047
+ const dy = (e.clientY - startY) / this.zoomScale;
1048
+
1049
+ nodeElement.style.left = (originalLeft + dx) / this.zoomScale + 'px';
1050
+ nodeElement.style.top = (originalTop + dy) / this.zoomScale + 'px';
1051
+
1052
+ // Update connected lines
1053
+ this.mindmap.layout.layout();
1054
+ };
1055
+
1056
+ const mouseUpHandler = () => {
1057
+ document.removeEventListener('mousemove', mouseMoveHandler);
1058
+ document.removeEventListener('mouseup', mouseUpHandler);
1059
+
1060
+ // Ensure the mindmap updates its internal positions
1061
+ this.mindmap.layout.layout();
1062
+ };
1063
+
1064
+ document.addEventListener('mousemove', mouseMoveHandler);
1065
+ document.addEventListener('mouseup', mouseUpHandler);
1066
+ }
1067
+
1068
+ setupPanning() {
1069
+ const container = document.querySelector('.fullscreen-mindmap');
1070
+ const mindmapWrapper = document.getElementById('mindmap');
1071
+ const panButton = document.getElementById('panButton');
1072
+
1073
+ panButton.addEventListener('click', () => {
1074
+ this.isPanning = !this.isPanning;
1075
+ panButton.classList.toggle('active');
1076
+ container.style.cursor = this.isPanning ? 'grab' : 'default';
1077
+ });
1078
+
1079
+ container.addEventListener('wheel', (e) => {
1080
+ if (e.ctrlKey) {
1081
+ e.preventDefault();
1082
+ const delta = e.deltaY * -0.01;
1083
+ const newScale = Math.min(Math.max(this.zoomScale + delta, 0.1), 3);
1084
+
1085
+ // Get mouse position relative to container
1086
+ const rect = container.getBoundingClientRect();
1087
+ const x = (e.clientX - rect.left) / this.zoomScale;
1088
+ const y = (e.clientY - rect.top) / this.zoomScale;
1089
+
1090
+ // Calculate new pan position to zoom towards mouse
1091
+ const scale = newScale / this.zoomScale;
1092
+ this.panX = x - (x - this.panX) * scale;
1093
+ this.panY = y - (y - this.panY) * scale;
1094
+
1095
+ this.zoomScale = newScale;
1096
+ this.applyTransform();
1097
+ }
1098
+ });
1099
+
1100
+ container.addEventListener('mousedown', (e) => {
1101
+ if (!this.isPanning) return;
1102
+
1103
+ container.style.cursor = 'grabbing';
1104
+ const startX = e.clientX - this.panX * this.zoomScale;
1105
+ const startY = e.clientY - this.panY * this.zoomScale;
1106
+
1107
+ const mouseMoveHandler = (e) => {
1108
+ if (!this.isPanning) return;
1109
+
1110
+ this.panX = (e.clientX - startX) / this.zoomScale;
1111
+ this.panY = (e.clientY - startY) / this.zoomScale;
1112
+ this.applyTransform();
1113
+ };
1114
+
1115
+ const mouseUpHandler = () => {
1116
+ container.style.cursor = this.isPanning ? 'grab' : 'default';
1117
+ document.removeEventListener('mousemove', mouseMoveHandler);
1118
+ document.removeEventListener('mouseup', mouseUpHandler);
1119
+ };
1120
+
1121
+ document.addEventListener('mousemove', mouseMoveHandler);
1122
+ document.addEventListener('mouseup', mouseUpHandler);
1123
+ });
1124
+ }
1125
+
1126
+ applyTransform() {
1127
+ const container = document.querySelector('.fullscreen-mindmap');
1128
+ const matrix = `matrix(${this.zoomScale}, 0, 0, ${this.zoomScale}, ${this.panX * this.zoomScale}, ${this.panY * this.zoomScale})`;
1129
+ container.style.transform = matrix;
1130
+ }
1131
+
1132
+ resetView() {
1133
+ this.zoomScale = 1;
1134
+ this.panX = 0;
1135
+ this.panY = 0;
1136
+ this.applyTransform();
1137
+
1138
+ const panButton = document.getElementById('panButton');
1139
+ this.isPanning = false;
1140
+ panButton.classList.remove('active');
1141
+ document.querySelector('.fullscreen-mindmap').style.cursor = 'default';
1142
+
1143
+ if (this.mindmap) {
1144
+ this.mindmap.resize();
1145
+
1146
+ // Center the mindmap after reset
1147
+ const container = document.querySelector('.fullscreen-mindmap');
1148
+ const mindmapEl = document.getElementById(this.container);
1149
+ if (container && mindmapEl) {
1150
+ const containerRect = container.getBoundingClientRect();
1151
+ const mindmapRect = mindmapEl.getBoundingClientRect();
1152
+
1153
+ this.panX = (containerRect.width - mindmapRect.width) / 2;
1154
+ this.panY = (containerRect.height - mindmapRect.height) / 2;
1155
+ this.applyTransform();
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ setupToolbar() {
1161
+ const backButton = document.getElementById('backButton');
1162
+ const editButton = document.getElementById('editButton');
1163
+ const zoomInButton = document.getElementById('zoomInButton');
1164
+ const zoomOutButton = document.getElementById('zoomOutButton');
1165
+ const resetButton = document.getElementById('resetButton');
1166
+ const panButton = document.getElementById('panButton');
1167
+
1168
+ if (backButton) {
1169
+ backButton.addEventListener('click', () => {
1170
+ // Hide video section when closing
1171
+ this.videoSection.classList.remove('active');
1172
+ this.toolbar.classList.remove('with-video');
1173
+
1174
+ // Stop video playback
1175
+ if (this.overlayVideo.src) {
1176
+ this.overlayVideo.pause();
1177
+ }
1178
+
1179
+ document.getElementById('mindmapOverlay').classList.remove('active');
1180
+ });
1181
+
1182
+ }
1183
+
1184
+ if (editButton) {
1185
+ editButton.addEventListener('click', () => {
1186
+ this.isEditing = !this.isEditing;
1187
+ editButton.classList.toggle('active');
1188
+ if (this.isEditing) {
1189
+ this.mindmap.enable_edit();
1190
+ editButton.innerHTML = '<i class="fas fa-edit"></i> Done';
1191
+ } else {
1192
+ this.mindmap.disable_edit();
1193
+ editButton.innerHTML = '<i class="fas fa-edit"></i> Edit';
1194
+ }
1195
+ });
1196
+ }
1197
+
1198
+ if (zoomInButton) {
1199
+ zoomInButton.addEventListener('click', () => this.zoomIn());
1200
+ }
1201
+
1202
+ if (zoomOutButton) {
1203
+ zoomOutButton.addEventListener('click', () => this.zoomOut());
1204
+ }
1205
+
1206
+ if (resetButton) {
1207
+ resetButton.addEventListener('click', () => this.resetView());
1208
+ }
1209
+ }
1210
+
1211
+ createEmptyMindMap() {
1212
+ return {
1213
+ meta: {
1214
+ name: 'Topics',
1215
+ author: 'Video Analyzer',
1216
+ version: '1.0'
1217
+ },
1218
+ format: 'node_tree',
1219
+ data: {
1220
+ id: 'root',
1221
+ topic: 'Video Topics',
1222
+ direction: 'center',
1223
+ children: []
1224
+ }
1225
+ };
1226
+ }
1227
+
1228
+ zoomIn() {
1229
+ this.zoomScale = Math.min(this.zoomScale + 0.1, 2);
1230
+ this.applyZoom();
1231
+ }
1232
+
1233
+ zoomOut() {
1234
+ this.zoomScale = Math.max(this.zoomScale - 0.1, 0.5);
1235
+ this.applyZoom();
1236
+ }
1237
+
1238
+ resetView() {
1239
+ this.zoomScale = 1;
1240
+ this.applyZoom();
1241
+ }
1242
+
1243
+ applyZoom() {
1244
+ const container = document.querySelector('.fullscreen-mindmap');
1245
+ if (container) {
1246
+ container.style.transform = `scale(${this.zoomScale})`;
1247
+ container.style.transformOrigin = 'center center';
1248
+ }
1249
+ }
1250
+
1251
+ createMindMapData(segments) {
1252
+ if (!segments || segments.length === 0) {
1253
+ return this.createEmptyMindMap();
1254
+ }
1255
+
1256
+ const mind = {
1257
+ meta: {
1258
+ name: 'Topics',
1259
+ author: 'Video Analyzer',
1260
+ version: '1.0'
1261
+ },
1262
+ format: 'node_tree',
1263
+ data: {
1264
+ id: 'root',
1265
+ topic: 'Video Topics',
1266
+ direction: 'center',
1267
+ expanded: true,
1268
+ children: []
1269
+ }
1270
+ };
1271
+
1272
+ // Clear previous segment data
1273
+ this.segmentData.clear();
1274
+
1275
+ // Process each segment
1276
+ let nodeId = 1;
1277
+
1278
+ segments.forEach(segment => {
1279
+ // Extract a cleaner topic name without repeating terms
1280
+ const topicName = segment.topic_name.split(':')[1]?.trim() || segment.topic_name.trim();
1281
+
1282
+ // Create the main topic node
1283
+ const topicNode = {
1284
+ id: `topic_${nodeId}`,
1285
+ topic: topicName,
1286
+ direction: this.getDirection(nodeId, segments.length),
1287
+ expanded: false,
1288
+ children: []
1289
+ };
1290
+
1291
+ // Store the timestamp for the main node
1292
+ this.segmentData.set(`topic_${nodeId}`, {
1293
+ timestamp: segment.timestamp || 0,
1294
+ content: Array.isArray(segment.content) ? segment.content.join('\n') : segment.content
1295
+ });
1296
+
1297
+ // If this is a parent topic with children, add them as subnodes
1298
+ if (segment.is_parent && segment.children && segment.children.length > 0) {
1299
+ segment.children.forEach((child, childIndex) => {
1300
+ const childName = child.topic_name.split(':')[1]?.trim() || child.topic_name.trim();
1301
+ const childContent = Array.isArray(child.content) ? child.content.join('\n') : child.content;
1302
+
1303
+ // Extract a meaningful snippet from the content
1304
+ let contentSnippet = '';
1305
+ const cleanContent = childContent.replace(/\[\d+(?:\.\d+)?\]\s*/g, '');
1306
+ contentSnippet = cleanContent.split('.')[0];
1307
+ if (contentSnippet.length > 50) {
1308
+ contentSnippet = contentSnippet.slice(0, 50) + '...';
1309
+ } else {
1310
+ contentSnippet += '...';
1311
+ }
1312
+
1313
+ const childNodeId = `content_${nodeId}_${childIndex}`;
1314
+
1315
+ topicNode.children.push({
1316
+ id: childNodeId,
1317
+ topic: childName,
1318
+ direction: topicNode.direction,
1319
+ expanded: false
1320
+ });
1321
+
1322
+ // Store the timestamp and content for this child node
1323
+ this.segmentData.set(childNodeId, {
1324
+ timestamp: child.timestamp || 0,
1325
+ content: childContent
1326
+ });
1327
+ });
1328
+ }
1329
+ // For regular segments or parents without children, add their content as subnodes
1330
+ else {
1331
+ // Get the content as an array
1332
+ const contentArray = Array.isArray(segment.content) ? segment.content : [segment.content];
1333
+
1334
+ // If there's a lot of content, create content summary nodes
1335
+ if (contentArray.length > 5) {
1336
+ // Create just one summary node for all content
1337
+ const contentNodeId = `content_${nodeId}_0`;
1338
+
1339
+ // Create a content summary
1340
+ const cleanContent = contentArray.join(' ').replace(/\[\d+(?:\.\d+)?\]\s*/g, '');
1341
+ let contentSnippet = cleanContent.split('.')[0];
1342
+ if (contentSnippet.length > 50) {
1343
+ contentSnippet = contentSnippet.slice(0, 50) + '...';
1344
+ } else {
1345
+ contentSnippet += '...';
1346
+ }
1347
+
1348
+ topicNode.children.push({
1349
+ id: contentNodeId,
1350
+ topic: contentSnippet,
1351
+ direction: topicNode.direction,
1352
+ expanded: false
1353
+ });
1354
+
1355
+ // Store the timestamp and full content
1356
+ this.segmentData.set(contentNodeId, {
1357
+ timestamp: segment.timestamp || 0,
1358
+ content: contentArray.join('\n')
1359
+ });
1360
+ }
1361
+ // For small segments, include separate nodes for each part
1362
+ else {
1363
+ contentArray.forEach((content, index) => {
1364
+ if (typeof content === 'string' && content.trim()) {
1365
+ const contentNodeId = `content_${nodeId}_${index}`;
1366
+
1367
+ // Clean up the content for display
1368
+ const cleanContent = content.replace(/\[\d+(?:\.\d+)?\]\s*/g, '');
1369
+ let contentSnippet = cleanContent.split('.')[0];
1370
+ if (contentSnippet.length > 50) {
1371
+ contentSnippet = contentSnippet.slice(0, 50) + '...';
1372
+ } else {
1373
+ contentSnippet += '...';
1374
+ }
1375
+
1376
+ topicNode.children.push({
1377
+ id: contentNodeId,
1378
+ topic: contentSnippet,
1379
+ direction: topicNode.direction,
1380
+ expanded: false
1381
+ });
1382
+
1383
+ // Store the timestamp and full content
1384
+ this.segmentData.set(contentNodeId, {
1385
+ timestamp: segment.timestamp || 0,
1386
+ content: content
1387
+ });
1388
+ }
1389
+ });
1390
+ }
1391
+ }
1392
+
1393
+ mind.data.children.push(topicNode);
1394
+ nodeId++;
1395
+ });
1396
+
1397
+ return mind;
1398
+ }
1399
+
1400
+ extractTimestamp(text) {
1401
+ // Look for timestamps in the format [X.X] or [X]
1402
+ const matches = text.match(/\[(\d+(?:\.\d+)?)\]/g);
1403
+ if (matches && matches.length > 0) {
1404
+ // Extract the first timestamp found
1405
+ const firstTimestamp = matches[0].replace('[', '').replace(']', '');
1406
+ // Round down to the nearest second (floor)
1407
+ const seconds = Math.floor(parseFloat(firstTimestamp));
1408
+ console.log('Extracted timestamp:', seconds, 'from text:', text);
1409
+ return seconds;
1410
+ }
1411
+ return 0;
1412
+ }
1413
+
1414
+ getDirection(index, total) {
1415
+ const halfTotal = Math.ceil(total / 2);
1416
+ if (index <= halfTotal) {
1417
+ return 'right';
1418
+ } else {
1419
+ return 'left';
1420
+ }
1421
+ }
1422
+
1423
+ update(segments) {
1424
+ if (!this.mindmap) {
1425
+ console.error('JSMind not initialized');
1426
+ return;
1427
+ }
1428
+
1429
+ try {
1430
+ // Store any current node colors before updating
1431
+ const nodeColors = this.saveNodeColors();
1432
+
1433
+ // Clear previous data
1434
+ this.segmentData.clear();
1435
+
1436
+ // Create and show new mindmap
1437
+ const mindData = this.createMindMapData(segments);
1438
+ this.mindmap.show(mindData);
1439
+
1440
+ // Save the mindmap to gallery
1441
+ this.saveMindmapToGallery(mindData);
1442
+
1443
+ // Restore node colors
1444
+ this.restoreNodeColors(nodeColors);
1445
+
1446
+ // Apply custom classes to expanders
1447
+ this.applyCustomExpanderClasses();
1448
+
1449
+ // Reset view and show overlay
1450
+ this.resetView();
1451
+ if (this.mindmapOverlay) {
1452
+ this.mindmapOverlay.classList.add('active');
1453
+ }
1454
+
1455
+ // Hide video section initially
1456
+ if (this.videoSection) {
1457
+ this.videoSection.style.display = 'none';
1458
+ }
1459
+
1460
+ // Reset mindmap section to full width
1461
+ const mindmapSection = document.querySelector('.mindmap-section');
1462
+ if (mindmapSection) {
1463
+ mindmapSection.style.width = '100%';
1464
+ }
1465
+
1466
+ // Remove video-related classes
1467
+ if (this.toolbar) {
1468
+ this.toolbar.classList.remove('with-video');
1469
+ }
1470
+
1471
+ // Reinitialize node interaction
1472
+ this.setupNodeInteraction();
1473
+
1474
+ } catch (error) {
1475
+ console.error('Error updating mindmap:', error);
1476
+ }
1477
+ }
1478
+
1479
+ saveMindmapToGallery(mindData) {
1480
+ // Create mindmap data object
1481
+ const mindmapData = {
1482
+ id: Date.now(),
1483
+ title: mindData.data.topic || 'Video Topics',
1484
+ data: mindData,
1485
+ date: new Date().toISOString()
1486
+ };
1487
+
1488
+ // Save to localStorage
1489
+ saveMindmap(mindmapData);
1490
+ }
1491
+
1492
+ saveNodeColors() {
1493
+ // Save current node colors
1494
+ const nodeColors = {};
1495
+ if (this.mindmap) {
1496
+ const nodes = document.querySelectorAll('jmnode');
1497
+ nodes.forEach(node => {
1498
+ const nodeId = node.getAttribute('nodeid');
1499
+ if (nodeId) {
1500
+ const colorClasses = [
1501
+ 'node-blue', 'node-green', 'node-red', 'node-purple',
1502
+ 'node-orange', 'node-yellow', 'node-teal', 'node-gray'
1503
+ ];
1504
+
1505
+ colorClasses.forEach(cls => {
1506
+ if (node.classList.contains(cls)) {
1507
+ nodeColors[nodeId] = cls.replace('node-', '');
1508
+ }
1509
+ });
1510
+ }
1511
+ });
1512
+ }
1513
+ return nodeColors;
1514
+ }
1515
+
1516
+ restoreNodeColors(nodeColors) {
1517
+ // Restore saved node colors
1518
+ if (nodeColors && Object.keys(nodeColors).length > 0) {
1519
+ setTimeout(() => {
1520
+ for (const [nodeId, color] of Object.entries(nodeColors)) {
1521
+ this.changeNodeColor(nodeId, color);
1522
+ }
1523
+ }, 100);
1524
+ }
1525
+ }
1526
+
1527
+ setupVideoCloseButton() {
1528
+ if (this.videoCloseButton) {
1529
+ this.videoCloseButton.addEventListener('click', () => {
1530
+ this.closeVideoOverlay();
1531
+ });
1532
+ }
1533
+ }
1534
+
1535
+ closeVideoOverlay() {
1536
+ // Hide video section
1537
+ if (this.videoSection) {
1538
+ this.videoSection.style.display = 'none';
1539
+
1540
+ // Stop video playback
1541
+ if (this.overlayVideo && this.overlayVideo.src) {
1542
+ this.overlayVideo.pause();
1543
+ }
1544
+ }
1545
+
1546
+ // Remove active class from toolbar
1547
+ if (this.toolbar) {
1548
+ this.toolbar.classList.remove('with-video');
1549
+ }
1550
+
1551
+ // Reset mindmap section to full width
1552
+ const mindmapSection = document.querySelector('.mindmap-section');
1553
+ if (mindmapSection) {
1554
+ mindmapSection.style.width = '100%';
1555
+ }
1556
+
1557
+ // Force mindmap refresh
1558
+ if (this.mindmap) {
1559
+ this.mindmap.resize();
1560
+ }
1561
+ }
1562
+
1563
+ applyCustomExpanderClasses() {
1564
+ // Add custom classes to expand/collapse buttons
1565
+ setTimeout(() => {
1566
+ this.processExpanders();
1567
+
1568
+ // Set up a mutation observer to handle future changes
1569
+ const observer = new MutationObserver(mutations => {
1570
+ mutations.forEach(mutation => {
1571
+ if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
1572
+ this.processExpanders();
1573
+ }
1574
+ });
1575
+ });
1576
+
1577
+ observer.observe(document.getElementById(this.container), {
1578
+ childList: true,
1579
+ subtree: true,
1580
+ characterData: true
1581
+ });
1582
+ }, 100);
1583
+ }
1584
+
1585
+ processExpanders() {
1586
+ const expanders = document.querySelectorAll('jmexpander');
1587
+ expanders.forEach(expander => {
1588
+ // Check if it's an expand or collapse button
1589
+ if (expander.textContent.includes('+')) {
1590
+ expander.classList.add('expand');
1591
+ // Clear content more aggressively
1592
+ expander.textContent = '';
1593
+ expander.innerText = '';
1594
+ expander.innerHTML = '';
1595
+ } else if (expander.textContent.includes('-')) {
1596
+ expander.classList.add('collapse');
1597
+ // Clear content more aggressively
1598
+ expander.textContent = '';
1599
+ expander.innerText = '';
1600
+ expander.innerHTML = '';
1601
+ }
1602
+ });
1603
+ }
1604
+
1605
+ setupContextMenu() {
1606
+ // Hide context menu when clicking elsewhere
1607
+ document.addEventListener('click', (e) => {
1608
+ if (!this.contextMenu.contains(e.target)) {
1609
+ this.hideContextMenu();
1610
+ }
1611
+ });
1612
+
1613
+ // Hide context menu on escape key
1614
+ document.addEventListener('keydown', (e) => {
1615
+ if (e.key === 'Escape') {
1616
+ this.hideContextMenu();
1617
+ }
1618
+ });
1619
+
1620
+ // Hide color picker by default and setup action handlers
1621
+ this.colorPicker.style.display = 'none';
1622
+ this.setupContextMenuActions();
1623
+
1624
+ // Add event listeners for right-click context menu
1625
+ const container = document.getElementById(this.container);
1626
+ if (!container) return;
1627
+
1628
+ container.addEventListener('contextmenu', (e) => {
1629
+ const target = e.target;
1630
+ if (target.tagName.toLowerCase() === 'jmnode') {
1631
+ e.preventDefault();
1632
+ const nodeId = target.getAttribute('nodeid');
1633
+ if (nodeId) {
1634
+ this.currentNodeId = nodeId;
1635
+ this.showContextMenu(e.clientX, e.clientY);
1636
+
1637
+ // Update expand/collapse text based on node state
1638
+ const expandAction = document.getElementById('expandCollapseAction');
1639
+ const node = this.mindmap.get_node(nodeId);
1640
+ const isExpanded = this.mindmap.get_node_expanded(node);
1641
+
1642
+ if (expandAction) {
1643
+ if (isExpanded) {
1644
+ expandAction.innerHTML = '<i class="fas fa-angle-double-up"></i> Collapse all children';
1645
+ } else {
1646
+ expandAction.innerHTML = '<i class="fas fa-angle-double-down"></i> Expand all children';
1647
+ }
1648
+ }
1649
+
1650
+ // Disable parent addition for root node
1651
+ const addParentAction = document.getElementById('addParentAction');
1652
+ if (addParentAction) {
1653
+ if (nodeId === 'root') {
1654
+ addParentAction.classList.add('disabled');
1655
+ addParentAction.style.opacity = '0.5';
1656
+ addParentAction.style.cursor = 'not-allowed';
1657
+ } else {
1658
+ addParentAction.classList.remove('disabled');
1659
+ addParentAction.style.opacity = '1';
1660
+ addParentAction.style.cursor = 'pointer';
1661
+ }
1662
+ }
1663
+ }
1664
+ }
1665
+ });
1666
+ }
1667
+
1668
+ setupContextMenuActions() {
1669
+ // Edit node action
1670
+ document.getElementById('editNodeAction').addEventListener('click', () => {
1671
+ this.editNodeText(this.currentNodeId);
1672
+ this.hideContextMenu();
1673
+ });
1674
+
1675
+ // Add child node action
1676
+ document.getElementById('addChildAction').addEventListener('click', () => {
1677
+ this.addChildNode(this.currentNodeId);
1678
+ this.hideContextMenu();
1679
+ });
1680
+
1681
+ // Add parent node action
1682
+ document.getElementById('addParentAction').addEventListener('click', () => {
1683
+ if (this.currentNodeId !== 'root') {
1684
+ this.addParentNode(this.currentNodeId);
1685
+ }
1686
+ this.hideContextMenu();
1687
+ });
1688
+
1689
+ // Focus on branch action
1690
+ document.getElementById('focusNodeAction').addEventListener('click', () => {
1691
+ this.focusOnNode(this.currentNodeId);
1692
+ this.hideContextMenu();
1693
+ });
1694
+
1695
+ // Expand/collapse all children action
1696
+ document.getElementById('expandCollapseAction').addEventListener('click', () => {
1697
+ this.toggleExpandCollapseAll(this.currentNodeId);
1698
+ this.hideContextMenu();
1699
+ });
1700
+
1701
+ // Open color picker
1702
+ document.getElementById('nodeColorAction').addEventListener('click', (e) => {
1703
+ e.stopPropagation();
1704
+ this.toggleColorPicker();
1705
+ });
1706
+
1707
+ // Delete node action
1708
+ document.getElementById('deleteNodeAction').addEventListener('click', () => {
1709
+ this.deleteNode(this.currentNodeId);
1710
+ this.hideContextMenu();
1711
+ });
1712
+
1713
+ // Color picker options
1714
+ const colorOptions = document.querySelectorAll('.color-option');
1715
+ colorOptions.forEach(option => {
1716
+ option.addEventListener('click', (e) => {
1717
+ const color = e.target.getAttribute('data-color');
1718
+ this.changeNodeColor(this.currentNodeId, color);
1719
+ this.hideContextMenu();
1720
+ });
1721
+ });
1722
+ }
1723
+
1724
+ showContextMenu(x, y) {
1725
+ // Reset color picker visibility
1726
+ this.colorPicker.style.display = 'none';
1727
+
1728
+ // Position the context menu
1729
+ this.contextMenu.style.left = `${x}px`;
1730
+ this.contextMenu.style.top = `${y}px`;
1731
+
1732
+ // Show the context menu
1733
+ this.contextMenu.classList.add('visible');
1734
+ }
1735
+
1736
+ hideContextMenu() {
1737
+ // Hide the context menu
1738
+ this.contextMenu.classList.remove('visible');
1739
+ this.colorPicker.style.display = 'none';
1740
+ }
1741
+
1742
+ toggleColorPicker() {
1743
+ // Toggle color picker visibility
1744
+ this.colorPicker.style.display = this.colorPicker.style.display === 'none' ? 'block' : 'none';
1745
+ }
1746
+
1747
+ editNodeText(nodeId) {
1748
+ // Get the node element and its position
1749
+ const node = this.mindmap.get_node(nodeId);
1750
+ const nodeElement = document.querySelector(`jmnode[nodeid="${nodeId}"]`);
1751
+
1752
+ if (!node || !nodeElement) return;
1753
+
1754
+ // Get node position
1755
+ const rect = nodeElement.getBoundingClientRect();
1756
+
1757
+ // Create input element
1758
+ const input = document.createElement('input');
1759
+ input.type = 'text';
1760
+ input.className = 'node-edit-input';
1761
+ input.value = node.topic;
1762
+
1763
+ // Position input over the node
1764
+ input.style.left = `${rect.left}px`;
1765
+ input.style.top = `${rect.top}px`;
1766
+ input.style.width = `${Math.max(rect.width, 150)}px`;
1767
+
1768
+ // Add input to DOM
1769
+ document.body.appendChild(input);
1770
+
1771
+ // Focus and select text
1772
+ input.focus();
1773
+ input.select();
1774
+
1775
+ // Handle completion of editing
1776
+ const completeEdit = () => {
1777
+ const newText = input.value.trim();
1778
+ if (newText && newText !== node.topic) {
1779
+ this.mindmap.update_node(nodeId, newText);
1780
+ }
1781
+ document.body.removeChild(input);
1782
+ };
1783
+
1784
+ // Event listeners for input
1785
+ input.addEventListener('blur', completeEdit);
1786
+ input.addEventListener('keydown', (e) => {
1787
+ if (e.key === 'Enter') {
1788
+ completeEdit();
1789
+ } else if (e.key === 'Escape') {
1790
+ document.body.removeChild(input);
1791
+ }
1792
+ });
1793
+ }
1794
+
1795
+ addChildNode(parentId) {
1796
+ // Generate a unique ID for the new node
1797
+ const newNodeId = `node_${Date.now()}`;
1798
+
1799
+ // Add a new child node
1800
+ this.mindmap.add_node(parentId, newNodeId, 'New Node');
1801
+
1802
+ // Ensure the parent is expanded
1803
+ this.mindmap.expand_node(parentId);
1804
+
1805
+ // After a small delay, edit the new node
1806
+ setTimeout(() => {
1807
+ this.editNodeText(newNodeId);
1808
+ }, 100);
1809
+ }
1810
+
1811
+ addParentNode(childId) {
1812
+ // This is a more complex operation as jsMind doesn't have direct support
1813
+ const child = this.mindmap.get_node(childId);
1814
+ if (!child || childId === 'root') return;
1815
+
1816
+ const originalParentId = child.parent.id;
1817
+
1818
+ // Generate a unique ID for the new parent node
1819
+ const newParentId = `node_${Date.now()}`;
1820
+
1821
+ // Add the new parent to the original parent
1822
+ this.mindmap.add_node(originalParentId, newParentId, 'New Parent');
1823
+
1824
+ // Move the child node to be under the new parent
1825
+ this.mindmap.move_node(childId, newParentId);
1826
+
1827
+ // Ensure nodes are expanded
1828
+ this.mindmap.expand_node(originalParentId);
1829
+ this.mindmap.expand_node(newParentId);
1830
+
1831
+ // After a small delay, edit the new node
1832
+ setTimeout(() => {
1833
+ this.editNodeText(newParentId);
1834
+ }, 100);
1835
+ }
1836
+
1837
+ focusOnNode(nodeId) {
1838
+ // First, ensure the node and all its ancestors are expanded
1839
+ let node = this.mindmap.get_node(nodeId);
1840
+ while (node && node.id !== 'root') {
1841
+ this.mindmap.expand_node(node.parent.id);
1842
+ node = node.parent;
1843
+ }
1844
+
1845
+ // Get the node element
1846
+ const nodeElement = document.querySelector(`jmnode[nodeid="${nodeId}"]`);
1847
+ if (!nodeElement) return;
1848
+
1849
+ // Get node position and center the view on it
1850
+ const rect = nodeElement.getBoundingClientRect();
1851
+ const containerRect = document.querySelector('.fullscreen-mindmap').getBoundingClientRect();
1852
+
1853
+ // Calculate the center position
1854
+ const centerX = containerRect.width / 2;
1855
+ const centerY = containerRect.height / 2;
1856
+
1857
+ // Calculate the target position to center the node
1858
+ this.panX = centerX - rect.left - rect.width / 2;
1859
+ this.panY = centerY - rect.top - rect.height / 2;
1860
+
1861
+ // Apply the transformation
1862
+ this.applyTransform();
1863
+ }
1864
+
1865
+ toggleExpandCollapseAll(nodeId) {
1866
+ const node = this.mindmap.get_node(nodeId);
1867
+ if (!node) return;
1868
+
1869
+ const isExpanded = this.mindmap.get_node_expanded(node);
1870
+
1871
+ if (isExpanded) {
1872
+ // Collapse all descendants
1873
+ this.collapseAllChildren(nodeId);
1874
+ } else {
1875
+ // Expand all descendants
1876
+ this.expandAllChildren(nodeId);
1877
+ }
1878
+ }
1879
+
1880
+ expandAllChildren(nodeId) {
1881
+ // Recursively expand all nodes
1882
+ const expandRecursively = (id) => {
1883
+ const node = this.mindmap.get_node(id);
1884
+ if (!node) return;
1885
+
1886
+ this.mindmap.expand_node(id);
1887
+
1888
+ if (node.children && node.children.length > 0) {
1889
+ node.children.forEach(child => {
1890
+ expandRecursively(child.id);
1891
+ });
1892
+ }
1893
+ };
1894
+
1895
+ expandRecursively(nodeId);
1896
+ }
1897
+
1898
+ collapseAllChildren(nodeId) {
1899
+ // Recursively collapse all nodes
1900
+ const collapseRecursively = (id) => {
1901
+ const node = this.mindmap.get_node(id);
1902
+ if (!node) return;
1903
+
1904
+ if (node.children && node.children.length > 0) {
1905
+ node.children.forEach(child => {
1906
+ collapseRecursively(child.id);
1907
+ });
1908
+ this.mindmap.collapse_node(id);
1909
+ }
1910
+ };
1911
+
1912
+ // We don't want to collapse the starting node itself
1913
+ const node = this.mindmap.get_node(nodeId);
1914
+ if (node && node.children && node.children.length > 0) {
1915
+ node.children.forEach(child => {
1916
+ collapseRecursively(child.id);
1917
+ });
1918
+ }
1919
+ }
1920
+
1921
+ changeNodeColor(nodeId, color) {
1922
+ if (!nodeId || !color) return;
1923
+
1924
+ // Get the node element
1925
+ const nodeElement = document.querySelector(`jmnode[nodeid="${nodeId}"]`);
1926
+ if (!nodeElement) return;
1927
+
1928
+ // Remove existing color classes
1929
+ const colorClasses = [
1930
+ 'node-blue', 'node-green', 'node-red', 'node-purple',
1931
+ 'node-orange', 'node-yellow', 'node-teal', 'node-gray'
1932
+ ];
1933
+
1934
+ colorClasses.forEach(cls => {
1935
+ nodeElement.classList.remove(cls);
1936
+ });
1937
+
1938
+ // Add the new color class
1939
+ nodeElement.classList.add(`node-${color}`);
1940
+
1941
+ // Store the color in the node data (if needed for persistence)
1942
+ const node = this.mindmap.get_node(nodeId);
1943
+ if (node) {
1944
+ node.data = node.data || {};
1945
+ node.data.color = color;
1946
+ }
1947
+ }
1948
+
1949
+ deleteNode(nodeId) {
1950
+ // Can't delete the root node
1951
+ if (nodeId === 'root') return;
1952
+
1953
+ // Confirm deletion
1954
+ if (confirm('Are you sure you want to delete this node and all its children?')) {
1955
+ this.mindmap.remove_node(nodeId);
1956
+ }
1957
+ }
1958
+ }
1959
+
1960
+ // Custom Video Player
1961
+ function initCustomVideoPlayer() {
1962
+ const videoPlayer = document.getElementById('videoPlayer');
1963
+ const videoControls = document.querySelector('.video-controls');
1964
+ const playPauseBtn = document.getElementById('playPauseBtn');
1965
+ const progressBar = document.querySelector('.video-progress-bar');
1966
+ const progress = document.querySelector('.video-progress');
1967
+ const currentTimeDisplay = document.getElementById('currentTime');
1968
+ const durationDisplay = document.getElementById('duration');
1969
+ const muteBtn = document.getElementById('muteBtn');
1970
+ const fullscreenBtn = document.getElementById('fullscreenBtn');
1971
+ const videoContainer = document.querySelector('.video-preview');
1972
+
1973
+ if (!videoPlayer || !videoControls) return;
1974
+
1975
+ // Hide default controls (but keep them accessible for mobile/compatibility)
1976
+ videoPlayer.controls = false;
1977
+
1978
+ // Play/Pause
1979
+ playPauseBtn.addEventListener('click', () => {
1980
+ if (videoPlayer.paused) {
1981
+ videoPlayer.play();
1982
+ videoContainer.classList.add('playing');
1983
+ } else {
1984
+ videoPlayer.pause();
1985
+ videoContainer.classList.remove('playing');
1986
+ }
1987
+ });
1988
+
1989
+ // Update progress bar
1990
+ videoPlayer.addEventListener('timeupdate', () => {
1991
+ const percent = (videoPlayer.currentTime / videoPlayer.duration) * 100;
1992
+ progressBar.style.width = `${percent}%`;
1993
+
1994
+ // Update time display
1995
+ currentTimeDisplay.textContent = formatTime(videoPlayer.currentTime);
1996
+ });
1997
+
1998
+ // Click on progress bar to seek
1999
+ progress.addEventListener('click', (e) => {
2000
+ const rect = progress.getBoundingClientRect();
2001
+ const pos = (e.clientX - rect.left) / rect.width;
2002
+ videoPlayer.currentTime = pos * videoPlayer.duration;
2003
+ });
2004
+
2005
+ // Load metadata (duration, etc.)
2006
+ videoPlayer.addEventListener('loadedmetadata', () => {
2007
+ durationDisplay.textContent = formatTime(videoPlayer.duration);
2008
+ });
2009
+
2010
+ // Mute/Unmute
2011
+ muteBtn.addEventListener('click', () => {
2012
+ videoPlayer.muted = !videoPlayer.muted;
2013
+ muteBtn.innerHTML = videoPlayer.muted ?
2014
+ '<i class="fas fa-volume-mute"></i>' :
2015
+ '<i class="fas fa-volume-up"></i>';
2016
+ });
2017
+
2018
+ // Fullscreen
2019
+ fullscreenBtn.addEventListener('click', () => {
2020
+ if (videoContainer.requestFullscreen) {
2021
+ videoContainer.requestFullscreen();
2022
+ } else if (videoContainer.webkitRequestFullscreen) {
2023
+ videoContainer.webkitRequestFullscreen();
2024
+ } else if (videoContainer.msRequestFullscreen) {
2025
+ videoContainer.msRequestFullscreen();
2026
+ }
2027
+ });
2028
+
2029
+ // Update UI on play/pause
2030
+ videoPlayer.addEventListener('play', () => {
2031
+ videoContainer.classList.add('playing');
2032
+ });
2033
+
2034
+ videoPlayer.addEventListener('pause', () => {
2035
+ videoContainer.classList.remove('playing');
2036
+ });
2037
+
2038
+ // Format time to MM:SS
2039
+ function formatTime(seconds) {
2040
+ const minutes = Math.floor(seconds / 60);
2041
+ seconds = Math.floor(seconds % 60);
2042
+ return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
2043
+ }
2044
+ }
2045
+
2046
+ // Upload Particles Animation
2047
+ function initUploadParticles() {
2048
+ const uploadContainer = document.querySelector('.upload-container');
2049
+ const particles = document.querySelector('.particles');
2050
+
2051
+ if (!uploadContainer || !particles) return;
2052
+
2053
+ // Create particles
2054
+ for (let i = 0; i < 15; i++) {
2055
+ const particle = document.createElement('div');
2056
+ particle.className = 'particle';
2057
+
2058
+ // Random position
2059
+ const posX = Math.random() * 100;
2060
+ particle.style.left = `${posX}%`;
2061
+ particle.style.bottom = '0';
2062
+
2063
+ // Random size
2064
+ const size = Math.random() * 4 + 2;
2065
+ particle.style.width = `${size}px`;
2066
+ particle.style.height = `${size}px`;
2067
+
2068
+ // Random animation delay and duration
2069
+ const delay = Math.random() * 2;
2070
+ const duration = Math.random() * 2 + 1;
2071
+ particle.style.animation = `particleAnimation ${duration}s ${delay}s infinite ease-out`;
2072
+
2073
+ // Add to container
2074
+ particles.appendChild(particle);
2075
+ }
2076
+
2077
+ // Show upload progress when uploading
2078
+ const fileInput = document.getElementById('fileInput');
2079
+
2080
+ uploadContainer.addEventListener('click', () => {
2081
+ fileInput.click();
2082
+ });
2083
+
2084
+ // Drag and drop functionality with improved feedback
2085
+ uploadContainer.addEventListener('dragover', (e) => {
2086
+ e.preventDefault();
2087
+ uploadContainer.classList.add('active');
2088
+ });
2089
+
2090
+ uploadContainer.addEventListener('dragleave', () => {
2091
+ uploadContainer.classList.remove('active');
2092
+ });
2093
+
2094
+ uploadContainer.addEventListener('drop', (e) => {
2095
+ e.preventDefault();
2096
+ uploadContainer.classList.remove('active');
2097
+
2098
+ if (e.dataTransfer.files.length) {
2099
+ fileInput.files = e.dataTransfer.files;
2100
+ fileInput.dispatchEvent(new Event('change'));
2101
+ }
2102
+ });
2103
+ }
2104
+
2105
+ // Progress Steps Visualization
2106
+ function initProgressSteps() {
2107
+ const uploadStep = document.getElementById('uploadStep');
2108
+ const transcribeStep = document.getElementById('transcribeStep');
2109
+ const analyzeStep = document.getElementById('analyzeStep');
2110
+ const visualizeStep = document.getElementById('visualizeStep');
2111
+ const progressBarFill = document.querySelector('.progress-bar-fill');
2112
+
2113
+ if (!uploadStep || !progressBarFill) return;
2114
+
2115
+ // Set initial state
2116
+ setActiveStep('upload');
2117
+
2118
+ // Update steps based on current operation
2119
+ fileInput.addEventListener('change', () => {
2120
+ setActiveStep('upload');
2121
+ });
2122
+
2123
+ transcribeButton.addEventListener('click', () => {
2124
+ setTimeout(() => setActiveStep('transcribe'), 100);
2125
+ });
2126
+
2127
+ analyzeButton.addEventListener('click', () => {
2128
+ setTimeout(() => setActiveStep('analyze'), 100);
2129
+ });
2130
+
2131
+ // Redefine the global setActiveStep function with the proper implementation
2132
+ window.setActiveStep = function(step) {
2133
+ // Skip if elements aren't available yet
2134
+ if (!uploadStep || !progressBarFill) return;
2135
+
2136
+ // Reset all steps
2137
+ uploadStep.className = 'progress-step';
2138
+ transcribeStep.className = 'progress-step';
2139
+ analyzeStep.className = 'progress-step';
2140
+ visualizeStep.className = 'progress-step';
2141
+
2142
+ // Set progress level
2143
+ let progressWidth = '0%';
2144
+
2145
+ switch(step) {
2146
+ case 'upload':
2147
+ uploadStep.className = 'progress-step active';
2148
+ progressWidth = '25%';
2149
+ break;
2150
+ case 'transcribe':
2151
+ uploadStep.className = 'progress-step completed';
2152
+ transcribeStep.className = 'progress-step active';
2153
+ progressWidth = '50%';
2154
+ break;
2155
+ case 'analyze':
2156
+ uploadStep.className = 'progress-step completed';
2157
+ transcribeStep.className = 'progress-step completed';
2158
+ analyzeStep.className = 'progress-step active';
2159
+ progressWidth = '75%';
2160
+ break;
2161
+ case 'visualize':
2162
+ uploadStep.className = 'progress-step completed';
2163
+ transcribeStep.className = 'progress-step completed';
2164
+ analyzeStep.className = 'progress-step completed';
2165
+ visualizeStep.className = 'progress-step active';
2166
+ progressWidth = '100%';
2167
+ break;
2168
+ }
2169
+
2170
+ // Animate progress bar
2171
+ progressBarFill.style.width = progressWidth;
2172
+ };
2173
+ }
static/styles.css ADDED
@@ -0,0 +1,1889 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Root Variables */
2
+ :root {
3
+ /* Light Mode Colors */
4
+ --primary-color: #3498db;
5
+ --primary-dark: #2980b9;
6
+ --secondary-color: #2ecc71;
7
+ --secondary-dark: #27ae60;
8
+ --accent-color: #9b59b6;
9
+ --accent-dark: #8e44ad;
10
+ --background-color: #f8f9fa;
11
+ --surface-color: #ffffff;
12
+ --text-primary: #2c3e50;
13
+ --text-secondary: #7f8c8d;
14
+ --border-color: #e0e0e0;
15
+ --shadow-sm: 0 2px 4px rgba(0,0,0,0.05);
16
+ --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
17
+ --shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
18
+ --radius-sm: 4px;
19
+ --radius-md: 8px;
20
+ --radius-lg: 12px;
21
+
22
+ /* Enhanced Typography */
23
+ --font-heading: 'Montserrat', 'Segoe UI', system-ui, -apple-system, sans-serif;
24
+ --font-body: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
25
+ --font-mono: 'JetBrains Mono', 'Courier New', monospace;
26
+ }
27
+
28
+ /* Dark Mode Colors */
29
+ [data-theme="dark"] {
30
+ --primary-color: #60a5fa;
31
+ --primary-dark: #3b82f6;
32
+ --secondary-color: #10b981;
33
+ --secondary-dark: #059669;
34
+ --accent-color: #c084fc;
35
+ --accent-dark: #a855f7;
36
+ --background-color: #111827;
37
+ --surface-color: #1f2937;
38
+ --text-primary: #f9fafb;
39
+ --text-secondary: #e5e7eb;
40
+ --border-color: #374151;
41
+ --shadow-sm: 0 2px 4px rgba(0,0,0,0.3);
42
+ --shadow-md: 0 4px 6px rgba(0,0,0,0.4);
43
+ --shadow-lg: 0 10px 15px rgba(0,0,0,0.5);
44
+ }
45
+
46
+ /* Reset & Base Styles */
47
+ * {
48
+ margin: 0;
49
+ padding: 0;
50
+ box-sizing: border-box;
51
+ }
52
+
53
+ /* Font imports */
54
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Montserrat:wght@500;600;700&display=swap');
55
+
56
+ body {
57
+ font-family: var(--font-body);
58
+ background-color: var(--background-color);
59
+ color: var(--text-primary);
60
+ line-height: 1.6;
61
+ transition: background-color 0.3s ease, color 0.3s ease;
62
+ }
63
+
64
+ h1, h2, h3, h4, h5, h6 {
65
+ font-family: var(--font-heading);
66
+ font-weight: 600;
67
+ line-height: 1.3;
68
+ margin-bottom: 0.5em;
69
+ color: var(--text-primary);
70
+ }
71
+
72
+ code, pre {
73
+ font-family: var(--font-mono);
74
+ font-size: 0.9em;
75
+ }
76
+
77
+ /* Theme Toggle */
78
+ .theme-toggle {
79
+ position: fixed;
80
+ top: 1rem;
81
+ right: 1rem;
82
+ z-index: 1001;
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 0.5rem;
86
+ background-color: var(--surface-color);
87
+ border-radius: 2rem;
88
+ padding: 0.35rem;
89
+ box-shadow: var(--shadow-md);
90
+ cursor: pointer;
91
+ border: 1px solid var(--border-color);
92
+ transition: all 0.2s ease;
93
+ }
94
+
95
+ .theme-toggle:hover {
96
+ transform: translateY(-2px);
97
+ box-shadow: var(--shadow-lg);
98
+ }
99
+
100
+ .theme-toggle-icon {
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ width: 2rem;
105
+ height: 2rem;
106
+ font-size: 1.2rem;
107
+ border-radius: 50%;
108
+ color: var(--text-primary);
109
+ transition: all 0.3s ease;
110
+ }
111
+
112
+ .theme-toggle-icon.light-icon {
113
+ background-color: var(--primary-color);
114
+ color: white;
115
+ transform: var(--theme-light-transform, scale(1));
116
+ opacity: var(--theme-light-opacity, 1);
117
+ }
118
+
119
+ .theme-toggle-icon.dark-icon {
120
+ background-color: transparent;
121
+ transform: var(--theme-dark-transform, scale(0.8));
122
+ opacity: var(--theme-dark-opacity, 0.5);
123
+ }
124
+
125
+ [data-theme="dark"] .theme-toggle-icon.light-icon {
126
+ background-color: transparent;
127
+ transform: scale(0.8);
128
+ opacity: 0.5;
129
+ }
130
+
131
+ [data-theme="dark"] .theme-toggle-icon.dark-icon {
132
+ background-color: var(--accent-color);
133
+ color: white;
134
+ transform: scale(1);
135
+ opacity: 1;
136
+ }
137
+
138
+ /* Layout */
139
+ .app-container {
140
+ min-height: 100vh;
141
+ display: flex;
142
+ flex-direction: column;
143
+ }
144
+
145
+ .app-header {
146
+ background-color: var(--surface-color);
147
+ padding: 1rem 2rem;
148
+ box-shadow: var(--shadow-sm);
149
+ }
150
+
151
+ .logo-container {
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 1rem;
155
+ }
156
+
157
+ .logo-icon {
158
+ font-size: 2rem;
159
+ color: var(--primary-color);
160
+ }
161
+
162
+ .main-content {
163
+ flex: 1;
164
+ max-width: 1400px;
165
+ margin: 2rem auto;
166
+ padding: 0 2rem;
167
+ display: grid;
168
+ gap: 2rem;
169
+ grid-template-rows: auto minmax(0, 1fr);
170
+ }
171
+
172
+ /* Upload Panel */
173
+ .upload-panel {
174
+ background-color: var(--surface-color);
175
+ border-radius: var(--radius-lg);
176
+ padding: 2rem;
177
+ box-shadow: var(--shadow-md);
178
+ max-height: fit-content;
179
+ overflow: hidden;
180
+ }
181
+
182
+ .upload-section {
183
+ display: grid;
184
+ grid-template-columns: 2fr 1fr;
185
+ gap: 2rem;
186
+ margin-bottom: 2rem;
187
+ }
188
+
189
+ .upload-container {
190
+ border: 2px dashed rgba(52, 152, 219, 0.3);
191
+ border-radius: var(--radius-lg);
192
+ padding: 2.5rem;
193
+ text-align: center;
194
+ transition: all 0.3s ease;
195
+ cursor: pointer;
196
+ background: linear-gradient(135deg, rgba(52, 152, 219, 0.02) 0%, rgba(52, 152, 219, 0.08) 100%);
197
+ position: relative;
198
+ overflow: hidden;
199
+ }
200
+
201
+ .upload-container::before {
202
+ content: "";
203
+ position: absolute;
204
+ top: 0;
205
+ left: 0;
206
+ right: 0;
207
+ bottom: 0;
208
+ background: radial-gradient(circle at center, rgba(52, 152, 219, 0.1) 0%, transparent 70%);
209
+ opacity: 0;
210
+ transition: opacity 0.3s ease;
211
+ }
212
+
213
+ .upload-container:hover {
214
+ border-color: var(--primary-color);
215
+ transform: translateY(-5px);
216
+ box-shadow: 0 15px 30px -10px rgba(52, 152, 219, 0.15);
217
+ }
218
+
219
+ .upload-container:hover::before {
220
+ opacity: 1;
221
+ }
222
+
223
+ .upload-icon-container {
224
+ width: 80px;
225
+ height: 80px;
226
+ border-radius: 50%;
227
+ background-color: rgba(52, 152, 219, 0.1);
228
+ margin: 0 auto 1.5rem;
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: center;
232
+ transition: all 0.3s ease;
233
+ }
234
+
235
+ .upload-container:hover .upload-icon-container {
236
+ background-color: rgba(52, 152, 219, 0.2);
237
+ transform: scale(1.1);
238
+ }
239
+
240
+ .upload-icon {
241
+ font-size: 2.5rem;
242
+ color: var(--primary-color);
243
+ transition: all 0.3s ease;
244
+ }
245
+
246
+ .upload-container:hover .upload-icon {
247
+ transform: translateY(-5px);
248
+ }
249
+
250
+ .upload-text {
251
+ font-family: var(--font-heading);
252
+ font-size: 1.25rem;
253
+ font-weight: 600;
254
+ color: var(--text-primary);
255
+ margin-bottom: 0.5rem;
256
+ }
257
+
258
+ .upload-subtext {
259
+ font-size: 0.95rem;
260
+ color: var(--text-secondary);
261
+ max-width: 80%;
262
+ margin: 0 auto;
263
+ }
264
+
265
+ /* Upload file particles */
266
+ .particles {
267
+ position: absolute;
268
+ top: 0;
269
+ left: 0;
270
+ width: 100%;
271
+ height: 100%;
272
+ pointer-events: none;
273
+ opacity: 0;
274
+ transition: opacity 0.3s ease;
275
+ }
276
+
277
+ .particle {
278
+ position: absolute;
279
+ width: 6px;
280
+ height: 6px;
281
+ border-radius: 50%;
282
+ background-color: var(--primary-color);
283
+ opacity: 0;
284
+ }
285
+
286
+ .upload-container:hover .particles {
287
+ opacity: 1;
288
+ }
289
+
290
+ @keyframes particleAnimation {
291
+ 0% { transform: translateY(0) scale(0); opacity: 0; }
292
+ 50% { opacity: 0.8; }
293
+ 100% { transform: translateY(-100px) scale(1); opacity: 0; }
294
+ }
295
+
296
+ /* Upload Progress Animations */
297
+ .upload-progress {
298
+ width: 100%;
299
+ height: 4px;
300
+ margin-top: 1.5rem;
301
+ background-color: rgba(52, 152, 219, 0.1);
302
+ border-radius: 2px;
303
+ overflow: hidden;
304
+ opacity: 0;
305
+ transition: opacity 0.3s ease;
306
+ }
307
+
308
+ .upload-progress-bar {
309
+ height: 100%;
310
+ width: 0;
311
+ background: linear-gradient(90deg, var(--primary-color), var(--primary-dark));
312
+ border-radius: 2px;
313
+ transition: width 0.3s ease;
314
+ }
315
+
316
+ .upload-container.uploading .upload-progress {
317
+ opacity: 1;
318
+ }
319
+
320
+ /* Dark Mode Adjustments */
321
+ [data-theme="dark"] .upload-container {
322
+ border-color: rgba(96, 165, 250, 0.3);
323
+ background: linear-gradient(135deg, rgba(30, 41, 59, 0.5) 0%, rgba(30, 41, 59, 0.8) 100%);
324
+ }
325
+
326
+ [data-theme="dark"] .upload-container::before {
327
+ background: radial-gradient(circle at center, rgba(96, 165, 250, 0.15) 0%, transparent 70%);
328
+ }
329
+
330
+ [data-theme="dark"] .upload-icon-container {
331
+ background-color: rgba(96, 165, 250, 0.15);
332
+ }
333
+
334
+ /* Model Selector */
335
+ .model-selector {
336
+ display: flex;
337
+ flex-direction: column;
338
+ gap: 0.5rem;
339
+ }
340
+
341
+ .model-label {
342
+ font-weight: 500;
343
+ color: var(--text-primary);
344
+ }
345
+
346
+ .select-wrapper {
347
+ position: relative;
348
+ }
349
+
350
+ .select-wrapper select {
351
+ width: 100%;
352
+ padding: 0.75rem;
353
+ border: 1px solid var(--border-color);
354
+ border-radius: var(--radius-sm);
355
+ appearance: none;
356
+ background-color: var(--surface-color);
357
+ font-size: 1rem;
358
+ color: var(--text-primary);
359
+ }
360
+
361
+ .select-icon {
362
+ position: absolute;
363
+ right: 1rem;
364
+ top: 50%;
365
+ transform: translateY(-50%);
366
+ color: var(--text-secondary);
367
+ pointer-events: none;
368
+ }
369
+
370
+ .model-status {
371
+ font-size: 0.9rem;
372
+ color: var(--text-secondary);
373
+ }
374
+
375
+ /* Video Preview */
376
+ .video-preview {
377
+ margin: 2rem 0;
378
+ border-radius: var(--radius-md);
379
+ overflow: hidden;
380
+ background-color: #000;
381
+ position: relative;
382
+ box-shadow: 0 15px 25px -5px rgba(0, 0, 0, 0.15),
383
+ 0 10px 10px -5px rgba(0, 0, 0, 0.05);
384
+ }
385
+
386
+ .video-preview video {
387
+ width: 100%;
388
+ object-fit: contain;
389
+ display: block;
390
+ }
391
+
392
+ .video-preview::after {
393
+ content: '';
394
+ position: absolute;
395
+ bottom: 0;
396
+ left: 0;
397
+ right: 0;
398
+ height: 90px;
399
+ background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 100%);
400
+ pointer-events: none;
401
+ opacity: 0;
402
+ transition: opacity 0.3s ease;
403
+ }
404
+
405
+ .video-preview:hover::after {
406
+ opacity: 1;
407
+ }
408
+
409
+ .video-controls {
410
+ position: absolute;
411
+ bottom: 0;
412
+ left: 0;
413
+ width: 100%;
414
+ padding: 1rem;
415
+ display: flex;
416
+ align-items: center;
417
+ gap: 1rem;
418
+ z-index: 10;
419
+ opacity: 0;
420
+ transform: translateY(10px);
421
+ transition: all 0.3s ease;
422
+ }
423
+
424
+ .video-preview:hover .video-controls {
425
+ opacity: 1;
426
+ transform: translateY(0);
427
+ }
428
+
429
+ .video-progress {
430
+ flex: 1;
431
+ height: 4px;
432
+ background-color: rgba(255, 255, 255, 0.2);
433
+ border-radius: 2px;
434
+ overflow: hidden;
435
+ cursor: pointer;
436
+ position: relative;
437
+ }
438
+
439
+ .video-progress-bar {
440
+ height: 100%;
441
+ width: 0;
442
+ background-color: var(--primary-color);
443
+ border-radius: 2px;
444
+ position: relative;
445
+ }
446
+
447
+ .video-progress-handle {
448
+ position: absolute;
449
+ right: -6px;
450
+ top: 50%;
451
+ transform: translateY(-50%);
452
+ width: 12px;
453
+ height: 12px;
454
+ border-radius: 50%;
455
+ background-color: white;
456
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
457
+ opacity: 0;
458
+ transition: opacity 0.2s ease;
459
+ }
460
+
461
+ .video-progress:hover .video-progress-handle {
462
+ opacity: 1;
463
+ }
464
+
465
+ .video-control-button {
466
+ background: transparent;
467
+ border: none;
468
+ color: white;
469
+ cursor: pointer;
470
+ width: 36px;
471
+ height: 36px;
472
+ display: flex;
473
+ align-items: center;
474
+ justify-content: center;
475
+ border-radius: 50%;
476
+ transition: all 0.2s ease;
477
+ }
478
+
479
+ .video-control-button:hover {
480
+ background-color: rgba(255, 255, 255, 0.2);
481
+ }
482
+
483
+ .video-time {
484
+ color: white;
485
+ font-family: var(--font-mono);
486
+ font-size: 0.85rem;
487
+ min-width: 50px;
488
+ text-align: center;
489
+ }
490
+
491
+ .video-preview.playing .video-play-icon {
492
+ display: none;
493
+ }
494
+
495
+ .video-preview:not(.playing) .video-pause-icon {
496
+ display: none;
497
+ }
498
+
499
+ /* Action Buttons */
500
+ .action-buttons {
501
+ display: flex;
502
+ gap: 1rem;
503
+ margin-bottom: 1.5rem;
504
+ }
505
+
506
+ .primary-button, .secondary-button {
507
+ padding: 0.75rem 1.5rem;
508
+ border: none;
509
+ border-radius: var(--radius-sm);
510
+ font-size: 1rem;
511
+ font-weight: 500;
512
+ cursor: pointer;
513
+ display: flex;
514
+ align-items: center;
515
+ gap: 0.5rem;
516
+ transition: all 0.2s ease;
517
+ }
518
+
519
+ .primary-button {
520
+ background-color: var(--primary-color);
521
+ color: white;
522
+ }
523
+
524
+ .primary-button:hover {
525
+ background-color: var(--primary-dark);
526
+ }
527
+
528
+ .secondary-button {
529
+ background-color: var(--secondary-color);
530
+ color: white;
531
+ }
532
+
533
+ .secondary-button:hover {
534
+ background-color: var(--secondary-dark);
535
+ }
536
+
537
+ .primary-button:disabled, .secondary-button:disabled {
538
+ background-color: var(--text-secondary);
539
+ cursor: not-allowed;
540
+ opacity: 0.7;
541
+ }
542
+
543
+ /* Progress Bar */
544
+ .progress-container {
545
+ background-color: var(--background-color);
546
+ padding: 1.5rem;
547
+ border-radius: var(--radius-md);
548
+ }
549
+
550
+ .status-text {
551
+ margin-bottom: 0.75rem;
552
+ color: var(--text-secondary);
553
+ }
554
+
555
+ .progress-bar {
556
+ height: 0.5rem;
557
+ background-color: var(--border-color);
558
+ border-radius: 999px;
559
+ overflow: hidden;
560
+ }
561
+
562
+ .progress {
563
+ height: 100%;
564
+ background-color: var(--primary-color);
565
+ transition: width 0.3s ease;
566
+ }
567
+
568
+ /* Results Panel */
569
+ .results-panel {
570
+ display: grid;
571
+ grid-template-columns: 1fr 1fr;
572
+ gap: 2rem;
573
+ max-height: calc(100vh - 600px);
574
+ min-height: 500px;
575
+ overflow: hidden;
576
+ }
577
+
578
+ .transcription-container, .topics-container {
579
+ background-color: var(--surface-color);
580
+ border-radius: var(--radius-lg);
581
+ padding: 1.5rem;
582
+ box-shadow: var(--shadow-md);
583
+ display: flex;
584
+ flex-direction: column;
585
+ max-height: 100%;
586
+ overflow: hidden;
587
+ }
588
+
589
+ h2 {
590
+ display: flex;
591
+ align-items: center;
592
+ gap: 0.5rem;
593
+ margin-bottom: 1rem;
594
+ padding-bottom: 0.75rem;
595
+ border-bottom: 2px solid var(--border-color);
596
+ }
597
+
598
+ .transcription-content, .topics-content {
599
+ max-height: 500px;
600
+ overflow-y: auto;
601
+ padding: 1rem;
602
+ background-color: var(--background-color);
603
+ border-radius: var(--radius-md);
604
+ font-size: 0.95rem;
605
+ line-height: 1.7;
606
+ height: 500px;
607
+ overflow-y: auto;
608
+ overflow-x: hidden;
609
+ word-wrap: break-word;
610
+ }
611
+
612
+ /* Topic Styles */
613
+ .topic {
614
+ margin-bottom: 1.5rem;
615
+ padding: 1rem;
616
+ background-color: var(--surface-color);
617
+ border-left: 4px solid var(--primary-color);
618
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
619
+ box-shadow: var(--shadow-sm);
620
+ }
621
+
622
+ .topic-header {
623
+ font-weight: 600;
624
+ color: var(--text-primary);
625
+ margin-bottom: 0.5rem;
626
+ }
627
+
628
+ .topic-content {
629
+ color: var(--text-secondary);
630
+ }
631
+
632
+ /* Mindmap Overlay */
633
+ .mindmap-overlay {
634
+ position: fixed;
635
+ top: 0;
636
+ left: 0;
637
+ width: 100vw;
638
+ height: 100vh;
639
+ background-color: var(--background-color);
640
+ z-index: 1000;
641
+ display: none;
642
+ }
643
+
644
+ .mindmap-overlay.active {
645
+ display: block;
646
+ }
647
+
648
+ .mindmap-content {
649
+ display: flex;
650
+ height: 100vh;
651
+ }
652
+
653
+ /* Video Section */
654
+ .video-section {
655
+ width: 40%;
656
+ padding: 2rem;
657
+ background-color: var(--surface-color);
658
+ border-right: 1px solid var(--border-color);
659
+ display: none;
660
+ }
661
+
662
+ .video-section.active {
663
+ display: block;
664
+ }
665
+
666
+ .video-player-container {
667
+ border-radius: var(--radius-md);
668
+ overflow: hidden;
669
+ background-color: #000;
670
+ margin-bottom: 1rem;
671
+ }
672
+
673
+ .video-player-container video {
674
+ width: 100%;
675
+ object-fit: contain;
676
+ display: block;
677
+ }
678
+
679
+ .video-info {
680
+ padding: 1rem;
681
+ background-color: var(--background-color);
682
+ border-radius: var(--radius-md);
683
+ font-size: 0.9rem;
684
+ color: var(--text-secondary);
685
+ }
686
+
687
+ /* Mindmap Section */
688
+ .mindmap-section {
689
+ flex: 1;
690
+ position: relative;
691
+ overflow: hidden;
692
+ }
693
+
694
+ .mindmap-toolbar {
695
+ position: absolute;
696
+ top: 1rem;
697
+ left: 1rem;
698
+ z-index: 100;
699
+ display: flex;
700
+ gap: 0.5rem;
701
+ background-color: var(--surface-color);
702
+ padding: 0.5rem;
703
+ border-radius: var(--radius-md);
704
+ box-shadow: var(--shadow-md);
705
+ }
706
+
707
+ .tool-button {
708
+ padding: 0.5rem 1rem;
709
+ border: none;
710
+ border-radius: var(--radius-sm);
711
+ background-color: var(--surface-color);
712
+ color: var(--text-primary);
713
+ cursor: pointer;
714
+ display: flex;
715
+ align-items: center;
716
+ gap: 0.5rem;
717
+ transition: all 0.2s ease;
718
+ }
719
+
720
+ .tool-button:hover {
721
+ background-color: var(--background-color);
722
+ color: var(--primary-color);
723
+ }
724
+
725
+ .tool-button.active {
726
+ background-color: var(--primary-color);
727
+ color: white;
728
+ }
729
+
730
+ .fullscreen-mindmap {
731
+ width: 100%;
732
+ height: calc(100vh - 4rem);
733
+ position: relative;
734
+ overflow: hidden;
735
+ }
736
+
737
+ /* Responsive Design */
738
+ @media (max-width: 1200px) {
739
+ .main-content {
740
+ padding: 0 1rem;
741
+ }
742
+
743
+ .results-panel {
744
+ grid-template-columns: 1fr;
745
+ }
746
+ }
747
+
748
+ @media (max-width: 768px) {
749
+ .upload-section {
750
+ grid-template-columns: 1fr;
751
+ }
752
+
753
+ .mindmap-content {
754
+ flex-direction: column;
755
+ }
756
+
757
+ .video-section {
758
+ width: 100%;
759
+ border-right: none;
760
+ border-bottom: 1px solid var(--border-color);
761
+ }
762
+
763
+ .tool-button {
764
+ padding: 0.5rem;
765
+ }
766
+
767
+ .tool-button span {
768
+ display: none;
769
+ }
770
+ }
771
+
772
+ @media (max-width: 480px) {
773
+ .action-buttons {
774
+ flex-direction: column;
775
+ }
776
+
777
+ .primary-button, .secondary-button {
778
+ width: 100%;
779
+ justify-content: center;
780
+ }
781
+ }
782
+
783
+ /* Custom Mindmap Node Styles */
784
+ jmnode {
785
+ box-shadow: var(--shadow-sm);
786
+ border-radius: var(--radius-md);
787
+ padding: 8px 12px !important;
788
+ transition: all 0.2s ease;
789
+ }
790
+
791
+ jmnode:hover {
792
+ box-shadow: var(--shadow-md);
793
+ transform: translateY(-1px);
794
+ }
795
+
796
+ /* Improved Expand/Collapse Buttons */
797
+ jmexpander {
798
+ width: 20px !important;
799
+ height: 20px !important;
800
+ border-radius: 50% !important;
801
+ background-color: var(--surface-color) !important;
802
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
803
+ display: flex !important;
804
+ align-items: center;
805
+ justify-content: center;
806
+ font-size: 16px !important;
807
+ font-weight: bold;
808
+ color: var(--primary-color) !important;
809
+ transition: all 0.2s ease;
810
+ text-indent: -9999px !important;
811
+ overflow: hidden !important;
812
+ font-size: 0 !important;
813
+ }
814
+
815
+ jmexpander:hover {
816
+ background-color: var(--primary-color) !important;
817
+ color: white !important;
818
+ transform: scale(1.1);
819
+ }
820
+
821
+ /* Custom icons for expander instead of +/- */
822
+ jmexpander.expand {
823
+ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%233498db" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>') !important;
824
+ background-position: center !important;
825
+ background-repeat: no-repeat !important;
826
+ background-size: 12px !important;
827
+ }
828
+
829
+ jmexpander.collapse {
830
+ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%233498db" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13H5v-2h14v2z"/></svg>') !important;
831
+ background-position: center !important;
832
+ background-repeat: no-repeat !important;
833
+ background-size: 12px !important;
834
+ }
835
+
836
+ jmexpander.expand:hover, jmexpander.collapse:hover {
837
+ background-color: var(--primary-color) !important;
838
+ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>') !important;
839
+ background-size: 12px !important;
840
+ }
841
+
842
+ jmexpander.collapse:hover {
843
+ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="24px" height="24px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13H5v-2h14v2z"/></svg>') !important;
844
+ }
845
+
846
+ /* Video Close Button */
847
+ .video-close-button {
848
+ position: absolute;
849
+ top: 1rem;
850
+ right: 1rem;
851
+ width: 36px;
852
+ height: 36px;
853
+ border-radius: 50%;
854
+ background-color: var(--surface-color);
855
+ color: var(--text-primary);
856
+ border: none;
857
+ display: flex;
858
+ align-items: center;
859
+ justify-content: center;
860
+ cursor: pointer;
861
+ box-shadow: var(--shadow-sm);
862
+ transition: all 0.2s ease;
863
+ z-index: 10;
864
+ }
865
+
866
+ .video-close-button:hover {
867
+ background-color: #f8f9fa;
868
+ color: var(--primary-color);
869
+ box-shadow: var(--shadow-md);
870
+ }
871
+
872
+ /* Line style improvements */
873
+ jmnode.root {
874
+ background-color: var(--primary-color) !important;
875
+ color: white !important;
876
+ }
877
+
878
+ jmnode.root jmexpander {
879
+ background-color: white !important;
880
+ }
881
+
882
+ /* Connection lines styling */
883
+ jmexpander::before {
884
+ border-color: var(--primary-color) !important;
885
+ }
886
+
887
+ /* Node Context Menu */
888
+ .node-context-menu {
889
+ position: absolute;
890
+ background-color: var(--surface-color);
891
+ border-radius: var(--radius-md);
892
+ box-shadow: var(--shadow-lg);
893
+ padding: 0.5rem 0;
894
+ z-index: 2000;
895
+ min-width: 180px;
896
+ font-size: 0.9rem;
897
+ opacity: 0;
898
+ transform: scale(0.95);
899
+ transform-origin: top left;
900
+ transition: opacity 0.15s ease, transform 0.15s ease;
901
+ pointer-events: none;
902
+ }
903
+
904
+ .node-context-menu.visible {
905
+ opacity: 1;
906
+ transform: scale(1);
907
+ pointer-events: auto;
908
+ }
909
+
910
+ .context-menu-item {
911
+ padding: 0.6rem 1rem;
912
+ display: flex;
913
+ align-items: center;
914
+ gap: 0.75rem;
915
+ cursor: pointer;
916
+ transition: all 0.15s ease;
917
+ color: var(--text-primary);
918
+ }
919
+
920
+ .context-menu-item:hover {
921
+ background-color: rgba(52, 152, 219, 0.1);
922
+ color: var(--primary-color);
923
+ }
924
+
925
+ .context-menu-item i {
926
+ width: 16px;
927
+ text-align: center;
928
+ }
929
+
930
+ .context-menu-separator {
931
+ height: 1px;
932
+ background-color: var(--border-color);
933
+ margin: 0.5rem 0;
934
+ }
935
+
936
+ /* Node color picker */
937
+ .color-picker-container {
938
+ padding: 0.5rem 1rem;
939
+ }
940
+
941
+ .color-picker-label {
942
+ display: block;
943
+ margin-bottom: 0.5rem;
944
+ font-size: 0.9rem;
945
+ color: var(--text-secondary);
946
+ }
947
+
948
+ .color-options {
949
+ display: flex;
950
+ flex-wrap: wrap;
951
+ gap: 0.5rem;
952
+ }
953
+
954
+ .color-option {
955
+ width: 24px;
956
+ height: 24px;
957
+ border-radius: 50%;
958
+ cursor: pointer;
959
+ position: relative;
960
+ transition: transform 0.2s ease;
961
+ }
962
+
963
+ .color-option:hover {
964
+ transform: scale(1.15);
965
+ }
966
+
967
+ .color-option.selected::after {
968
+ content: '';
969
+ position: absolute;
970
+ top: 50%;
971
+ left: 50%;
972
+ transform: translate(-50%, -50%);
973
+ width: 10px;
974
+ height: 10px;
975
+ border-radius: 50%;
976
+ background-color: white;
977
+ }
978
+
979
+ /* Node edit input */
980
+ .node-edit-input {
981
+ position: absolute;
982
+ z-index: 2000;
983
+ padding: 8px 12px;
984
+ border: 2px solid var(--primary-color);
985
+ border-radius: var(--radius-md);
986
+ box-shadow: var(--shadow-md);
987
+ font-family: inherit;
988
+ font-size: 14px;
989
+ background-color: var(--surface-color);
990
+ width: auto;
991
+ min-width: 150px;
992
+ }
993
+
994
+ /* Custom node colors */
995
+ jmnode.node-blue {
996
+ background-color: #3498db !important;
997
+ color: white !important;
998
+ }
999
+
1000
+ jmnode.node-green {
1001
+ background-color: #2ecc71 !important;
1002
+ color: white !important;
1003
+ }
1004
+
1005
+ jmnode.node-red {
1006
+ background-color: #e74c3c !important;
1007
+ color: white !important;
1008
+ }
1009
+
1010
+ jmnode.node-purple {
1011
+ background-color: #9b59b6 !important;
1012
+ color: white !important;
1013
+ }
1014
+
1015
+ jmnode.node-orange {
1016
+ background-color: #e67e22 !important;
1017
+ color: white !important;
1018
+ }
1019
+
1020
+ jmnode.node-yellow {
1021
+ background-color: #f1c40f !important;
1022
+ color: #34495e !important;
1023
+ }
1024
+
1025
+ jmnode.node-teal {
1026
+ background-color: #1abc9c !important;
1027
+ color: white !important;
1028
+ }
1029
+
1030
+ jmnode.node-gray {
1031
+ background-color: #95a5a6 !important;
1032
+ color: white !important;
1033
+ }
1034
+
1035
+ /* Node hover states with color indication */
1036
+ jmnode.node-blue:hover, jmnode.node-green:hover, jmnode.node-red:hover,
1037
+ jmnode.node-purple:hover, jmnode.node-orange:hover, jmnode.node-yellow:hover,
1038
+ jmnode.node-teal:hover, jmnode.node-gray:hover {
1039
+ filter: brightness(1.1);
1040
+ }
1041
+
1042
+ /* Floating Action Button (FAB) */
1043
+ .fab-container {
1044
+ position: fixed;
1045
+ bottom: 2rem;
1046
+ right: 2rem;
1047
+ z-index: 999;
1048
+ display: flex;
1049
+ flex-direction: column-reverse;
1050
+ align-items: center;
1051
+ gap: 0.75rem;
1052
+ }
1053
+
1054
+ .fab-button {
1055
+ width: 3.5rem;
1056
+ height: 3.5rem;
1057
+ border-radius: 50%;
1058
+ background-color: var(--primary-color);
1059
+ color: white;
1060
+ border: none;
1061
+ box-shadow: var(--shadow-lg);
1062
+ display: flex;
1063
+ align-items: center;
1064
+ justify-content: center;
1065
+ font-size: 1.5rem;
1066
+ cursor: pointer;
1067
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
1068
+ }
1069
+
1070
+ .fab-button:hover {
1071
+ transform: scale(1.1);
1072
+ background-color: var(--primary-dark);
1073
+ }
1074
+
1075
+ .fab-button:active {
1076
+ transform: scale(0.95);
1077
+ }
1078
+
1079
+ .fab-options {
1080
+ display: flex;
1081
+ flex-direction: column;
1082
+ gap: 0.75rem;
1083
+ opacity: 0;
1084
+ pointer-events: none;
1085
+ transform: translateY(1rem);
1086
+ transition: all 0.3s ease;
1087
+ }
1088
+
1089
+ .fab-container.active .fab-options {
1090
+ opacity: 1;
1091
+ pointer-events: auto;
1092
+ transform: translateY(0);
1093
+ }
1094
+
1095
+ .fab-container.active .fab-button {
1096
+ transform: rotate(45deg);
1097
+ background-color: var(--accent-color);
1098
+ }
1099
+
1100
+ .fab-option {
1101
+ width: 2.5rem;
1102
+ height: 2.5rem;
1103
+ border-radius: 50%;
1104
+ background-color: var(--surface-color);
1105
+ color: var(--text-primary);
1106
+ border: none;
1107
+ box-shadow: var(--shadow-md);
1108
+ display: flex;
1109
+ align-items: center;
1110
+ justify-content: center;
1111
+ font-size: 1rem;
1112
+ cursor: pointer;
1113
+ transition: all 0.2s ease;
1114
+ position: relative;
1115
+ }
1116
+
1117
+ .fab-option:hover {
1118
+ transform: scale(1.1);
1119
+ color: var(--primary-color);
1120
+ }
1121
+
1122
+ .fab-option:hover::after {
1123
+ content: attr(title);
1124
+ position: absolute;
1125
+ right: calc(100% + 0.75rem);
1126
+ background-color: var(--surface-color);
1127
+ color: var(--text-primary);
1128
+ padding: 0.25rem 0.5rem;
1129
+ border-radius: var(--radius-sm);
1130
+ font-size: 0.8rem;
1131
+ white-space: nowrap;
1132
+ box-shadow: var(--shadow-sm);
1133
+ }
1134
+
1135
+ /* Results Gallery */
1136
+ .gallery-container {
1137
+ position: fixed;
1138
+ top: 0;
1139
+ left: 0;
1140
+ width: 100%;
1141
+ height: 100%;
1142
+ background-color: rgba(0, 0, 0, 0.5);
1143
+ z-index: 2000;
1144
+ display: flex;
1145
+ align-items: center;
1146
+ justify-content: center;
1147
+ opacity: 0;
1148
+ pointer-events: none;
1149
+ transition: opacity 0.3s ease;
1150
+ }
1151
+
1152
+ .gallery-container.active {
1153
+ opacity: 1;
1154
+ pointer-events: auto;
1155
+ }
1156
+
1157
+ .gallery-content {
1158
+ width: 90%;
1159
+ max-width: 1200px;
1160
+ height: 80%;
1161
+ background-color: var(--surface-color);
1162
+ border-radius: var(--radius-lg);
1163
+ box-shadow: var(--shadow-lg);
1164
+ overflow: hidden;
1165
+ display: flex;
1166
+ flex-direction: column;
1167
+ transform: scale(0.95);
1168
+ transition: transform 0.3s ease;
1169
+ }
1170
+
1171
+ .gallery-container.active .gallery-content {
1172
+ transform: scale(1);
1173
+ }
1174
+
1175
+ .gallery-header {
1176
+ display: flex;
1177
+ align-items: center;
1178
+ justify-content: space-between;
1179
+ padding: 1rem 1.5rem;
1180
+ border-bottom: 1px solid var(--border-color);
1181
+ }
1182
+
1183
+ .gallery-header h2 {
1184
+ margin: 0;
1185
+ font-size: 1.5rem;
1186
+ color: var(--text-primary);
1187
+ }
1188
+
1189
+ .gallery-close {
1190
+ width: 2rem;
1191
+ height: 2rem;
1192
+ border-radius: 50%;
1193
+ background-color: transparent;
1194
+ color: var(--text-primary);
1195
+ border: none;
1196
+ display: flex;
1197
+ align-items: center;
1198
+ justify-content: center;
1199
+ font-size: 1.2rem;
1200
+ cursor: pointer;
1201
+ transition: all 0.2s ease;
1202
+ }
1203
+
1204
+ .gallery-close:hover {
1205
+ background-color: rgba(0, 0, 0, 0.1);
1206
+ color: var(--primary-color);
1207
+ }
1208
+
1209
+ .gallery-content {
1210
+ padding: 1.5rem;
1211
+ overflow-y: auto;
1212
+ display: grid;
1213
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1214
+ gap: 1.5rem;
1215
+ }
1216
+
1217
+ .gallery-item {
1218
+ border-radius: var(--radius-md);
1219
+ overflow: hidden;
1220
+ box-shadow: var(--shadow-md);
1221
+ background-color: var(--background-color);
1222
+ transition: all 0.2s ease;
1223
+ cursor: pointer;
1224
+ aspect-ratio: 16/9;
1225
+ position: relative;
1226
+ }
1227
+
1228
+ .gallery-item:hover {
1229
+ transform: translateY(-0.5rem);
1230
+ box-shadow: var(--shadow-lg);
1231
+ }
1232
+
1233
+ .gallery-item-preview {
1234
+ width: 100%;
1235
+ height: 75%;
1236
+ object-fit: cover;
1237
+ background-color: var(--primary-color);
1238
+ opacity: 0.8;
1239
+ }
1240
+
1241
+ .gallery-item-info {
1242
+ padding: 0.75rem;
1243
+ display: flex;
1244
+ flex-direction: column;
1245
+ }
1246
+
1247
+ .gallery-item-title {
1248
+ font-weight: 600;
1249
+ font-size: 1rem;
1250
+ color: var(--text-primary);
1251
+ margin-bottom: 0.25rem;
1252
+ display: -webkit-box;
1253
+ -webkit-line-clamp: 1;
1254
+ -webkit-box-orient: vertical;
1255
+ overflow: hidden;
1256
+ }
1257
+
1258
+ .gallery-item-date {
1259
+ font-size: 0.8rem;
1260
+ color: var(--text-secondary);
1261
+ }
1262
+
1263
+ /* Dark Mode Adjustments for existing components */
1264
+ [data-theme="dark"] .app-header {
1265
+ background-color: var(--surface-color);
1266
+ border-bottom: 1px solid var(--border-color);
1267
+ }
1268
+
1269
+ [data-theme="dark"] .logo-icon {
1270
+ color: var(--primary-color);
1271
+ }
1272
+
1273
+ [data-theme="dark"] .transcription-content,
1274
+ [data-theme="dark"] .topics-content {
1275
+ background-color: #2d3748;
1276
+ border: 1px solid var(--border-color);
1277
+ }
1278
+
1279
+ [data-theme="dark"] .progress-container {
1280
+ background-color: #2d3748;
1281
+ }
1282
+
1283
+ [data-theme="dark"] .topic {
1284
+ background-color: #2d3748;
1285
+ border-left: 4px solid var(--primary-color);
1286
+ }
1287
+
1288
+ [data-theme="dark"] jmnode {
1289
+ background-color: #2d3748 !important;
1290
+ color: var(--text-primary) !important;
1291
+ }
1292
+
1293
+ [data-theme="dark"] jmnode.root {
1294
+ background-color: var(--primary-color) !important;
1295
+ color: white !important;
1296
+ }
1297
+
1298
+ [data-theme="dark"] jmexpander {
1299
+ background-color: #4a5568 !important;
1300
+ }
1301
+
1302
+ [data-theme="dark"] .node-context-menu {
1303
+ background-color: #2d3748;
1304
+ border: 1px solid var(--border-color);
1305
+ }
1306
+
1307
+ [data-theme="dark"] .context-menu-separator {
1308
+ background-color: var(--border-color);
1309
+ }
1310
+
1311
+ [data-theme="dark"] .context-menu-item:hover {
1312
+ background-color: #4a5568;
1313
+ }
1314
+
1315
+ /* 1. Elevated Card Design System */
1316
+ .card {
1317
+ background-color: var(--surface-color);
1318
+ border-radius: var(--radius-lg);
1319
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05),
1320
+ 0 8px 10px -6px rgba(0, 0, 0, 0.02);
1321
+ overflow: hidden;
1322
+ border: 1px solid rgba(0, 0, 0, 0.04);
1323
+ transition: all 0.3s ease;
1324
+ }
1325
+
1326
+ .card:hover {
1327
+ box-shadow: 0 15px 30px -5px rgba(0, 0, 0, 0.07),
1328
+ 0 10px 15px -3px rgba(0, 0, 0, 0.04);
1329
+ transform: translateY(-2px);
1330
+ }
1331
+
1332
+ .card-header {
1333
+ padding: 1.25rem 1.5rem;
1334
+ border-bottom: 1px solid var(--border-color);
1335
+ display: flex;
1336
+ align-items: center;
1337
+ justify-content: space-between;
1338
+ }
1339
+
1340
+ .card-title {
1341
+ font-family: var(--font-heading);
1342
+ font-weight: 600;
1343
+ font-size: 1.2rem;
1344
+ margin: 0;
1345
+ display: flex;
1346
+ align-items: center;
1347
+ gap: 0.75rem;
1348
+ color: var(--text-primary);
1349
+ }
1350
+
1351
+ .card-title i {
1352
+ color: var(--primary-color);
1353
+ font-size: 1.3rem;
1354
+ }
1355
+
1356
+ .card-body {
1357
+ padding: 1.5rem;
1358
+ }
1359
+
1360
+ .card-footer {
1361
+ padding: 1.25rem 1.5rem;
1362
+ border-top: 1px solid var(--border-color);
1363
+ background-color: rgba(0, 0, 0, 0.01);
1364
+ }
1365
+
1366
+ /* 2. Micro-Interactions & Transitions */
1367
+ @keyframes fadeIn {
1368
+ from { opacity: 0; transform: translateY(10px); }
1369
+ to { opacity: 1; transform: translateY(0); }
1370
+ }
1371
+
1372
+ @keyframes pulse {
1373
+ 0% { transform: scale(1); }
1374
+ 50% { transform: scale(1.05); }
1375
+ 100% { transform: scale(1); }
1376
+ }
1377
+
1378
+ @keyframes shimmer {
1379
+ 0% { background-position: -1000px 0; }
1380
+ 100% { background-position: 1000px 0; }
1381
+ }
1382
+
1383
+ .animate-fade-in {
1384
+ animation: fadeIn 0.4s ease forwards;
1385
+ }
1386
+
1387
+ .animate-pulse {
1388
+ animation: pulse 2s infinite ease-in-out;
1389
+ }
1390
+
1391
+ /* 4. Custom Video Player Controls */
1392
+ .video-preview {
1393
+ margin: 2rem 0;
1394
+ border-radius: var(--radius-md);
1395
+ overflow: hidden;
1396
+ background-color: #000;
1397
+ position: relative;
1398
+ box-shadow: 0 15px 25px -5px rgba(0, 0, 0, 0.15),
1399
+ 0 10px 10px -5px rgba(0, 0, 0, 0.05);
1400
+ }
1401
+
1402
+ .video-preview::after {
1403
+ content: '';
1404
+ position: absolute;
1405
+ bottom: 0;
1406
+ left: 0;
1407
+ right: 0;
1408
+ height: 90px;
1409
+ background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 100%);
1410
+ pointer-events: none;
1411
+ opacity: 0;
1412
+ transition: opacity 0.3s ease;
1413
+ }
1414
+
1415
+ .video-preview:hover::after {
1416
+ opacity: 1;
1417
+ }
1418
+
1419
+ .video-controls {
1420
+ position: absolute;
1421
+ bottom: 0;
1422
+ left: 0;
1423
+ width: 100%;
1424
+ padding: 1rem;
1425
+ display: flex;
1426
+ align-items: center;
1427
+ gap: 1rem;
1428
+ z-index: 10;
1429
+ opacity: 0;
1430
+ transform: translateY(10px);
1431
+ transition: all 0.3s ease;
1432
+ }
1433
+
1434
+ .video-preview:hover .video-controls {
1435
+ opacity: 1;
1436
+ transform: translateY(0);
1437
+ }
1438
+
1439
+ .video-progress {
1440
+ flex: 1;
1441
+ height: 4px;
1442
+ background-color: rgba(255, 255, 255, 0.2);
1443
+ border-radius: 2px;
1444
+ overflow: hidden;
1445
+ cursor: pointer;
1446
+ position: relative;
1447
+ }
1448
+
1449
+ .video-progress-bar {
1450
+ height: 100%;
1451
+ width: 0;
1452
+ background-color: var(--primary-color);
1453
+ border-radius: 2px;
1454
+ position: relative;
1455
+ }
1456
+
1457
+ .video-progress-handle {
1458
+ position: absolute;
1459
+ right: -6px;
1460
+ top: 50%;
1461
+ transform: translateY(-50%);
1462
+ width: 12px;
1463
+ height: 12px;
1464
+ border-radius: 50%;
1465
+ background-color: white;
1466
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
1467
+ opacity: 0;
1468
+ transition: opacity 0.2s ease;
1469
+ }
1470
+
1471
+ .video-progress:hover .video-progress-handle {
1472
+ opacity: 1;
1473
+ }
1474
+
1475
+ .video-control-button {
1476
+ background: transparent;
1477
+ border: none;
1478
+ color: white;
1479
+ cursor: pointer;
1480
+ width: 36px;
1481
+ height: 36px;
1482
+ display: flex;
1483
+ align-items: center;
1484
+ justify-content: center;
1485
+ border-radius: 50%;
1486
+ transition: all 0.2s ease;
1487
+ }
1488
+
1489
+ .video-control-button:hover {
1490
+ background-color: rgba(255, 255, 255, 0.2);
1491
+ }
1492
+
1493
+ .video-time {
1494
+ color: white;
1495
+ font-family: var(--font-mono);
1496
+ font-size: 0.85rem;
1497
+ min-width: 50px;
1498
+ text-align: center;
1499
+ }
1500
+
1501
+ .video-preview.playing .video-play-icon {
1502
+ display: none;
1503
+ }
1504
+
1505
+ .video-preview:not(.playing) .video-pause-icon {
1506
+ display: none;
1507
+ }
1508
+
1509
+ /* 5. Streamlined Action Buttons */
1510
+ .action-button {
1511
+ padding: 0.9rem 1.8rem;
1512
+ border: none;
1513
+ border-radius: var(--radius-md);
1514
+ font-size: 1rem;
1515
+ font-weight: 600;
1516
+ font-family: var(--font-heading);
1517
+ cursor: pointer;
1518
+ display: flex;
1519
+ align-items: center;
1520
+ justify-content: center;
1521
+ gap: 0.75rem;
1522
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
1523
+ position: relative;
1524
+ overflow: hidden;
1525
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
1526
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
1527
+ }
1528
+
1529
+ .action-button::before {
1530
+ content: '';
1531
+ position: absolute;
1532
+ top: 0;
1533
+ left: 0;
1534
+ width: 100%;
1535
+ height: 100%;
1536
+ background: linear-gradient(120deg, rgba(255,255,255,0) 30%, rgba(255,255,255,0.3) 50%, rgba(255,255,255,0) 70%);
1537
+ transform: translateX(-100%);
1538
+ transition: all 0.6s ease;
1539
+ }
1540
+
1541
+ .action-button:hover::before {
1542
+ transform: translateX(100%);
1543
+ }
1544
+
1545
+ .action-button i {
1546
+ font-size: 1.1rem;
1547
+ transition: transform 0.3s ease;
1548
+ }
1549
+
1550
+ .action-button:hover i {
1551
+ transform: scale(1.2);
1552
+ }
1553
+
1554
+ .action-button:active {
1555
+ transform: translateY(2px);
1556
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
1557
+ }
1558
+
1559
+ .action-button.primary {
1560
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
1561
+ color: white;
1562
+ }
1563
+
1564
+ .action-button.primary:hover {
1565
+ background: linear-gradient(135deg, var(--primary-dark), var(--primary-color));
1566
+ box-shadow: 0 10px 15px -3px rgba(52, 152, 219, 0.3),
1567
+ 0 4px 6px -2px rgba(52, 152, 219, 0.15);
1568
+ }
1569
+
1570
+ .action-button.secondary {
1571
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-dark));
1572
+ color: white;
1573
+ }
1574
+
1575
+ .action-button.secondary:hover {
1576
+ background: linear-gradient(135deg, var(--secondary-dark), var(--secondary-color));
1577
+ box-shadow: 0 10px 15px -3px rgba(46, 204, 113, 0.3),
1578
+ 0 4px 6px -2px rgba(46, 204, 113, 0.15);
1579
+ }
1580
+
1581
+ .action-button.disabled {
1582
+ opacity: 0.7;
1583
+ cursor: not-allowed;
1584
+ background: #95a5a6;
1585
+ }
1586
+
1587
+ .action-button.disabled::before {
1588
+ display: none;
1589
+ }
1590
+
1591
+ .action-button.processing {
1592
+ position: relative;
1593
+ overflow: hidden;
1594
+ }
1595
+
1596
+ .action-button.processing::after {
1597
+ content: "";
1598
+ position: absolute;
1599
+ top: 0;
1600
+ left: 0;
1601
+ width: 100%;
1602
+ height: 100%;
1603
+ background: linear-gradient(90deg,
1604
+ transparent 25%,
1605
+ rgba(255, 255, 255, 0.3) 50%,
1606
+ transparent 75%);
1607
+ background-size: 200% 100%;
1608
+ animation: shimmer 1.5s infinite linear;
1609
+ }
1610
+
1611
+ /* 6. Content Container Improvements */
1612
+ .content-container {
1613
+ background-color: var(--surface-color);
1614
+ border-radius: var(--radius-lg);
1615
+ overflow: hidden;
1616
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05),
1617
+ 0 8px 10px -6px rgba(0, 0, 0, 0.02);
1618
+ display: flex;
1619
+ flex-direction: column;
1620
+ height: 100%;
1621
+ transition: all 0.3s ease;
1622
+ border: 1px solid var(--border-color);
1623
+ }
1624
+
1625
+ .content-header {
1626
+ padding: 1.25rem 1.5rem;
1627
+ border-bottom: 1px solid var(--border-color);
1628
+ display: flex;
1629
+ align-items: center;
1630
+ gap: 0.75rem;
1631
+ }
1632
+
1633
+ .content-title {
1634
+ font-family: var(--font-heading);
1635
+ font-weight: 600;
1636
+ font-size: 1.2rem;
1637
+ margin: 0;
1638
+ color: var(--text-primary);
1639
+ display: flex;
1640
+ align-items: center;
1641
+ gap: 0.75rem;
1642
+ }
1643
+
1644
+ .content-title i {
1645
+ color: var(--primary-color);
1646
+ font-size: 1.3rem;
1647
+ }
1648
+
1649
+ .content-body {
1650
+ flex: 1;
1651
+ overflow: hidden;
1652
+ position: relative;
1653
+ }
1654
+
1655
+ .content-scroll {
1656
+ height: 100%;
1657
+ overflow-y: auto;
1658
+ padding: 1.5rem;
1659
+ scrollbar-width: thin;
1660
+ scrollbar-color: var(--border-color) transparent;
1661
+ }
1662
+
1663
+ .content-scroll::-webkit-scrollbar {
1664
+ width: 6px;
1665
+ }
1666
+
1667
+ .content-scroll::-webkit-scrollbar-track {
1668
+ background: transparent;
1669
+ }
1670
+
1671
+ .content-scroll::-webkit-scrollbar-thumb {
1672
+ background-color: var(--border-color);
1673
+ border-radius: 20px;
1674
+ }
1675
+
1676
+ .empty-state {
1677
+ display: flex;
1678
+ flex-direction: column;
1679
+ align-items: center;
1680
+ justify-content: center;
1681
+ padding: 3rem 1.5rem;
1682
+ color: var(--text-secondary);
1683
+ text-align: center;
1684
+ }
1685
+
1686
+ .empty-state i {
1687
+ font-size: 3rem;
1688
+ margin-bottom: 1rem;
1689
+ opacity: 0.5;
1690
+ }
1691
+
1692
+ .empty-state-title {
1693
+ font-weight: 600;
1694
+ margin-bottom: 0.5rem;
1695
+ color: var(--text-primary);
1696
+ }
1697
+
1698
+ .empty-state-description {
1699
+ max-width: 300px;
1700
+ margin: 0 auto;
1701
+ font-size: 0.95rem;
1702
+ }
1703
+
1704
+ /* 7. Status/Progress Visualization */
1705
+ .status-progress {
1706
+ margin-top: 1.5rem;
1707
+ display: flex;
1708
+ flex-direction: column;
1709
+ gap: 0.75rem;
1710
+ }
1711
+
1712
+ .status-text {
1713
+ display: flex;
1714
+ align-items: center;
1715
+ gap: 0.5rem;
1716
+ color: var(--text-secondary);
1717
+ font-size: 0.95rem;
1718
+ }
1719
+
1720
+ .status-text i {
1721
+ color: var(--primary-color);
1722
+ }
1723
+
1724
+ .progress-container {
1725
+ background-color: var(--background-color);
1726
+ padding: 1.5rem;
1727
+ border-radius: var(--radius-md);
1728
+ overflow: hidden;
1729
+ }
1730
+
1731
+ .progress-steps {
1732
+ display: flex;
1733
+ margin-bottom: 1rem;
1734
+ position: relative;
1735
+ }
1736
+
1737
+ .progress-step {
1738
+ flex: 1;
1739
+ text-align: center;
1740
+ position: relative;
1741
+ padding-bottom: 1.5rem;
1742
+ }
1743
+
1744
+ .progress-step-indicator {
1745
+ width: 24px;
1746
+ height: 24px;
1747
+ border-radius: 50%;
1748
+ background-color: var(--surface-color);
1749
+ border: 2px solid var(--border-color);
1750
+ margin: 0 auto 0.5rem;
1751
+ display: flex;
1752
+ align-items: center;
1753
+ justify-content: center;
1754
+ position: relative;
1755
+ z-index: 2;
1756
+ transition: all 0.3s ease;
1757
+ }
1758
+
1759
+ .progress-step-line {
1760
+ position: absolute;
1761
+ top: 12px;
1762
+ left: 0;
1763
+ right: 0;
1764
+ height: 2px;
1765
+ background-color: var(--border-color);
1766
+ z-index: 1;
1767
+ }
1768
+
1769
+ .progress-step-icon {
1770
+ color: var(--text-secondary);
1771
+ font-size: 0.8rem;
1772
+ opacity: 0;
1773
+ transition: all 0.3s ease;
1774
+ }
1775
+
1776
+ .progress-step-label {
1777
+ font-size: 0.8rem;
1778
+ font-weight: 500;
1779
+ color: var(--text-secondary);
1780
+ transition: all 0.3s ease;
1781
+ }
1782
+
1783
+ .progress-step.active .progress-step-indicator {
1784
+ background-color: var(--primary-color);
1785
+ border-color: var(--primary-color);
1786
+ transform: scale(1.1);
1787
+ box-shadow: 0 0 0 4px rgba(52, 152, 219, 0.2);
1788
+ }
1789
+
1790
+ .progress-step.active .progress-step-icon {
1791
+ opacity: 1;
1792
+ color: white;
1793
+ }
1794
+
1795
+ .progress-step.active .progress-step-label {
1796
+ color: var(--primary-color);
1797
+ font-weight: 600;
1798
+ }
1799
+
1800
+ .progress-step.completed .progress-step-indicator {
1801
+ background-color: var(--primary-color);
1802
+ border-color: var(--primary-color);
1803
+ }
1804
+
1805
+ .progress-step.completed .progress-step-icon {
1806
+ opacity: 1;
1807
+ color: white;
1808
+ }
1809
+
1810
+ .progress-step.completed + .progress-step .progress-step-line,
1811
+ .progress-step.active + .progress-step .progress-step-line {
1812
+ background: linear-gradient(to right, var(--primary-color) 0%, var(--border-color) 100%);
1813
+ }
1814
+
1815
+ .progress-step.completed .progress-step-line {
1816
+ background-color: var(--primary-color);
1817
+ }
1818
+
1819
+ .progress-bar {
1820
+ height: 6px;
1821
+ background-color: var(--border-color);
1822
+ border-radius: 3px;
1823
+ overflow: hidden;
1824
+ margin: 0.5rem 0;
1825
+ }
1826
+
1827
+ .progress-bar-fill {
1828
+ height: 100%;
1829
+ background: linear-gradient(90deg, var(--primary-color), var(--primary-dark));
1830
+ border-radius: 3px;
1831
+ transition: width 0.3s ease;
1832
+ position: relative;
1833
+ overflow: hidden;
1834
+ }
1835
+
1836
+ .progress-bar-fill::after {
1837
+ content: "";
1838
+ position: absolute;
1839
+ top: 0;
1840
+ left: 0;
1841
+ right: 0;
1842
+ bottom: 0;
1843
+ background: linear-gradient(
1844
+ 90deg,
1845
+ rgba(255, 255, 255, 0) 0%,
1846
+ rgba(255, 255, 255, 0.4) 50%,
1847
+ rgba(255, 255, 255, 0) 100%
1848
+ );
1849
+ animation: shimmer 2s infinite linear;
1850
+ transform: translateX(-100%);
1851
+ }
1852
+
1853
+ /* Dark mode adjustments */
1854
+ [data-theme="dark"] .content-container {
1855
+ border-color: var(--border-color);
1856
+ }
1857
+
1858
+ [data-theme="dark"] .progress-container {
1859
+ background-color: #2d3748;
1860
+ }
1861
+
1862
+ [data-theme="dark"] .progress-step-indicator {
1863
+ background-color: #4a5568;
1864
+ border-color: #4a5568;
1865
+ }
1866
+
1867
+ [data-theme="dark"] .progress-step-line {
1868
+ background-color: #4a5568;
1869
+ }
1870
+
1871
+ [data-theme="dark"] .progress-bar {
1872
+ background-color: #4a5568;
1873
+ }
1874
+
1875
+ .transcription-content {
1876
+ width: 100%;
1877
+ line-height: 1.6;
1878
+ }
1879
+
1880
+ .transcription-content p {
1881
+ margin-bottom: 8px;
1882
+ padding: 8px;
1883
+ border-radius: var(--radius-sm);
1884
+ transition: background-color 0.2s ease;
1885
+ }
1886
+
1887
+ .transcription-content p:hover {
1888
+ background-color: rgba(52, 152, 219, 0.1);
1889
+ }
templates/index.html ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Flowify</title>
7
+
8
+ <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
9
+ <link type="text/css" rel="stylesheet" href="https://unpkg.com/jsmind@0.8.6/style/jsmind.css" />
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
11
+ </head>
12
+ <body>
13
+ <div class="app-container">
14
+ <header class="app-header">
15
+ <div class="logo-container">
16
+ <i class="fas fa-project-diagram logo-icon"></i>
17
+ <h1>Flowify</h1>
18
+ </div>
19
+ </header>
20
+
21
+ <!-- Theme Toggle Button -->
22
+ <div class="theme-toggle" id="themeToggle">
23
+ <div class="theme-toggle-icon light-icon">
24
+ <i class="fas fa-sun"></i>
25
+ </div>
26
+ <div class="theme-toggle-icon dark-icon">
27
+ <i class="fas fa-moon"></i>
28
+ </div>
29
+ </div>
30
+
31
+ <main class="main-content">
32
+ <div class="upload-panel card">
33
+ <div class="card-body">
34
+ <div class="upload-section">
35
+ <div class="upload-container" id="dropZone">
36
+ <div class="particles">
37
+ <!-- Particles will be added dynamically via JS -->
38
+ </div>
39
+ <div class="upload-icon-container">
40
+ <i class="fas fa-cloud-upload-alt upload-icon"></i>
41
+ </div>
42
+ <h3 class="upload-text">Drop video file here or click to upload</h3>
43
+ <p class="upload-subtext">Supports MP4, WebM, MOV formats</p>
44
+ <input type="file" id="fileInput" accept="video/*" style="display:none;">
45
+ <div class="upload-progress">
46
+ <div class="upload-progress-bar" style="width: 0%"></div>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="model-selector card">
51
+ <div class="card-header">
52
+ <h3 class="card-title"><i class="fas fa-cog"></i> Processing Model</h3>
53
+ </div>
54
+ <div class="card-body">
55
+ <div class="select-wrapper">
56
+ <select id="modelSelector">
57
+ <option value="Xenova/whisper-tiny.en">Whisper Tiny (Fast)</option>
58
+ <option value="Xenova/whisper-base.en">Whisper Base</option>
59
+ <option value="Xenova/whisper-small.en" selected>Whisper Small</option>
60
+ <option value="Xenova/whisper-medium.en">Whisper Medium</option>
61
+ </select>
62
+ <i class="fas fa-chevron-down select-icon"></i>
63
+ </div>
64
+ <div id="modelStatus" class="model-status">
65
+ <i class="fas fa-check-circle"></i> Model available
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="video-preview">
72
+ <video id="videoPlayer" controls></video>
73
+
74
+ <!-- Custom Video Controls -->
75
+ <div class="video-controls">
76
+ <button class="video-control-button" id="playPauseBtn">
77
+ <i class="fas fa-play video-play-icon"></i>
78
+ <i class="fas fa-pause video-pause-icon"></i>
79
+ </button>
80
+ <div class="video-time" id="currentTime">0:00</div>
81
+ <div class="video-progress">
82
+ <div class="video-progress-bar">
83
+ <div class="video-progress-handle"></div>
84
+ </div>
85
+ </div>
86
+ <div class="video-time" id="duration">0:00</div>
87
+ <button class="video-control-button" id="muteBtn">
88
+ <i class="fas fa-volume-up"></i>
89
+ </button>
90
+ <button class="video-control-button" id="fullscreenBtn">
91
+ <i class="fas fa-expand"></i>
92
+ </button>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="action-buttons">
97
+ <button id="transcribeButton" class="action-button primary" disabled>
98
+ <i class="fas fa-microphone"></i>
99
+ Transcribe Video
100
+ </button>
101
+ <button id="analyzeButton" class="action-button secondary" disabled>
102
+ <i class="fas fa-brain"></i>
103
+ Analyze Topics
104
+ </button>
105
+ </div>
106
+
107
+ <!-- New Progress Visualization -->
108
+ <div class="status-progress animate-fade-in">
109
+ <div class="status-text" id="status">
110
+ <i class="fas fa-info-circle"></i>
111
+ <span>Ready to transcribe</span>
112
+ </div>
113
+
114
+ <div class="progress-container">
115
+ <div class="progress-steps">
116
+ <div class="progress-step" id="uploadStep">
117
+ <div class="progress-step-line"></div>
118
+ <div class="progress-step-indicator">
119
+ <i class="fas fa-upload progress-step-icon"></i>
120
+ </div>
121
+ <div class="progress-step-label">Upload</div>
122
+ </div>
123
+ <div class="progress-step" id="transcribeStep">
124
+ <div class="progress-step-line"></div>
125
+ <div class="progress-step-indicator">
126
+ <i class="fas fa-microphone progress-step-icon"></i>
127
+ </div>
128
+ <div class="progress-step-label">Transcribe</div>
129
+ </div>
130
+ <div class="progress-step" id="analyzeStep">
131
+ <div class="progress-step-line"></div>
132
+ <div class="progress-step-indicator">
133
+ <i class="fas fa-brain progress-step-icon"></i>
134
+ </div>
135
+ <div class="progress-step-label">Analyze</div>
136
+ </div>
137
+ <div class="progress-step" id="visualizeStep">
138
+ <div class="progress-step-indicator">
139
+ <i class="fas fa-project-diagram progress-step-icon"></i>
140
+ </div>
141
+ <div class="progress-step-label">Visualize</div>
142
+ </div>
143
+ </div>
144
+
145
+ <div class="progress-bar">
146
+ <div class="progress-bar-fill" style="width: 0%"></div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+
153
+ <div class="results-panel">
154
+ <div class="content-container animate-fade-in">
155
+ <div class="content-header">
156
+ <h2 class="content-title"><i class="fas fa-file-alt"></i> Transcription</h2>
157
+ </div>
158
+ <div class="content-body">
159
+ <div id="transcription" class="content-scroll">
160
+ <div class="empty-state">
161
+ <i class="fas fa-file-alt"></i>
162
+ <h3 class="empty-state-title">No transcription yet</h3>
163
+ <p class="empty-state-description">Upload a video and click "Transcribe Video" to see the transcription here</p>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <div class="content-container animate-fade-in">
170
+ <div class="content-header">
171
+ <h2 class="content-title"><i class="fas fa-list"></i> Topics</h2>
172
+ </div>
173
+ <div class="content-body">
174
+ <div id="topicResults" class="content-scroll">
175
+ <div class="empty-state">
176
+ <i class="fas fa-brain"></i>
177
+ <h3 class="empty-state-title">No topics analyzed</h3>
178
+ <p class="empty-state-description">After transcribing, click "Analyze Topics" to identify key topics in your video</p>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </main>
185
+ </div>
186
+
187
+ <!-- Mindmap Overlay -->
188
+ <div id="mindmapOverlay" class="mindmap-overlay">
189
+ <div class="mindmap-content">
190
+ <div id="videoSection" class="video-section">
191
+ <div class="video-player-container">
192
+ <video id="overlayVideo" controls>
193
+ Your browser does not support the video element.
194
+ </video>
195
+ <button id="videoCloseButton" class="video-close-button">
196
+ <i class="fas fa-times"></i>
197
+ </button>
198
+ </div>
199
+ <div id="videoInfo" class="video-info"></div>
200
+ </div>
201
+
202
+ <div class="mindmap-section">
203
+ <div class="mindmap-toolbar">
204
+ <button id="backButton" class="tool-button">
205
+ <i class="fas fa-arrow-left"></i> Back
206
+ </button>
207
+ <button id="editButton" class="tool-button">
208
+ <i class="fas fa-edit"></i> Edit
209
+ </button>
210
+ <button id="zoomInButton" class="tool-button">
211
+ <i class="fas fa-search-plus"></i> Zoom In
212
+ </button>
213
+ <button id="zoomOutButton" class="tool-button">
214
+ <i class="fas fa-search-minus"></i> Zoom Out
215
+ </button>
216
+ <button id="panButton" class="tool-button">
217
+ <i class="fas fa-hand-paper"></i> Pan
218
+ </button>
219
+ <button id="resetButton" class="tool-button">
220
+ <i class="fas fa-sync"></i> Reset
221
+ </button>
222
+ </div>
223
+ <div id="mindmap" class="fullscreen-mindmap"></div>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <!-- Node Context Menu -->
229
+ <div id="nodeContextMenu" class="node-context-menu">
230
+ <div class="context-menu-item" id="editNodeAction">
231
+ <i class="fas fa-edit"></i>
232
+ Edit node
233
+ </div>
234
+ <div class="context-menu-item" id="addChildAction">
235
+ <i class="fas fa-plus-circle"></i>
236
+ Add child node
237
+ </div>
238
+ <div class="context-menu-item" id="addParentAction">
239
+ <i class="fas fa-level-up-alt"></i>
240
+ Add parent node
241
+ </div>
242
+ <div class="context-menu-separator"></div>
243
+ <div class="context-menu-item" id="focusNodeAction">
244
+ <i class="fas fa-search-plus"></i>
245
+ Focus on this branch
246
+ </div>
247
+ <div class="context-menu-item" id="expandCollapseAction">
248
+ <i class="fas fa-angle-double-down"></i>
249
+ Expand all children
250
+ </div>
251
+ <div class="context-menu-separator"></div>
252
+ <div class="context-menu-item" id="nodeColorAction">
253
+ <i class="fas fa-palette"></i>
254
+ Change color
255
+ </div>
256
+ <div class="color-picker-container" id="colorPicker" style="display: none;">
257
+ <div class="color-picker-label">Select a color:</div>
258
+ <div class="color-options">
259
+ <div class="color-option" style="background-color: #3498db;" data-color="blue"></div>
260
+ <div class="color-option" style="background-color: #2ecc71;" data-color="green"></div>
261
+ <div class="color-option" style="background-color: #e74c3c;" data-color="red"></div>
262
+ <div class="color-option" style="background-color: #9b59b6;" data-color="purple"></div>
263
+ <div class="color-option" style="background-color: #e67e22;" data-color="orange"></div>
264
+ <div class="color-option" style="background-color: #f1c40f;" data-color="yellow"></div>
265
+ <div class="color-option" style="background-color: #1abc9c;" data-color="teal"></div>
266
+ <div class="color-option" style="background-color: #95a5a6;" data-color="gray"></div>
267
+ </div>
268
+ </div>
269
+ <div class="context-menu-separator"></div>
270
+ <div class="context-menu-item" id="deleteNodeAction">
271
+ <i class="fas fa-trash-alt"></i>
272
+ Delete node
273
+ </div>
274
+ </div>
275
+
276
+ <!-- Floating Action Button (FAB) -->
277
+ <div class="fab-container" id="fabContainer">
278
+ <button class="fab-button" id="mainFab">
279
+ <i class="fas fa-plus"></i>
280
+ </button>
281
+ <div class="fab-options">
282
+ <button class="fab-option" id="exportPdfBtn" title="Export as PDF">
283
+ <i class="fas fa-file-pdf"></i>
284
+ </button>
285
+ <button class="fab-option" id="exportPngBtn" title="Export as PNG">
286
+ <i class="fas fa-file-image"></i>
287
+ </button>
288
+ <button class="fab-option" id="exportSvgBtn" title="Export as SVG">
289
+ <i class="fas fa-file-code"></i>
290
+ </button>
291
+ <button class="fab-option" id="shareMindmapBtn" title="Share Mindmap">
292
+ <i class="fas fa-share-alt"></i>
293
+ </button>
294
+ </div>
295
+ </div>
296
+
297
+ <!-- Results Gallery -->
298
+ <div class="gallery-container" id="resultsGallery">
299
+ <div class="gallery-header">
300
+ <h2>Recent Mindmaps</h2>
301
+ <button class="gallery-close" id="galleryClose">
302
+ <i class="fas fa-times"></i>
303
+ </button>
304
+ </div>
305
+ <div class="gallery-content" id="galleryContent">
306
+ <!-- Gallery items will be populated by JavaScript -->
307
+ </div>
308
+ </div>
309
+
310
+ <script src="https://cdn.jsdelivr.net/npm/@xenova/transformers@2.16.0"></script>
311
+ <script type="text/javascript" src="https://unpkg.com/jsmind@0.8.6/es6/jsmind.js"></script>
312
+ <script>window.jsMind = jsMind;</script>
313
+ <script type="module" src="{{ url_for('static', filename='script.js') }}"></script>
314
+ </body>
315
+ </html>
topic_segmenter.py ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from sklearn.feature_extraction.text import TfidfVectorizer
3
+ from sklearn.metrics.pairwise import cosine_similarity
4
+ from nltk.tokenize import sent_tokenize, word_tokenize
5
+ from nltk.stem import WordNetLemmatizer
6
+ from keybert import KeyBERT
7
+ import re
8
+ from collections import defaultdict
9
+ from sklearn.cluster import AgglomerativeClustering
10
+
11
+ class TopicSegmenter:
12
+ def __init__(self, window_size=3, similarity_threshold=0.2, context_size=2, min_segment_size=2, topic_similarity_threshold=0.3,
13
+ max_topics=8, hierarchical_threshold=0.6):
14
+ self.window_size = window_size
15
+ self.similarity_threshold = similarity_threshold
16
+ self.context_size = context_size
17
+ self.min_segment_size = min_segment_size
18
+ self.topic_similarity_threshold = topic_similarity_threshold
19
+ self.max_topics = max_topics # Maximum number of main topics
20
+ self.hierarchical_threshold = hierarchical_threshold # Threshold for hierarchical clustering
21
+ self.keyword_extractor = KeyBERT()
22
+
23
+ self.vectorizer = TfidfVectorizer(
24
+ stop_words='english',
25
+ min_df=1,
26
+ max_df=0.95,
27
+ ngram_range=(1, 2)
28
+ )
29
+
30
+ self.segment_vectorizer = TfidfVectorizer(
31
+ stop_words='english',
32
+ min_df=1,
33
+ max_df=1.0,
34
+ ngram_range=(1, 2)
35
+ )
36
+
37
+ self.lemmatizer = WordNetLemmatizer()
38
+ self.topic_history = []
39
+ self.topic_hierarchy = {} # Store parent-child relationships
40
+ self.fitted_vectorizer = None
41
+
42
+ def preprocess_text(self, text):
43
+ # First, split the text by timestamps
44
+ timestamp_chunks = re.findall(r'\[(\d+)\](.*?)(?=\[\d+\]|$)', text, re.DOTALL)
45
+
46
+ all_sentences = []
47
+ original_sentences = []
48
+ timestamp_mapping = [] # To track which timestamp each sentence belongs to
49
+
50
+ for timestamp, chunk_text in timestamp_chunks:
51
+ # Split each chunk into sentences
52
+ chunk_sentences = sent_tokenize(chunk_text.strip())
53
+ for sentence in chunk_sentences:
54
+ if sentence.strip():
55
+ # Keep track of the original sentence
56
+ original_sentences.append(sentence.strip())
57
+
58
+ # Clean and process the sentence for analysis
59
+ clean_sentence = re.sub(r'^[^:]+:', '', sentence).strip()
60
+ clean_sentence = re.sub(r'[^\w\s]', '', clean_sentence.lower())
61
+ words = word_tokenize(clean_sentence)
62
+ lemmatized = [self.lemmatizer.lemmatize(word) for word in words]
63
+ processed_sentence = ' '.join(lemmatized)
64
+
65
+ all_sentences.append(processed_sentence)
66
+ timestamp_mapping.append(int(timestamp))
67
+
68
+ return all_sentences, original_sentences, timestamp_mapping
69
+
70
+ def get_topic_fingerprint(self, segment_text):
71
+ cleaned_text = ' '.join([re.sub(r'^[^:]+:', '', sent).strip() for sent in segment_text])
72
+
73
+ if self.fitted_vectorizer is None:
74
+ self.fitted_vectorizer = self.segment_vectorizer.fit([cleaned_text])
75
+ tfidf_matrix = self.fitted_vectorizer.transform([cleaned_text])
76
+ else:
77
+ tfidf_matrix = self.fitted_vectorizer.transform([cleaned_text])
78
+
79
+ return tfidf_matrix.toarray()[0]
80
+
81
+ def compare_with_previous_topics(self, current_segment):
82
+ if not self.topic_history:
83
+ return None, 0.0
84
+
85
+ current_fingerprint = self.get_topic_fingerprint(current_segment)
86
+ max_similarity = 0.0
87
+ best_match_idx = None
88
+
89
+ for idx, (topic_fingerprint, _, _, _, _) in enumerate(self.topic_history):
90
+ if len(topic_fingerprint) != len(current_fingerprint):
91
+ continue
92
+
93
+ similarity = cosine_similarity([topic_fingerprint], [current_fingerprint])[0][0]
94
+ if similarity > max_similarity:
95
+ max_similarity = similarity
96
+ best_match_idx = idx
97
+
98
+ return best_match_idx, max_similarity
99
+
100
+ def extract_keywords(self, sentences, top_n=3):
101
+ if isinstance(sentences, list):
102
+ text = ' '.join(sentences)
103
+ else:
104
+ text = sentences
105
+
106
+ keywords = self.keyword_extractor.extract_keywords(
107
+ text,
108
+ keyphrase_ngram_range=(1, 2), # Allow for 1-2 word keyphrases
109
+ stop_words='english',
110
+ top_n=top_n
111
+ )
112
+
113
+ sorted_keywords = sorted(keywords, key=lambda x: x[1], reverse=True)
114
+ return [kw[0] for kw in sorted_keywords[:top_n]]
115
+
116
+ def detect_speaker_changes(self, text):
117
+ """
118
+ Detect points in the transcript where speakers change.
119
+ Returns a list of indices where speaker changes occur.
120
+ """
121
+ speaker_pattern = re.compile(r'(?:^|\s)(?:I\'m|And I\'m|Hey I\'m|I am)\s+([A-Z][a-z]+)', re.MULTILINE)
122
+ matches = list(speaker_pattern.finditer(text))
123
+
124
+ speaker_boundaries = []
125
+ for match in matches:
126
+ # Find the sentence containing this speaker introduction
127
+ sentences = sent_tokenize(text[:match.end()])
128
+ if sentences:
129
+ speaker_boundaries.append(len(sentences) - 1)
130
+
131
+ return speaker_boundaries
132
+
133
+ def find_closest_timestamp(self, sentence, transcript):
134
+ """
135
+ Find the timestamp after the most recent timestamp before the sentence.
136
+ """
137
+ chunks = re.findall(r'\[(\d+)\](.*?)(?=\[\d+\]|$)', transcript, re.DOTALL)
138
+ sentence = sentence.strip().lower()
139
+
140
+ previous_timestamp = 0
141
+ current_timestamp = 0
142
+
143
+ for i, (timestamp, text) in enumerate(chunks):
144
+ current_timestamp = int(timestamp)
145
+ text = text.strip().lower()
146
+
147
+ if sentence in text or text in sentence:
148
+ if i > 0 and i < len(chunks):
149
+ return current_timestamp
150
+ return previous_timestamp
151
+
152
+ previous_timestamp = current_timestamp
153
+
154
+ return current_timestamp
155
+
156
+ def calculate_topic_importance(self, segment, total_segments):
157
+ """
158
+ Calculate importance score for a topic based on:
159
+ 1. Length of the segment
160
+ 2. Position in the transcript
161
+ 3. Keyword significance
162
+ """
163
+ # Length factor - longer segments likely more important
164
+ length_score = min(1.0, len(segment) / 10) # Cap at 1.0 for segments of 10+ sentences
165
+
166
+ # Position factor - beginning and end often contain important information
167
+ position_in_doc = self.topic_history.index(segment) / total_segments
168
+ position_score = 1.0 - min(abs(position_in_doc - 0.0), abs(position_in_doc - 1.0))
169
+
170
+ # Keyword significance - look for key terms indicating importance
171
+ importance_indicators = ['important', 'key', 'main', 'primary', 'critical', 'essential',
172
+ 'significant', 'revolutionary', 'breakthrough', 'innovative']
173
+
174
+ text = ' '.join(segment)
175
+ significance_score = 0.0
176
+ for indicator in importance_indicators:
177
+ if indicator in text.lower():
178
+ significance_score += 0.2 # Add 0.2 for each indicator found
179
+ significance_score = min(1.0, significance_score) # Cap at 1.0
180
+
181
+ # Combined score, weighted
182
+ combined_score = (0.4 * length_score) + (0.3 * position_score) + (0.3 * significance_score)
183
+ return combined_score
184
+
185
+ def cluster_topics(self, segments):
186
+ """
187
+ Cluster similar topics together to form a hierarchy
188
+ """
189
+ if len(segments) <= 1:
190
+ return segments, []
191
+
192
+ # Extract features for each segment
193
+ feature_vectors = []
194
+ for segment_idx, segment in enumerate(segments):
195
+ feature_vectors.append(self.get_topic_fingerprint(segment))
196
+
197
+ feature_matrix = np.array(feature_vectors)
198
+
199
+ # Use hierarchical clustering with compatible parameters
200
+ num_clusters = min(self.max_topics, len(segments))
201
+
202
+ # Try different parameter combinations based on scikit-learn version compatibility
203
+ try:
204
+ # First attempt with basic parameters
205
+ clustering = AgglomerativeClustering(
206
+ n_clusters=num_clusters
207
+ )
208
+ labels = clustering.fit_predict(feature_matrix)
209
+ except Exception as e:
210
+ print(f"Clustering with default parameters failed: {str(e)}")
211
+ # Fallback to manual clustering based on similarity
212
+ labels = self.manual_clustering(feature_matrix, num_clusters)
213
+
214
+ # Group segments by cluster
215
+ clusters = defaultdict(list)
216
+ for segment_idx, cluster_id in enumerate(labels):
217
+ clusters[cluster_id].append(segment_idx)
218
+
219
+ # Create parent-child relationships
220
+ parent_child_map = {}
221
+
222
+ for cluster_id, segment_indices in clusters.items():
223
+ # If cluster has multiple segments, create a parent topic
224
+ if len(segment_indices) > 1:
225
+ # Combine segments to create a parent topic
226
+ all_content = []
227
+ for idx in segment_indices:
228
+ all_content.extend(segments[idx])
229
+
230
+ # Extract the most representative keywords
231
+ keywords = self.extract_keywords(all_content, top_n=3)
232
+ parent_topic = f"{', '.join(keywords)}"
233
+
234
+ # Add parent-child relationships
235
+ for idx in segment_indices:
236
+ parent_child_map[idx] = parent_topic
237
+
238
+ return labels, parent_child_map
239
+
240
+ def manual_clustering(self, feature_matrix, num_clusters):
241
+ """
242
+ Implement a simple similarity-based clustering as fallback
243
+ """
244
+ n_samples = feature_matrix.shape[0]
245
+
246
+ # If we have very few samples, each gets its own cluster
247
+ if n_samples <= num_clusters:
248
+ return np.arange(n_samples)
249
+
250
+ # Calculate similarity matrix
251
+ similarity = cosine_similarity(feature_matrix)
252
+
253
+ labels = np.arange(n_samples)
254
+ current_num_clusters = n_samples
255
+
256
+ while current_num_clusters > num_clusters:
257
+ max_similarity = -1
258
+ merge_i, merge_j = 0, 0
259
+
260
+ for i in range(n_samples):
261
+ for j in range(i+1, n_samples):
262
+
263
+ if labels[i] == labels[j]:
264
+ continue
265
+
266
+ if similarity[i, j] > max_similarity:
267
+ max_similarity = similarity[i, j]
268
+ merge_i, merge_j = i, j
269
+
270
+ old_label = labels[merge_j]
271
+ new_label = labels[merge_i]
272
+
273
+ for i in range(n_samples):
274
+ if labels[i] == old_label:
275
+ labels[i] = new_label
276
+
277
+ unique_labels = np.unique(labels)
278
+ mapping = {old: new for new, old in enumerate(unique_labels)}
279
+ labels = np.array([mapping[label] for label in labels])
280
+
281
+ current_num_clusters -= 1
282
+
283
+ return labels
284
+
285
+ def segment_transcript(self, text):
286
+ cleaned_sentences, original_sentences, timestamp_mapping = self.preprocess_text(text)
287
+
288
+ if len(cleaned_sentences) == 0:
289
+ return [], [], [], {}
290
+
291
+ similarity_matrix = self.calculate_similarity_matrix(cleaned_sentences)
292
+
293
+ content_boundaries = self.detect_topic_boundaries(similarity_matrix)
294
+
295
+ speaker_boundaries = self.detect_speaker_changes(text)
296
+
297
+ timestamp_boundaries = []
298
+ for i in range(1, len(timestamp_mapping)):
299
+ if timestamp_mapping[i] - timestamp_mapping[i-1] > 5: # If there's a gap of more than 5 seconds
300
+ timestamp_boundaries.append(i)
301
+
302
+ all_boundaries = sorted(set(content_boundaries + speaker_boundaries + timestamp_boundaries))
303
+
304
+ initial_segments = []
305
+ initial_segment_texts = []
306
+ current_topic_id = 0
307
+ self.topic_history = [] # Reset topic history
308
+
309
+ start_idx = 0
310
+ for boundary in all_boundaries + [len(original_sentences)]:
311
+ if boundary - start_idx < self.min_segment_size:
312
+ continue
313
+
314
+ current_segment = original_sentences[start_idx:boundary]
315
+ segment_timestamps = timestamp_mapping[start_idx:boundary]
316
+
317
+ closest_timestamp = segment_timestamps[0] if segment_timestamps else 0
318
+
319
+ # Extract keywords directly from content without predefined categories
320
+ keywords = self.extract_keywords(current_segment, top_n=4)
321
+
322
+ # Create topic name directly from the top keywords - completely dynamic
323
+ topic_name = f"{', '.join(keywords[:2])}"
324
+
325
+ # Calculate importance score
326
+ topic_importance = len(current_segment) / 3 # Simple approach: longer segments are more important
327
+
328
+ # Create a topic fingerprint
329
+ topic_fingerprint = self.get_topic_fingerprint(current_segment)
330
+
331
+ # Store topic information
332
+ self.topic_history.append((topic_fingerprint, topic_name, [current_segment], closest_timestamp, topic_importance))
333
+ initial_segments.append(current_segment)
334
+ initial_segment_texts.append(topic_name)
335
+
336
+ start_idx = boundary
337
+
338
+ # Cluster segments to form hierarchical topics
339
+ if len(initial_segments) > 1:
340
+ cluster_labels, parent_child_map = self.cluster_topics(initial_segments)
341
+ else:
342
+ cluster_labels = [0] if initial_segments else []
343
+ parent_child_map = {}
344
+
345
+ # Organize segments into hierarchical structure
346
+ final_segments = []
347
+ topic_mappings = []
348
+ topic_hierarchies = {}
349
+
350
+ # Group by parent topics
351
+ parent_topic_groups = defaultdict(list)
352
+
353
+ for i, segment in enumerate(initial_segments):
354
+ topic_id = i
355
+
356
+ # Check if this segment belongs to a parent topic
357
+ if i in parent_child_map:
358
+ parent_topic = parent_child_map[i]
359
+ parent_id = len(self.topic_history)
360
+
361
+ # If this is the first time seeing this parent topic
362
+ if parent_topic not in parent_topic_groups:
363
+ # Add the parent topic to our history
364
+ all_content = []
365
+ for j, seg in enumerate(initial_segments):
366
+ if j in parent_child_map and parent_child_map[j] == parent_topic:
367
+ all_content.extend(seg)
368
+
369
+ parent_fingerprint = self.get_topic_fingerprint(all_content)
370
+ parent_timestamp = self.topic_history[i][3] # Use timestamp of first segment
371
+ parent_importance = 1.0 # Parent topics are most important
372
+
373
+ self.topic_history.append((parent_fingerprint, parent_topic, [all_content], parent_timestamp, parent_importance))
374
+ topic_hierarchies[parent_id] = [j for j, seg in enumerate(initial_segments)
375
+ if j in parent_child_map and parent_child_map[j] == parent_topic]
376
+
377
+ # Find the parent ID
378
+ for j, (_, name, _, _, _) in enumerate(self.topic_history):
379
+ if name == parent_topic:
380
+ parent_id = j
381
+ break
382
+
383
+ # Add to parent topic group
384
+ parent_topic_groups[parent_topic].append((segment, topic_id))
385
+ else:
386
+ # This is already a main topic
387
+ final_segments.append(segment)
388
+ topic_mappings.append(topic_id)
389
+
390
+ # Add grouped segments under their parent topics
391
+ for parent_topic, segments_and_ids in parent_topic_groups.items():
392
+ # Find the parent ID
393
+ parent_id = None
394
+ for i, (_, name, _, _, _) in enumerate(self.topic_history):
395
+ if name == parent_topic:
396
+ parent_id = i
397
+ break
398
+
399
+ if parent_id is not None:
400
+ # Add parent as a segment
401
+ parent_content = []
402
+ for segment, _ in segments_and_ids:
403
+ parent_content.extend(segment)
404
+
405
+ final_segments.append(parent_content)
406
+ topic_mappings.append(parent_id)
407
+
408
+ # Add children segments
409
+ for segment, child_id in segments_and_ids:
410
+ final_segments.append(segment)
411
+ topic_mappings.append(child_id)
412
+
413
+ # For topics that don't have many segments, merge them if they're similar
414
+ if len(final_segments) > self.max_topics * 2:
415
+ merged_segments = []
416
+ merged_mappings = []
417
+ i = 0
418
+
419
+ while i < len(final_segments):
420
+ current_segment = final_segments[i]
421
+ current_mapping = topic_mappings[i]
422
+
423
+ # If this is a short segment, try to merge with next
424
+ if len(current_segment) <= 2 and i + 1 < len(final_segments):
425
+ next_segment = final_segments[i+1]
426
+ combined = current_segment + next_segment
427
+
428
+ # Use keywords to see if they're related
429
+ current_keywords = self.extract_keywords(current_segment)
430
+ next_keywords = self.extract_keywords(next_segment)
431
+
432
+ # Check for keyword overlap
433
+ overlap = any(kw in next_keywords for kw in current_keywords)
434
+
435
+ if overlap:
436
+ # Merge the segments
437
+ merged_segments.append(combined)
438
+ merged_mappings.append(current_mapping) # Keep the first mapping
439
+ i += 2 # Skip both segments
440
+ continue
441
+
442
+ # If no merge, keep as is
443
+ merged_segments.append(current_segment)
444
+ merged_mappings.append(current_mapping)
445
+ i += 1
446
+
447
+ final_segments = merged_segments
448
+ topic_mappings = merged_mappings
449
+
450
+ return final_segments, topic_mappings, self.topic_history, topic_hierarchies
451
+
452
+ def calculate_similarity_matrix(self, sentences):
453
+ if not sentences:
454
+ return np.array([[]])
455
+ tfidf_matrix = self.vectorizer.fit_transform(sentences)
456
+ return cosine_similarity(tfidf_matrix)
457
+
458
+ def detect_topic_boundaries(self, similarity_matrix):
459
+ boundaries = []
460
+ n_sentences = len(similarity_matrix)
461
+
462
+ if n_sentences <= 2 * self.window_size:
463
+ return []
464
+
465
+ for i in range(self.window_size, n_sentences - self.window_size):
466
+ prev_window = similarity_matrix[i-self.window_size:i, i-self.window_size:i]
467
+ prev_similarity = np.mean(prev_window)
468
+
469
+ next_window = similarity_matrix[i:i+self.window_size, i:i+self.window_size]
470
+ next_similarity = np.mean(next_window)
471
+
472
+ cross_window = similarity_matrix[i-self.window_size:i, i:i+self.window_size]
473
+ cross_similarity = np.mean(cross_window)
474
+
475
+ # Stricter conditions for detecting boundaries
476
+ if (cross_similarity < self.similarity_threshold and
477
+ cross_similarity < prev_similarity * 0.7 and # Increased constraint
478
+ cross_similarity < next_similarity * 0.7 and # Increased constraint
479
+ (len(boundaries) == 0 or i - boundaries[-1] >= self.min_segment_size)):
480
+ boundaries.append(i)
481
+
482
+ return boundaries
483
+
484
+ if __name__ == "__main__":
485
+ transcript = """
486
+ [0] Hey I'm Stanley, I'm a Stanford CS major. I did fun at Engineering at Facebook. [4] Hey I'm Andy, I'm also a Stanford CS major and I did platform engineering at Facebook. [8] I'm Evan, I was on the founding team of Vivo, the music video service. [12] And I'm Tony, I was a product editor at Square. [15] And the four of us came together about six months ago to work on software for small business owners. [20] But we didn't have a need at first so we just went out and talked to all the small business owners we could find. [24] [6] After over a hundred interviews, we came across a really interesting problem with small business restaurants in an area like this. [30] Yeah, so it turns out restaurants in Palo Alto don't deliver even though they really [34] want to, but they can't afford it. [36] But their consumers are craving for it, but the places that the consumers love just can't [41] deliver. [42] And we also found out about these delivery drivers who had a ton of spare time and they [45] all wanted an extra cash during that downtime. [48] Right, so that's when we built an initial product, PaloAltodelivery.com, and how it [53] worked is the customer goes to the website and places it all in order that gets automatically [59] [32] since the restaurant [60] And then we as the dispatchers with some pretty neat routing and badging algorithms were able to send the drivers efficiently to [66] Get the orders to the customers at a really fast time. Yeah, and the four of us actually started off as delivery drivers and [74] Over time we hired more as we grew and in our first month of launch with not much marketing [80] We got over 150 paying customers in the Palo Alto area, which was really awesome [84] And from that we generated over $10,000 in sales
487
+ """
488
+
489
+ segmenter = TopicSegmenter(
490
+ window_size=2, # Smaller window for more granular segmentation
491
+ similarity_threshold=0.15, # Lower threshold to detect more subtle changes
492
+ context_size=1,
493
+ min_segment_size=2, # Smaller minimum size for segments
494
+ topic_similarity_threshold=0.25, # Lower threshold to better distinguish topics
495
+ max_topics=5, # Target number of main topics
496
+ hierarchical_threshold=0.6 # Threshold for hierarchical clustering
497
+ )
498
+
499
+ segments, topic_mappings, topic_history, topic_hierarchies = segmenter.segment_transcript(transcript)
500
+
501
+ print("Topic Segmentation Analysis:\n")
502
+ print(f"Found {len(set(topic_mappings))} distinct topics")
503
+
504
+ # Print hierarchical topic structure
505
+ print("\nTopic Hierarchy:")
506
+ for parent_id, child_ids in topic_hierarchies.items():
507
+ parent_name = topic_history[parent_id][1]
508
+ print(f"Main Topic: {parent_name}")
509
+ for child_id in child_ids:
510
+ child_name = topic_history[child_id][1]
511
+ print(f" - Subtopic: {child_name}")
512
+
513
+ print("\nDetailed Segments:")
514
+ for i, (segment, topic_id) in enumerate(zip(segments, topic_mappings)):
515
+ print(f"Segment {i+1} (Part of {topic_history[topic_id][1]}):")
516
+ print(f"Closest Timestamp: {topic_history[topic_id][3]} seconds")
517
+ print("-" * 50)
518
+ print("\n".join(segment[:2]) + "..." if len(segment) > 2 else "\n".join(segment))
519
+ print("-" * 50 + "\n")