earwigmoth commited on
Commit
3e64e35
Β·
verified Β·
1 Parent(s): e928646

Upload 3 files

Browse files
Files changed (3) hide show
  1. script.js +1076 -0
  2. session.js +179 -0
  3. style.css +918 -0
script.js ADDED
@@ -0,0 +1,1076 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ SpeakFlow AI – script.js
3
+ Complete frontend logic for speech stuttering detection
4
+ ============================================================ */
5
+ // Auto-detect the backend URL so this works both locally and when deployed
6
+ // (e.g. Hugging Face Spaces). Locally it still points at the Flask dev
7
+ // server on 127.0.0.1:5000, exactly like before. Everywhere else, it calls
8
+ // the same origin the page was loaded from (no hardcoded localhost).
9
+ const API_BASE = (['localhost', '127.0.0.1'].includes(window.location.hostname))
10
+ ? "http://127.0.0.1:5000"
11
+ : window.location.origin; // Flask backend
12
+ 'use strict';
13
+
14
+ /* ─── GLOBAL STATE ─────────────────────────────────────────── */
15
+ let currentFile = null; // Uploaded File object
16
+ let currentResult = null; // Last analysis result object
17
+ let mediaRecorder = null; // MediaRecorder instance
18
+ let recordedChunks = []; // Collected audio chunks
19
+ let recordingTimer = null; // Interval for timer
20
+ let recordingSeconds = 0; // Seconds elapsed
21
+ let recBlob = null; // Blob from recorded audio
22
+ let liveWaveformAnim = null; // rAF handle for live waveform
23
+ let heroWaveformAnim = null; // rAF handle for hero waveform
24
+ let pieChartInst = null;
25
+ let barChartInst = null;
26
+ let gaugeChartInst = null;
27
+ let qualityChartInst = null;
28
+
29
+ /* ─── DOM HELPERS ──────────────────────────────────────────── */
30
+ const $ = (sel, ctx = document) => ctx.querySelector(sel);
31
+ const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
32
+
33
+ /* ─── DARK MODE ─────────────────────────────────────────────── */
34
+ (function initDarkMode() {
35
+ const saved = localStorage.getItem('speakflow-theme') || 'light';
36
+ setTheme(saved);
37
+ })();
38
+
39
+ function setTheme(theme) {
40
+ document.documentElement.setAttribute('data-theme', theme);
41
+ const icon = $('#darkModeIcon');
42
+ icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
43
+ localStorage.setItem('speakflow-theme', theme);
44
+ }
45
+
46
+ const darkToggle = $('#darkModeToggle');
47
+ if (darkToggle) {
48
+ darkToggle.addEventListener('click', () => {
49
+ const current = document.documentElement.getAttribute('data-theme');
50
+ setTheme(current === 'dark' ? 'light' : 'dark');
51
+ });
52
+ }
53
+
54
+ /* ─── MOBILE MENU ───────────────────────────────────────────── */
55
+ $('#hamburger').addEventListener('click', () => {
56
+ $('#navLinks').classList.toggle('open');
57
+ });
58
+ // Close menu on link click
59
+ $$('#navLinks a').forEach(a => a.addEventListener('click', () => {
60
+ $('#navLinks').classList.remove('open');
61
+ }));
62
+
63
+ /* ─── NAVBAR SCROLL ─────────────────────────────────────────── */
64
+ window.addEventListener('scroll', () => {
65
+ const navbar = $('#navbar');
66
+ if (window.scrollY > 40) {
67
+ navbar.style.boxShadow = '0 4px 24px rgba(0,0,0,0.12)';
68
+ } else {
69
+ navbar.style.boxShadow = 'none';
70
+ }
71
+ // Show/hide scroll-to-top button
72
+ const scrollBtn = $('#scrollTop');
73
+ if (window.scrollY > 400) {
74
+ scrollBtn.classList.add('visible');
75
+ } else {
76
+ scrollBtn.classList.remove('visible');
77
+ }
78
+ });
79
+
80
+ $('#scrollTop').addEventListener('click', () => {
81
+ window.scrollTo({ top: 0, behavior: 'smooth' });
82
+ });
83
+
84
+ /* ─── FLOATING PARTICLES ────────────────────────────────────── */
85
+ (function createParticles() {
86
+ const container = $('#particles');
87
+ const count = 28;
88
+ for (let i = 0; i < count; i++) {
89
+ const p = document.createElement('div');
90
+ p.className = 'particle';
91
+ const size = Math.random() * 6 + 3;
92
+ const left = Math.random() * 100;
93
+ const delay = Math.random() * 15;
94
+ const duration = Math.random() * 20 + 15;
95
+ p.style.cssText = `
96
+ width: ${size}px;
97
+ height: ${size}px;
98
+ left: ${left}%;
99
+ bottom: -${size}px;
100
+ animation-duration: ${duration}s;
101
+ animation-delay: ${delay}s;
102
+ opacity: ${Math.random() * 0.5 + 0.1};
103
+ `;
104
+ container.appendChild(p);
105
+ }
106
+ })();
107
+
108
+ /* ─── HERO WAVEFORM ANIMATION ───────────────────────────────── */
109
+ (function initHeroWaveform() {
110
+ const canvas = $('#heroWaveform');
111
+ if (!canvas) return;
112
+ const ctx = canvas.getContext('2d');
113
+ let t = 0;
114
+
115
+ function draw() {
116
+ const W = canvas.width;
117
+ const H = canvas.height;
118
+ ctx.clearRect(0, 0, W, H);
119
+ ctx.beginPath();
120
+ ctx.strokeStyle = 'rgba(59,130,246,0.8)';
121
+ ctx.lineWidth = 2;
122
+ for (let x = 0; x < W; x++) {
123
+ const freq1 = Math.sin((x / W) * Math.PI * 6 + t);
124
+ const freq2 = Math.sin((x / W) * Math.PI * 12 + t * 1.5) * 0.4;
125
+ const y = H / 2 + (freq1 + freq2) * (H / 2 - 8);
126
+ x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
127
+ }
128
+ ctx.stroke();
129
+
130
+ // Secondary wave
131
+ ctx.beginPath();
132
+ ctx.strokeStyle = 'rgba(20,184,166,0.5)';
133
+ ctx.lineWidth = 1.5;
134
+ for (let x = 0; x < W; x++) {
135
+ const freq1 = Math.sin((x / W) * Math.PI * 8 + t * 1.2);
136
+ const y = H / 2 + freq1 * (H / 2 - 12);
137
+ x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
138
+ }
139
+ ctx.stroke();
140
+
141
+ t += 0.04;
142
+ heroWaveformAnim = requestAnimationFrame(draw);
143
+ }
144
+ draw();
145
+ })();
146
+
147
+ /* ─── FILE UPLOAD ────────────────────────────────────────────── */
148
+ const dropZone = $('#dropZone');
149
+ const fileInput = $('#fileInput');
150
+ const browseBtn = $('#browseBtn');
151
+
152
+ browseBtn.addEventListener('click', () => fileInput.click());
153
+
154
+ fileInput.addEventListener('change', () => {
155
+ if (fileInput.files[0]) handleFile(fileInput.files[0]);
156
+ });
157
+
158
+ // Drag & Drop events
159
+ dropZone.addEventListener('dragover', e => {
160
+ e.preventDefault();
161
+ dropZone.classList.add('drag-over');
162
+ });
163
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
164
+ dropZone.addEventListener('drop', e => {
165
+ e.preventDefault();
166
+ dropZone.classList.remove('drag-over');
167
+ const f = e.dataTransfer.files[0];
168
+ if (f) handleFile(f);
169
+ });
170
+
171
+ /**
172
+ * Validate and display file preview.
173
+ */
174
+ function handleFile(file) {
175
+ const allowed = ['audio/wav', 'audio/mpeg', 'audio/mp3', 'audio/wave'];
176
+ const ext = file.name.split('.').pop().toLowerCase();
177
+ if (!allowed.includes(file.type) && !['wav','mp3'].includes(ext)) {
178
+ showToast('❌ Invalid file type. Please upload WAV or MP3.', 'error');
179
+ return;
180
+ }
181
+
182
+ currentFile = file;
183
+ const url = URL.createObjectURL(file);
184
+ const player = $('#audioPlayer');
185
+ player.src = url;
186
+
187
+ // File name & size
188
+ $('#fileName').textContent = file.name;
189
+ $('#fileSize').textContent = formatBytes(file.size);
190
+
191
+ // Get duration
192
+ const tempAudio = new Audio(url);
193
+ tempAudio.onloadedmetadata = () => {
194
+ $('#fileDuration').textContent = formatDuration(tempAudio.duration);
195
+ };
196
+
197
+ dropZone.style.display = 'none';
198
+ $('#filePreview').style.display = 'flex';
199
+ $('#filePreview').style.flexDirection = 'column';
200
+
201
+ showToast('βœ… Audio file loaded successfully!', 'success');
202
+ }
203
+
204
+ function formatBytes(bytes) {
205
+ if (bytes < 1024) return bytes + ' B';
206
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
207
+ return (bytes / 1048576).toFixed(1) + ' MB';
208
+ }
209
+
210
+ function formatDuration(sec) {
211
+ const m = Math.floor(sec / 60);
212
+ const s = Math.floor(sec % 60);
213
+ return `${m}:${s.toString().padStart(2, '0')}`;
214
+ }
215
+
216
+ // Clear file
217
+ $('#clearFileBtn').addEventListener('click', clearFile);
218
+ $('#clearBtn').addEventListener('click', clearFile);
219
+
220
+ function clearFile() {
221
+ currentFile = null;
222
+ fileInput.value = '';
223
+ $('#audioPlayer').src = '';
224
+ $('#filePreview').style.display = 'none';
225
+ dropZone.style.display = 'block';
226
+ }
227
+
228
+ /* ─── ANALYZE BUTTON ─────────────────────────────────────────── */
229
+ $('#analyzeBtn').addEventListener('click', () => {
230
+ if (!currentFile) { showToast('Please upload an audio file first.', 'error'); return; }
231
+ startAnalysis(currentFile.name);
232
+ });
233
+
234
+ /* ─── LIVE RECORDER ──────────────────────────────────────────── */
235
+ const startRecBtn = $('#startRecBtn');
236
+ const stopRecBtn = $('#stopRecBtn');
237
+ const playRecBtn = $('#playRecBtn');
238
+ const deleteRecBtn = $('#deleteRecBtn');
239
+ const analyzeRecBtn = $('#analyzeRecBtn');
240
+ const timerDisplay = $('#timerDisplay');
241
+ const recIndicator = $('#recIndicator');
242
+ const recPlayer = $('#recPlayer');
243
+ const liveCanvas = $('#liveWaveform');
244
+ const liveCtx = liveCanvas.getContext('2d');
245
+
246
+ let audioCtx, analyserNode, sourceNode, liveStream;
247
+
248
+ startRecBtn.addEventListener('click', async () => {
249
+ try {
250
+ liveStream = await navigator.mediaDevices.getUserMedia({ audio: true });
251
+ } catch {
252
+ showToast('Microphone access denied. Please allow microphone permission.', 'error');
253
+ return;
254
+ }
255
+
256
+ // Audio analyser for live waveform
257
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
258
+ analyserNode = audioCtx.createAnalyser();
259
+ analyserNode.fftSize = 256;
260
+ sourceNode = audioCtx.createMediaStreamSource(liveStream);
261
+ sourceNode.connect(analyserNode);
262
+ drawLiveWaveform();
263
+
264
+ recordedChunks = [];
265
+ mediaRecorder = new MediaRecorder(liveStream);
266
+ mediaRecorder.ondataavailable = e => { if (e.data.size > 0) recordedChunks.push(e.data); };
267
+ mediaRecorder.onstop = () => {
268
+ recBlob = new Blob(recordedChunks, { type: 'audio/webm' });
269
+ recPlayer.src = URL.createObjectURL(recBlob);
270
+ playRecBtn.disabled = false;
271
+ deleteRecBtn.disabled = false;
272
+ analyzeRecBtn.disabled = false;
273
+ analyzeRecBtn.style.display = 'inline-flex';
274
+ stopLiveWaveform();
275
+ audioCtx.close();
276
+ liveStream.getTracks().forEach(t => t.stop());
277
+ };
278
+ mediaRecorder.start();
279
+
280
+ // Timer
281
+ recordingSeconds = 0;
282
+ timerDisplay.textContent = '00:00';
283
+ recordingTimer = setInterval(() => {
284
+ recordingSeconds++;
285
+ const m = Math.floor(recordingSeconds / 60);
286
+ const s = recordingSeconds % 60;
287
+ timerDisplay.textContent = `${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`;
288
+ }, 1000);
289
+
290
+ recIndicator.classList.add('recording');
291
+ startRecBtn.disabled = true;
292
+ stopRecBtn.disabled = false;
293
+ playRecBtn.disabled = true;
294
+ deleteRecBtn.disabled = true;
295
+ });
296
+
297
+ stopRecBtn.addEventListener('click', () => {
298
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
299
+ mediaRecorder.stop();
300
+ }
301
+ clearInterval(recordingTimer);
302
+ recIndicator.classList.remove('recording');
303
+ startRecBtn.disabled = false;
304
+ stopRecBtn.disabled = true;
305
+ });
306
+
307
+ playRecBtn.addEventListener('click', () => { recPlayer.play(); });
308
+
309
+ deleteRecBtn.addEventListener('click', () => {
310
+ recBlob = null;
311
+ recPlayer.src = '';
312
+ recordingSeconds = 0;
313
+ timerDisplay.textContent = '00:00';
314
+ playRecBtn.disabled = true;
315
+ deleteRecBtn.disabled = true;
316
+ analyzeRecBtn.disabled = true;
317
+ analyzeRecBtn.style.display = 'none';
318
+ clearLiveWaveform();
319
+ showToast('Recording deleted.', 'info');
320
+ });
321
+
322
+ analyzeRecBtn.addEventListener('click', () => {
323
+ if (!recBlob) return;
324
+ startAnalysis('live-recording.webm');
325
+ });
326
+
327
+ /* Live waveform drawing */
328
+ function drawLiveWaveform() {
329
+ const W = liveCanvas.width;
330
+ const H = liveCanvas.height;
331
+ const bufferLen = analyserNode.frequencyBinCount;
332
+ const dataArr = new Uint8Array(bufferLen);
333
+
334
+ function render() {
335
+ liveWaveformAnim = requestAnimationFrame(render);
336
+ analyserNode.getByteTimeDomainData(dataArr);
337
+
338
+ liveCtx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--surface') || '#fff';
339
+ liveCtx.fillRect(0, 0, W, H);
340
+ liveCtx.lineWidth = 2;
341
+ liveCtx.strokeStyle = '#2563EB';
342
+ liveCtx.beginPath();
343
+ const sliceW = W / bufferLen;
344
+ let x = 0;
345
+ for (let i = 0; i < bufferLen; i++) {
346
+ const v = dataArr[i] / 128.0;
347
+ const y = v * H / 2;
348
+ i === 0 ? liveCtx.moveTo(x, y) : liveCtx.lineTo(x, y);
349
+ x += sliceW;
350
+ }
351
+ liveCtx.lineTo(W, H / 2);
352
+ liveCtx.stroke();
353
+ }
354
+ render();
355
+ }
356
+
357
+ function stopLiveWaveform() {
358
+ if (liveWaveformAnim) cancelAnimationFrame(liveWaveformAnim);
359
+ }
360
+
361
+ function clearLiveWaveform() {
362
+ liveCtx.clearRect(0, 0, liveCanvas.width, liveCanvas.height);
363
+ }
364
+
365
+ /* ─── ANALYSIS SIMULATION ────────────────────────────────────── */
366
+ /**
367
+ * Simulates the AI processing pipeline.
368
+ * In production this would POST to /api/predict and receive real results.
369
+ */
370
+ function startAnalysis(fileName) {
371
+ if (!currentFile && !recBlob) return;
372
+
373
+ const fileToSend = currentFile || recBlob;
374
+ sendToBackend(fileToSend, fileName);
375
+ }
376
+
377
+ /**
378
+ * Generate a simulated AI result and render the dashboard.
379
+ */
380
+ function generateResult(fileName) {
381
+ // Randomize result for demo purposes
382
+ const isNormal = Math.random() > 0.45;
383
+ const confidence = (Math.random() * 20 + 78).toFixed(1); // 78–98%
384
+ const fluency = isNormal
385
+ ? (Math.random() * 15 + 82).toFixed(1)
386
+ : (Math.random() * 30 + 30).toFixed(1);
387
+ const processingMs = Math.floor(Math.random() * 600 + 800);
388
+ const normalProb = isNormal
389
+ ? parseFloat(confidence)
390
+ : (100 - parseFloat(confidence)).toFixed(1);
391
+ const stutterProb = (100 - parseFloat(normalProb)).toFixed(1);
392
+
393
+ currentResult = {
394
+ fileName,
395
+ isNormal,
396
+ confidence: parseFloat(confidence),
397
+ fluency: parseFloat(fluency),
398
+ processingMs,
399
+ normalProb: parseFloat(normalProb),
400
+ stutterProb: parseFloat(stutterProb),
401
+ date: new Date().toLocaleString(),
402
+ };
403
+
404
+ renderResults(currentResult);
405
+ saveToHistory(currentResult);
406
+ renderHistory();
407
+
408
+ // Scroll to results
409
+ document.getElementById('results').scrollIntoView({ behavior: 'smooth' });
410
+ showToast('βœ… Analysis complete!', 'success');
411
+ }
412
+
413
+ /* ─── RENDER RESULTS ─────────────────────────────────────────── */
414
+ function renderResults(r) {
415
+ $('#resultsPlaceholder').style.display = 'none';
416
+ const content = $('#resultsContent');
417
+ content.style.display = 'flex';
418
+
419
+ // Prediction card
420
+ const card = $('#predictionCard');
421
+ card.className = 'prediction-card ' + (r.isNormal ? 'normal' : 'stutter');
422
+ $('#predIcon').textContent = r.isNormal ? 'βœ…' : '⚠️';
423
+ $('#predLabel').textContent = r.isNormal ? 'Normal Speech Detected' : 'Speech Stuttering Detected';
424
+ $('#predLabel').style.color = r.isNormal ? 'var(--green)' : 'var(--red)';
425
+ $('#predSublabel').textContent = r.isNormal
426
+ ? 'Your speech shows normal fluency characteristics.'
427
+ : 'Stuttering patterns were identified in your speech.';
428
+
429
+ // Score cards
430
+ $('#confidenceScore').textContent = r.confidence + '%';
431
+ $('#fluencyScore').textContent = r.fluency + '%';
432
+ $('#processingTime').textContent = r.processingMs + 'ms';
433
+ $('#predProbability').textContent = (r.isNormal ? r.normalProb : r.stutterProb) + '%';
434
+
435
+ // Charts
436
+ renderPieChart(r);
437
+ renderBarChart();
438
+ renderGaugeChart(r);
439
+ renderQualityChart(r);
440
+ renderResultWaveform(r);
441
+ }
442
+
443
+ function renderPieChart(r) {
444
+ if (pieChartInst) pieChartInst.destroy();
445
+ const ctx = $('#pieChart').getContext('2d');
446
+ pieChartInst = new Chart(ctx, {
447
+ type: 'doughnut',
448
+ data: {
449
+ labels: ['Normal', 'Stutter'],
450
+ datasets: [{
451
+ data: [r.normalProb, r.stutterProb],
452
+ backgroundColor: ['rgba(16,185,129,0.80)', 'rgba(239,68,68,0.80)'],
453
+ borderColor: ['#10B981', '#EF4444'],
454
+ borderWidth: 2,
455
+ hoverOffset: 6,
456
+ }]
457
+ },
458
+ options: {
459
+ responsive: true,
460
+ cutout: '65%',
461
+ plugins: {
462
+ legend: { position: 'bottom', labels: { padding: 14, font: { size: 12 } } },
463
+ tooltip: { callbacks: { label: (i) => ` ${i.label}: ${i.raw}%` } }
464
+ }
465
+ }
466
+ });
467
+ }
468
+
469
+ function renderBarChart() {
470
+ if (barChartInst) barChartInst.destroy();
471
+ const ctx = $('#barChart').getContext('2d');
472
+ const features = ['MFCC 1','MFCC 2','MFCC 3','MFCC 4','MFCC 5','Energy','ZCR'];
473
+ const values = features.map(() => +(Math.random() * 0.6 + 0.2).toFixed(2));
474
+ barChartInst = new Chart(ctx, {
475
+ type: 'bar',
476
+ data: {
477
+ labels: features,
478
+ datasets: [{
479
+ label: 'Importance',
480
+ data: values,
481
+ backgroundColor: 'rgba(37,99,235,0.75)',
482
+ borderColor: '#2563EB',
483
+ borderWidth: 1.5,
484
+ borderRadius: 6,
485
+ }]
486
+ },
487
+ options: {
488
+ responsive: true,
489
+ scales: {
490
+ y: { beginAtZero: true, max: 1, ticks: { font: { size: 11 } } },
491
+ x: { ticks: { font: { size: 11 } } }
492
+ },
493
+ plugins: { legend: { display: false } }
494
+ }
495
+ });
496
+ }
497
+
498
+ function renderGaugeChart(r) {
499
+ if (gaugeChartInst) gaugeChartInst.destroy();
500
+ const ctx = $('#gaugeChart').getContext('2d');
501
+ const val = r.confidence;
502
+ gaugeChartInst = new Chart(ctx, {
503
+ type: 'doughnut',
504
+ data: {
505
+ datasets: [{
506
+ data: [val, 100 - val],
507
+ backgroundColor: [
508
+ val >= 85 ? 'rgba(16,185,129,0.80)' : val >= 70 ? 'rgba(245,158,11,0.80)' : 'rgba(239,68,68,0.80)',
509
+ 'rgba(226,232,240,0.40)'
510
+ ],
511
+ borderWidth: 0,
512
+ circumference: 180,
513
+ rotation: 270,
514
+ }]
515
+ },
516
+ options: {
517
+ responsive: true,
518
+ cutout: '72%',
519
+ plugins: {
520
+ legend: { display: false },
521
+ tooltip: { enabled: false },
522
+ afterDraw: (chart) => {
523
+ const { ctx: c, chartArea: { left, right, bottom } } = chart;
524
+ c.fillStyle = '#1E293B';
525
+ c.font = 'bold 22px Space Grotesk, sans-serif';
526
+ c.textAlign = 'center';
527
+ c.fillText(val + '%', (left + right) / 2, bottom - 8);
528
+ }
529
+ }
530
+ },
531
+ plugins: [{
532
+ id: 'centerText',
533
+ afterDraw(chart) {
534
+ const { ctx: c, chartArea: { left, right, bottom } } = chart;
535
+ c.save();
536
+ c.font = 'bold 20px Inter, sans-serif';
537
+ c.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--text') || '#1E293B';
538
+ c.textAlign = 'center';
539
+ c.fillText(val + '%', (left + right) / 2, bottom - 10);
540
+ c.restore();
541
+ }
542
+ }]
543
+ });
544
+ }
545
+
546
+ function renderQualityChart(r) {
547
+ if (qualityChartInst) qualityChartInst.destroy();
548
+ const ctx = $('#qualityChart').getContext('2d');
549
+ qualityChartInst = new Chart(ctx, {
550
+ type: 'radar',
551
+ data: {
552
+ labels: ['Fluency', 'Clarity', 'Rhythm', 'Pace', 'Pitch'],
553
+ datasets: [{
554
+ label: 'Speech Quality',
555
+ data: [
556
+ r.fluency,
557
+ +(Math.random() * 20 + 70).toFixed(1),
558
+ +(Math.random() * 20 + 65).toFixed(1),
559
+ +(Math.random() * 20 + 68).toFixed(1),
560
+ +(Math.random() * 20 + 72).toFixed(1),
561
+ ],
562
+ backgroundColor: 'rgba(20,184,166,0.20)',
563
+ borderColor: 'rgba(20,184,166,0.90)',
564
+ borderWidth: 2,
565
+ pointBackgroundColor: '#14B8A6',
566
+ pointRadius: 4,
567
+ }]
568
+ },
569
+ options: {
570
+ responsive: true,
571
+ scales: { r: { min: 0, max: 100, ticks: { stepSize: 25, font: { size: 10 } }, pointLabels: { font: { size: 11 } } } },
572
+ plugins: { legend: { display: false } }
573
+ }
574
+ });
575
+ }
576
+
577
+ function renderResultWaveform(r) {
578
+ const canvas = $('#resultWaveform');
579
+ const ctx = canvas.getContext('2d');
580
+ const W = canvas.width, H = canvas.height;
581
+ ctx.clearRect(0, 0, W, H);
582
+
583
+ const color = r.isNormal ? '#10B981' : '#EF4444';
584
+ ctx.strokeStyle = color;
585
+ ctx.lineWidth = 2;
586
+ ctx.beginPath();
587
+ for (let x = 0; x < W; x++) {
588
+ const base = Math.sin((x / W) * Math.PI * 10) * 0.6;
589
+ const noise = r.isNormal
590
+ ? Math.sin((x / W) * Math.PI * 20) * 0.2
591
+ : Math.sin((x / W) * Math.PI * 30) * 0.5 + (Math.random() - 0.5) * 0.4;
592
+ const y = H / 2 + (base + noise) * (H / 2 - 6);
593
+ x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
594
+ }
595
+ ctx.stroke();
596
+ }
597
+
598
+ /* ─── LOCAL STORAGE HISTORY ──────────────────────────────────── */
599
+ function getHistory() {
600
+ return JSON.parse(localStorage.getItem('sf_analysis_history') || '[]');
601
+ }
602
+
603
+ function saveToHistory(result) {
604
+ const history = getHistory();
605
+ history.unshift({
606
+ id: Date.now(),
607
+ date: result.date,
608
+ fileName: result.fileName,
609
+ isNormal: result.isNormal,
610
+ confidence: result.confidence,
611
+ });
612
+ // Keep max 50 records
613
+ localStorage.setItem('sf_analysis_history', JSON.stringify(history.slice(0, 50)));
614
+ }
615
+
616
+ function deleteHistoryItem(id) {
617
+ const history = getHistory().filter(item => item.id !== id);
618
+ localStorage.setItem('sf_analysis_history', JSON.stringify(history));
619
+ renderHistory();
620
+ }
621
+
622
+ function clearAllHistory() {
623
+ localStorage.removeItem('sf_analysis_history');
624
+ renderHistory();
625
+ }
626
+
627
+ /**
628
+ * Render the history table, optionally filtered by a query string.
629
+ */
630
+ function renderHistory(filter = '') {
631
+ const tbody = $('#historyBody');
632
+ const emptyEl = $('#historyEmpty');
633
+ const history = getHistory();
634
+ const filtered = filter
635
+ ? history.filter(item =>
636
+ item.fileName.toLowerCase().includes(filter.toLowerCase()) ||
637
+ (item.isNormal ? 'normal' : 'stutter').includes(filter.toLowerCase())
638
+ )
639
+ : history;
640
+
641
+ tbody.innerHTML = '';
642
+
643
+ if (filtered.length === 0) {
644
+ emptyEl.style.display = 'block';
645
+ return;
646
+ }
647
+ emptyEl.style.display = 'none';
648
+
649
+ filtered.forEach((item, idx) => {
650
+ const tr = document.createElement('tr');
651
+ tr.innerHTML = `
652
+ <td>${idx + 1}</td>
653
+ <td>${item.date}</td>
654
+ <td title="${item.fileName}">${item.fileName.length > 30 ? item.fileName.slice(0, 30) + '…' : item.fileName}</td>
655
+ <td><span class="badge ${item.isNormal ? 'badge-normal' : 'badge-stutter'}">${item.isNormal ? 'Normal' : 'Stutter'}</span></td>
656
+ <td>${item.confidence}%</td>
657
+ <td><button class="delete-row-btn" data-id="${item.id}"><i class="fas fa-trash"></i> Delete</button></td>
658
+ `;
659
+ tbody.appendChild(tr);
660
+ });
661
+
662
+ // Bind delete buttons
663
+ $$('.delete-row-btn').forEach(btn => {
664
+ btn.addEventListener('click', () => {
665
+ deleteHistoryItem(parseInt(btn.dataset.id));
666
+ });
667
+ });
668
+ }
669
+
670
+ // Search
671
+ $('#historySearch').addEventListener('input', e => renderHistory(e.target.value));
672
+
673
+ // Clear all
674
+ $('#clearAllBtn').addEventListener('click', () => {
675
+ if (confirm('Clear all detection history? This cannot be undone.')) clearAllHistory();
676
+ });
677
+
678
+ // Render on load
679
+ renderHistory();
680
+
681
+ /* ─── NEW ANALYSIS & RESET ───────────────────────────────────── */
682
+ $('#newAnalysisBtn').addEventListener('click', () => {
683
+ currentResult = null;
684
+ currentFile = null;
685
+ clearFile();
686
+ $('#resultsContent').style.display = 'none';
687
+ $('#resultsPlaceholder').style.display = 'block';
688
+ document.getElementById('analysis').scrollIntoView({ behavior: 'smooth' });
689
+ });
690
+
691
+ /* ─── PDF REPORT ─────────────────────────────────────────────── */
692
+ $('#downloadReportBtn').addEventListener('click', () => {
693
+ if (!currentResult) { showToast('No result to export.', 'error'); return; }
694
+ generatePDFReport(currentResult);
695
+ });
696
+
697
+ function generatePDFReport(r) {
698
+ const { jsPDF } = window.jspdf;
699
+ const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
700
+
701
+ const blue = [37, 99, 235];
702
+ const teal = [20, 184, 166];
703
+ const dark = [30, 41, 59];
704
+ const gray = [100, 116, 139];
705
+ const light = [241, 245, 249];
706
+ const green = [16, 185, 129];
707
+ const red = [239, 68, 68];
708
+ const W = 210, H = 297;
709
+
710
+ // Header background
711
+ doc.setFillColor(...blue);
712
+ doc.rect(0, 0, W, 50, 'F');
713
+
714
+ // Logo / title
715
+ doc.setTextColor(255, 255, 255);
716
+ doc.setFont('helvetica', 'bold');
717
+ doc.setFontSize(22);
718
+ doc.text('SpeakFlow AI', 16, 22);
719
+ doc.setFontSize(11);
720
+ doc.setFont('helvetica', 'normal');
721
+ doc.text('Speech Stuttering Detection System', 16, 32);
722
+ doc.setFontSize(9);
723
+ doc.text('The University of Faisalabad Β· BS Software Engineering', 16, 41);
724
+
725
+ // Report label
726
+ doc.setFillColor(...teal);
727
+ doc.roundedRect(W - 60, 16, 50, 16, 4, 4, 'F');
728
+ doc.setTextColor(255, 255, 255);
729
+ doc.setFontSize(10);
730
+ doc.setFont('helvetica', 'bold');
731
+ doc.text('ANALYSIS REPORT', W - 35, 26, { align: 'center' });
732
+
733
+ // Section: File Information
734
+ let y = 62;
735
+ sectionHeader(doc, 'File Information', 14, y, W, dark, light);
736
+ y += 14;
737
+ infoRow(doc, 'File Name', r.fileName, 16, y, gray, dark); y += 9;
738
+ infoRow(doc, 'Analysis Date', r.date, 16, y, gray, dark); y += 9;
739
+ infoRow(doc, 'Processing', r.processingMs + ' ms', 16, y, gray, dark); y += 16;
740
+
741
+ // Section: Prediction Result
742
+ sectionHeader(doc, 'Prediction Result', 14, y, W, dark, light);
743
+ y += 14;
744
+
745
+ const resColor = r.isNormal ? green : red;
746
+ doc.setFillColor(...resColor);
747
+ doc.roundedRect(16, y, W - 32, 22, 6, 6, 'F');
748
+ doc.setTextColor(255, 255, 255);
749
+ doc.setFontSize(14);
750
+ doc.setFont('helvetica', 'bold');
751
+ doc.text(r.isNormal ? 'βœ“ Normal Speech Detected' : 'βœ— Speech Stuttering Detected', W / 2, y + 14, { align: 'center' });
752
+ y += 32;
753
+
754
+ // Section: Scores
755
+ sectionHeader(doc, 'Analysis Scores', 14, y, W, dark, light);
756
+ y += 16;
757
+
758
+ const scores = [
759
+ ['Confidence Score', r.confidence + '%'],
760
+ ['Speech Fluency Score', r.fluency + '%'],
761
+ ['Prediction Probability', (r.isNormal ? r.normalProb : r.stutterProb) + '%'],
762
+ ['Processing Time', r.processingMs + ' ms'],
763
+ ];
764
+
765
+ const colW = (W - 32) / 2;
766
+ scores.forEach((s, i) => {
767
+ const col = i % 2;
768
+ const row = Math.floor(i / 2);
769
+ const bx = 16 + col * (colW + 4);
770
+ const by = y + row * 28;
771
+ doc.setFillColor(...light);
772
+ doc.roundedRect(bx, by, colW, 22, 4, 4, 'F');
773
+ doc.setTextColor(...gray);
774
+ doc.setFontSize(8);
775
+ doc.setFont('helvetica', 'normal');
776
+ doc.text(s[0].toUpperCase(), bx + 8, by + 9);
777
+ doc.setTextColor(...blue);
778
+ doc.setFontSize(13);
779
+ doc.setFont('helvetica', 'bold');
780
+ doc.text(s[1], bx + 8, by + 18);
781
+ });
782
+ y += 70;
783
+
784
+ // Section: Model Information
785
+ sectionHeader(doc, 'Model Information', 14, y, W, dark, light);
786
+ y += 14;
787
+ infoRow(doc, 'Algorithm', 'Random Forest Classifier', 16, y, gray, dark); y += 9;
788
+ infoRow(doc, 'Feature Extraction', 'MFCC (40 Coefficients) via Librosa', 16, y, gray, dark); y += 9;
789
+ infoRow(doc, 'Dataset', '8,000+ Labeled Audio Samples', 16, y, gray, dark); y += 9;
790
+ infoRow(doc, 'Classes', 'Normal Speech / Stuttered Speech', 16, y, gray, dark); y += 16;
791
+
792
+ // Disclaimer
793
+ doc.setFillColor(255, 247, 237);
794
+ doc.roundedRect(16, y, W - 32, 18, 4, 4, 'F');
795
+ doc.setTextColor(180, 100, 20);
796
+ doc.setFontSize(8);
797
+ doc.setFont('helvetica', 'italic');
798
+ doc.text('This report is generated by an AI system for educational/research purposes only. It does not constitute a medical diagnosis.', W / 2, y + 7, { align: 'center', maxWidth: W - 40 });
799
+ doc.text('Consult a qualified speech-language pathologist for clinical evaluation.', W / 2, y + 13, { align: 'center' });
800
+ y += 26;
801
+
802
+ // Footer
803
+ doc.setFillColor(...dark);
804
+ doc.rect(0, H - 18, W, 18, 'F');
805
+ doc.setTextColor(148, 163, 184);
806
+ doc.setFontSize(8);
807
+ doc.setFont('helvetica', 'normal');
808
+ doc.text('SpeakFlow AI Β· The University of Faisalabad Β· BS Software Engineering', W / 2, H - 7, { align: 'center' });
809
+
810
+ doc.save(`SpeakFlow_Report_${Date.now()}.pdf`);
811
+ showToast('πŸ“„ PDF report downloaded!', 'success');
812
+ }
813
+
814
+ function sectionHeader(doc, title, fontSize, y, W, dark, light) {
815
+ doc.setFillColor(...light);
816
+ doc.rect(0, y, W, 12, 'F');
817
+ doc.setTextColor(...dark);
818
+ doc.setFontSize(fontSize - 2);
819
+ doc.setFont('helvetica', 'bold');
820
+ doc.text(title, 16, y + 8);
821
+ }
822
+
823
+ function infoRow(doc, label, value, x, y, gray, dark) {
824
+ doc.setTextColor(...gray);
825
+ doc.setFontSize(8);
826
+ doc.setFont('helvetica', 'normal');
827
+ doc.text(label + ':', x, y);
828
+ doc.setTextColor(...dark);
829
+ doc.setFont('helvetica', 'bold');
830
+ doc.text(value, x + 56, y);
831
+ }
832
+
833
+ /* ─── ANIMATED COUNTERS ─────────────────────────────────────── */
834
+ (function initCounters() {
835
+ const statCards = $$('.stat-card');
836
+ const observer = new IntersectionObserver((entries) => {
837
+ entries.forEach(entry => {
838
+ if (entry.isIntersecting) {
839
+ animateCounter(entry.target);
840
+ observer.unobserve(entry.target);
841
+ }
842
+ });
843
+ }, { threshold: 0.4 });
844
+
845
+ statCards.forEach(card => observer.observe(card));
846
+ })();
847
+
848
+ function animateCounter(card) {
849
+ const target = parseInt(card.dataset.count);
850
+ const suffix = card.dataset.suffix || '';
851
+ const el = card.querySelector('.stat-number');
852
+ const duration = 1800;
853
+ const start = performance.now();
854
+
855
+ function update(now) {
856
+ const progress = Math.min((now - start) / duration, 1);
857
+ const ease = 1 - Math.pow(1 - progress, 3); // cubic ease-out
858
+ const current = Math.round(ease * target);
859
+ el.textContent = current.toLocaleString() + suffix;
860
+ if (progress < 1) requestAnimationFrame(update);
861
+ }
862
+ requestAnimationFrame(update);
863
+ }
864
+
865
+ /* ─── TOAST NOTIFICATIONS ────────────────────────────────────── */
866
+ function showToast(message, type = 'info') {
867
+ // Remove existing toasts
868
+ $$('.toast').forEach(t => t.remove());
869
+
870
+ const toast = document.createElement('div');
871
+ toast.className = 'toast';
872
+ const colors = { success: '#10B981', error: '#EF4444', info: '#2563EB' };
873
+ toast.style.cssText = `
874
+ position: fixed;
875
+ bottom: 90px;
876
+ right: 24px;
877
+ background: var(--surface);
878
+ border: 1px solid ${colors[type] || colors.info};
879
+ border-left: 4px solid ${colors[type] || colors.info};
880
+ color: var(--text);
881
+ padding: 14px 20px;
882
+ border-radius: 12px;
883
+ font-family: var(--font-body);
884
+ font-size: 0.9rem;
885
+ font-weight: 500;
886
+ box-shadow: 0 8px 24px rgba(0,0,0,0.15);
887
+ z-index: 99999;
888
+ max-width: 320px;
889
+ animation: slideIn 0.3s ease;
890
+ `;
891
+ toast.textContent = message;
892
+
893
+ const style = document.createElement('style');
894
+ style.textContent = `
895
+ @keyframes slideIn { from { transform: translateX(120%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
896
+ @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(120%); opacity: 0; } }
897
+ `;
898
+ if (!document.getElementById('toastStyle')) {
899
+ style.id = 'toastStyle';
900
+ document.head.appendChild(style);
901
+ }
902
+
903
+ document.body.appendChild(toast);
904
+ setTimeout(() => {
905
+ toast.style.animation = 'slideOut 0.3s ease forwards';
906
+ setTimeout(() => toast.remove(), 300);
907
+ }, 3500);
908
+ }
909
+
910
+ /* ─── SMOOTH SCROLL FOR ANCHOR LINKS ────────────────────────── */
911
+ $$('a[href^="#"]').forEach(a => {
912
+ a.addEventListener('click', e => {
913
+ const target = document.querySelector(a.getAttribute('href'));
914
+ if (target) {
915
+ e.preventDefault();
916
+ const offset = 80; // navbar height
917
+ const top = target.getBoundingClientRect().top + window.scrollY - offset;
918
+ window.scrollTo({ top, behavior: 'smooth' });
919
+ }
920
+ });
921
+ });
922
+
923
+ /* ─── SCROLL REVEAL ─────────────────────────────────────────── */
924
+ (function initScrollReveal() {
925
+ const elements = $$('.overview-card, .timeline-item, .stat-card, .score-card, .chart-card');
926
+ const observer = new IntersectionObserver((entries) => {
927
+ entries.forEach(entry => {
928
+ if (entry.isIntersecting) {
929
+ entry.target.style.opacity = '1';
930
+ entry.target.style.transform = 'translateY(0)';
931
+ observer.unobserve(entry.target);
932
+ }
933
+ });
934
+ }, { threshold: 0.12 });
935
+
936
+ elements.forEach((el, i) => {
937
+ el.style.opacity = '0';
938
+ el.style.transform = 'translateY(24px)';
939
+ el.style.transition = `opacity 0.5s ease ${i * 0.06}s, transform 0.5s ease ${i * 0.06}s`;
940
+ observer.observe(el);
941
+ });
942
+ })();
943
+
944
+ /* ─── INIT ──────────────────────────────────────────────────── */
945
+ console.log('%cSpeakFlow AI Initialized βœ“', 'color: #2563EB; font-weight: bold; font-size: 14px;');
946
+
947
+ /* ============================================================
948
+ SpeakFlow AI – Analysis History Storage (Auth Integration)
949
+ ============================================================ */
950
+ (function patchAnalysisStorage() {
951
+ const observer = new MutationObserver(() => {
952
+ const resultSection =
953
+ document.getElementById('resultSection') ||
954
+ document.querySelector('.result-section') ||
955
+ document.querySelector('[id*="result"]');
956
+
957
+ if (!resultSection) return;
958
+
959
+ const innerObs = new MutationObserver(() => {
960
+ saveAnalysisToHistory();
961
+ });
962
+
963
+ innerObs.observe(resultSection, {
964
+ childList: true,
965
+ subtree: true
966
+ });
967
+
968
+ observer.disconnect();
969
+ });
970
+
971
+ observer.observe(document.body, {
972
+ childList: true,
973
+ subtree: true
974
+ });
975
+
976
+ function saveAnalysisToHistory() {
977
+ try {
978
+ const session = JSON.parse(localStorage.getItem('sf_session') || 'null');
979
+ if (!session || !session.loggedIn) return;
980
+
981
+ const resultEl = document.querySelector('.result-label, .detection-result, [class*="result"]');
982
+ const confEl = document.querySelector('.confidence-score, [class*="confidence"]');
983
+ const fileEl = document.querySelector('.file-name, [class*="filename"]');
984
+
985
+ const resultText = resultEl ? resultEl.textContent.trim() : null;
986
+ if (!resultText || resultText.length < 3) return;
987
+
988
+ if (resultEl.dataset.saved === '1') return;
989
+ resultEl.dataset.saved = '1';
990
+
991
+ const history = JSON.parse(localStorage.getItem('sf_analysis_history') || '[]');
992
+
993
+ history.push({
994
+ id: 'ana_' + Date.now(),
995
+ userId: session.id || '',
996
+ userName: session.name || 'Unknown',
997
+ userEmail: session.email || '',
998
+ fileName: fileEl ? fileEl.textContent.trim() : ('recording_' + Date.now() + '.wav'),
999
+ date: new Date().toISOString(),
1000
+ result: resultText.includes('Normal') ? 'Normal' : 'Stutter Detected',
1001
+ confidence: confEl ? parseInt(confEl.textContent) || 85 : 85,
1002
+ });
1003
+
1004
+ localStorage.setItem('sf_analysis_history', JSON.stringify(history));
1005
+ } catch (err) {
1006
+ console.warn('SpeakFlow: could not save analysis to history', err);
1007
+ }
1008
+ }
1009
+ })();
1010
+
1011
+ /* ============================================================
1012
+ BACKEND INTEGRATION
1013
+ ============================================================ */
1014
+
1015
+ async function sendToBackend(file, fileName = "audio.wav") {
1016
+ const overlay = $('#loadingOverlay');
1017
+ const bar = $('#progressBar');
1018
+ const pct = $('#progressPct');
1019
+
1020
+ if (overlay) overlay.style.display = 'flex';
1021
+ if (bar) bar.style.width = '30%';
1022
+ if (pct) pct.textContent = '30%';
1023
+
1024
+ try {
1025
+ const formData = new FormData();
1026
+
1027
+ if (file instanceof Blob) {
1028
+ formData.append("file", file, fileName);
1029
+ } else {
1030
+ formData.append("file", file);
1031
+ }
1032
+
1033
+ const res = await fetch(`${API_BASE}/api/predict`, {
1034
+ method: "POST",
1035
+ body: formData
1036
+ });
1037
+
1038
+ if (!res.ok) throw new Error("Server error");
1039
+
1040
+ if (bar) bar.style.width = '80%';
1041
+ if (pct) pct.textContent = '80%';
1042
+
1043
+ const data = await res.json();
1044
+
1045
+ const result = {
1046
+ fileName: fileName || currentFile?.name || "recording",
1047
+ date: new Date().toLocaleString(),
1048
+ isNormal: data.isNormal,
1049
+ confidence: data.confidence,
1050
+ fluency: data.fluency,
1051
+ processingMs: data.processingMs,
1052
+ normalProb: data.normalProb,
1053
+ stutterProb: data.stutterProb
1054
+ };
1055
+
1056
+ currentResult = result;
1057
+
1058
+ renderResults(result);
1059
+ saveToHistory(result);
1060
+ renderHistory();
1061
+
1062
+ if (bar) bar.style.width = '100%';
1063
+ if (pct) pct.textContent = '100%';
1064
+
1065
+ setTimeout(() => {
1066
+ if (overlay) overlay.style.display = 'none';
1067
+ }, 400);
1068
+
1069
+ showToast("Analysis complete!", "success");
1070
+
1071
+ } catch (err) {
1072
+ console.error(err);
1073
+ if (overlay) overlay.style.display = 'none';
1074
+ showToast("Flask backend not running or API error", "error");
1075
+ }
1076
+ }
session.js ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ SpeakFlow AI – session.js
3
+ Shared session management for index.html
4
+ ============================================================ */
5
+ 'use strict';
6
+
7
+ (function initSession() {
8
+ const session = JSON.parse(localStorage.getItem('sf_session') || 'null');
9
+
10
+ // Route protection – redirect to login if not logged in
11
+ if (!session || !session.loggedIn) {
12
+ window.location.href = 'login.html';
13
+ return;
14
+ }
15
+
16
+ // Show user info in navbar
17
+ const info = document.getElementById('navUserInfo');
18
+ const nameEl = document.getElementById('navUserName');
19
+ if (info && nameEl) {
20
+ nameEl.textContent = session.name || 'User';
21
+ info.style.display = 'flex';
22
+ }
23
+
24
+ // Profile button β†’ open profile modal
25
+ const profileBtn = document.getElementById('navProfileBtn');
26
+ if (profileBtn) {
27
+ profileBtn.addEventListener('click', openProfileModal);
28
+ }
29
+
30
+ // Logout
31
+ const logoutBtn = document.getElementById('navLogoutBtn');
32
+ if (logoutBtn) {
33
+ logoutBtn.addEventListener('click', () => {
34
+ localStorage.removeItem('sf_session');
35
+ window.location.href = 'login.html';
36
+ });
37
+ }
38
+
39
+ // ── Profile Modal ────────────────────────────────────────
40
+ function openProfileModal() {
41
+ if (document.getElementById('sfProfileModal')) return;
42
+
43
+ const users = JSON.parse(localStorage.getItem('sf_users') || '[]');
44
+ const user = users.find(u => u.email === session.email) || {};
45
+ const history = JSON.parse(localStorage.getItem('sf_analysis_history') || '[]');
46
+ const userAnalyses = history.filter(h => h.userEmail === session.email).length;
47
+
48
+ const modal = document.createElement('div');
49
+ modal.id = 'sfProfileModal';
50
+ modal.innerHTML = `
51
+ <div class="sf-modal-overlay" id="sfModalOverlay">
52
+ <div class="sf-modal-box glass-card">
53
+ <div class="sf-modal-header">
54
+ <div class="sf-modal-avatar"><i class="fas fa-user-circle"></i></div>
55
+ <h2>My Profile</h2>
56
+ <button class="sf-modal-close" id="sfModalClose"><i class="fas fa-times"></i></button>
57
+ </div>
58
+ <div class="sf-modal-body" id="sfProfileView">
59
+ <div class="sf-profile-row"><span><i class="fas fa-user"></i> Full Name</span><strong id="pName">${session.name}</strong></div>
60
+ <div class="sf-profile-row"><span><i class="fas fa-envelope"></i> Email</span><strong>${session.email}</strong></div>
61
+ <div class="sf-profile-row"><span><i class="fas fa-chart-bar"></i> Total Analyses</span><strong>${userAnalyses}</strong></div>
62
+ <div class="sf-profile-row"><span><i class="fas fa-calendar-alt"></i> Member Since</span><strong>${user.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'}</strong></div>
63
+ <div class="sf-modal-actions">
64
+ <button class="btn btn-primary" id="sfEditProfileBtn"><i class="fas fa-edit"></i> Edit Profile</button>
65
+ <button class="btn btn-outline" id="sfChangePassBtn"><i class="fas fa-key"></i> Change Password</button>
66
+ </div>
67
+ </div>
68
+ <div class="sf-modal-body" id="sfEditView" style="display:none;">
69
+ <label>Full Name</label>
70
+ <input class="sf-input" type="text" id="sfNewName" value="${session.name}" placeholder="Full Name" />
71
+ <div class="sf-modal-actions">
72
+ <button class="btn btn-primary" id="sfSaveNameBtn"><i class="fas fa-save"></i> Save Changes</button>
73
+ <button class="btn btn-ghost" id="sfCancelEditBtn"><i class="fas fa-arrow-left"></i> Back</button>
74
+ </div>
75
+ <p id="sfEditMsg" style="color:var(--green);margin-top:8px;display:none;"></p>
76
+ </div>
77
+ <div class="sf-modal-body" id="sfPassView" style="display:none;">
78
+ <label>Current Password</label>
79
+ <input class="sf-input" type="password" id="sfOldPass" placeholder="Current password" />
80
+ <label>New Password</label>
81
+ <input class="sf-input" type="password" id="sfNewPass" placeholder="New password" />
82
+ <label>Confirm New Password</label>
83
+ <input class="sf-input" type="password" id="sfConfPass" placeholder="Confirm new password" />
84
+ <div class="sf-modal-actions">
85
+ <button class="btn btn-primary" id="sfSavePassBtn"><i class="fas fa-save"></i> Update Password</button>
86
+ <button class="btn btn-ghost" id="sfCancelPassBtn"><i class="fas fa-arrow-left"></i> Back</button>
87
+ </div>
88
+ <p id="sfPassMsg" style="margin-top:8px;display:none;"></p>
89
+ </div>
90
+ </div>
91
+ </div>`;
92
+ document.body.appendChild(modal);
93
+
94
+ // inject modal CSS if not already
95
+ if (!document.getElementById('sfModalStyle')) {
96
+ const s = document.createElement('style');
97
+ s.id = 'sfModalStyle';
98
+ s.textContent = `
99
+ #sfProfileModal { position:fixed;inset:0;z-index:9999; }
100
+ .sf-modal-overlay { position:absolute;inset:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:16px; }
101
+ .sf-modal-box { background:var(--surface);border-radius:24px;padding:32px;width:100%;max-width:480px;position:relative;box-shadow:var(--shadow-lg); }
102
+ .sf-modal-header { display:flex;align-items:center;gap:16px;margin-bottom:24px; }
103
+ .sf-modal-avatar { font-size:2.5rem;color:var(--blue); }
104
+ .sf-modal-header h2 { font-size:1.4rem;font-weight:700;flex:1; }
105
+ .sf-modal-close { background:var(--surface-2);border-radius:50%;width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:1rem;cursor:pointer;border:none;color:var(--text-muted); }
106
+ .sf-profile-row { display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-bottom:1px solid var(--border);font-size:0.95rem; }
107
+ .sf-profile-row span { color:var(--text-muted);display:flex;align-items:center;gap:8px; }
108
+ .sf-modal-actions { display:flex;gap:12px;margin-top:24px;flex-wrap:wrap; }
109
+ .sf-input { width:100%;padding:12px 16px;border:1px solid var(--border);border-radius:12px;background:var(--surface-2);color:var(--text);font-size:0.95rem;margin-bottom:16px;margin-top:4px;outline:none; }
110
+ .sf-input:focus { border-color:var(--blue); }
111
+ .sf-modal-body label { font-size:0.85rem;font-weight:600;color:var(--text-muted); }
112
+ .nav-user-info { display:flex;align-items:center;gap:8px; }
113
+ .btn-user-profile { display:flex;align-items:center;gap:8px;padding:8px 14px;background:rgba(37,99,235,0.1);border-radius:50px;color:var(--blue);font-weight:600;font-size:0.9rem;cursor:pointer;border:none;transition:all .2s; }
114
+ .btn-user-profile:hover { background:rgba(37,99,235,0.2); }
115
+ .btn-logout { display:flex;align-items:center;gap:6px;padding:8px 14px;background:rgba(239,68,68,0.1);border-radius:50px;color:var(--red);font-weight:600;font-size:0.9rem;cursor:pointer;border:none;transition:all .2s; }
116
+ .btn-logout:hover { background:rgba(239,68,68,0.2); }
117
+ `;
118
+ document.head.appendChild(s);
119
+ }
120
+
121
+ document.getElementById('sfModalClose').onclick = closeModal;
122
+ document.getElementById('sfModalOverlay').onclick = (e) => { if(e.target.id==='sfModalOverlay') closeModal(); };
123
+
124
+ document.getElementById('sfEditProfileBtn').onclick = () => {
125
+ document.getElementById('sfProfileView').style.display = 'none';
126
+ document.getElementById('sfEditView').style.display = 'block';
127
+ };
128
+ document.getElementById('sfCancelEditBtn').onclick = () => {
129
+ document.getElementById('sfEditView').style.display = 'none';
130
+ document.getElementById('sfProfileView').style.display = 'block';
131
+ };
132
+ document.getElementById('sfSaveNameBtn').onclick = () => {
133
+ const newName = document.getElementById('sfNewName').value.trim();
134
+ if (!newName) return;
135
+ // Update user in localStorage
136
+ const users2 = JSON.parse(localStorage.getItem('sf_users') || '[]');
137
+ const idx = users2.findIndex(u => u.email === session.email);
138
+ if (idx !== -1) { users2[idx].name = newName; localStorage.setItem('sf_users', JSON.stringify(users2)); }
139
+ session.name = newName;
140
+ localStorage.setItem('sf_session', JSON.stringify(session));
141
+ document.getElementById('navUserName').textContent = newName;
142
+ document.getElementById('pName').textContent = newName;
143
+ const msg = document.getElementById('sfEditMsg');
144
+ msg.textContent = 'βœ… Name updated successfully!';
145
+ msg.style.display = 'block';
146
+ };
147
+
148
+ document.getElementById('sfChangePassBtn').onclick = () => {
149
+ document.getElementById('sfProfileView').style.display = 'none';
150
+ document.getElementById('sfPassView').style.display = 'block';
151
+ };
152
+ document.getElementById('sfCancelPassBtn').onclick = () => {
153
+ document.getElementById('sfPassView').style.display = 'none';
154
+ document.getElementById('sfProfileView').style.display = 'block';
155
+ };
156
+ document.getElementById('sfSavePassBtn').onclick = () => {
157
+ const old = document.getElementById('sfOldPass').value;
158
+ const nw = document.getElementById('sfNewPass').value;
159
+ const conf = document.getElementById('sfConfPass').value;
160
+ const msg = document.getElementById('sfPassMsg');
161
+ const users3 = JSON.parse(localStorage.getItem('sf_users') || '[]');
162
+ const idx = users3.findIndex(u => u.email === session.email);
163
+ if (idx === -1 || users3[idx].password !== old) {
164
+ msg.textContent = '❌ Current password is incorrect.'; msg.style.color='var(--red)'; msg.style.display='block'; return;
165
+ }
166
+ if (nw.length < 6) { msg.textContent = '❌ Password must be at least 6 characters.'; msg.style.color='var(--red)'; msg.style.display='block'; return; }
167
+ if (nw !== conf) { msg.textContent = '❌ Passwords do not match.'; msg.style.color='var(--red)'; msg.style.display='block'; return; }
168
+ users3[idx].password = nw;
169
+ localStorage.setItem('sf_users', JSON.stringify(users3));
170
+ msg.textContent = 'βœ… Password updated!'; msg.style.color='var(--green)'; msg.style.display='block';
171
+ };
172
+ }
173
+
174
+ function closeModal() {
175
+ const m = document.getElementById('sfProfileModal');
176
+ if (m) m.remove();
177
+ }
178
+
179
+ })();
style.css ADDED
@@ -0,0 +1,918 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ SpeakFlow AI – style.css
3
+ Modern AI Healthcare Platform
4
+ ============================================================ */
5
+
6
+ /* ─── CSS CUSTOM PROPERTIES ─────────────────────────────── */
7
+ :root {
8
+ --blue: #2563EB;
9
+ --blue-light: #3B82F6;
10
+ --blue-dark: #1D4ED8;
11
+ --teal: #14B8A6;
12
+ --teal-light: #2DD4BF;
13
+ --purple: #7C3AED;
14
+ --purple-light:#A78BFA;
15
+ --green: #10B981;
16
+ --orange: #F59E0B;
17
+ --red: #EF4444;
18
+ --red-light: #FCA5A5;
19
+
20
+ --bg: #F8FAFC;
21
+ --bg-alt: #EEF2FF;
22
+ --surface: #FFFFFF;
23
+ --surface-2: #F1F5F9;
24
+ --border: #E2E8F0;
25
+ --text: #1E293B;
26
+ --text-muted: #64748B;
27
+ --text-light: #94A3B8;
28
+
29
+ --radius-sm: 8px;
30
+ --radius: 16px;
31
+ --radius-lg: 24px;
32
+ --radius-xl: 32px;
33
+
34
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
35
+ --shadow: 0 4px 16px rgba(0,0,0,0.10);
36
+ --shadow-lg: 0 16px 48px rgba(0,0,0,0.12);
37
+ --shadow-glow: 0 0 32px rgba(37,99,235,0.25);
38
+
39
+ --font-display: 'Space Grotesk', sans-serif;
40
+ --font-body: 'Inter', sans-serif;
41
+
42
+ --transition: 0.25s ease;
43
+ }
44
+
45
+ [data-theme="dark"] {
46
+ --bg: #0F172A;
47
+ --bg-alt: #1E1B4B;
48
+ --surface: #1E293B;
49
+ --surface-2: #263148;
50
+ --border: #334155;
51
+ --text: #F1F5F9;
52
+ --text-muted: #94A3B8;
53
+ --text-light: #64748B;
54
+ --shadow: 0 4px 16px rgba(0,0,0,0.40);
55
+ --shadow-lg: 0 16px 48px rgba(0,0,0,0.50);
56
+ }
57
+
58
+ /* ─── RESET ─────────────────────────────────────────────── */
59
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
60
+ html { scroll-behavior: smooth; }
61
+ body {
62
+ font-family: var(--font-body);
63
+ background: var(--bg);
64
+ color: var(--text);
65
+ line-height: 1.6;
66
+ transition: background var(--transition), color var(--transition);
67
+ }
68
+ img { max-width: 100%; display: block; }
69
+ a { color: inherit; text-decoration: none; }
70
+ button { cursor: pointer; font-family: var(--font-body); border: none; background: none; }
71
+ ul { list-style: none; }
72
+ canvas { display: block; }
73
+
74
+ /* ─── UTILITY ────────────────────────────────────────────── */
75
+ .container { max-width: 1180px; margin: 0 auto; padding: 0 24px; }
76
+ .gradient-text {
77
+ background: linear-gradient(135deg, var(--blue), var(--teal));
78
+ -webkit-background-clip: text;
79
+ -webkit-text-fill-color: transparent;
80
+ background-clip: text;
81
+ }
82
+ .mt-16 { margin-top: 16px; }
83
+ .text-muted { color: var(--text-muted); }
84
+
85
+ /* ─── BUTTONS ────────────────────────────────────────────── */
86
+ .btn {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ gap: 8px;
90
+ padding: 12px 24px;
91
+ border-radius: 50px;
92
+ font-size: 0.95rem;
93
+ font-weight: 600;
94
+ transition: all var(--transition);
95
+ white-space: nowrap;
96
+ }
97
+ .btn-primary {
98
+ background: linear-gradient(135deg, var(--blue), var(--blue-dark));
99
+ color: #fff;
100
+ box-shadow: 0 4px 20px rgba(37,99,235,0.30);
101
+ }
102
+ .btn-primary:hover {
103
+ transform: translateY(-2px);
104
+ box-shadow: 0 8px 28px rgba(37,99,235,0.40);
105
+ }
106
+ .btn-outline {
107
+ border: 2px solid var(--blue);
108
+ color: var(--blue);
109
+ background: transparent;
110
+ }
111
+ .btn-outline:hover {
112
+ background: var(--blue);
113
+ color: #fff;
114
+ transform: translateY(-2px);
115
+ }
116
+ .btn-ghost {
117
+ background: var(--surface-2);
118
+ color: var(--text-muted);
119
+ border-radius: 50px;
120
+ }
121
+ .btn-ghost:hover { background: var(--border); color: var(--text); }
122
+ .btn-danger {
123
+ background: var(--red);
124
+ color: #fff;
125
+ border-radius: 50px;
126
+ }
127
+ .btn-danger:hover { background: #DC2626; }
128
+ .btn-sm { padding: 8px 18px; font-size: 0.875rem; }
129
+ .btn-lg { padding: 16px 36px; font-size: 1.05rem; }
130
+
131
+ /* ─── NAVBAR ─────────────────────────────────────────────── */
132
+ .navbar {
133
+ position: fixed;
134
+ top: 0; left: 0; right: 0;
135
+ z-index: 1000;
136
+ background: rgba(255,255,255,0.85);
137
+ backdrop-filter: blur(20px);
138
+ -webkit-backdrop-filter: blur(20px);
139
+ border-bottom: 1px solid var(--border);
140
+ transition: background var(--transition);
141
+ }
142
+ [data-theme="dark"] .navbar {
143
+ background: rgba(15,23,42,0.90);
144
+ }
145
+ .nav-container {
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: space-between;
149
+ max-width: 1180px;
150
+ margin: 0 auto;
151
+ padding: 16px 24px;
152
+ }
153
+ .nav-logo {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 10px;
157
+ font-family: var(--font-display);
158
+ font-size: 1.3rem;
159
+ font-weight: 700;
160
+ color: var(--text);
161
+ }
162
+ .nav-logo i { color: var(--blue); font-size: 1.5rem; }
163
+ .logo-ai { color: var(--blue); }
164
+ .nav-links {
165
+ display: flex;
166
+ gap: 4px;
167
+ }
168
+ .nav-links a {
169
+ padding: 8px 14px;
170
+ border-radius: var(--radius-sm);
171
+ font-size: 0.9rem;
172
+ font-weight: 500;
173
+ color: var(--text-muted);
174
+ transition: all var(--transition);
175
+ }
176
+ .nav-links a:hover { color: var(--blue); background: var(--bg-alt); }
177
+ .nav-actions { display: flex; align-items: center; gap: 12px; }
178
+ .dark-mode-toggle {
179
+ width: 40px; height: 40px;
180
+ border-radius: 50%;
181
+ background: var(--surface-2);
182
+ border: 1px solid var(--border);
183
+ color: var(--text-muted);
184
+ font-size: 1rem;
185
+ display: flex; align-items: center; justify-content: center;
186
+ transition: all var(--transition);
187
+ }
188
+ .dark-mode-toggle:hover { color: var(--blue); border-color: var(--blue); }
189
+ .hamburger {
190
+ display: none;
191
+ width: 40px; height: 40px;
192
+ align-items: center; justify-content: center;
193
+ border-radius: var(--radius-sm);
194
+ background: var(--surface-2);
195
+ color: var(--text);
196
+ font-size: 1.1rem;
197
+ }
198
+
199
+ /* ─── SECTIONS ───────────────────────────────────────────── */
200
+ .section { padding: 96px 0; }
201
+ .section-alt { background: var(--bg-alt); }
202
+ [data-theme="dark"] .section-alt { background: rgba(124,58,237,0.05); }
203
+
204
+ .section-header { text-align: center; margin-bottom: 56px; }
205
+ .section-eyebrow {
206
+ display: inline-block;
207
+ font-size: 0.78rem;
208
+ font-weight: 700;
209
+ letter-spacing: 0.12em;
210
+ text-transform: uppercase;
211
+ color: var(--blue);
212
+ background: rgba(37,99,235,0.10);
213
+ padding: 6px 16px;
214
+ border-radius: 50px;
215
+ margin-bottom: 16px;
216
+ }
217
+ .section-title {
218
+ font-family: var(--font-display);
219
+ font-size: clamp(1.8rem, 3vw, 2.6rem);
220
+ font-weight: 700;
221
+ color: var(--text);
222
+ margin-bottom: 16px;
223
+ line-height: 1.25;
224
+ }
225
+ .section-desc { font-size: 1.05rem; color: var(--text-muted); max-width: 520px; margin: 0 auto; }
226
+
227
+ /* ─── HERO ───────────────────────────────────────────────── */
228
+ .hero {
229
+ min-height: 100vh;
230
+ display: flex;
231
+ align-items: center;
232
+ padding: 120px 0 80px;
233
+ position: relative;
234
+ overflow: hidden;
235
+ background: linear-gradient(145deg, #0F172A 0%, #1E1B4B 50%, #0F172A 100%);
236
+ }
237
+ .particles {
238
+ position: absolute;
239
+ inset: 0;
240
+ pointer-events: none;
241
+ z-index: 0;
242
+ }
243
+ .particle {
244
+ position: absolute;
245
+ border-radius: 50%;
246
+ background: radial-gradient(circle, rgba(37,99,235,0.6), transparent);
247
+ animation: floatParticle linear infinite;
248
+ }
249
+ @keyframes floatParticle {
250
+ 0% { transform: translateY(0) scale(1); opacity: 0.6; }
251
+ 50% { opacity: 0.2; }
252
+ 100% { transform: translateY(-120vh) scale(0.5); opacity: 0; }
253
+ }
254
+
255
+ .hero-container {
256
+ max-width: 1180px;
257
+ margin: 0 auto;
258
+ padding: 0 24px;
259
+ display: grid;
260
+ grid-template-columns: 1fr 1fr;
261
+ align-items: center;
262
+ gap: 64px;
263
+ position: relative;
264
+ z-index: 1;
265
+ }
266
+ .hero-badge {
267
+ display: inline-flex;
268
+ align-items: center;
269
+ gap: 8px;
270
+ background: rgba(37,99,235,0.20);
271
+ border: 1px solid rgba(37,99,235,0.40);
272
+ color: #93C5FD;
273
+ padding: 6px 16px;
274
+ border-radius: 50px;
275
+ font-size: 0.85rem;
276
+ font-weight: 600;
277
+ margin-bottom: 24px;
278
+ }
279
+ .hero-title {
280
+ font-family: var(--font-display);
281
+ font-size: clamp(2.4rem, 5vw, 3.6rem);
282
+ font-weight: 700;
283
+ color: #fff;
284
+ line-height: 1.15;
285
+ margin-bottom: 24px;
286
+ }
287
+ .hero-subtitle {
288
+ font-size: 1.1rem;
289
+ color: #94A3B8;
290
+ margin-bottom: 40px;
291
+ max-width: 480px;
292
+ line-height: 1.7;
293
+ }
294
+ .hero-buttons { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 48px; }
295
+ .hero-stats {
296
+ display: flex;
297
+ gap: 32px;
298
+ flex-wrap: wrap;
299
+ }
300
+ .hero-stat span {
301
+ display: block;
302
+ font-family: var(--font-display);
303
+ font-size: 1.6rem;
304
+ font-weight: 700;
305
+ color: #fff;
306
+ }
307
+ .hero-stat label { font-size: 0.8rem; color: #64748B; }
308
+
309
+ /* AI Sphere */
310
+ .hero-visual { display: flex; flex-direction: column; align-items: center; gap: 32px; }
311
+ .ai-sphere {
312
+ position: relative;
313
+ width: 240px; height: 240px;
314
+ display: flex; align-items: center; justify-content: center;
315
+ }
316
+ .sphere-ring {
317
+ position: absolute;
318
+ border-radius: 50%;
319
+ border: 2px solid transparent;
320
+ }
321
+ .ring-1 {
322
+ width: 240px; height: 240px;
323
+ border-color: rgba(37,99,235,0.30);
324
+ animation: spinRing 8s linear infinite;
325
+ }
326
+ .ring-2 {
327
+ width: 180px; height: 180px;
328
+ border-color: rgba(20,184,166,0.40);
329
+ animation: spinRing 5s linear infinite reverse;
330
+ }
331
+ .ring-3 {
332
+ width: 120px; height: 120px;
333
+ border-color: rgba(124,58,237,0.50);
334
+ animation: spinRing 3s linear infinite;
335
+ }
336
+ @keyframes spinRing {
337
+ from { transform: rotateY(0deg) rotateX(20deg); }
338
+ to { transform: rotateY(360deg) rotateX(20deg); }
339
+ }
340
+ .sphere-core {
341
+ width: 80px; height: 80px;
342
+ border-radius: 50%;
343
+ background: linear-gradient(135deg, var(--blue), var(--purple));
344
+ display: flex; align-items: center; justify-content: center;
345
+ font-size: 2rem; color: #fff;
346
+ box-shadow: 0 0 40px rgba(37,99,235,0.50);
347
+ animation: pulse 2s ease-in-out infinite;
348
+ }
349
+ @keyframes pulse {
350
+ 0%, 100% { transform: scale(1); box-shadow: 0 0 40px rgba(37,99,235,0.50); }
351
+ 50% { transform: scale(1.05); box-shadow: 0 0 60px rgba(37,99,235,0.70); }
352
+ }
353
+ .waveform-container {
354
+ width: 100%;
355
+ background: rgba(255,255,255,0.05);
356
+ border: 1px solid rgba(255,255,255,0.10);
357
+ border-radius: var(--radius);
358
+ padding: 12px;
359
+ backdrop-filter: blur(10px);
360
+ }
361
+
362
+ /* ─── OVERVIEW GRID ──────────────────────────────────────── */
363
+ .overview-grid {
364
+ display: grid;
365
+ grid-template-columns: repeat(5, 1fr);
366
+ gap: 20px;
367
+ }
368
+ .overview-card {
369
+ background: var(--surface);
370
+ border: 1px solid var(--border);
371
+ border-radius: var(--radius);
372
+ padding: 28px 20px;
373
+ text-align: center;
374
+ transition: all var(--transition);
375
+ position: relative;
376
+ overflow: hidden;
377
+ }
378
+ .overview-card::before {
379
+ content: '';
380
+ position: absolute;
381
+ top: 0; left: 0; right: 0;
382
+ height: 3px;
383
+ background: linear-gradient(90deg, var(--blue), var(--teal));
384
+ transform: scaleX(0);
385
+ transform-origin: left;
386
+ transition: transform var(--transition);
387
+ }
388
+ .overview-card:hover { transform: translateY(-6px); box-shadow: var(--shadow-lg); }
389
+ .overview-card:hover::before { transform: scaleX(1); }
390
+ .card-icon {
391
+ width: 52px; height: 52px;
392
+ border-radius: var(--radius-sm);
393
+ display: flex; align-items: center; justify-content: center;
394
+ font-size: 1.4rem;
395
+ margin: 0 auto 16px;
396
+ }
397
+ .icon-blue { background: rgba(37,99,235,0.12); color: var(--blue); }
398
+ .icon-teal { background: rgba(20,184,166,0.12); color: var(--teal); }
399
+ .icon-purple { background: rgba(124,58,237,0.12); color: var(--purple); }
400
+ .icon-green { background: rgba(16,185,129,0.12); color: var(--green); }
401
+ .icon-orange { background: rgba(245,158,11,0.12); color: var(--orange); }
402
+
403
+ .overview-card h3 { font-family: var(--font-display); font-size: 1rem; font-weight: 600; margin-bottom: 10px; }
404
+ .overview-card p { font-size: 0.85rem; color: var(--text-muted); line-height: 1.6; margin-bottom: 14px; }
405
+ .card-tag {
406
+ display: inline-block;
407
+ padding: 3px 10px;
408
+ border-radius: 50px;
409
+ font-size: 0.75rem;
410
+ font-weight: 600;
411
+ background: rgba(37,99,235,0.10);
412
+ color: var(--blue);
413
+ }
414
+
415
+ /* ─── ANALYSIS SECTION ───────────────────────────────────── */
416
+ .analysis-layout {
417
+ display: grid;
418
+ grid-template-columns: 1fr 380px;
419
+ gap: 32px;
420
+ align-items: start;
421
+ }
422
+ .upload-panel { display: flex; flex-direction: column; gap: 20px; }
423
+
424
+ .drop-zone {
425
+ border: 2px dashed var(--border);
426
+ border-radius: var(--radius-lg);
427
+ background: var(--surface);
428
+ padding: 56px 32px;
429
+ text-align: center;
430
+ cursor: pointer;
431
+ transition: all var(--transition);
432
+ position: relative;
433
+ }
434
+ .drop-zone.drag-over {
435
+ border-color: var(--blue);
436
+ background: rgba(37,99,235,0.05);
437
+ transform: scale(1.01);
438
+ }
439
+ .drop-icon {
440
+ font-size: 3rem;
441
+ color: var(--blue);
442
+ margin-bottom: 16px;
443
+ animation: float 3s ease-in-out infinite;
444
+ }
445
+ @keyframes float {
446
+ 0%, 100% { transform: translateY(0); }
447
+ 50% { transform: translateY(-8px); }
448
+ }
449
+ .drop-zone h3 { font-family: var(--font-display); font-size: 1.2rem; font-weight: 600; margin-bottom: 8px; }
450
+ .drop-zone p { color: var(--text-muted); margin-bottom: 20px; font-size: 0.95rem; }
451
+
452
+ .file-preview {
453
+ background: var(--surface);
454
+ border: 1px solid var(--border);
455
+ border-radius: var(--radius-lg);
456
+ padding: 28px;
457
+ display: flex;
458
+ flex-direction: column;
459
+ gap: 20px;
460
+ }
461
+ .file-info {
462
+ display: flex;
463
+ align-items: center;
464
+ gap: 16px;
465
+ }
466
+ .file-icon {
467
+ width: 52px; height: 52px;
468
+ background: rgba(37,99,235,0.10);
469
+ border-radius: var(--radius-sm);
470
+ display: flex; align-items: center; justify-content: center;
471
+ color: var(--blue);
472
+ font-size: 1.4rem;
473
+ flex-shrink: 0;
474
+ }
475
+ .file-name { font-weight: 600; font-size: 0.95rem; display: block; margin-bottom: 4px; }
476
+ .file-meta { font-size: 0.82rem; color: var(--text-muted); display: flex; gap: 8px; }
477
+ .file-remove {
478
+ margin-left: auto;
479
+ width: 34px; height: 34px;
480
+ border-radius: 50%;
481
+ background: rgba(239,68,68,0.10);
482
+ color: var(--red);
483
+ display: flex; align-items: center; justify-content: center;
484
+ font-size: 1rem;
485
+ transition: all var(--transition);
486
+ flex-shrink: 0;
487
+ }
488
+ .file-remove:hover { background: var(--red); color: #fff; }
489
+ audio { width: 100%; border-radius: var(--radius-sm); }
490
+ .action-buttons { display: flex; gap: 12px; }
491
+
492
+ .upload-info {
493
+ background: var(--surface);
494
+ border: 1px solid var(--border);
495
+ border-radius: var(--radius-lg);
496
+ padding: 32px;
497
+ }
498
+ .upload-info h3 { font-family: var(--font-display); font-size: 1.1rem; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; color: var(--blue); }
499
+ .info-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 24px; }
500
+ .info-list li { display: flex; align-items: center; gap: 10px; font-size: 0.9rem; }
501
+ .info-list li i { color: var(--green); }
502
+ .format-chips { display: flex; gap: 8px; }
503
+ .chip {
504
+ display: inline-flex; align-items: center; gap: 6px;
505
+ padding: 6px 14px;
506
+ border-radius: 50px;
507
+ font-size: 0.82rem; font-weight: 600;
508
+ }
509
+ .chip-blue { background: rgba(37,99,235,0.12); color: var(--blue); }
510
+ .chip-teal { background: rgba(20,184,166,0.12); color: var(--teal); }
511
+
512
+ /* ─── LOADING OVERLAY ────────────────────────────────────── */
513
+ .loading-overlay {
514
+ position: fixed;
515
+ inset: 0;
516
+ z-index: 9999;
517
+ background: rgba(15,23,42,0.90);
518
+ backdrop-filter: blur(10px);
519
+ display: flex; align-items: center; justify-content: center;
520
+ }
521
+ .loading-card {
522
+ background: var(--surface);
523
+ border: 1px solid var(--border);
524
+ border-radius: var(--radius-xl);
525
+ padding: 48px 40px;
526
+ max-width: 440px;
527
+ width: 90%;
528
+ text-align: center;
529
+ box-shadow: var(--shadow-lg);
530
+ }
531
+ .loading-sphere {
532
+ width: 80px; height: 80px;
533
+ border-radius: 50%;
534
+ background: linear-gradient(135deg, var(--blue), var(--purple));
535
+ display: flex; align-items: center; justify-content: center;
536
+ font-size: 2rem; color: #fff;
537
+ margin: 0 auto 24px;
538
+ animation: pulse 1.5s ease-in-out infinite;
539
+ }
540
+ .loading-card h3 { font-family: var(--font-display); font-size: 1.4rem; margin-bottom: 8px; }
541
+ .loading-card p { color: var(--text-muted); margin-bottom: 28px; }
542
+ .progress-bar-wrap {
543
+ background: var(--surface-2);
544
+ border-radius: 50px;
545
+ height: 8px;
546
+ overflow: hidden;
547
+ margin-bottom: 10px;
548
+ }
549
+ .progress-bar {
550
+ height: 100%;
551
+ background: linear-gradient(90deg, var(--blue), var(--teal));
552
+ border-radius: 50px;
553
+ width: 0%;
554
+ transition: width 0.3s ease;
555
+ }
556
+ .progress-pct { font-family: var(--font-display); font-weight: 700; color: var(--blue); margin-bottom: 28px; }
557
+ .loading-steps { text-align: left; display: flex; flex-direction: column; gap: 12px; }
558
+ .step { display: flex; align-items: center; gap: 12px; font-size: 0.9rem; color: var(--text-muted); }
559
+ .step.done { color: var(--green); }
560
+ .step.done i::before { content: '\f058'; font-family: 'Font Awesome 6 Free'; font-weight: 900; }
561
+ .step.active { color: var(--blue); }
562
+
563
+ /* ─── RECORDER ───────────────────────────────────────────── */
564
+ .recorder-card {
565
+ background: var(--surface);
566
+ border: 1px solid var(--border);
567
+ border-radius: var(--radius-xl);
568
+ padding: 40px;
569
+ max-width: 680px;
570
+ margin: 0 auto;
571
+ text-align: center;
572
+ box-shadow: var(--shadow);
573
+ }
574
+ .recorder-top { display: flex; align-items: center; justify-content: center; gap: 16px; margin-bottom: 24px; }
575
+ .timer-display {
576
+ font-family: var(--font-display);
577
+ font-size: 2.4rem;
578
+ font-weight: 700;
579
+ color: var(--text);
580
+ letter-spacing: 0.05em;
581
+ }
582
+ .rec-indicator {
583
+ width: 14px; height: 14px;
584
+ border-radius: 50%;
585
+ background: var(--text-light);
586
+ transition: background var(--transition);
587
+ }
588
+ .rec-indicator.recording {
589
+ background: var(--red);
590
+ animation: blink 1s ease-in-out infinite;
591
+ }
592
+ @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } }
593
+
594
+ #liveWaveform { width: 100%; border-radius: var(--radius); margin-bottom: 28px; }
595
+ .recorder-controls { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
596
+ .rec-btn {
597
+ padding: 12px 20px;
598
+ border-radius: 50px;
599
+ font-size: 0.9rem;
600
+ font-weight: 600;
601
+ display: flex; align-items: center; gap: 8px;
602
+ transition: all var(--transition);
603
+ border: 2px solid transparent;
604
+ }
605
+ .btn-start { background: linear-gradient(135deg,var(--blue),var(--blue-dark)); color:#fff; }
606
+ .btn-start:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(37,99,235,0.35); }
607
+ .btn-stop { background: rgba(239,68,68,0.12); color: var(--red); border-color: var(--red); }
608
+ .btn-stop:hover:not(:disabled) { background: var(--red); color:#fff; }
609
+ .btn-play { background: rgba(16,185,129,0.12); color: var(--green); border-color: var(--green); }
610
+ .btn-play:hover:not(:disabled) { background: var(--green); color:#fff; }
611
+ .btn-delete{ background: rgba(245,158,11,0.12); color: var(--orange); border-color: var(--orange); }
612
+ .btn-delete:hover:not(:disabled){ background: var(--orange); color:#fff; }
613
+ .rec-btn:disabled { opacity: 0.35; cursor: not-allowed; }
614
+
615
+ /* ─── RESULTS ────────────────────────────────────────────── */
616
+ .results-placeholder {
617
+ text-align: center;
618
+ padding: 80px 20px;
619
+ color: var(--text-muted);
620
+ }
621
+ .results-placeholder i { font-size: 4rem; margin-bottom: 16px; color: var(--border); }
622
+ .results-content { display: flex; flex-direction: column; gap: 32px; }
623
+ .results-grid { display: flex; flex-direction: column; gap: 24px; }
624
+
625
+ .prediction-card {
626
+ border-radius: var(--radius-lg);
627
+ padding: 36px;
628
+ text-align: center;
629
+ display: flex;
630
+ align-items: center;
631
+ justify-content: center;
632
+ gap: 24px;
633
+ flex-wrap: wrap;
634
+ }
635
+ .prediction-card.normal {
636
+ background: linear-gradient(135deg, rgba(16,185,129,0.15), rgba(20,184,166,0.10));
637
+ border: 2px solid rgba(16,185,129,0.40);
638
+ }
639
+ .prediction-card.stutter {
640
+ background: linear-gradient(135deg, rgba(239,68,68,0.15), rgba(245,158,11,0.10));
641
+ border: 2px solid rgba(239,68,68,0.40);
642
+ }
643
+ .pred-icon { font-size: 3rem; }
644
+ .pred-label { font-family: var(--font-display); font-size: 2rem; font-weight: 700; }
645
+ .pred-sublabel { font-size: 0.95rem; color: var(--text-muted); }
646
+
647
+ .score-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
648
+ .score-card {
649
+ background: var(--surface);
650
+ border: 1px solid var(--border);
651
+ border-radius: var(--radius);
652
+ padding: 24px 20px;
653
+ text-align: center;
654
+ position: relative;
655
+ overflow: hidden;
656
+ }
657
+ .score-value {
658
+ font-family: var(--font-display);
659
+ font-size: 2rem;
660
+ font-weight: 700;
661
+ color: var(--blue);
662
+ margin-bottom: 6px;
663
+ }
664
+ .score-label { font-size: 0.82rem; color: var(--text-muted); font-weight: 500; }
665
+ .score-icon {
666
+ position: absolute;
667
+ top: 16px; right: 16px;
668
+ font-size: 1rem;
669
+ color: var(--border);
670
+ }
671
+
672
+ .charts-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
673
+ .chart-card {
674
+ background: var(--surface);
675
+ border: 1px solid var(--border);
676
+ border-radius: var(--radius);
677
+ padding: 20px 16px;
678
+ text-align: center;
679
+ }
680
+ .chart-card h4 {
681
+ font-size: 0.82rem;
682
+ font-weight: 600;
683
+ color: var(--text-muted);
684
+ margin-bottom: 12px;
685
+ display: flex; align-items: center; justify-content: center; gap: 6px;
686
+ }
687
+ .chart-card canvas { width: 100% !important; }
688
+
689
+ .waveform-result-card {
690
+ background: var(--surface);
691
+ border: 1px solid var(--border);
692
+ border-radius: var(--radius);
693
+ padding: 20px;
694
+ }
695
+ .waveform-result-card h4 { font-size: 0.9rem; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; color: var(--text-muted); }
696
+ #resultWaveform { width: 100% !important; }
697
+
698
+ .report-section { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
699
+
700
+ /* ─── HISTORY ────────────────────────────────────────────── */
701
+ .history-controls { display: flex; gap: 16px; align-items: center; margin-bottom: 24px; flex-wrap: wrap; }
702
+ .search-box {
703
+ flex: 1;
704
+ display: flex;
705
+ align-items: center;
706
+ gap: 10px;
707
+ background: var(--surface);
708
+ border: 1px solid var(--border);
709
+ border-radius: 50px;
710
+ padding: 10px 20px;
711
+ min-width: 220px;
712
+ }
713
+ .search-box i { color: var(--text-muted); }
714
+ .search-box input {
715
+ border: none;
716
+ outline: none;
717
+ background: transparent;
718
+ color: var(--text);
719
+ font-size: 0.9rem;
720
+ width: 100%;
721
+ font-family: var(--font-body);
722
+ }
723
+ .history-table-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
724
+ .history-table { width: 100%; border-collapse: collapse; }
725
+ .history-table thead { background: var(--bg-alt); }
726
+ .history-table th {
727
+ padding: 14px 20px;
728
+ text-align: left;
729
+ font-size: 0.82rem;
730
+ font-weight: 700;
731
+ text-transform: uppercase;
732
+ letter-spacing: 0.06em;
733
+ color: var(--text-muted);
734
+ }
735
+ .history-table td { padding: 14px 20px; font-size: 0.9rem; border-top: 1px solid var(--border); }
736
+ .history-table tbody tr:hover { background: var(--bg-alt); }
737
+ .badge {
738
+ display: inline-block;
739
+ padding: 3px 12px;
740
+ border-radius: 50px;
741
+ font-size: 0.78rem;
742
+ font-weight: 700;
743
+ }
744
+ .badge-normal { background: rgba(16,185,129,0.15); color: var(--green); }
745
+ .badge-stutter { background: rgba(239,68,68,0.15); color: var(--red); }
746
+ .history-empty {
747
+ display: none;
748
+ text-align: center;
749
+ padding: 60px;
750
+ color: var(--text-muted);
751
+ }
752
+ .history-empty i { font-size: 3rem; margin-bottom: 14px; color: var(--border); }
753
+ .delete-row-btn {
754
+ background: rgba(239,68,68,0.10);
755
+ color: var(--red);
756
+ border: none;
757
+ border-radius: var(--radius-sm);
758
+ padding: 6px 10px;
759
+ cursor: pointer;
760
+ font-size: 0.8rem;
761
+ transition: all var(--transition);
762
+ }
763
+ .delete-row-btn:hover { background: var(--red); color: #fff; }
764
+
765
+ /* ─── TIMELINE ───────────────────────────────────────────── */
766
+ .timeline { max-width: 720px; margin: 0 auto; display: flex; flex-direction: column; gap: 0; }
767
+ .timeline-item {
768
+ display: grid;
769
+ grid-template-columns: 56px 1fr;
770
+ gap: 24px;
771
+ position: relative;
772
+ }
773
+ .timeline-item:not(:last-child)::after {
774
+ content: '';
775
+ position: absolute;
776
+ left: 27px;
777
+ top: 56px;
778
+ bottom: -24px;
779
+ width: 2px;
780
+ background: var(--border);
781
+ }
782
+ .timeline-dot {
783
+ width: 56px; height: 56px;
784
+ border-radius: 50%;
785
+ display: flex; align-items: center; justify-content: center;
786
+ font-size: 1.2rem;
787
+ flex-shrink: 0;
788
+ position: relative;
789
+ z-index: 1;
790
+ }
791
+ .dot-blue { background: rgba(37,99,235,0.15); color: var(--blue); border: 2px solid var(--blue); }
792
+ .dot-teal { background: rgba(20,184,166,0.15); color: var(--teal); border: 2px solid var(--teal); }
793
+ .dot-purple { background: rgba(124,58,237,0.15); color: var(--purple); border: 2px solid var(--purple); }
794
+ .dot-green { background: rgba(16,185,129,0.15); color: var(--green); border: 2px solid var(--green); }
795
+ .dot-orange { background: rgba(245,158,11,0.15); color: var(--orange); border: 2px solid var(--orange); }
796
+
797
+ .timeline-content { padding: 8px 0 40px; }
798
+ .timeline-step {
799
+ font-size: 0.78rem;
800
+ font-weight: 700;
801
+ letter-spacing: 0.10em;
802
+ text-transform: uppercase;
803
+ color: var(--blue);
804
+ margin-bottom: 8px;
805
+ }
806
+ .timeline-content h3 { font-family: var(--font-display); font-size: 1.15rem; font-weight: 600; margin-bottom: 10px; }
807
+ .timeline-content p { color: var(--text-muted); font-size: 0.95rem; line-height: 1.7; }
808
+
809
+ /* ─── STATS SECTION ──────────────────────────────────────── */
810
+ .stats-section {
811
+ background: linear-gradient(135deg, #0F172A 0%, #1E1B4B 50%, #0F172A 100%);
812
+ }
813
+ .stats-section .section-eyebrow { background: rgba(37,99,235,0.25); color: #93C5FD; }
814
+ .stats-section .section-title { color: #fff; }
815
+ .stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 24px; }
816
+ .stat-card {
817
+ background: rgba(255,255,255,0.05);
818
+ border: 1px solid rgba(255,255,255,0.10);
819
+ border-radius: var(--radius-lg);
820
+ padding: 36px 24px;
821
+ text-align: center;
822
+ backdrop-filter: blur(10px);
823
+ transition: all var(--transition);
824
+ }
825
+ .stat-card:hover { transform: translateY(-6px); background: rgba(37,99,235,0.15); }
826
+ .stat-icon { font-size: 2rem; color: var(--teal); margin-bottom: 16px; }
827
+ .stat-number {
828
+ font-family: var(--font-display);
829
+ font-size: 2.8rem;
830
+ font-weight: 700;
831
+ color: #fff;
832
+ margin-bottom: 8px;
833
+ }
834
+ .stat-label { color: #94A3B8; font-size: 0.9rem; }
835
+
836
+ /* ─── FOOTER ─────────────────────────────────────────────── */
837
+ .footer { background: #020617; color: #94A3B8; padding: 64px 0 32px; }
838
+ .footer-top { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; margin-bottom: 48px; }
839
+ .footer-brand .nav-logo { color: #fff; margin-bottom: 16px; }
840
+ .footer-brand p { font-size: 0.9rem; line-height: 1.7; max-width: 320px; }
841
+ .footer-info { display: flex; gap: 48px; }
842
+ .footer-block h4 { color: #fff; font-weight: 600; margin-bottom: 10px; }
843
+ .footer-block p { font-size: 0.9rem; line-height: 1.8; }
844
+ .footer-bottom {
845
+ border-top: 1px solid rgba(255,255,255,0.08);
846
+ padding-top: 28px;
847
+ display: flex;
848
+ align-items: center;
849
+ justify-content: space-between;
850
+ flex-wrap: wrap;
851
+ gap: 16px;
852
+ }
853
+ .footer-copy { font-size: 0.85rem; }
854
+ .footer-socials { display: flex; gap: 12px; }
855
+ .social-link {
856
+ width: 40px; height: 40px;
857
+ border-radius: 50%;
858
+ border: 1px solid rgba(255,255,255,0.15);
859
+ display: flex; align-items: center; justify-content: center;
860
+ color: #94A3B8;
861
+ transition: all var(--transition);
862
+ }
863
+ .social-link:hover { border-color: var(--blue); color: var(--blue); background: rgba(37,99,235,0.10); }
864
+
865
+ /* ─── SCROLL TO TOP ──────────────────────────────────────── */
866
+ .scroll-top {
867
+ position: fixed;
868
+ bottom: 32px; right: 32px;
869
+ width: 48px; height: 48px;
870
+ border-radius: 50%;
871
+ background: linear-gradient(135deg, var(--blue), var(--blue-dark));
872
+ color: #fff;
873
+ font-size: 1rem;
874
+ display: flex; align-items: center; justify-content: center;
875
+ box-shadow: 0 4px 16px rgba(37,99,235,0.40);
876
+ opacity: 0;
877
+ transform: translateY(16px);
878
+ transition: all var(--transition);
879
+ z-index: 999;
880
+ }
881
+ .scroll-top.visible { opacity: 1; transform: translateY(0); }
882
+ .scroll-top:hover { transform: translateY(-4px); }
883
+
884
+ /* ─── RESPONSIVE ─────────────────────────────────────────── */
885
+ @media (max-width: 1024px) {
886
+ .overview-grid { grid-template-columns: repeat(3, 1fr); }
887
+ .score-cards { grid-template-columns: repeat(2, 1fr); }
888
+ .charts-row { grid-template-columns: repeat(2, 1fr); }
889
+ .analysis-layout { grid-template-columns: 1fr; }
890
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
891
+ }
892
+ @media (max-width: 768px) {
893
+ .nav-links { display: none; flex-direction: column; position: fixed; top: 73px; left: 0; right: 0; background: var(--surface); padding: 20px; border-bottom: 1px solid var(--border); z-index: 999; }
894
+ .nav-links.open { display: flex; }
895
+ .hamburger { display: flex; }
896
+ .hero-container { grid-template-columns: 1fr; text-align: center; gap: 40px; }
897
+ .hero-visual { display: none; }
898
+ .hero-buttons { justify-content: center; }
899
+ .hero-stats { justify-content: center; }
900
+ .overview-grid { grid-template-columns: repeat(2, 1fr); }
901
+ .score-cards { grid-template-columns: 1fr 1fr; }
902
+ .charts-row { grid-template-columns: 1fr 1fr; }
903
+ .footer-top { grid-template-columns: 1fr; }
904
+ .footer-info { flex-direction: column; gap: 24px; }
905
+ .stats-grid { grid-template-columns: 1fr 1fr; }
906
+ .recorder-controls{ gap: 8px; }
907
+ .rec-btn { padding: 10px 14px; font-size: 0.82rem; }
908
+ .section { padding: 64px 0; }
909
+ }
910
+ @media (max-width: 480px) {
911
+ .overview-grid { grid-template-columns: 1fr; }
912
+ .score-cards { grid-template-columns: 1fr; }
913
+ .charts-row { grid-template-columns: 1fr; }
914
+ .stats-grid { grid-template-columns: 1fr; }
915
+ .prediction-card { flex-direction: column; gap: 12px; }
916
+ .pred-label { font-size: 1.5rem; }
917
+ .hero-title { font-size: 2rem; }
918
+ }