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.
<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()andstopListening(). - 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:
- Embeds the experience in presenter mode with manual speech input — the SDK does not auto-capture the learner.
- When the user taps the player-owned start button,
onStartedfires (audio unlocked) and the app speaks a welcome line viaexperience.speak(). - 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.
<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
onReadyandready()mean the manifest is resolved and the player is attached.onStartedandstarted()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
| Prop | Type | Description |
|---|---|---|
experienceId | string | Experience ID. SDK mints via /v1/public-sessions (browser mint). |
manifest | SessionManifest | Pre-minted manifest from your backend. See SSR warning below. |
sessionEndpoint | string | Same-origin route that mints a manifest. For server-session embeds. |
acceptCredentialExposure | boolean | Required when passing a manifest that includes sessionToken. |
language | 'en' | 'es' | Session language override. |
mode | ExperienceMode | Conversation or presenter behavior. |
responseMode | ResponseMode | Deprecated; response ownership is derived from mode. |
speechInputMode | SpeechInputMode | Automatic, manual, or disabled speech capture. |
startButton | StartButtonOptions | Player-owned startup button configuration. |
conversationProcessor | ConversationProcessorFn | Browser-owned conversation processor. |
speechOnly | boolean | Run 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, locationId | string | Public-session avatar or location overrides. |
embedBaseUrl | string | Player embed origin override, normally only for local development. |
debug | boolean | Enable SDK debug behavior. |
returnUrl | string | Fallback 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.
<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}
/> | Callback | When it runs |
|---|---|
onReady | Manifest resolved and player attached. |
onStarted | User gesture completed and audio unlocked. |
onUserTranscript | Partial or final user transcript update. |
onStateUpdate | Embedded player state changes. |
onClose | Player requests close; supplying it overrides default close navigation. |
onError | Initialization or runtime operation reports an error. |
Additional typed callbacks are onUserSpeechStarted, onUserSpeechEnded, onCharacterSpeechStarted, onCharacterSpeechEnded, onConversationUpdate, onMessage, onListeningState, onModeChange, and onConversationProcessorError.
ExperienceHandle
| Method | Result |
|---|---|
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';