PolicyAgent / components /InteractiveAvatar.tsx
eyuvaraj's picture
Upload 23 files
710adbf verified
raw
history blame
8.31 kB
import type { StartAvatarResponse } from "@heygen/streaming-avatar";
import StreamingAvatar, {
AvatarQuality,
StreamingEvents,
TaskMode,
TaskType,
VoiceEmotion,
} from "@heygen/streaming-avatar";
import {
Button,
Card,
CardBody,
CardFooter,
Divider,
Spinner,
Chip,
Tabs,
Tab,
} from "@nextui-org/react";
import { useEffect, useRef, useState } from "react";
import { useMemoizedFn, usePrevious } from "ahooks";
import InteractiveAvatarTextInput from "./InteractiveAvatarTextInput";
export default function InteractiveAvatar() {
const [isLoadingSession, setIsLoadingSession] = useState(false);
const [isLoadingRepeat, setIsLoadingRepeat] = useState(false);
const [stream, setStream] = useState<MediaStream>();
const [debug, setDebug] = useState<string>();
// Hardcoded values for avatarId and knowledgeId
const [knowledgeId] = useState<string>("c60c40259b034917b235da7d007002b6");
const [avatarId] = useState<string>("eb0a8cc8046f476da551a5559fbb5c82");
const [language] = useState<string>("en");
const [data, setData] = useState<StartAvatarResponse>();
const [text, setText] = useState<string>("");
const mediaStream = useRef<HTMLVideoElement>(null);
const avatar = useRef<StreamingAvatar | null>(null);
const [chatMode, setChatMode] = useState("text_mode");
const [isUserTalking, setIsUserTalking] = useState(false);
async function fetchAccessToken() {
try {
const response = await fetch("/api/get-access-token", {
method: "POST",
});
const token = await response.text();
console.log("Access Token:", token);
return token;
} catch (error) {
console.error("Error fetching access token:", error);
}
return "";
}
async function startSession() {
setIsLoadingSession(true);
const newToken = await fetchAccessToken();
avatar.current = new StreamingAvatar({
token: newToken,
});
avatar.current.on(StreamingEvents.AVATAR_START_TALKING, (e) => {
console.log("Avatar started talking", e);
});
avatar.current.on(StreamingEvents.AVATAR_STOP_TALKING, (e) => {
console.log("Avatar stopped talking", e);
});
avatar.current.on(StreamingEvents.STREAM_DISCONNECTED, () => {
console.log("Stream disconnected");
endSession();
});
avatar.current?.on(StreamingEvents.STREAM_READY, (event) => {
console.log(">>>>> Stream ready:", event.detail);
setStream(event.detail);
});
avatar.current?.on(StreamingEvents.USER_START, (event) => {
console.log(">>>>> User started talking:", event);
setIsUserTalking(true);
});
avatar.current?.on(StreamingEvents.USER_STOP, (event) => {
console.log(">>>>> User stopped talking:", event);
setIsUserTalking(false);
});
try {
const res = await avatar.current.createStartAvatar({
quality: AvatarQuality.Low,
avatarName: avatarId,
knowledgeId: knowledgeId,
voice: {
rate: 1.5, // 0.5 ~ 1.5
emotion: VoiceEmotion.EXCITED,
},
language: language,
disableIdleTimeout: true,
});
setData(res);
await avatar.current?.startVoiceChat({
useSilencePrompt: false,
});
setChatMode("voice_mode");
} catch (error) {
console.error("Error starting avatar session:", error);
} finally {
setIsLoadingSession(false);
}
}
async function handleSpeak() {
setIsLoadingRepeat(true);
if (!avatar.current) {
setDebug("Avatar API not initialized");
return;
}
await avatar.current
.speak({ text: text, taskType: TaskType.REPEAT, taskMode: TaskMode.SYNC })
.catch((e) => {
setDebug(e.message);
});
setIsLoadingRepeat(false);
}
async function handleInterrupt() {
if (!avatar.current) {
setDebug("Avatar API not initialized");
return;
}
await avatar.current.interrupt().catch((e) => {
setDebug(e.message);
});
}
async function endSession() {
await avatar.current?.stopAvatar();
setStream(undefined);
}
const handleChangeChatMode = useMemoizedFn(async (v) => {
if (v === chatMode) {
return;
}
if (v === "text_mode") {
avatar.current?.closeVoiceChat();
} else {
await avatar.current?.startVoiceChat();
}
setChatMode(v);
});
const previousText = usePrevious(text);
useEffect(() => {
if (!previousText && text) {
avatar.current?.startListening();
} else if (previousText && !text) {
avatar?.current?.stopListening();
}
}, [text, previousText]);
useEffect(() => {
return () => {
endSession();
};
}, []);
useEffect(() => {
if (stream && mediaStream.current) {
mediaStream.current.srcObject = stream;
mediaStream.current.onloadedmetadata = () => {
mediaStream.current!.play();
setDebug("Playing");
};
}
}, [mediaStream, stream]);
return (
<div className="w-full flex flex-col gap-4">
<Card>
<CardBody className="h-[500px] flex flex-col justify-center items-center">
{stream ? (
<div className="h-[500px] w-[900px] justify-center items-center flex rounded-lg overflow-hidden">
<video
ref={mediaStream}
autoPlay
playsInline
style={{
width: "100%",
height: "100%",
objectFit: "contain",
}}
>
<track kind="captions" />
</video>
<div className="flex flex-col gap-2 absolute bottom-3 right-3">
<Button
className="bg-gradient-to-tr from-indigo-500 to-indigo-300 text-white rounded-lg"
size="md"
variant="shadow"
onClick={handleInterrupt}
>
Interrupt task
</Button>
<Button
className="bg-gradient-to-tr from-indigo-500 to-indigo-300 text-white rounded-lg"
size="md"
variant="shadow"
onClick={endSession}
>
End session
</Button>
</div>
</div>
) : !isLoadingSession ? (
<div className="h-full justify-center items-center flex flex-col gap-8 w-[500px] self-center">
<Button
className="bg-gradient-to-tr from-indigo-500 to-indigo-300 w-full text-white"
size="md"
variant="shadow"
onClick={startSession}
>
Start session
</Button>
</div>
) : (
<Spinner color="default" size="lg" />
)}
</CardBody>
<Divider />
<CardFooter className="flex flex-col gap-3 relative">
<Tabs
aria-label="Options"
selectedKey={chatMode}
onSelectionChange={(v) => {
handleChangeChatMode(v);
}}
>
<Tab key="text_mode" title="Text mode" />
<Tab key="voice_mode" title="Voice mode" />
</Tabs>
{chatMode === "text_mode" ? (
<div className="w-full flex relative">
<InteractiveAvatarTextInput
disabled={!stream}
input={text}
label="Chat"
loading={isLoadingRepeat}
placeholder="Type something for the avatar to respond"
setInput={setText}
onSubmit={handleSpeak}
/>
{text && (
<Chip className="absolute right-16 top-3">Listening</Chip>
)}
</div>
) : (
<div className="w-full text-center">
<Button
isDisabled={!isUserTalking}
className="bg-gradient-to-tr from-indigo-500 to-indigo-300 text-white"
size="md"
variant="shadow"
>
{isUserTalking ? "Listening" : "Voice chat"}
</Button>
</div>
)}
</CardFooter>
</Card>
</div>
);
}