@convai/web-widget 0.2.1 → 0.3.0
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/README.md +27 -5
- package/index.js +1 -1
- package/package.json +1 -1
- package/react.js +88 -2
- package/ui/react/components/ConvaiWidget.d.ts +8 -7
- package/ui/react/hooks/useLiveClient.d.ts +26 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ production** — [step two](#before-you-ship) moves it to your server.
|
|
|
13
13
|
|
|
14
14
|
```html
|
|
15
15
|
<convai-widget character-id="YOUR_CHARACTER_ID" api-key="YOUR_API_KEY"></convai-widget>
|
|
16
|
-
<script src="https://cdn.jsdelivr.net/npm/@convai/web-widget@0.
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/@convai/web-widget@0.3.0/v1.js" async></script>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
A launcher bubble appears in the bottom-right corner. Click it: the runtime
|
|
@@ -26,7 +26,7 @@ The script tag goes in your HTML shell; the element is ordinary JSX.
|
|
|
26
26
|
|
|
27
27
|
```html
|
|
28
28
|
<!-- index.html -->
|
|
29
|
-
<script src="https://cdn.jsdelivr.net/npm/@convai/web-widget@0.
|
|
29
|
+
<script src="https://cdn.jsdelivr.net/npm/@convai/web-widget@0.3.0/v1.js" async></script>
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
```jsx
|
|
@@ -65,6 +65,28 @@ export function Chat() {
|
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
`convaiClient` takes any `ConvaiClient`, not only `useConvaiClient`'s: one you
|
|
69
|
+
built with `new ConvaiClient(...)`, or the character's own client from
|
|
70
|
+
`@convai/embody`. The widget subscribes to its events itself and never opens a
|
|
71
|
+
second connection.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { useState } from 'react'
|
|
75
|
+
import type { ConvaiClient } from '@convai/web-sdk/core'
|
|
76
|
+
import { ConvaiCharacter } from '@convai/embody/react'
|
|
77
|
+
import { ConvaiWidget } from '@convai/web-widget/react'
|
|
78
|
+
|
|
79
|
+
export function CharacterWithChat() {
|
|
80
|
+
const [client, setClient] = useState<ConvaiClient | null>(null)
|
|
81
|
+
return (
|
|
82
|
+
<>
|
|
83
|
+
<ConvaiCharacter apiKey={API_KEY} characterId={CHARACTER_ID} onReady={(c) => setClient(c.client)} />
|
|
84
|
+
{client && <ConvaiWidget convaiClient={client} />}
|
|
85
|
+
</>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
68
90
|
**No framework**
|
|
69
91
|
|
|
70
92
|
```ts
|
|
@@ -145,16 +167,16 @@ guide; theming and `::part()` hooks; CSP requirements; and guidance on
|
|
|
145
167
|
|
|
146
168
|
## Versioning
|
|
147
169
|
|
|
148
|
-
`0.
|
|
170
|
+
`0.3.0` is a stable release: `npm install @convai/web-widget` and
|
|
149
171
|
jsDelivr's unversioned URL both resolve it. Pin the exact version anyway for
|
|
150
172
|
reproducible deploys —
|
|
151
173
|
|
|
152
174
|
```html
|
|
153
|
-
<script src="https://cdn.jsdelivr.net/npm/@convai/web-widget@0.
|
|
175
|
+
<script src="https://cdn.jsdelivr.net/npm/@convai/web-widget@0.3.0/v1.js" async></script>
|
|
154
176
|
```
|
|
155
177
|
|
|
156
178
|
```bash
|
|
157
|
-
npm install @convai/web-widget@0.
|
|
179
|
+
npm install @convai/web-widget@0.3.0 # only needed to self-host the two files
|
|
158
180
|
```
|
|
159
181
|
|
|
160
182
|
This package is early, and it bundles a prerelease of `@convai/web-sdk`. The
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@convai/web-widget",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Two-line script-tag embed for the Convai chat widget. A <convai-widget> custom element with a closed shadow root and a lazily loaded runtime.",
|
|
5
5
|
"homepage": "https://docs.convai.com/embed",
|
|
6
6
|
"repository": {
|
package/react.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React, {
|
|
2
|
+
import React, { useMemo, useReducer, useEffect, useState, useRef, useCallback } from "react";
|
|
3
3
|
import { motion, AnimatePresence } from "framer-motion";
|
|
4
4
|
import { useLocalParticipant, useRoomContext, RoomContext, VideoTrack } from "@livekit/components-react";
|
|
5
5
|
import { RoomContext as RoomContext2, RoomAudioRenderer } from "@livekit/components-react";
|
|
@@ -9,6 +9,91 @@ import { freshPublicationGrantOptions } from "@convai/web-sdk/embed";
|
|
|
9
9
|
import styled from "styled-components";
|
|
10
10
|
import { p as presentationStyles, M as MAX_CARDS_PER_MESSAGE, i as injectKeyframes, P as PREVIEW_TIMEOUT_MS, g as glyph, o as openLightbox } from "./chunks/lightbox-Dxfdw3VU.js";
|
|
11
11
|
import { Track } from "livekit-client";
|
|
12
|
+
function listen(target, event, fn) {
|
|
13
|
+
if (!(target == null ? void 0 : target.on)) return () => {
|
|
14
|
+
};
|
|
15
|
+
const result = target.on(event, fn);
|
|
16
|
+
if (typeof result === "function") return result;
|
|
17
|
+
return () => {
|
|
18
|
+
var _a;
|
|
19
|
+
return (_a = target.off) == null ? void 0 : _a.call(target, event, fn);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function activityOf(state) {
|
|
23
|
+
if (!state) return "Idle";
|
|
24
|
+
if (state.isSpeaking) return "Speaking";
|
|
25
|
+
if (state.isListening) return "Listening";
|
|
26
|
+
if (state.isThinking) return "Thinking";
|
|
27
|
+
if (state.isConnected) return "Connected";
|
|
28
|
+
return "Idle";
|
|
29
|
+
}
|
|
30
|
+
function isManagedClient(client) {
|
|
31
|
+
return typeof client.isAudioMuted === "boolean";
|
|
32
|
+
}
|
|
33
|
+
function useLiveClientView(client) {
|
|
34
|
+
const { fields, version, managed } = useLiveClientState(client);
|
|
35
|
+
return useMemo(() => {
|
|
36
|
+
if (managed) return client;
|
|
37
|
+
return new Proxy(client, {
|
|
38
|
+
get(target, prop, receiver) {
|
|
39
|
+
if (typeof prop === "string" && prop in fields) return fields[prop];
|
|
40
|
+
const value = Reflect.get(target, prop, target);
|
|
41
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
42
|
+
},
|
|
43
|
+
has(target, prop) {
|
|
44
|
+
return typeof prop === "string" && prop in fields || Reflect.has(target, prop);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}, [client, managed, version]);
|
|
48
|
+
}
|
|
49
|
+
function useLiveClientState(client) {
|
|
50
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
51
|
+
const managed = isManagedClient(client);
|
|
52
|
+
const [version, rerender] = useReducer((n) => n + 1, 0);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (managed) return;
|
|
55
|
+
const c = client;
|
|
56
|
+
const unsubs = [
|
|
57
|
+
...[
|
|
58
|
+
"stateChange",
|
|
59
|
+
"messagesChange",
|
|
60
|
+
"userTranscriptionChange",
|
|
61
|
+
"speakingChange",
|
|
62
|
+
"botReady",
|
|
63
|
+
"connect",
|
|
64
|
+
"disconnect"
|
|
65
|
+
].map((event) => listen(c, event, rerender)),
|
|
66
|
+
listen(c.audioControls, "audioStateChange", rerender),
|
|
67
|
+
listen(c.videoControls, "videoStateChange", rerender),
|
|
68
|
+
listen(c.screenShareControls, "screenShareStateChange", rerender)
|
|
69
|
+
];
|
|
70
|
+
rerender();
|
|
71
|
+
return () => unsubs.forEach((unsubscribe) => unsubscribe());
|
|
72
|
+
}, [client, managed]);
|
|
73
|
+
if (managed) {
|
|
74
|
+
return {
|
|
75
|
+
managed,
|
|
76
|
+
version,
|
|
77
|
+
fields: {
|
|
78
|
+
activity: (_a = client.activity) != null ? _a : activityOf(client.state),
|
|
79
|
+
isAudioMuted: client.isAudioMuted,
|
|
80
|
+
isVideoEnabled: Boolean(client.isVideoEnabled),
|
|
81
|
+
isScreenShareActive: Boolean(client.isScreenShareActive)
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const controls = client;
|
|
86
|
+
return {
|
|
87
|
+
managed,
|
|
88
|
+
version,
|
|
89
|
+
fields: {
|
|
90
|
+
activity: activityOf(client.state),
|
|
91
|
+
isAudioMuted: (_c = (_b = controls.audioControls) == null ? void 0 : _b.isAudioMuted) != null ? _c : true,
|
|
92
|
+
isVideoEnabled: (_e = (_d = controls.videoControls) == null ? void 0 : _d.isVideoEnabled) != null ? _e : false,
|
|
93
|
+
isScreenShareActive: (_g = (_f = controls.screenShareControls) == null ? void 0 : _f.isScreenShareActive) != null ? _g : false
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
12
97
|
function renderTree(tree) {
|
|
13
98
|
return tree.map(
|
|
14
99
|
(node, i) => React.createElement(node.tag, { key: i, ...node.attr }, renderTree(node.child))
|
|
@@ -2702,7 +2787,7 @@ const VoiceModeOverlay = ({
|
|
|
2702
2787
|
);
|
|
2703
2788
|
};
|
|
2704
2789
|
const ConvaiWidget = ({
|
|
2705
|
-
convaiClient,
|
|
2790
|
+
convaiClient: givenClient,
|
|
2706
2791
|
showVideo = true,
|
|
2707
2792
|
showScreenShare = true,
|
|
2708
2793
|
defaultVoiceMode = true,
|
|
@@ -2712,6 +2797,7 @@ const ConvaiWidget = ({
|
|
|
2712
2797
|
linkPreviewResolver
|
|
2713
2798
|
}) => {
|
|
2714
2799
|
var _a, _b, _c, _d;
|
|
2800
|
+
const convaiClient = useLiveClientView(givenClient);
|
|
2715
2801
|
const isTextOnly = Boolean(requestPublicationGrant) || convaiClient.connectionType === "text";
|
|
2716
2802
|
const {
|
|
2717
2803
|
name: fetchedCharacterName,
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { type LiveClientFields } from "../hooks/useLiveClient";
|
|
1
2
|
import type { LinkPreviewResolver } from "@convai/web-sdk/core";
|
|
2
3
|
import type { IConvaiClient, PublishedChatConnectOptions } from "@convai/web-sdk/core";
|
|
3
4
|
import type { PublishedChatDescriptor } from "@convai/web-sdk/embed";
|
|
4
5
|
interface ConvaiWidgetProps {
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The Convai client (required). Either `useConvaiClient`'s result, or any
|
|
8
|
+
* `ConvaiClient` — one built with `new ConvaiClient(...)`, or the character's
|
|
9
|
+
* own client from `@convai/embody`. The widget subscribes to a bare client's
|
|
10
|
+
* events itself and never opens a second connection.
|
|
11
|
+
*/
|
|
12
|
+
convaiClient: IConvaiClient & Partial<LiveClientFields>;
|
|
12
13
|
/**
|
|
13
14
|
* Show video toggle button in settings (default: true).
|
|
14
15
|
* Only works if connection type is "video". If false, hides the video button.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { IConvaiClient } from "@convai/web-sdk/core";
|
|
2
|
+
/** The fields `useConvaiClient` adds on top of the client. */
|
|
3
|
+
export interface LiveClientFields {
|
|
4
|
+
activity: string;
|
|
5
|
+
isAudioMuted: boolean;
|
|
6
|
+
isVideoEnabled: boolean;
|
|
7
|
+
isScreenShareActive: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** True for a `useConvaiClient` result, which carries the UI fields itself. */
|
|
10
|
+
export declare function isManagedClient(client: object): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The four UI fields for `client`: its own when `useConvaiClient` supplied
|
|
13
|
+
* them, otherwise read live from the client, re-rendering on every change.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useLiveClient(client: IConvaiClient & Partial<LiveClientFields>): LiveClientFields;
|
|
16
|
+
/**
|
|
17
|
+
* The client as the widget should see it: a `useConvaiClient` result as-is,
|
|
18
|
+
* otherwise a view that adds the four UI fields and changes identity on every
|
|
19
|
+
* client event — exactly how a `useConvaiClient` result behaves, so the widget
|
|
20
|
+
* and its children re-render and re-run their effects the same way.
|
|
21
|
+
*
|
|
22
|
+
* A Proxy, not a copy: reads, writes and `this` inside methods all reach the
|
|
23
|
+
* real client, and only the four fields are answered here. The app's own
|
|
24
|
+
* reference to the client is never modified.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useLiveClientView<C extends IConvaiClient & Partial<LiveClientFields>>(client: C): C & LiveClientFields;
|