Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58,433 changes: 8,544 additions & 49,889 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/app/components/social/GroupDiscussionThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export default function GroupDiscussionThread({ messages, onPost }: GroupDiscuss

// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
const end = messagesEndRef.current;
if (typeof end?.scrollIntoView === 'function') {
end.scrollIntoView({ behavior: 'smooth' });
}
}, [messages]);

const handlePost = () => {
Expand Down
11 changes: 11 additions & 0 deletions src/app/components/video/AdvancedVideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {
} from 'lucide-react';
import { useVideoPlayer } from '../../hooks/useVideoPlayer';
import { useVideoLazyLoad } from '../../hooks/useVideoLazyLoad';
import { useAudioEnhancement } from '../../hooks/useAudioEnhancement';
import { PlaybackControls } from './PlaybackControls';
import { AudioEnhancement } from './AudioEnhancement';
import { VideoNotes } from './VideoNotes';
import { VideoBookmarks } from './VideoBookmarks';
import { TranscriptView } from './TranscriptView';
Expand Down Expand Up @@ -114,6 +116,8 @@ export function AdvancedVideoPlayer(props: AdvancedVideoPlayerProps) {
isMuted,
} = useVideoPlayer(videoRef);

const audioEnhancement = useAudioEnhancement(videoRef);

const analytics = usePlaybackAnalytics({
lessonId,
userId,
Expand Down Expand Up @@ -267,6 +271,10 @@ export function AdvancedVideoPlayer(props: AdvancedVideoPlayerProps) {
e.preventDefault();
if (document.pictureInPictureEnabled) void togglePiP();
break;
case 'e':
e.preventDefault();
audioEnhancement.toggle();
break;
}
};

Expand All @@ -283,6 +291,7 @@ export function AdvancedVideoPlayer(props: AdvancedVideoPlayerProps) {
toggleMute,
togglePiP,
volume,
audioEnhancement,
]);

// Touch gesture handlers (double tap play/pause, swipe seek).
Expand Down Expand Up @@ -369,6 +378,7 @@ export function AdvancedVideoPlayer(props: AdvancedVideoPlayerProps) {
onBookmark: (b: { time: number; title: string; note?: string }) =>
analytics.registerBookmarkAdded(b.time),
onNote: (n: { time: number; text: string }) => analytics.registerNoteAdded(n.time),
audioEnhancement,
};

return (
Expand Down Expand Up @@ -584,6 +594,7 @@ export function AdvancedVideoPlayer(props: AdvancedVideoPlayerProps) {
</div>

<PlaybackControls />
<AudioEnhancement />

<div className="mt-2 text-xs text-white/80 flex items-center justify-between">
<span>Watched: {Math.round(analytics.snapshot.watchSeconds)}s</span>
Expand Down
138 changes: 138 additions & 0 deletions src/app/components/video/AudioEnhancement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
'use client';

import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Wand2, ChevronDown } from 'lucide-react';
import { useVideoPlayerContext } from './VideoPlayerContext';

export const AudioEnhancement: React.FC = () => {
const {
audioEnhancement: {
enabled,
bassBoost,
voiceClarity,
noiseReduction,
toggle,
setBassBoost,
setVoiceClarity,
setNoiseReduction,
isSupported,
},
} = useVideoPlayerContext();

const [showPanel, setShowPanel] = useState(false);

if (!isSupported) return null;

return (
<div className="relative">
<button
type="button"
onClick={() => setShowPanel((s) => !s)}
aria-label="Audio enhancement settings"
aria-expanded={showPanel}
className={`flex items-center space-x-1 px-3 py-1 rounded transition-colors text-sm ${
enabled ? 'bg-blue-500 text-white' : 'bg-white/20 hover:bg-white/30 text-white'
}`}
>
<Wand2 size={12} />
<span>Enhance</span>
<ChevronDown size={12} />
</button>

<AnimatePresence>
{showPanel && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="absolute bottom-full left-0 mb-2 w-56 bg-gray-800 rounded-lg shadow-lg p-3 z-10 space-y-3"
>
{/* Toggle */}
<div className="flex items-center justify-between">
<span className="text-white text-sm font-medium">Audio Enhancement</span>
<button
type="button"
onClick={toggle}
aria-label={enabled ? 'Disable audio enhancement' : 'Enable audio enhancement'}
aria-pressed={enabled}
className={`w-10 h-5 rounded-full transition-colors relative ${
enabled ? 'bg-blue-500' : 'bg-gray-600'
}`}
>
<span
className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${
enabled ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</button>
</div>

{/* Sliders — only interactive when enabled */}
<div className={`space-y-2 ${!enabled ? 'opacity-50 pointer-events-none' : ''}`}>
<Slider
label="Bass Boost"
value={bassBoost}
min={0}
max={20}
step={1}
unit="dB"
onChange={setBassBoost}
/>
<Slider
label="Voice Clarity"
value={voiceClarity}
min={0}
max={20}
step={1}
unit="dB"
onChange={setVoiceClarity}
/>
<Slider
label="Noise Reduction"
value={Math.round(noiseReduction * 100)}
min={0}
max={100}
step={5}
unit="%"
onChange={(v) => setNoiseReduction(v / 100)}
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};

interface SliderProps {
label: string;
value: number;
min: number;
max: number;
step: number;
unit: string;
onChange: (value: number) => void;
}

const Slider: React.FC<SliderProps> = ({ label, value, min, max, step, unit, onChange }) => (
<div>
<div className="flex justify-between text-xs text-gray-300 mb-1">
<span>{label}</span>
<span>
{value}
{unit}
</span>
</div>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
aria-label={label}
className="w-full h-1 bg-gray-600 rounded-full appearance-none cursor-pointer accent-blue-500"
/>
</div>
);
6 changes: 5 additions & 1 deletion src/app/components/video/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { VideoPlayerContext } from './VideoPlayerContext';
import type { VideoPlayerContextValue } from './VideoPlayerContext';
import { useVideoPlayer } from '../../hooks/useVideoPlayer';
import { useVideoLazyLoad } from '../../hooks/useVideoLazyLoad';
import { useAudioEnhancement } from '../../hooks/useAudioEnhancement';

interface VideoPlayerProps {
src: string;
Expand Down Expand Up @@ -82,6 +83,8 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
resetError,
} = useVideoPlayer(videoRef);

const audioEnhancement = useAudioEnhancement(videoRef);

// Auto-hide controls
useEffect(() => {
if (!isPlaying) return;
Expand Down Expand Up @@ -296,8 +299,9 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
setAutoQualityLearning: () => undefined,
onBookmark: onBookmark ?? (() => undefined),
onNote: onNote ?? (() => undefined),
audioEnhancement,
}),
[transcript, currentTime, duration, playbackRate, seekTo, setPlaybackRate, onBookmark, onNote],
[transcript, currentTime, duration, playbackRate, seekTo, setPlaybackRate, onBookmark, onNote, audioEnhancement],
);

if (error) {
Expand Down
2 changes: 2 additions & 0 deletions src/app/components/video/VideoPlayerContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { createContext, useContext } from 'react';
import type { UseAudioEnhancementReturn } from '../../hooks/useAudioEnhancement';

export interface VideoQualityControlOption {
label: string;
Expand All @@ -26,6 +27,7 @@ export interface VideoPlayerContextValue {
setAutoQualityLearning: (auto: boolean) => void;
onBookmark: (bookmark: { time: number; title: string; note?: string }) => void;
onNote: (note: { time: number; text: string }) => void;
audioEnhancement: UseAudioEnhancementReturn;
}

export const VideoPlayerContext = createContext<VideoPlayerContextValue | null>(null);
Expand Down
Loading
Loading