npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@trilok-zs/realtime-avatar

v0.3.0

Published

Framework-independent <realtime-avatar> Web Component for LiveAvatar/LiveKit realtime avatars. Bundles React + livekit-client; ships no urls, ids or credentials.

Readme

@trilok-zs/realtime-avatar

Framework-independent Web Component wrapper around the Virtual Rep Trainer Realtime (LiveAvatar) avatar.

LiveAvatarCustom (existing React UI/logic)
        ↓
React Web Component adapter  (RealtimeAvatarElement)
        ↓
Custom Element  <realtime-avatar>
        ↓
React / Vue / Angular / plain HTML consumers

React, react-dom and livekit-client are bundled into dist/. Consumers install nothing else and never mount React themselves.

npm install @trilok-zs/realtime-avatar
import "@trilok-zs/realtime-avatar";
<realtime-avatar base-url="..." conversation-id="..."></realtime-avatar>

What was ported, and from where

| Original (virtual-rep-trainer-web) | Here | | --- | --- | | src/avatar/live-avatar-custom.tsx | src/components/LiveAvatarCustom.tsx | | src/avatar/live-avatar-custom.scss | src/components/styles.ts (injected into the shadow root) | | src/avatar/utils/loaderStatus.ts | STATUS_BUCKETS in LiveAvatarCustom.tsx | | Common-Components/horizontal-loader | src/components/HorizontalLoader.tsx (MUI-free) | | src/avatar/useTimeoutDialog.tsx + TimeoutDialog | src/components/Dialogs.tsx | | utils/enums/requestConfirmation.ts | ErrorDialog in src/components/Dialogs.tsx | | src/avatar/services/log-session-data-service.ts | AvatarApi.logSessionData() | | utils/enums/apiEndpoints.ts (LiveAvatar entries) | src/services/apiEndpoints.ts | | modules/shared/services/htttp-wrapper.service.ts + common.service.ts | src/services/httpClient.ts | | store.tslogHeygenDetails, fetchAndSetVideoUrl | AvatarApi.logEvent(), AvatarApi.getVideoUrl() | | utils/enums/enum.ts LiveAvatar constants | src/config/defaults.ts |

Host dependencies that became inputs

| Original source | Web Component input | | --- | --- | | window.APP_CONFIG.BASE_URL | base-url / config.baseUrl | | useActiveAuth()auth.userApiData.user_id | user-id / config.auth.userId | | atob(localStorage.user)._t | bearer-token (or auth-token) / config.auth.token | | window.APP_CONFIG.ENVIRONMENT + NO_IDM | auth-mode / config.auth.authMode (the token now decides — see Authentication) | | trainer.selectedChat.conversation_id | conversation-id | | trainer.selectedChat.practice_id | practice-id | | trainer.selectedChat.customer_id | customer-id | | trainer.selectedChat.image_id | image-id | | trainer.language | language | | avatar_details.live_avatar_id | live-avatar-id | | avatar_details.intro_text | intro-text | | avatar_details.video_url / video_timer | video-url / video-timer | | liveAvatar_context_id | context-id (required) | | sandbox_liveAvatar_id + FORCE_HEYGEN_SANDBOX_MODE | sandbox-avatar-id + sandbox | | avatar_type + sessionActiveTab (is_videoMode_liveAvatar) | mode="video" \| "audio" | | maleVoices / femaleVoices | config.voices | | mic_listening, mic_isLoading (zustand) | element.micState = {...} | | t("...") (i18next) | config.labels | | setEnabled_chatElements / setDisabled_chatElements | chat-enabled / chat-disabled events | | setIsRealtimeAvatarWebRTCReady(true) | avatar-ready event | | setMsg("assistant", …) | avatar-message event | | callSessionTabChange(t("chat_session")) | session-timeout event | | eventBus.emit("callHeygenSpeak", …) | element.sendMessage(text, attachment) | | onAudioDeltaRef.current(type, msg) | element.pushAudioEvent(type, msg) |


Modes

mode reproduces the original is_videoMode_liveAvatar switch.

  • video — the component owns the text/voice pipeline WebSocket, the intro video, the intro text and the 90 s/115 s inactivity timer. (Original: AvatarMain<LiveAvatarCustom setMsg=… />.)
  • audio — no pipeline socket and no timer; an external audio source drives the avatar through pushAudioEvent(). (Original: LiveAvatarStaticWindow<LiveAvatarCustom onAudioDeltaRef=… />.)

Configuration

Simple scalars can be HTML attributes; anything structured goes through the config property. Attributes win for the keys they explicitly set.

<realtime-avatar
  base-url="https://api.example.com/simulate"
  conversation-id="c-123"
  customer-id="cust-9"
  image-id="F2"
  language="en"
  user-id="u-42"
  context-id="67808e4d-…"
  mode="video"
