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_T0I7ACMQLBMPG6K" />

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.speech.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?.speech.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_T0I7ACMQLBMPG6K"
  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 Session Launch 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/browser-sessions (browser mint).
launchstringAdvanced: client-fetched opaque launch. Prefer sessionEndpoint.
sessionEndpointstringSame-origin route that returns SessionLaunchResponse. For server-session embeds.
localestringPrimary / user language (BCP 47). Defaults to navigator.language when omitted.
secondaryLocalestringDual: paired / learning language (BCP 47). Non-dual Match: immerses the session in this language. Required for dual when Studio left the paired axis as Match.
learningLocalestringAlias of secondaryLocale — same value works for tutors and roleplay in language-learning apps.
modeExperienceModeConversation or presenter behavior.
speechInputModeSpeechInputModeAutomatic, manual, or disabled speech capture.
startButtonStartButtonOptionsPlayer-owned startup button configuration.
themePlayerThemeGlobal player chrome (introduction, feedback, panels, shared buttons). Presentation only — does not remint. Use startButton for CTA-only overrides.
showFeedbackbooleanWhen true (default), the player shows end-of-session Feedback step-through. Set false to hide Liforma Feedback UI and render scores yourself via onFeedback / feedback. Presentation only — does not remint; not a Studio setting.
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.
fit'full' | 'medium' | 'face'Scene framing in the host container. Default 'full' height-fits the whole avatar and location. 'medium' is a bust window (0.75× face-mesh height above the oval, 2× below). 'face' frames the mesh face oval with 0.5H above and below and at least 0.25W on each side (location background zooms with the face) — useful for small inset / PIP layouts. Changing the prop (or calling setFit()) updates the live player without reminting. Orthogonal to location video-call presentation (hasZoomVariant).
avatarId, backdropIdstringPublic-session avatar or backdrop overrides. locationId remains a deprecated alias for backdropId.
embedBaseUrlstringPlayer embed origin override, normally only for local development.
debugbooleanEnable SDK debug behavior.
returnUrlstringClose destination URL (player / attach chrome — does not remint).

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_T0I7ACMQLBMPG6K"
  onReady={({ session }) => console.log('Attached', session.id)}
  onStarted={({ mode }) => console.log('Audio unlocked', mode)}
  onUserTranscript={(update) => console.log('Transcript', update.text)}
  onPlayerStatusChange={(status) => console.log('Player status', status)}
  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.
onPlayerStatusChangeEmbedded player status (loading | scene | ready | error).
onActivityChangeSession activity envelope (data: idle | listening | thinking | speaking).
onClosePlayer requests close; supplying it overrides default close navigation.
onErrorInitialization or runtime operation reports an error.

Additional typed callbacks are onUserSpeechStarted, onUserSpeechEnded, onCharacterSpeechStarted, onCharacterSpeechEnded, onCharacterFocusChanged, onConversationUpdate, onMessage, onListeningState, 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.
focusCharacter(characterId)Presentation-only focus on a multi-Character node; no-op on single-Character nodes.
setFit(fit)Live scene framing: 'full', 'medium', or 'face'. Does not remint.
end()Ends the current session.

Reactive restart behavior

Changing developer-intent props restarts the owned session: experienceId, sessionEndpoint, launch, locale, secondaryLocale / learningLocale, mode, speechInputMode, conversationProcessor, speechOnly, avatarId, backdropId (deprecated alias locationId), embedBaseUrl, or debug. You do not need a {#key ...} block.

Presentation props (fit, startButton, theme, showFeedback, closeButton, settingsButton, theatreButton, fullscreenButton, returnUrl, ui) update the live player without reminting. Callback-only changes do not restart the session.

Server-session embed

<Experience
  experienceId="exp_T0I7ACMQLBMPG6K"
  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_T0I7ACMQLBMPG6K" />
</div>

For a face-sized inset (for example bottom-right PIP), size the host div and set fit="face" so the runtime zooms onto the mesh face oval instead of fitting the full body:

<div class="avatar-inset">
  <Experience experienceId="exp_T0I7ACMQLBMPG6K" fit="face" />
</div>

<style>
  .avatar-inset {
    position: fixed;
    right: 16px;
    bottom: 16px;
    width: 280px;
    height: 280px;
  }
</style>

SSR safety

Do not put opaque launch through server load functions into page data — it would be embedded in HTML. Prefer experienceId for browser embeds or a same-origin sessionEndpoint.

Close behavior

Without onClose, the component follows the returnUrl prop (player / attach chrome). Supplying onClose disables 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';