Experience (Svelte)

The canonical Svelte component for embedding Avatar Experiences.

Overview

<Experience> is the canonical Svelte API for simple embeds and host-controlled experiences. It owns session creation, attachment, event subscriptions, restart, and cleanup.

Basic usage

For a standard embed, pass experienceId (and optional session options). The component mints the session, attaches the player, and runs the experience according to mode and speechInputMode. You do not need bind:this unless your app must drive speech or listening from its own UI.

App.svelte
<script>
  import { Experience } from '@liforma/client/svelte';
<\/script>

<Experience experienceId="exp_01DEMO1SPANISHCAFE" />

Host-controlled speech and listening

“Advanced” here does not mean a separate API — it means your host app controls when the character speaks and when the microphone listens, instead of leaving that entirely to automatic conversation mode.

Typical cases:

  • Scripted lessons — you call speak() with predetermined tutor lines.
  • Manual capture — your Start/Stop buttons call startListening() and stopListening().
  • Custom lesson flow — turn state, feedback, and “Next” live in your page; the component still owns session lifecycle.

You still use <Experience>. Add bind:this to hold an ExperienceHandle — the typed controller for speak(), startListening(), and related methods. Type it as ExperienceHandle | undefined because the binding is undefined before mount.

What the example below does:

  1. Embeds the experience in presenter mode with manual speech input — the SDK does not auto-capture the learner.
  2. When the user taps the player-owned start button, onStarted fires (audio unlocked) and the app speaks a welcome line via experience.speak().
  3. Host Start answer / Stop answer buttons call listening methods on the handle; they stay disabled until startup completes.

For a full lesson loop (speak → Start → Stop → feedback → Next), see Guided Scripted Practice and the guided-practice SvelteKit example.

Lesson.svelte
<script lang="ts">
  import {
    Experience,
    type ExperienceHandle
  } from '@liforma/client/svelte';

  // Controller from bind:this — undefined until the component mounts.
  let experience: ExperienceHandle | undefined = $state();
  let audioUnlocked = $state(false);

  // Safe point to speak: player start button has unlocked audio.
  async function handleStarted() {
    audioUnlocked = true;
    await experience?.speak({ text: 'Welcome to the lesson.' });
  }

  // Host-owned Start/Stop — manual speech capture for the learner.
  async function startAnswer() {
    if (!experience) return;
    await experience.startListening();
  }

  async function finishAnswer() {
    if (!experience) return;
    const utterance = await experience.stopListening();
    console.log(utterance.text);
  }
<\/script>

<Experience
  bind:this={experience}
  experienceId="exp_01DEMO1SPANISHCAFE"
  mode="presenter"
  speechInputMode="manual"
  onStarted={handleStarted}
/>

<button disabled={!audioUnlocked || !experience} onclick={startAnswer}>Start answer</button>
<button disabled={!audioUnlocked || !experience} onclick={finishAnswer}>Stop answer</button>

Readiness

  • onReady and ready() mean the manifest is resolved and the player is attached.
  • onStarted and started() mean the player start button has been used and audio is unlocked.

speak(), startListening(), stopListening(), and listenOnce() preserve the root Experience API's state validation. They wait for component session creation if necessary, but they do not silently queue until audio is unlocked. A call made before started rejects. Call speech methods from onStarted, await experience.started(), or keep controls disabled until startup completes.

ExperienceProps

PropTypeDescription
experienceIdstringExperience ID. SDK mints via /v1/public-sessions (browser mint).
manifestSessionManifestPre-minted manifest from your backend. See SSR warning below.
sessionEndpointstringSame-origin route that mints a manifest. For server-session embeds.
acceptCredentialExposurebooleanRequired when passing a manifest that includes sessionToken.
language'en' | 'es'Session language override.
modeExperienceModeConversation or presenter behavior.
responseModeResponseModeDeprecated; response ownership is derived from mode.
speechInputModeSpeechInputModeAutomatic, manual, or disabled speech capture.
startButtonStartButtonOptionsPlayer-owned startup button configuration.
conversationProcessorConversationProcessorFnBrowser-owned conversation processor.
speechOnlybooleanRun STT, TTS, and conversation without loading the avatar renderer or location scene assets. Same experienceId as a full embed. Default false. Bare speechOnly is equivalent to speechOnly=true.
avatarId, locationIdstringPublic-session avatar or location overrides.
embedBaseUrlstringPlayer embed origin override, normally only for local development.
debugbooleanEnable SDK debug behavior.
returnUrlstringFallback close destination when the manifest omits one.

