| | "use client"; |
| |
|
| | import React, { useEffect, useMemo, useRef, useState } from "react" |
| | import { useAppSelector } from "@/common" |
| | import { TrulienceAvatar } from "trulience-sdk" |
| | import { IMicrophoneAudioTrack } from "agora-rtc-sdk-ng" |
| | import { Maximize, Minimize } from "lucide-react"; |
| | import { cn } from "@/lib/utils"; |
| | import { toast } from "sonner"; |
| | import { Progress, ProgressIndicator } from "../ui/progress"; |
| |
|
| | interface AvatarProps { |
| | audioTrack?: IMicrophoneAudioTrack, |
| | localAudioTrack?: IMicrophoneAudioTrack |
| | } |
| |
|
| | export default function Avatar({ audioTrack }: AvatarProps) { |
| | const agentConnected = useAppSelector((state) => state.global.agentConnected) |
| | const trulienceSettings = useAppSelector((state) => state.global.trulienceSettings) |
| | const trulienceAvatarRef = useRef<TrulienceAvatar>(null) |
| | const [errorMessage, setErrorMessage] = useState<string>("") |
| |
|
| | |
| | const [loadProgress, setLoadProgress] = useState(0) |
| |
|
| | |
| | const [finalAvatarId, setFinalAvatarId] = useState("") |
| |
|
| | |
| | const [fullscreen, setFullscreen] = useState(false) |
| |
|
| | |
| | useEffect(() => { |
| | if (typeof window !== "undefined") { |
| | const urlParams = new URLSearchParams(window.location.search) |
| | const avatarIdFromURL = urlParams.get("avatarId") |
| | setFinalAvatarId( |
| | avatarIdFromURL || trulienceSettings.avatarId || "" |
| | ) |
| | } |
| | }, []) |
| |
|
| | |
| | const eventCallbacks = useMemo(() => { |
| | return { |
| | "auth-success": (resp: string) => { |
| | console.log("Trulience Avatar auth-success:", resp) |
| | }, |
| | "auth-fail": (resp: any) => { |
| | console.log("Trulience Avatar auth-fail:", resp) |
| | setErrorMessage(resp.message) |
| | }, |
| | "websocket-connect": (resp: string) => { |
| | console.log("Trulience Avatar websocket-connect:", resp) |
| | }, |
| | "load-progress": (details: Record<string, any>) => { |
| | console.log("Trulience Avatar load-progress:", details.progress) |
| | setLoadProgress(details.progress) |
| | }, |
| | } |
| | }, []) |
| |
|
| | |
| | const trulienceAvatarInstance = useMemo(() => { |
| | if (!finalAvatarId) return null |
| | return ( |
| | <TrulienceAvatar |
| | url={trulienceSettings.trulienceSDK} |
| | ref={trulienceAvatarRef} |
| | avatarId={finalAvatarId} |
| | token={trulienceSettings.avatarToken} |
| | eventCallbacks={eventCallbacks} |
| | width="100%" |
| | height="100%" |
| | /> |
| | ) |
| | }, [finalAvatarId, eventCallbacks]) |
| |
|
| | |
| | useEffect(() => { |
| | if (trulienceAvatarRef.current) { |
| | if (audioTrack && agentConnected) { |
| | const stream = new MediaStream([audioTrack.getMediaStreamTrack()]) |
| | trulienceAvatarRef.current.setMediaStream(null) |
| | trulienceAvatarRef.current.setMediaStream(stream) |
| | console.warn("[TrulienceAvatar] MediaStream set:", stream) |
| | } else if (!agentConnected) { |
| | const trulienceObj = trulienceAvatarRef.current.getTrulienceObject() |
| | trulienceObj?.sendMessageToAvatar("<trl-stop-background-audio immediate='true' />") |
| | trulienceObj?.sendMessageToAvatar("<trl-content position='DefaultCenter' />") |
| | } |
| | } |
| |
|
| | |
| | return () => { |
| | trulienceAvatarRef.current?.setMediaStream(null) |
| | } |
| | }, [audioTrack, agentConnected]) |
| |
|
| | return ( |
| | <div className={cn("relative h-full w-full overflow-hidden rounded-lg", { |
| | ["absolute top-0 left-0 h-screen w-screen rounded-none"]: fullscreen |
| | })}> |
| | <button |
| | className="absolute z-10 top-2 right-2 bg-black/50 p-2 rounded-lg hover:bg-black/70 transition" |
| | onClick={() => setFullscreen(prevValue => !prevValue)} |
| | > |
| | {fullscreen ? <Minimize className="text-white" size={24} /> : <Maximize className="text-white" size={24} />} |
| | </button> |
| | |
| | {/* Render the TrulienceAvatar */} |
| | {trulienceAvatarInstance} |
| | |
| | {/* Show a loader overlay while progress < 1 */} |
| | {errorMessage ? ( |
| | <div className="absolute inset-0 z-10 flex items-center justify-center bg-red-500 bg-opacity-80 text-white"> |
| | <div>{errorMessage}</div> |
| | </div> |
| | ) : loadProgress < 1 && ( |
| | <div className="absolute inset-0 z-10 flex items-center justify-center bg-black bg-opacity-80"> |
| | {/* Simple Tailwind spinner */} |
| | <Progress |
| | className="relative h-[15px] w-[200px] overflow-hidden rounded-full bg-blackA6" |
| | style={{ |
| | // Fix overflow clipping in Safari |
| | // https://gist.github.com/domske/b66047671c780a238b51c51ffde8d3a0 |
| | transform: "translateZ(0)", |
| | }} |
| | value={loadProgress*100} |
| | > |
| | <ProgressIndicator |
| | className="ease-[cubic-bezier(0.65, 0, 0.35, 1)] size-full bg-white transition-transform duration-[660ms]" |
| | style={{ transform: `translateX(-${100 - loadProgress*100}%)` }} |
| | /> |
| | </Progress> |
| | </div> |
| | )} |
| | </div> |
| | ) |
| | } |
| |
|