></realtime-avatar>
const avatar = document.querySelector("realtime-avatar");

avatar.config = {
  baseUrl: "https://api.example.com/simulate",
  mode: "video",
  autoStart: true,
  auth: {
    userId: "u-42",
    userName: "Ada Lovelace",
    token: "…",               // → `Authorization: Bearer …`, no cookies sent
    // authScheme: "Bearer",  // default; "" sends `token` verbatim
    // authMode: "auto",      // "token" | "cookie" force the mode
    headers: { "x-tenant": "acme" },
  },
  conversation: { conversationId: "c-123", customerId: "cust-9", imageId: "F2", language: "en" },
  avatarDetails: { liveAvatarId: "…", introText: "Hi there", videoUrl: "intro.mp4", videoTimer: 4 },
  session: { contextId: "…", isSandbox: false, liveAvatarApiBase: "https://api.liveavatar.com" },
  voices: { male: ["cedar"], female: ["marin"] },
  timeout: { enabled: true, warningSeconds: 90, endSeconds: 115, dialogSeconds: 25 },
  labels: { timeoutTitle: "Are you still there?", continueCall: "Continue call" },
  ui: { showLoader: true, showTimeoutDialog: true, showErrorDialog: true },
};

Nothing is hard-coded

The package ships no environment, tenant or vendor values. These are all required inputs; if any cannot be resolved the component emits avatar-error{ phase: "config" } listing exactly what is missing, and makes no network calls:

| Required | Attribute | | --- | --- | | baseUrl | base-url | | auth.userId | user-id | | conversation.conversationId | conversation-id | | session.contextId | context-id | | every endpoint the config uses (see below) | endpoint-* | | an avatar id — avatarDetails.liveAvatarId, or the gendered session.fallbackMale/FemaleAvatarId, or session.sandboxAvatarId when sandboxed | live-avatar-id / fallback-male-avatar-id / fallback-female-avatar-id / sandbox-avatar-id | | voices.male or voices.female (matching image-id, video mode only) | male-voices / female-voices |

The only values with defaults are protocol/UX constants that carry no deployment information: session.mode ("LITE"), session.modeName ("realtime_avatar_mode"), the 90 s/115 s/25 s inactivity thresholds and the English label text. All are overridable.

A pre-publish guard (scripts/verify-package.mjs) fails the build if a URL, API path, UUID or credential ever reappears in the bundle.


Endpoints (required)

The package ships no URLs. Every endpoint is supplied by the host, as a template string or a builder function, by property or by attribute.

| Config key | Attribute | Required when | | --- | --- | --- | | sessionToken | endpoint-session-token | always | | startSession | endpoint-start-session | always | | logSessionData | endpoint-log-session-data | always | | stopSession | endpoint-stop-session | always | | keepAlive | endpoint-keep-alive | mode="video" | | pipelineSocket | endpoint-pipeline-socket | mode="video" | | speechToText | endpoint-speech-to-text | the mic is enabled | | logEvent | endpoint-log-event | enableEventLogging (default on) | | presignedVideoUrl | endpoint-presigned-video-url | avatarDetails.videoUrl is set |

requiredEndpoints(config) returns exactly which ones a given configuration needs; anything missing is reported through avatar-error{ phase: "config" } before a single request is made.

Placeholders: {baseUrl} {liveAvatarApiBase} {sessionId} {filePath} {conversationId} {customerId} {language} {voice}. Unknown placeholders are left untouched.

<realtime-avatar endpoint-session-token="{baseUrl}/v2/liveavatar/token"></realtime-avatar>
avatar.config = {
  endpoints: {
    sessionToken: "{baseUrl}/v2/liveavatar/token",
    // a builder function gets the full context — use it when you need encoding
    keepAlive: (ctx) => `${ctx.baseUrl}/v2/${encodeURIComponent(ctx.sessionId)}/ping`,
  },
};

resolveEndpoint(), ENDPOINT_KEYS and ENDPOINT_PLACEHOLDERS are exported if you want to build or validate templates yourself.

Full attribute list

base-url, live-avatar-api-base, environment, mode, session-tab-label, debug, auto-start, user-id, user-name, first-name, last-name, bearer-token, auth-token, auth-scheme, auth-mode, conversation-id, practice-id, customer-id, image-id, language, live-avatar-id, intro-text, video-url, video-timer, context-id, session-mode, session-mode-name, sandbox, sandbox-avatar-id, fallback-male-avatar-id, fallback-female-avatar-id, male-voices, female-voices, endpoint-session-token, endpoint-start-session, endpoint-log-session-data, endpoint-keep-alive, endpoint-stop-session, endpoint-log-event, endpoint-presigned-video-url, endpoint-pipeline-socket, show-loader, show-timeout-dialog, show-error-dialog, fallback-intro-video-url, timeout-enabled, timeout-warning-seconds, timeout-end-seconds, timeout-dialog-seconds, enable-event-logging.

