@convai/embody 0.4.0 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @convai/embody
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 62deba9: `useWidgetClient` hides the server's `user-transcription` echo of a typed message, so `<ConvaiWidget/>` shows each sent message once instead of twice.
8
+
3
9
  ## 0.4.0
4
10
 
5
11
  ### Minor Changes
@@ -6,6 +6,31 @@ export type WidgetClient = ConvaiClient & {
6
6
  isVideoEnabled: boolean;
7
7
  isScreenShareActive: boolean;
8
8
  };
9
+ type ChatMessage = ConvaiClient['chatMessages'][number];
10
+ /**
11
+ * How long after a typed message its server echo can still arrive. Measured
12
+ * at ~2 s on staging; the window is generous because a slow turn is still an
13
+ * echo, while the same words said aloud half a minute later are a new message.
14
+ */
15
+ export declare const TYPED_ECHO_WINDOW_MS = 30000;
16
+ /**
17
+ * The transcript without the server's echo of typed messages.
18
+ *
19
+ * `sendUserTextMessage` appends the text locally as `user-llm-text`, and the
20
+ * SDK drops the server's `user-llm-text` echo so the message appears once.
21
+ * But the server ALSO answers typed text with a final `user-transcription`
22
+ * carrying the same words — with no `logicalTurnId` to correlate on — which
23
+ * the SDK stores as a new message and `<ConvaiWidget/>` renders as a second
24
+ * "You" bubble. Every typed message showed twice; every reply once. Verified
25
+ * on web-sdk 1.8.0 and 1.9.0-beta.3, which filter identically.
26
+ *
27
+ * A transcription is an echo when an unmatched typed message with the same
28
+ * trimmed text precedes it within the window; each typed message cancels at
29
+ * most one. Spoken turns never produce a `user-llm-text`, so a real
30
+ * transcription is never dropped. An echo that streams in chunks can show
31
+ * its partial text until the final chunk matches — a flicker, not a duplicate.
32
+ */
33
+ export declare function withoutTypedEchoes(messages: readonly ChatMessage[]): ChatMessage[];
9
34
  /**
10
35
  * ```tsx
11
36
  * const { client } = useCharacter({ apiKey, characterId })
@@ -16,3 +41,4 @@ export type WidgetClient = ConvaiClient & {
16
41
  * Returns `null` until the character's connection exists.
17
42
  */
18
43
  export declare function useWidgetClient(client: ConvaiClient | null): WidgetClient | null;
44
+ export {};
@@ -12,6 +12,47 @@
12
12
  * for activity, and the three control emitters for the toggles.
13
13
  */
14
14
  import { useEffect, useMemo, useState } from 'react';
15
+ /**
16
+ * How long after a typed message its server echo can still arrive. Measured
17
+ * at ~2 s on staging; the window is generous because a slow turn is still an
18
+ * echo, while the same words said aloud half a minute later are a new message.
19
+ */
20
+ export const TYPED_ECHO_WINDOW_MS = 30_000;
21
+ /**
22
+ * The transcript without the server's echo of typed messages.
23
+ *
24
+ * `sendUserTextMessage` appends the text locally as `user-llm-text`, and the
25
+ * SDK drops the server's `user-llm-text` echo so the message appears once.
26
+ * But the server ALSO answers typed text with a final `user-transcription`
27
+ * carrying the same words — with no `logicalTurnId` to correlate on — which
28
+ * the SDK stores as a new message and `<ConvaiWidget/>` renders as a second
29
+ * "You" bubble. Every typed message showed twice; every reply once. Verified
30
+ * on web-sdk 1.8.0 and 1.9.0-beta.3, which filter identically.
31
+ *
32
+ * A transcription is an echo when an unmatched typed message with the same
33
+ * trimmed text precedes it within the window; each typed message cancels at
34
+ * most one. Spoken turns never produce a `user-llm-text`, so a real
35
+ * transcription is never dropped. An echo that streams in chunks can show
36
+ * its partial text until the final chunk matches — a flicker, not a duplicate.
37
+ */
38
+ export function withoutTypedEchoes(messages) {
39
+ const pending = [];
40
+ return messages.filter((m) => {
41
+ const content = m.content.trim();
42
+ const at = Date.parse(m.timestamp);
43
+ if (m.type === 'user-llm-text') {
44
+ pending.push({ content, at });
45
+ return true;
46
+ }
47
+ if (m.type !== 'user-transcription')
48
+ return true;
49
+ const i = pending.findIndex((t) => t.content === content && at - t.at >= 0 && at - t.at <= TYPED_ECHO_WINDOW_MS);
50
+ if (i === -1)
51
+ return true;
52
+ pending.splice(i, 1);
53
+ return false;
54
+ });
55
+ }
15
56
  /** Priority order matches the SDK's: speaking > listening > thinking > connected. */
16
57
  function activityOf(state) {
17
58
  if (state.isSpeaking)
@@ -89,6 +130,9 @@ export function useWidgetClient(client) {
89
130
  };
90
131
  return new Proxy(client, {
91
132
  get(target, prop, receiver) {
133
+ // The widget renders straight from this; see withoutTypedEchoes.
134
+ if (prop === 'chatMessages')
135
+ return withoutTypedEchoes(Reflect.get(target, prop, target));
92
136
  if (typeof prop === 'string' && prop in overrides)
93
137
  return overrides[prop];
94
138
  // Receiver is the TARGET, not the proxy, so getters and methods run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@convai/embody",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Give a Convai character a body: resolve 3D characters by characterId and drive them with lipsync and animation. Layered on @convai/web-sdk.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Convai <support@convai.com>",