190 lines
5.1 KiB
TypeScript
190 lines
5.1 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { ChatPanel } from './components/ChatPanel';
|
||
import { VoiceControls } from './components/VoiceControls';
|
||
import { AvatarView } from './components/AvatarView';
|
||
import { Captions } from './components/Captions';
|
||
import { Settings } from './components/Settings';
|
||
import { useSession } from './hooks/useSession';
|
||
import { useConversation } from './hooks/useConversation';
|
||
import { useWebRTC } from './hooks/useWebRTC';
|
||
import { PostMessageAPI } from './services/postMessage';
|
||
import { WidgetConfig } from './types';
|
||
import './App.css';
|
||
|
||
// Default config - can be overridden via postMessage or data attributes
|
||
const getConfig = (): WidgetConfig => {
|
||
const script = document.querySelector('script[data-tenant-id]');
|
||
if (script) {
|
||
return {
|
||
tenantId: script.getAttribute('data-tenant-id') || 'default',
|
||
userId: script.getAttribute('data-user-id') || undefined,
|
||
authToken: script.getAttribute('data-auth-token') || undefined,
|
||
apiUrl: script.getAttribute('data-api-url') || undefined,
|
||
avatarEnabled: script.getAttribute('data-avatar-enabled') !== 'false',
|
||
};
|
||
}
|
||
|
||
return {
|
||
tenantId: 'default',
|
||
avatarEnabled: true,
|
||
};
|
||
};
|
||
|
||
export const App: React.FC = () => {
|
||
const [config] = useState<WidgetConfig>(getConfig());
|
||
const [showSettings, setShowSettings] = useState(false);
|
||
const [showCaptions, setShowCaptions] = useState(true);
|
||
const [avatarEnabled, setAvatarEnabled] = useState(config.avatarEnabled ?? true);
|
||
const [volume, setVolume] = useState(100);
|
||
const [isMuted, setIsMuted] = useState(false);
|
||
const [captionText, setCaptionText] = useState('');
|
||
|
||
const postMessage = new PostMessageAPI();
|
||
const { session, loading, error, createSession, endSession } = useSession(config);
|
||
const {
|
||
messages,
|
||
isListening,
|
||
isSpeaking,
|
||
setIsListening,
|
||
setIsSpeaking,
|
||
sendMessage,
|
||
receiveMessage,
|
||
} = useConversation();
|
||
const { isConnected, remoteStream, initializeWebRTC, closeWebRTC } = useWebRTC();
|
||
|
||
// Initialize session on mount
|
||
useEffect(() => {
|
||
createSession();
|
||
}, []);
|
||
|
||
// Initialize WebRTC when session is ready
|
||
useEffect(() => {
|
||
if (session && !isConnected) {
|
||
initializeWebRTC();
|
||
}
|
||
}, [session, isConnected]);
|
||
|
||
// Cleanup on unmount
|
||
useEffect(() => {
|
||
return () => {
|
||
endSession();
|
||
closeWebRTC();
|
||
};
|
||
}, []);
|
||
|
||
// Send ready event
|
||
useEffect(() => {
|
||
if (session) {
|
||
postMessage.ready();
|
||
postMessage.sessionStarted(session.sessionId);
|
||
}
|
||
}, [session]);
|
||
|
||
// Listen for messages from host
|
||
useEffect(() => {
|
||
const unsubscribe = postMessage.on('open', () => {
|
||
// Widget opened
|
||
});
|
||
|
||
return unsubscribe;
|
||
}, []);
|
||
|
||
const handleSendMessage = (message: string) => {
|
||
sendMessage(message);
|
||
// TODO: Send to backend via WebRTC or WebSocket
|
||
};
|
||
|
||
const handlePushToTalk = () => {
|
||
setIsListening(true);
|
||
// TODO: Start audio capture
|
||
};
|
||
|
||
const handleHandsFree = () => {
|
||
setIsListening(true);
|
||
// TODO: Enable continuous listening
|
||
};
|
||
|
||
const handleToggleMute = () => {
|
||
setIsMuted(!isMuted);
|
||
// TODO: Mute/unmute audio
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="widget-container loading">
|
||
<div className="loading-spinner">Loading...</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="widget-container error">
|
||
<div className="error-message">Error: {error}</div>
|
||
<button onClick={createSession}>Retry</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="widget-container">
|
||
<div className="widget-header">
|
||
<h1>Virtual Banker</h1>
|
||
<button
|
||
onClick={() => setShowSettings(true)}
|
||
className="settings-button"
|
||
aria-label="Settings"
|
||
>
|
||
⚙️
|
||
</button>
|
||
</div>
|
||
|
||
<div className="widget-content">
|
||
{avatarEnabled && (
|
||
<div className="widget-avatar-section">
|
||
<AvatarView
|
||
enabled={avatarEnabled}
|
||
videoStream={remoteStream || undefined}
|
||
onToggle={() => setAvatarEnabled(false)}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
<div className="widget-chat-section">
|
||
<ChatPanel
|
||
messages={messages}
|
||
onSendMessage={handleSendMessage}
|
||
isListening={isListening}
|
||
isSpeaking={isSpeaking}
|
||
showCaptions={showCaptions}
|
||
onToggleCaptions={() => setShowCaptions(!showCaptions)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<VoiceControls
|
||
onPushToTalk={handlePushToTalk}
|
||
onHandsFree={handleHandsFree}
|
||
isListening={isListening}
|
||
isMuted={isMuted}
|
||
onToggleMute={handleToggleMute}
|
||
volume={volume}
|
||
onVolumeChange={setVolume}
|
||
/>
|
||
|
||
<Captions text={captionText} visible={showCaptions} />
|
||
|
||
{showSettings && (
|
||
<Settings
|
||
showCaptions={showCaptions}
|
||
onToggleCaptions={() => setShowCaptions(!showCaptions)}
|
||
avatarEnabled={avatarEnabled}
|
||
onToggleAvatar={() => setAvatarEnabled(!avatarEnabled)}
|
||
onClose={() => setShowSettings(false)}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|