Callback props

Svelte integrations use typed Svelte 5 callback props, not manual .on() subscriptions or legacy on: component events.

App.svelte
<script lang="ts">
  import { Experience } from '@liforma/client/svelte';

  function reportError(error: Error) {
    console.error(error);
  }
<\/script>

<Experience
  experienceId="exp_01DEMO1SPANISHCAFE"
  onReady={({ manifest }) => console.log('Attached', manifest.sessionId)}
  onStarted={({ mode }) => console.log('Audio unlocked', mode)}
  onUserTranscript={(update) => console.log('Transcript', update.text)}
  onStateUpdate={(state) => console.log('Player state', state)}
  onClose={(event) => console.log('Player closed', event)}
  onError={reportError}
/>
CallbackWhen it runs
onReadyManifest resolved and player attached.
onStartedUser gesture completed and audio unlocked.
onUserTranscriptPartial or final user transcript update.
onStateUpdateEmbedded player state changes.
onClosePlayer requests close; supplying it overrides default close navigation.
onErrorInitialization or runtime operation reports an error.

Additional typed callbacks are onUserSpeechStarted, onUserSpeechEnded, onCharacterSpeechStarted, onCharacterSpeechEnded, onConversationUpdate, onMessage, onListeningState, onModeChange, and onConversationProcessorError.

ExperienceHandle

MethodResult
ready()Promise<ReadyEvent>
started()Promise<StartedEvent>
speak(options)Promise<SpeechResult>
startListening()Promise<void>
stopListening()Promise<UtteranceResult>
listenOnce(options?)Promise<UtteranceResult>
getManifest()Current SessionManifest, or null before ready.
getConversation()Current immutable conversation snapshot.
end()Ends the current session.

Reactive restart behavior

Changing launch-defining props restarts the owned session: experienceId, sessionEndpoint, manifest, acceptCredentialExposure, language, mode, responseMode, speechInputMode, startButton, conversationProcessor, speechOnly, avatarId, locationId, embedBaseUrl, or debug. You do not need a {#key ...} block.

Callback-only changes do not restart the session; the component calls the latest callback. Structurally equivalent startButton objects also do not restart. Keep startButton stable where practical, and note that changing the conversationProcessor function reference does restart.

Server-session embed

<Experience
  experienceId="exp_01DEMO1SPANISHCAFE"
  sessionEndpoint="/api/liforma-session"
/>

Container sizing

The component fills its parent container. Set explicit dimensions on the parent for embedded layouts, or use full viewport for immersive experiences.

<div style="width: 400px; height: 600px;">
  <Experience experienceId="exp_01DEMO1SPANISHCAFE" />
</div>

SSR safety

Do not pass credential-bearing manifests through server load functions into page data. The sessionToken would be embedded in HTML. Prefer experienceId for browser embeds or a same-origin sessionEndpoint.

Close behavior

Without onClose, the component follows the manifest return URL, then the returnUrl prop, with /meet as its final fallback. Supplying onClose disables that automatic navigation so your app can handle the event.

Deprecated alias

LiformaExperience remains available from @liforma/client/svelte during the temporary deprecation window. New code should use Experience.

Using the imperative API too

If a file genuinely needs both APIs, import the framework-neutral class with an alias. Most Svelte integrations should let the component own the lifecycle.

import { Experience } from '@liforma/client/svelte';
import { Experience as ExperienceApi } from '@liforma/client';