diff --git a/frontend/src/components/KiraAvatar.tsx b/frontend/src/components/KiraAvatar.tsx
index be2e9e3..5a814ea 100644
--- a/frontend/src/components/KiraAvatar.tsx
+++ b/frontend/src/components/KiraAvatar.tsx
@@ -223,25 +223,40 @@ export default function KiraAvatar(props: Props) {
)}
- {/* Expression indicator */}
+ {/* Expression indicator + Talk button */}
{live2dReady && (
-
- {EXPRESSIONS.map((expr) => (
-
- ))}
-
+ <>
+
+ {EXPRESSIONS.map((expr) => (
+
+ ))}
+
+
+ {/* Talk button â mic toggle */}
+
+ >
)}
{/* Status bar */}
@@ -251,6 +266,16 @@ export default function KiraAvatar(props: Props) {
{props.isSpeaking ? 'speaking...' : props.isListening ? 'listening...' : live2dReady ? 'here with you' : 'loading...'}
+
+
);
}
diff --git a/frontend/src/components/MusicPlayer.tsx b/frontend/src/components/MusicPlayer.tsx
index bba899f..df1ad32 100644
--- a/frontend/src/components/MusicPlayer.tsx
+++ b/frontend/src/components/MusicPlayer.tsx
@@ -1,27 +1,87 @@
-import { useState, useRef, useEffect } from 'react';
+import { useState, useRef, useEffect, useCallback } from 'react';
-const LOFI_PLAYLISTS = [
- { id: 'lofi-girl', name: 'lofi hip hop radio', url: 'https://www.youtube.com/embed/jfKfPfyJRdk', icon: 'đ§' },
- { id: 'lofi-chill', name: 'Chill lofi', url: 'https://www.youtube.com/embed/5qap5aO4i9A', icon: 'đĩ' },
- { id: 'lofi-synth', name: 'Synthwave lofi', url: 'https://www.youtube.com/embed/MVPTmgNG4x0', icon: 'đ' },
+interface Playlist {
+ id: string;
+ name: string;
+ videoId: string;
+ icon: string;
+}
+
+const LOFI_PLAYLISTS: Playlist[] = [
+ { id: 'lofi-girl', name: 'lofi hip hop radio', videoId: 'jfKfPfyJRdk', icon: 'đ§' },
+ { id: 'lofi-chill', name: 'Chill lofi', videoId: '5qap5aO4i9A', icon: 'đĩ' },
+ { id: 'lofi-synth', name: 'Synthwave lofi', videoId: 'MVPTmgNG4x0', icon: 'đ' },
];
export default function MusicPlayer() {
- const [active, setActive] = useState('lofi-girl');
+ const [activeId, setActiveId] = useState('lofi-girl');
const [volume, setVolume] = useState(0.3);
- const iframeRef = useRef(null);
+ const [started, setStarted] = useState(false);
+ const wrapperRef = useRef(null);
+ const playerRef = useRef(null);
+ const apiReady = useRef(false);
- useEffect(() => {
- // Post volume to YouTube iframe when it changes
- const timer = setTimeout(() => {
- if (iframeRef.current?.contentWindow) {
- iframeRef.current.contentWindow.postMessage(
- JSON.stringify({ event: 'command', func: 'setVolume', args: [volume * 100] }),
- '*'
- );
+ // Load YouTube IFrame API on first user interaction
+ const startPlayer = useCallback(() => {
+ if (apiReady.current) return;
+ apiReady.current = true;
+
+ const tag = document.createElement('script');
+ tag.src = 'https://www.youtube.com/iframe_api';
+ const first = document.getElementsByTagName('script')[0];
+ first.parentNode?.insertBefore(tag, first);
+
+ (window as any).onYouTubeIframeAPIReady = () => {
+ const active = LOFI_PLAYLISTS.find((p) => p.id === activeId);
+ if (!active) return;
+
+ playerRef.current = new (window as any).YT.Player('kira-youtube-player', {
+ height: '0',
+ width: '0',
+ videoId: active.videoId,
+ playerVars: {
+ autoplay: 1,
+ controls: 0,
+ loop: 1,
+ playlist: active.videoId,
+ enablejsapi: 1,
+ },
+ events: {
+ onReady: (e: any) => {
+ e.target.setVolume(volume * 100);
+ e.target.playVideo();
+ },
+ },
+ });
+ };
+ }, [activeId, volume]);
+
+ // Handle play button click (first user gesture)
+ const handlePlay = () => {
+ if (!started) {
+ setStarted(true);
+ startPlayer();
+ }
+ };
+
+ // Change station
+ const changeStation = (id: string) => {
+ setActiveId(id);
+ if (playerRef.current && (window as any).YT) {
+ const active = LOFI_PLAYLISTS.find((p) => p.id === id);
+ if (active) {
+ playerRef.current.loadVideoById(active.videoId);
+ playerRef.current.setVolume(volume * 100);
+ playerRef.current.playVideo();
}
- }, 500);
- return () => clearTimeout(timer);
+ }
+ };
+
+ // Volume
+ useEffect(() => {
+ if (playerRef.current) {
+ playerRef.current.setVolume(volume * 100);
+ }
}, [volume]);
return (
@@ -44,18 +104,16 @@ export default function MusicPlayer() {
-
+
{LOFI_PLAYLISTS.map((p) => (
-
);