male-voices / female-voices are comma-separated (male-voices="cedar, echo").

Changing a session-critical attribute (ids, language, mode, auth, base URL) while running restarts the session.


Public methods

| Method | Maps to | | --- | --- | | start() | mounts the tree and runs createSession() | | stop(reason?) | unmounts → the original cleanup path + POST .../liveavatar/stop/{id} | | destroy() | stop() + marks the element unusable | | sendMessage(text, attachment?) | eventBus.emit("callHeygenSpeak", …){type:"text_input"} | | pushAudioEvent(type, message) | onAudioDeltaRef.current(type, message) | | interrupt() | {type:"agent.interrupt"} | | keepAlive() | POST .../liveavatar/keep-alive/{id} (10 s throttle) | | setConversation({...}) | swap conversation + restart | | updateConfig(patch, { restart? }) | merge config; restarts if session-critical |

Read-only: state, sessionId, isRunning, resolvedConfig. Writable: config, micState.


CustomEvents

All bubble and are composed: true.

| Event | detail | Original behaviour | | --- | --- | --- | | avatar-ready | RealtimeAvatarState | setIsRealtimeAvatarWebRTCReady(true) | | avatar-started | { sessionId, livekitUrl, wsUrl, apiKeyUsed } | after /v1/sessions/start | | avatar-stopped | { sessionId, reason } | teardown / timeout / error | | avatar-error | { message, phase, status?, cause? } | requestConfirmation({topIcon:"warning"}) | | avatar-message | { role, type, content } | setMsg("assistant", undefined, text) | | avatar-state-change | { state, reason } | loader progress + agent.speak_started/ended | | chat-enabled / chat-disabled | — | setEnabled_/setDisabled_chatElements() | | session-timeout-warning | { elapsedSeconds, dialogSeconds } | 90 s TimerWarningDialog() | | session-timeout | { elapsedSeconds } | 115 s → callSessionTabChange("chat session") | | conversation-change | { conversation } | new |

avatar.addEventListener("avatar-ready", (event) => console.log(event.detail));

Backend contract (unchanged)

The component performs exactly the calls the original React implementation did. The URLs below are the ones SIMULATE uses — supply them via config.endpoints.

| # | Call | Notes | | --- | --- | --- | | 1 | POST {baseUrl}/hygen/liveavatar/token | {mode, avatar_id, avatar_persona:{context_id}, is_sandbox}{code:1000, api_key_used, data:{session_id, session_token}} | | 2 | POST {liveAvatarApiBase}/v1/sessions/start | accept + authorization: Bearer <session_token> only, no body | | 3 | POST {baseUrl}/hygen/liveavatar/log-session-data | {session_id, key_name, mode_name, conversation_id} | | 4 | POST {baseUrl}/existing-conversations/full/liveavatar/keep-alive/{id} | throttled to 10 s | | 5 | POST {baseUrl}/existing-conversations/full/liveavatar/stop/{id} | never rejects | | 6 | POST {baseUrl}/hygen/log-event | telemetry; disable with enable-event-logging="false" | | 7 | GET {baseUrl}/hygen/generate-presigned-url/{file} | intro video | | 8 | WS {baseUrl}/existing-conversations/full/ws/text-voice-pipeline?chat_id&voice&language&customer_id | video mode only | | 9 | LiveKit room (livekit_url + livekit_client_token) | WebRTC video/audio | | 10 | Avatar control WS ws_url | agent.speak / agent.speak_end / agent.interrupt |

Not part of this component

The host application keeps calling these; they belong to the OpenAI/ElevenLabs realtime pipeline that produces the audio, which reaches the avatar through pushAudioEvent() (the original onAudioDeltaRef):

  • POST {baseUrl}/existing-conversations/full/realtime-resolver/{conversationId}chat.tsxresolveRealtimeSession()
  • POST {baseUrl}/existing-conversations/full/realtime/sessionrealTimeChatDirect.tsxrequestSession()
  • POST {baseUrl}/existing-conversations/full/realtime/transcript

https://<project>.livekit.cloud/settings/regions is livekit-client's own region discovery — automatic, nothing to configure.

Authentication

Every request carries the x-userid header from auth.userId. Beyond that there are exactly two modes, and the presence of a bearer token picks one:

| auth.token | Mode | Headers | Cookies | | --- | --- | --- | --- | | set | bearer | Authorization: Bearer <token> + Accept | credentials: "omit" | | empty / unset | cookie (IDM) | Content-Security-Policy | credentials: "include" |

