@convai/embody 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The MIND half of a character: the @convai/web-sdk connection.
3
+ *
4
+ * embody is a wrapper package, so an app should never have to stand up its
5
+ * own `ConvaiClient` alongside ours — that is two connections, two copies of
6
+ * the credentials, and a `blendshapeQueue` the app has to hand-plumb into
7
+ * every frame. This module owns that connection so `createCharacter` can
8
+ * hand back one object that is both body and mind.
9
+ *
10
+ * Nothing here is embody-specific: it is the exact three-step dance the SDK
11
+ * asks for (construct, connect, attach audio), plus teardown in the reverse
12
+ * order. Apps that already own a client pass it in as `client` and this
13
+ * module steps aside entirely.
14
+ */
15
+ import { ConvaiClient, type BlendshapeQueue, type ConvaiConfig } from '@convai/web-sdk/core';
16
+ export interface SessionOptions extends ConvaiConfig {
17
+ /**
18
+ * Attach an `AudioRenderer` to the room so the character is audible.
19
+ * Default `true`. Set `false` when the app renders SDK audio itself —
20
+ * React apps using the SDK's `<AudioRenderer/>` component, for instance,
21
+ * or anything doing its own WebAudio routing.
22
+ */
23
+ audio?: boolean;
24
+ }
25
+ export interface SessionHandle {
26
+ /**
27
+ * The live SDK client. Everything the SDK can do — `sendUserTextMessage`,
28
+ * `on('transcript', …)`, mic control, memory — is reached through here;
29
+ * embody deliberately does not re-wrap that surface.
30
+ */
31
+ client: ConvaiClient;
32
+ /** Lipsync frames for the turn currently being spoken. */
33
+ readonly queue: BlendshapeQueue;
34
+ /** Whether this session owns the client (and so should close it). */
35
+ readonly owned: boolean;
36
+ dispose(): Promise<void>;
37
+ }
38
+ /**
39
+ * Connect a client and make it audible.
40
+ *
41
+ * `audio` is attached only after `connect()` resolves: `AudioRenderer` takes
42
+ * the LiveKit room and subscribes to its track events, so it needs a room
43
+ * that exists. Attaching earlier silently produces a mute character.
44
+ */
45
+ export declare function createSession(opts: SessionOptions): Promise<SessionHandle>;
46
+ /**
47
+ * Adopt a client the app already created and connected. embody will drive
48
+ * lipsync from its queue but will not disconnect it on teardown — the app
49
+ * owns its own client's lifetime.
50
+ */
51
+ export declare function adoptSession(client: ConvaiClient): SessionHandle;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The MIND half of a character: the @convai/web-sdk connection.
3
+ *
4
+ * embody is a wrapper package, so an app should never have to stand up its
5
+ * own `ConvaiClient` alongside ours — that is two connections, two copies of
6
+ * the credentials, and a `blendshapeQueue` the app has to hand-plumb into
7
+ * every frame. This module owns that connection so `createCharacter` can
8
+ * hand back one object that is both body and mind.
9
+ *
10
+ * Nothing here is embody-specific: it is the exact three-step dance the SDK
11
+ * asks for (construct, connect, attach audio), plus teardown in the reverse
12
+ * order. Apps that already own a client pass it in as `client` and this
13
+ * module steps aside entirely.
14
+ */
15
+ import { ConvaiClient } from '@convai/web-sdk/core';
16
+ import { AudioRenderer } from '@convai/web-sdk/vanilla';
17
+ /**
18
+ * Connect a client and make it audible.
19
+ *
20
+ * `audio` is attached only after `connect()` resolves: `AudioRenderer` takes
21
+ * the LiveKit room and subscribes to its track events, so it needs a room
22
+ * that exists. Attaching earlier silently produces a mute character.
23
+ */
24
+ export async function createSession(opts) {
25
+ const { audio = true, ...config } = opts;
26
+ const client = new ConvaiClient(config);
27
+ await client.connect();
28
+ const renderer = audio ? new AudioRenderer(client.room) : null;
29
+ return wrapSession(client, renderer, true);
30
+ }
31
+ /**
32
+ * Adopt a client the app already created and connected. embody will drive
33
+ * lipsync from its queue but will not disconnect it on teardown — the app
34
+ * owns its own client's lifetime.
35
+ */
36
+ export function adoptSession(client) {
37
+ return wrapSession(client, null, false);
38
+ }
39
+ function wrapSession(client, renderer, owned) {
40
+ let disposed = false;
41
+ return {
42
+ client,
43
+ owned,
44
+ // A getter, not a snapshot: the SDK replaces the queue across
45
+ // reconnects, and a captured reference would go quietly stale — the
46
+ // character would keep rendering frames from a dead turn.
47
+ get queue() {
48
+ return client.blendshapeQueue;
49
+ },
50
+ async dispose() {
51
+ // Idempotent: React StrictMode double-invokes cleanup, and a second
52
+ // disconnect on an already-closed client throws.
53
+ if (disposed)
54
+ return;
55
+ disposed = true;
56
+ renderer?.destroy();
57
+ // Only close what we opened.
58
+ if (owned)
59
+ await client.disconnect();
60
+ },
61
+ };
62
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,24 @@
1
+ /**
2
+ * @convai/embody — 3D bodies for Convai characters.
3
+ *
4
+ * embody wraps @convai/web-sdk rather than sitting beside it: one credential,
5
+ * one character id, one connection. Apps should not need to depend on the SDK
6
+ * directly, so its conversational surface is re-exported below.
7
+ *
8
+ * ```ts
9
+ * import { createCharacter } from '@convai/embody'
10
+ *
11
+ * const character = await createCharacter({ apiKey, characterId })
12
+ * scene.add(character.root)
13
+ * character.client?.sendUserTextMessage('hello')
14
+ * // per frame:
15
+ * character.update(delta)
16
+ * ```
17
+ */
18
+ export { ConvaiClient, BlendshapeQueue, AudioManager, VideoManager, ScreenShareManager, MemoryManager, getDisconnectReasonMessage, } from '@convai/web-sdk/core';
19
+ export type { ConvaiConfig, ConvaiClientType, TurnStats } from '@convai/web-sdk/core';
20
+ export { createSession, adoptSession } from './connect/session.js';
21
+ export type { SessionOptions, SessionHandle } from './connect/session.js';
1
22
  export { httpResolver, pickRepresentation, DEFAULT_BASE_URL } from './resolve/resolver.js';
2
23
  export type { HttpResolverOptions } from './resolve/resolver.js';
3
24
  export type { CharacterResolver, CharacterBundle, Representation, ModelVariant } from './resolve/resolver.js';
package/dist/index.js CHANGED
@@ -1,3 +1,26 @@
1
+ /**
2
+ * @convai/embody — 3D bodies for Convai characters.
3
+ *
4
+ * embody wraps @convai/web-sdk rather than sitting beside it: one credential,
5
+ * one character id, one connection. Apps should not need to depend on the SDK
6
+ * directly, so its conversational surface is re-exported below.
7
+ *
8
+ * ```ts
9
+ * import { createCharacter } from '@convai/embody'
10
+ *
11
+ * const character = await createCharacter({ apiKey, characterId })
12
+ * scene.add(character.root)
13
+ * character.client?.sendUserTextMessage('hello')
14
+ * // per frame:
15
+ * character.update(delta)
16
+ * ```
17
+ */
18
+ // ── The conversational half, re-exported from @convai/web-sdk ──────────────
19
+ // Everything an app needs to drive the conversation, so `@convai/embody` is
20
+ // the only import. These are the SDK's own types — not wrappers — so they
21
+ // stay in step with it and interop with an SDK client the app already has.
22
+ export { ConvaiClient, BlendshapeQueue, AudioManager, VideoManager, ScreenShareManager, MemoryManager, getDisconnectReasonMessage, } from '@convai/web-sdk/core';
23
+ export { createSession, adoptSession } from './connect/session.js';
1
24
  export { httpResolver, pickRepresentation, DEFAULT_BASE_URL } from './resolve/resolver.js';
2
25
  export { createCharacter } from './character/createCharacter.js';
3
26
  export { AnimationController } from './clips/animations.js';
@@ -22,7 +22,7 @@ export interface BreathingConfig {
22
22
  * A FIRST fix composed `rest * breath` absolutely every frame, using the
23
23
  * bone's bind pose captured once at `resolveRig` time. That bounded the
24
24
  * no-clip case but silently broke the WITH-clip case: every clip in
25
- * `demo/fixtures/japanese-f-animations.glb` (Idle included, which the demo
25
+ * `convai-playhouse/pagoda-act/public/japanese-f-animations.glb` (Idle included, which the demo
26
26
  * actually plays) animates `spine_02`/`spine_04` — the exact two bones this
27
27
  * system drives — so composing onto a STATIC captured rest pose discarded
28
28
  * the clip's own spine motion outright, every frame, on the normal
@@ -18,7 +18,7 @@ function freshAnchor() {
18
18
  * A FIRST fix composed `rest * breath` absolutely every frame, using the
19
19
  * bone's bind pose captured once at `resolveRig` time. That bounded the
20
20
  * no-clip case but silently broke the WITH-clip case: every clip in
21
- * `demo/fixtures/japanese-f-animations.glb` (Idle included, which the demo
21
+ * `convai-playhouse/pagoda-act/public/japanese-f-animations.glb` (Idle included, which the demo
22
22
  * actually plays) animates `spine_02`/`spine_04` — the exact two bones this
23
23
  * system drives — so composing onto a STATIC captured rest pose discarded
24
24
  * the clip's own spine motion outright, every frame, on the normal
@@ -0,0 +1,8 @@
1
+ import { type UseCharacterOptions } from './useCharacter.js';
2
+ import type { CharacterHandle } from '../character/createCharacter.js';
3
+ export interface CharacterModelProps extends UseCharacterOptions {
4
+ /** Called once the body is loaded and the connection is live. */
5
+ onReady?: (character: CharacterHandle) => void;
6
+ onError?: (error: Error) => void;
7
+ }
8
+ export declare function CharacterModel({ onReady, onError, ...opts }: CharacterModelProps): import("react").JSX.Element | null;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * The character as a scene object, for an app that already has its own
4
+ * `<Canvas>`, lighting and camera. Must be rendered INSIDE a `<Canvas>`.
5
+ *
6
+ * Most apps want the default export of `@convai/embody/react` instead, which
7
+ * brings its own canvas and needs nothing around it. Reach for this one when
8
+ * the character joins a scene you are already composing.
9
+ *
10
+ * ```tsx
11
+ * <Canvas>
12
+ * <MyLights />
13
+ * <CharacterModel token={token} characterId={characterId} />
14
+ * <MyProps />
15
+ * </Canvas>
16
+ * ```
17
+ *
18
+ * Thin wiring only — all lifecycle/tick behaviour lives in `useCharacter`
19
+ * (which delegates cancellation to `../character/lifecycle`). This adds
20
+ * nothing but the `<primitive>` render and the onReady/onError plumbing.
21
+ */
22
+ import { useEffect } from 'react';
23
+ import { useCharacter } from './useCharacter.js';
24
+ export function CharacterModel({ onReady, onError, ...opts }) {
25
+ const { character, error } = useCharacter(opts);
26
+ useEffect(() => {
27
+ if (character && onReady)
28
+ onReady(character);
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ }, [character, onReady]);
31
+ useEffect(() => {
32
+ if (error && onError)
33
+ onError(error);
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
+ }, [error, onError]);
36
+ if (!character)
37
+ return null;
38
+ return _jsx("primitive", { object: character.root });
39
+ }
@@ -1,7 +1,21 @@
1
- import { type UseCharacterOptions } from './useCharacter.js';
2
- import type { CharacterHandle } from '../character/createCharacter.js';
3
- export interface ConvaiCharacterProps extends UseCharacterOptions {
4
- onReady?: (character: CharacterHandle) => void;
5
- onError?: (error: Error) => void;
1
+ import { type CharacterModelProps } from './CharacterModel.js';
2
+ export interface ConvaiCharacterProps extends CharacterModelProps {
3
+ /** Wrapper style. Defaults to filling the parent. */
4
+ style?: React.CSSProperties;
5
+ className?: string;
6
+ /** Scene background. `null` renders transparent, to composite over a page. */
7
+ background?: string | null;
8
+ /**
9
+ * Camera framing, in metres, tuned for a standing human-scale character:
10
+ * eye-level and about three and a half metres back.
11
+ */
12
+ camera?: {
13
+ position?: [number, number, number];
14
+ fov?: number;
15
+ target?: [number, number, number];
16
+ };
17
+ /** Rendered while the body is still loading. */
18
+ fallback?: React.ReactNode;
6
19
  }
7
- export declare function ConvaiCharacter({ onReady, onError, ...opts }: ConvaiCharacterProps): import("react").JSX.Element | null;
20
+ export declare function ConvaiCharacter({ style, className, background, camera, fallback, ...character }: ConvaiCharacterProps): import("react").JSX.Element;
21
+ export default ConvaiCharacter;
@@ -1,25 +1,40 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * Thin wiring only. All lifecycle/tick behaviour lives in `useCharacter`
4
- * (which itself delegates cancellation to `../character/lifecycle`) — this
5
- * component adds nothing but the `<primitive>` render and the
6
- * onReady/onError callback plumbing.
3
+ * The whole package in one component.
4
+ *
5
+ * ```tsx
6
+ * import ConvaiCharacter from '@convai/embody/react'
7
+ *
8
+ * export default function App() {
9
+ * return <ConvaiCharacter token={token} characterId={characterId} />
10
+ * }
11
+ * ```
12
+ *
13
+ * That renders the character. Nothing else is needed — no `<Canvas>`, no
14
+ * lights, no camera, no `ConvaiClient`, no resolver, no frame loop. embody
15
+ * resolves the body, opens the @convai/web-sdk connection, renders its audio
16
+ * and drives lipsync, gaze, breathing and blink.
17
+ *
18
+ * It fills its parent, so give that parent a size (the default is a full
19
+ * viewport when the parent has no height of its own).
20
+ *
21
+ * When the character should join a scene you are already composing, use the
22
+ * named `CharacterModel` export inside your own `<Canvas>` instead.
7
23
  */
8
- import { useEffect } from 'react';
9
- import { useCharacter } from './useCharacter.js';
10
- export function ConvaiCharacter({ onReady, onError, ...opts }) {
11
- const { character, error } = useCharacter(opts);
12
- useEffect(() => {
13
- if (character && onReady)
14
- onReady(character);
15
- // eslint-disable-next-line react-hooks/exhaustive-deps
16
- }, [character, onReady]);
17
- useEffect(() => {
18
- if (error && onError)
19
- onError(error);
20
- // eslint-disable-next-line react-hooks/exhaustive-deps
21
- }, [error, onError]);
22
- if (!character)
23
- return null;
24
- return _jsx("primitive", { object: character.root });
24
+ import { Suspense } from 'react';
25
+ import { Canvas } from '@react-three/fiber';
26
+ import { CharacterModel } from './CharacterModel.js';
27
+ const DEFAULT_POSITION = [0, 1.45, 3.6];
28
+ const DEFAULT_TARGET = [0, 0.92, 0];
29
+ export function ConvaiCharacter({ style, className, background = '#17181c', camera, fallback = null, ...character }) {
30
+ const position = camera?.position ?? DEFAULT_POSITION;
31
+ const target = camera?.target ?? DEFAULT_TARGET;
32
+ return (_jsxs("div", { className: className, style: { width: '100%', height: '100%', minHeight: '100vh', ...style }, children: [_jsxs(Canvas, { camera: { position, fov: camera?.fov ?? 35, near: 0.05, far: 100 },
33
+ // Transparent when background is null, so the character can sit over
34
+ // existing page content rather than in a box of its own.
35
+ gl: { antialias: true, alpha: background === null },
36
+ // Aim at the character's head rather than the origin at its feet;
37
+ // r3f points the camera at [0,0,0] otherwise.
38
+ onCreated: ({ camera: cam }) => cam.lookAt(...target), children: [background !== null && _jsx("color", { attach: "background", args: [background] }), _jsx("hemisphereLight", { args: [0xffffff, 0x334455, 1.4] }), _jsx("directionalLight", { position: [2, 4, 3], intensity: 2.2 }), _jsx(Suspense, { fallback: null, children: _jsx(CharacterModel, { ...character }) })] }), fallback] }));
25
39
  }
40
+ export default ConvaiCharacter;
@@ -1,4 +1,38 @@
1
+ /**
2
+ * React entry point.
3
+ *
4
+ * ```tsx
5
+ * import { ConvaiCharacter } from '@convai/embody/react'
6
+ *
7
+ * <Canvas>
8
+ * <ConvaiCharacter apiKey={apiKey} characterId={characterId} />
9
+ * </Canvas>
10
+ * ```
11
+ *
12
+ * The SDK's own React surface is re-exported so an app can build chat UI
13
+ * without adding a second dependency — or a second connection. `ConvaiWidget`
14
+ * takes a client, and `character.client` is one.
15
+ *
16
+ * Chat UI is DOM and the character is a `<Canvas>` child, so lift the client
17
+ * out rather than rendering the widget from inside the scene:
18
+ *
19
+ * ```tsx
20
+ * const [client, setClient] = useState<ConvaiClient | null>(null)
21
+ * <Canvas>
22
+ * <ConvaiCharacter apiKey={apiKey} characterId={characterId}
23
+ * onReady={(c) => setClient(c.client)} />
24
+ * </Canvas>
25
+ * {client && <ConvaiWidget convaiClient={client} />}
26
+ * ```
27
+ */
1
28
  export { useCharacter } from './useCharacter.js';
2
29
  export type { UseCharacterOptions, UseCharacterResult } from './useCharacter.js';
3
- export { ConvaiCharacter } from './ConvaiCharacter.js';
30
+ export { ConvaiCharacter, default } from './ConvaiCharacter.js';
4
31
  export type { ConvaiCharacterProps } from './ConvaiCharacter.js';
32
+ export { CharacterModel } from './CharacterModel.js';
33
+ export type { CharacterModelProps } from './CharacterModel.js';
34
+ export { useWidgetClient } from './useWidgetClient.js';
35
+ export type { WidgetClient } from './useWidgetClient.js';
36
+ export { ConvaiWidget, useConvaiClient, AudioRenderer, AudioContext } from '@convai/web-sdk/react';
37
+ export { ConvaiClient } from '@convai/web-sdk/core';
38
+ export type { ConvaiConfig } from '@convai/web-sdk/core';
@@ -1,2 +1,34 @@
1
+ /**
2
+ * React entry point.
3
+ *
4
+ * ```tsx
5
+ * import { ConvaiCharacter } from '@convai/embody/react'
6
+ *
7
+ * <Canvas>
8
+ * <ConvaiCharacter apiKey={apiKey} characterId={characterId} />
9
+ * </Canvas>
10
+ * ```
11
+ *
12
+ * The SDK's own React surface is re-exported so an app can build chat UI
13
+ * without adding a second dependency — or a second connection. `ConvaiWidget`
14
+ * takes a client, and `character.client` is one.
15
+ *
16
+ * Chat UI is DOM and the character is a `<Canvas>` child, so lift the client
17
+ * out rather than rendering the widget from inside the scene:
18
+ *
19
+ * ```tsx
20
+ * const [client, setClient] = useState<ConvaiClient | null>(null)
21
+ * <Canvas>
22
+ * <ConvaiCharacter apiKey={apiKey} characterId={characterId}
23
+ * onReady={(c) => setClient(c.client)} />
24
+ * </Canvas>
25
+ * {client && <ConvaiWidget convaiClient={client} />}
26
+ * ```
27
+ */
1
28
  export { useCharacter } from './useCharacter.js';
2
- export { ConvaiCharacter } from './ConvaiCharacter.js';
29
+ export { ConvaiCharacter, default } from './ConvaiCharacter.js';
30
+ export { CharacterModel } from './CharacterModel.js';
31
+ export { useWidgetClient } from './useWidgetClient.js';
32
+ // ── Chat / conversation surface, re-exported from @convai/web-sdk/react ────
33
+ export { ConvaiWidget, useConvaiClient, AudioRenderer, AudioContext } from '@convai/web-sdk/react';
34
+ export { ConvaiClient } from '@convai/web-sdk/core';
@@ -1,11 +1,22 @@
1
- import type { BlendshapeQueue } from '@convai/web-sdk/core';
1
+ import type { BlendshapeQueue, ConvaiClient } from '@convai/web-sdk/core';
2
2
  import { type CharacterHandle, type CreateCharacterOptions } from '../character/createCharacter.js';
3
3
  export interface UseCharacterOptions extends CreateCharacterOptions {
4
- /** Pass the queue only while this character owns the conversational turn. */
4
+ /**
5
+ * Override which turn this body lipsyncs to. Only needed when one client
6
+ * drives several characters; otherwise the character uses its own session.
7
+ */
5
8
  queue?: BlendshapeQueue;
6
9
  }
7
10
  export interface UseCharacterResult {
8
11
  character: CharacterHandle | null;
12
+ /**
13
+ * The connected SDK client, once ready — the same object as
14
+ * `character.client`, lifted out so chat UI can be written without
15
+ * null-checking the body first.
16
+ */
17
+ client: ConvaiClient | null;
9
18
  error: Error | null;
19
+ /** `loading` until body and mind are both up, then `ready` or `error`. */
20
+ status: 'loading' | 'ready' | 'error';
10
21
  }
11
22
  export declare function useCharacter(opts: UseCharacterOptions): UseCharacterResult;
@@ -2,10 +2,13 @@
2
2
  * Thin wiring only — see the module doc on `../character/lifecycle` for why
3
3
  * the cancel/dispose behaviour lives there and not here. This hook does
4
4
  * nothing `createCharacterLifecycle` doesn't already own: it creates a
5
- * lifecycle per `characterId`, feeds it `createCharacter`, cancels it on
6
- * unmount/re-run, and forwards the two pieces of state React needs
7
- * (`character`, `error`). Ticking and camera assignment are similarly
8
- * one-line pass-throughs to `CharacterHandle.update`/`.camera`.
5
+ * lifecycle per identity, feeds it `createCharacter`, cancels it on
6
+ * unmount/re-run, and forwards the state React needs. Ticking and camera
7
+ * assignment are one-line pass-throughs to `CharacterHandle.update`/`.camera`.
8
+ *
9
+ * The lipsync queue is NOT a prop: the character owns its own connection, so
10
+ * it already knows which frames are its own. Apps driving several bodies from
11
+ * one client pass `queue` to take that decision back.
9
12
  */
10
13
  import { useEffect, useRef, useState } from 'react';
11
14
  import { useFrame, useThree } from '@react-three/fiber';
@@ -32,14 +35,20 @@ export function useCharacter(opts) {
32
35
  setError(null);
33
36
  }, (c) => setCharacter(c), (e) => setError(e));
34
37
  return () => lifecycle.cancel();
35
- // Re-create only when the identity of the character changes — matching
36
- // the lifecycle contract of one `start` per instance.
38
+ // Re-create when the identity of the character OR the credentials change
39
+ // — a new key/token means a different session, and reusing the old one
40
+ // would leave the body talking over a connection the app has revoked.
37
41
  // eslint-disable-next-line react-hooks/exhaustive-deps
38
- }, [opts.characterId]);
42
+ }, [opts.characterId, opts.apiKey, opts.authToken, opts.token, opts.baseUrl]);
39
43
  useEffect(() => {
40
44
  if (character)
41
45
  character.camera = camera;
42
46
  }, [character, camera]);
43
47
  useFrame((_, delta) => character?.update(delta, queueRef.current));
44
- return { character, error };
48
+ return {
49
+ character,
50
+ client: character?.client ?? null,
51
+ error,
52
+ status: error ? 'error' : character ? 'ready' : 'loading',
53
+ };
45
54
  }
@@ -0,0 +1,18 @@
1
+ import type { ConvaiClient } from '@convai/web-sdk/core';
2
+ /** The shape `<ConvaiWidget convaiClient={…} />` requires. */
3
+ export type WidgetClient = ConvaiClient & {
4
+ activity: string;
5
+ isAudioMuted: boolean;
6
+ isVideoEnabled: boolean;
7
+ isScreenShareActive: boolean;
8
+ };
9
+ /**
10
+ * ```tsx
11
+ * const { client } = useCharacter({ apiKey, characterId })
12
+ * const widgetClient = useWidgetClient(client)
13
+ * return widgetClient && <ConvaiWidget convaiClient={widgetClient} />
14
+ * ```
15
+ *
16
+ * Returns `null` until the character's connection exists.
17
+ */
18
+ export declare function useWidgetClient(client: ConvaiClient | null): WidgetClient | null;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Makes embody's client usable by the SDK's `<ConvaiWidget/>`.
3
+ *
4
+ * The widget wants `IConvaiClient` plus four pieces of reactive UI state
5
+ * (`activity`, `isAudioMuted`, `isVideoEnabled`, `isScreenShareActive`). The
6
+ * SDK supplies those through `useConvaiClient`, but that hook CONSTRUCTS its
7
+ * own client — using it next to embody would open a second connection, which
8
+ * is the whole thing embody exists to avoid.
9
+ *
10
+ * So this hook does the same composition against a client that already
11
+ * exists. It mirrors `useConvaiClient`'s subscriptions exactly: `stateChange`
12
+ * for activity, and the three control emitters for the toggles.
13
+ */
14
+ import { useEffect, useMemo, useState } from 'react';
15
+ /** Priority order matches the SDK's: speaking > listening > thinking > connected. */
16
+ function activityOf(state) {
17
+ if (state.isSpeaking)
18
+ return 'Speaking';
19
+ if (state.isListening)
20
+ return 'Listening';
21
+ if (state.isThinking)
22
+ return 'Thinking';
23
+ if (state.isConnected)
24
+ return 'Connected';
25
+ return 'Idle';
26
+ }
27
+ /**
28
+ * ```tsx
29
+ * const { client } = useCharacter({ apiKey, characterId })
30
+ * const widgetClient = useWidgetClient(client)
31
+ * return widgetClient && <ConvaiWidget convaiClient={widgetClient} />
32
+ * ```
33
+ *
34
+ * Returns `null` until the character's connection exists.
35
+ */
36
+ export function useWidgetClient(client) {
37
+ const c = client;
38
+ const [activity, setActivity] = useState('Idle');
39
+ const [isAudioMuted, setAudioMuted] = useState(false);
40
+ const [isVideoEnabled, setVideoEnabled] = useState(false);
41
+ const [isScreenShareActive, setScreenShareActive] = useState(false);
42
+ useEffect(() => {
43
+ if (!c)
44
+ return;
45
+ // Seed from the current values: the client may already be connected and
46
+ // mid-turn by the time this hook mounts, and waiting for the next event
47
+ // would show a stale "Idle" until the user did something.
48
+ setActivity(activityOf(c.state));
49
+ setAudioMuted(c.audioControls.isAudioMuted);
50
+ setVideoEnabled(c.videoControls.isVideoEnabled);
51
+ setScreenShareActive(c.screenShareControls.isScreenShareActive);
52
+ const unsubs = [
53
+ c.on('stateChange', (s) => setActivity(activityOf(s))),
54
+ c.audioControls.on('audioStateChange', (s) => {
55
+ if (s.isAudioMuted !== undefined)
56
+ setAudioMuted(s.isAudioMuted);
57
+ }),
58
+ c.videoControls.on('videoStateChange', (s) => {
59
+ if (s.isVideoEnabled !== undefined)
60
+ setVideoEnabled(s.isVideoEnabled);
61
+ }),
62
+ c.screenShareControls.on('screenShareStateChange', (s) => {
63
+ if (s.isScreenShareActive !== undefined)
64
+ setScreenShareActive(s.isScreenShareActive);
65
+ }),
66
+ ];
67
+ return () => unsubs.forEach((u) => u());
68
+ }, [c]);
69
+ // A view over the client, never a mutation of it: the same client drives
70
+ // the 3D character and anything else the app holds, and stamping React
71
+ // state onto it would make those views disagree.
72
+ //
73
+ // A Proxy rather than `Object.create` + assign, because the widget calls
74
+ // methods on this object. Under `Object.create` any `this.x = y` inside a
75
+ // method writes to the derived object instead of the client, silently
76
+ // losing the mutation. Here reads, writes and `this` all reach the real
77
+ // client; only the four UI fields are intercepted.
78
+ //
79
+ // Re-created when those four change so a memoised widget re-renders, and
80
+ // stable between changes so it does not re-render for nothing.
81
+ return useMemo(() => {
82
+ if (!client)
83
+ return null;
84
+ const overrides = {
85
+ activity,
86
+ isAudioMuted,
87
+ isVideoEnabled,
88
+ isScreenShareActive,
89
+ };
90
+ return new Proxy(client, {
91
+ get(target, prop, receiver) {
92
+ if (typeof prop === 'string' && prop in overrides)
93
+ return overrides[prop];
94
+ // Receiver is the TARGET, not the proxy, so getters and methods run
95
+ // with `this` bound to the real client.
96
+ const value = Reflect.get(target, prop, target);
97
+ return typeof value === 'function' ? value.bind(target) : value;
98
+ },
99
+ set(target, prop, value) {
100
+ return Reflect.set(target, prop, value, target);
101
+ },
102
+ });
103
+ // eslint-disable-next-line react-hooks/exhaustive-deps
104
+ }, [client, activity, isAudioMuted, isVideoEnabled, isScreenShareActive]);
105
+ }
@@ -26,6 +26,15 @@ export interface CharacterBundle {
26
26
  release?: number;
27
27
  representations: Representation[];
28
28
  thumbnail?: string;
29
+ /**
30
+ * `true` when this is a stand-in body, not the character's own: the
31
+ * character exists and is viewable, but has no finished embodiment, so the
32
+ * backend served the configured default for its gender.
33
+ *
34
+ * Worth surfacing. A placeholder that nobody notices is a character
35
+ * shipping as visibly the wrong person.
36
+ */
37
+ fallback?: boolean;
29
38
  }
30
39
  export interface CharacterResolver {
31
40
  resolve(characterId: string): Promise<CharacterBundle>;
@@ -48,9 +57,13 @@ export declare const DEFAULT_BASE_URL = "https://api-stg.convai.com";
48
57
  export interface HttpResolverOptions {
49
58
  /** Base url of the embodiment backend. Defaults to the release branch's host. */
50
59
  baseUrl?: string;
51
- /** Optional CONVAI-API-KEY; public characters resolve as guest without it. */
60
+ /** CONVAI-API-KEY. Supply this or `authToken`. */
52
61
  apiKey?: string;
62
+ /** Convai auth token. Takes precedence over `apiKey` when both are set. */
63
+ authToken?: string;
53
64
  }
65
+ /** Shared so `createCharacter` and the resolver cannot drift apart. */
66
+ export declare const MISSING_CREDENTIAL: string;
54
67
  export declare function httpResolver(opts?: HttpResolverOptions): CharacterResolver;
55
68
  /** Pick the representation for a rig profile this build of embody understands. */
56
69
  export declare function pickRepresentation(bundle: CharacterBundle, supported: string[]): Representation;