import { useState, useCallback, useEffect } from "react"; import { TopBar } from "@/components/transcript/TopBar"; import { TranscriptPanel } from "@/components/transcript/TranscriptPanel"; import { InputBar } from "@/components/transcript/InputBar"; import { SettingsPanel, TranscriptSettings } from "@/components/transcript/SettingsPanel"; import { TranscriptMessageData } from "@/components/transcript/TranscriptMessage"; import { AvatarFrame } from "@/components/AvatarFrame"; import { useHeyGenAvatar } from "@/hooks/useHeyGenAvatar"; import { supabase } from "@/integrations/supabase/client"; import { toast } from "@/hooks/use-toast"; import { VoiceEmotion } from "@heygen/streaming-avatar";
const Index = () => {
const [messages, setMessages] = useState<TranscriptMessageData[] data-preserve-html-node="true">([]);
const [isTyping, setIsTyping] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isDark, setIsDark] = useState(true);
const [settings, setSettings] = useState
const [isSpeaking, setIsSpeaking] = useState(false); const [isMuted, setIsMuted] = useState(false); const [isVideoOff, setIsVideoOff] = useState(false);
// Apply theme useEffect(() => { document.documentElement.classList.toggle("dark", isDark); }, [isDark]);
const { isConnected, isLoading, error, mediaStream, connect, disconnect, speak, } = useHeyGenAvatar({ avatarId: "default", rate: 1.0, emotion: VoiceEmotion.FRIENDLY, onSpeakingStart: () => setIsSpeaking(true), onSpeakingEnd: () => setIsSpeaking(false), });
// Show error toast useEffect(() => { if (error) { toast({ title: "Connection Error", description: error, variant: "destructive", }); } }, [error]);
const handleConnect = useCallback(async () => { await connect(); toast({ title: "Connecting", description: "Establishing connection to HeyGen avatar...", }); }, [connect]);
const handleDisconnect = useCallback(async () => { await disconnect(); toast({ title: "Disconnected", description: "Session ended.", }); }, [disconnect]);
const handleSendMessage = useCallback( async (content: string) => { const userMessage: TranscriptMessageData = { id: Date.now().toString(), role: "user", content, timestamp: new Date(), }; setMessages((prev) => [...prev, userMessage]); setIsTyping(true);
try {
const { data, error: fnError } = await supabase.functions.invoke("chat", {
body: {
messages: [
...messages.map((m) => ({
role: m.role === "avatar" ? "assistant" : m.role,
content: m.content,
})),
{ role: "user", content },
],
},
});
if (fnError) throw fnError;
const aiResponse = data?.content || "I'm sorry, I couldn't generate a response.";
if (isConnected) {
await speak(aiResponse);
}
const avatarMessage: TranscriptMessageData = {
id: (Date.now() + 1).toString(),
role: "avatar",
content: aiResponse,
timestamp: new Date(),
};
setMessages((prev) => [...prev, avatarMessage]);
} catch (err) {
console.error("Failed to send message:", err);
toast({
title: "Error",
description: err instanceof Error ? err.message : "Failed to get response",
variant: "destructive",
});
} finally {
setIsTyping(false);
}
},
[speak, messages, isConnected]
);
const handleClearConversation = useCallback(() => { setMessages([]); toast({ title: "Cleared", description: "Conversation cleared.", }); }, []);
const handleCopyAll = useCallback(() => {
const markdown = messages
.map((m) => **${m.role === "user" ? "You" : "Avatar"}:** ${m.content})
.join("\n\n");
const plain = messages
.map((m) => `${m.role === "user" ? "You" : "Avatar"}: ${m.content}`)
.join("\n\n");
navigator.clipboard.writeText(plain).then(() => {
toast({
title: "Copied",
description: "Full conversation copied to clipboard.",
});
});
}, [messages]);
return (
{/* Main content: Avatar + Transcript side by side */}
<div className="flex-1 flex gap-4 p-4 overflow-hidden">
{/* Avatar Frame */}
<div className="w-1/2 min-w-[300px]">
<AvatarFrame
mediaStream={mediaStream}
isConnected={isConnected}
isLoading={isLoading}
isSpeaking={isSpeaking}
isMuted={isMuted}
isVideoOff={isVideoOff}
onToggleMute={() => setIsMuted(!isMuted)}
onToggleVideo={() => setIsVideoOff(!isVideoOff)}
/>
</div>
{/* Transcript Panel */}
<div className="flex-1 min-w-[300px]">
<TranscriptPanel
messages={messages}
isTyping={isTyping}
showTimestamps={settings.showTimestamps}
showAvatars={settings.showAvatars}
/>
</div>
</div>
<InputBar onSendMessage={handleSendMessage} disabled={!isConnected && settings.connectionMethod === "embedded"} />
<SettingsPanel
isOpen={isSettingsOpen}
onClose={() => setIsSettingsOpen(false)}
settings={settings}
onUpdateSettings={setSettings}
/>
</div>
); };
export default Index;