<!-- bearer: no cookies are sent -->
<realtime-avatar bearer-token="eyJhbGci…" user-id="u-42"></realtime-avatar>

<!-- no token: falls back to cookie auth -->
<realtime-avatar user-id="u-42"></realtime-avatar>
avatar.config = { auth: { userId: "u-42", token: idToken } };  // bearer
avatar.setAttribute("bearer-token", refreshedToken);           // rotate later
avatar.removeAttribute("bearer-token");                        // back to cookies

Details:

  • bearer-token and auth-token are the same input (config.auth.token); bearer-token is the clearer name, auth-token is kept for compatibility. If both are present, bearer-token wins.
  • The scheme is added for you. "eyJhbGci…" and "Bearer eyJhbGci…" both produce Authorization: Bearer eyJhbGci…. Set auth-scheme to change the prefix, or to "" to send the token exactly as given.
  • An empty or whitespace-only token counts as absent, so a not-yet-loaded token falls back to cookie auth instead of sending Authorization: .
  • auth-mode overrides the choice: "cookie" keeps cookies even with a token configured, "token" never sends cookies even before a token arrives. auth.credentials still overrides the resulting credentials value outright.
  • Changing the token restarts the session, since it feeds the token request.
  • The vendor (LiveAvatar) calls are unaffected — they always use the session_token the backend returns, never auth.token.

Two deliberate differences from the original:

  • A 401 emits avatar-error{status:401} instead of navigating to /login. A Web Component must not redirect its host.
  • The mode no longer comes from environment + NO_IDM (demo, stg). Those fields are still accepted but ignored for auth; a token in a prod environment now uses bearer auth rather than cookies. Pass auth-mode="cookie" to restore the old behaviour for such a config.
  • WebSocket URLs cannot carry headers. If your pipeline socket needs the token, put it in the query string with an endpoints.pipelineSocket function.

Never leaving a session open

A LiveAvatar session keeps running (and billing) until it is stopped or expires, so the component closes it on every exit path.

Idle user. While a session is live an inactivity timer runs. At timeout.warningSeconds it emits session-timeout-warning and shows a dialog with a countdown; "End call" — or letting the countdown lapse — POSTs the stop endpoint and tears the whole tree down. Reaching timeout.endSeconds does the same without asking. Sending a message or starting the mic resets the clock.

avatar.config = {
  timeout: { enabled: true, warningSeconds: 30, endSeconds: 45, dialogSeconds: 15 },
};

timeout.enabled defaults to true in video mode and false in audio mode, matching the original. Set it explicitly to run the timer in either.

Page close / reload / navigation. A normal fetch is cancelled when the document goes away, so the stop request is sent with keepalive: true from the pagehide handler — it outlives the page. (sendBeacon cannot set the x-userid header the API requires, so it is not used.)

<realtime-avatar unload-stop-session="true" unload-confirm="true"></realtime-avatar>

| Option | Default | Effect | | --- | --- | --- | | unload.stopSession | true | POST the stop endpoint on pagehide | | unload.confirm | false | native "Leave site?" prompt while a session is live |

unload.confirm triggers the browser's own dialog — custom wording is ignored, and Chrome requires a prior user interaction with the page before it will show.

The stop endpoint is POSTed at most once per session, so unload followed by element removal does not double-fire.


Lifecycle & cleanup

element.remove() (or stop() / destroy()) runs the original teardown:

  • clears the pipeline reconnect timer, the ElevenLabs audio-done timer and the inactivity interval
  • removes the intro-video timeupdate listener
  • detaches every subscribed LiveKit track, then removeAllListeners() + disconnect()
  • pauses both <video> elements and the <audio> element and nulls their srcObject
  • closes the pipeline and avatar-control WebSockets with their handlers nulled first
  • POST .../liveavatar/stop/{sessionId}
  • unmounts the React root and drops the controls reference

Late async callbacks are guarded by a disposed flag, so a session-start response arriving after removal cannot reconnect anything.


Multiple instances

Supported. Each element owns its own shadow root, React root, HttpClient, AvatarApi, LiveKit Room, sockets, timers and session id. There is no module-level mutable state.

The limit is the backend/vendor account: each instance consumes its own LiveAvatar session and concurrency is capped by your LiveAvatar plan.


TypeScript

dist/types/index.d.ts exports the configuration, request, response, state, event and control types, plus a global declaration so document.querySelector and TSX both resolve the element. The JSX declaration is intentionally React-type-free so Vue/Angular consumers typecheck without @types/react.


Scripts

npm install
npm run build        # vite lib build + tsc declarations
npm run typecheck
npm test             # 29 integration tests (happy-dom, stubbed backend/WS/LiveKit)
npm run dev