polymorph-sdk 0.2.0 → 0.2.3
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 +1 -2
- package/dist/index.css +1 -1
- package/dist/index.d.ts +18 -5
- package/dist/index.js +3376 -3252
- package/package.json +6 -2
- package/src/ChatThread.tsx +73 -9
- package/src/PolymorphWidget.tsx +89 -17
- package/src/RoomHandler.tsx +99 -0
- package/src/VoiceOverlay.tsx +22 -2
- package/src/WidgetPanel.tsx +169 -132
- package/src/__tests__/exports.test.ts +13 -0
- package/src/index.ts +6 -1
- package/src/styles.module.css +38 -237
- package/src/types.ts +18 -5
- package/src/usePolymorphSession.ts +150 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polymorph-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.ts"
|
|
10
10
|
},
|
|
11
|
-
"files": [
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
12
16
|
"publishConfig": {
|
|
13
17
|
"access": "public",
|
|
14
18
|
"main": "dist/index.js",
|
package/src/ChatThread.tsx
CHANGED
|
@@ -1,26 +1,90 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ChatMessage } from "./types";
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
3
2
|
import styles from "./styles.module.css";
|
|
3
|
+
import type { ChatMessage, SessionStatus } from "./types";
|
|
4
4
|
|
|
5
|
-
export function ChatThread({
|
|
5
|
+
export function ChatThread({
|
|
6
|
+
messages,
|
|
7
|
+
primaryColor,
|
|
8
|
+
status,
|
|
9
|
+
}: {
|
|
10
|
+
messages: ChatMessage[];
|
|
11
|
+
primaryColor: string;
|
|
12
|
+
status: SessionStatus;
|
|
13
|
+
}) {
|
|
6
14
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
const isNearBottom = useRef(true);
|
|
16
|
+
const prevItemCount = useRef(0);
|
|
17
|
+
const agentCountAtConnect = useRef<number | null>(null);
|
|
18
|
+
const agentMessageCount = messages.filter((m) => m.role === "agent").length;
|
|
19
|
+
const lastMessageIsUser =
|
|
20
|
+
messages.length > 0 && messages[messages.length - 1].role === "user";
|
|
7
21
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
const handleScroll = useCallback(() => {
|
|
23
|
+
const el = scrollRef.current;
|
|
24
|
+
if (!el) return;
|
|
25
|
+
isNearBottom.current =
|
|
26
|
+
el.scrollHeight - el.scrollTop - el.clientHeight < 100;
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
// Snapshot agent message count when connecting starts
|
|
30
|
+
if (status === "connecting" && agentCountAtConnect.current === null) {
|
|
31
|
+
agentCountAtConnect.current = agentMessageCount;
|
|
32
|
+
}
|
|
33
|
+
if (status === "idle" || status === "error") {
|
|
34
|
+
agentCountAtConnect.current = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const initialConnectThinking =
|
|
38
|
+
(status === "connecting" || status === "connected") &&
|
|
39
|
+
agentCountAtConnect.current !== null &&
|
|
40
|
+
agentMessageCount <= agentCountAtConnect.current;
|
|
41
|
+
const waitingForAgentReply =
|
|
42
|
+
status === "connected" &&
|
|
43
|
+
messages.length > 0 &&
|
|
44
|
+
messages[messages.length - 1].role === "user";
|
|
45
|
+
const showThinking = initialConnectThinking || waitingForAgentReply;
|
|
46
|
+
|
|
47
|
+
// Auto-scroll when new messages arrive or thinking indicator appears.
|
|
48
|
+
// Uses render-time comparison instead of useEffect to avoid lint issues
|
|
49
|
+
// with intentional "trigger" dependencies.
|
|
50
|
+
const itemCount = messages.length + (showThinking ? 1 : 0);
|
|
51
|
+
if (itemCount !== prevItemCount.current) {
|
|
52
|
+
prevItemCount.current = itemCount;
|
|
53
|
+
if (isNearBottom.current || lastMessageIsUser) {
|
|
54
|
+
queueMicrotask(() => {
|
|
55
|
+
scrollRef.current?.scrollTo({
|
|
56
|
+
top: scrollRef.current.scrollHeight,
|
|
57
|
+
behavior: "smooth",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
11
62
|
|
|
12
63
|
return (
|
|
13
|
-
<div ref={scrollRef} className={styles.chatThread}>
|
|
64
|
+
<div ref={scrollRef} className={styles.chatThread} onScroll={handleScroll}>
|
|
14
65
|
{messages.map((msg) => (
|
|
15
66
|
<div
|
|
16
67
|
key={msg.id}
|
|
17
68
|
className={`${styles.messageBubble} ${msg.role === "user" ? styles.userMessage : styles.agentMessage}`}
|
|
18
|
-
style={
|
|
69
|
+
style={
|
|
70
|
+
msg.role === "user" ? { backgroundColor: primaryColor } : undefined
|
|
71
|
+
}
|
|
19
72
|
>
|
|
20
73
|
{msg.text}
|
|
21
|
-
{msg.source === "voice" &&
|
|
74
|
+
{msg.source === "voice" && (
|
|
75
|
+
<div className={styles.voiceLabel}>voice</div>
|
|
76
|
+
)}
|
|
22
77
|
</div>
|
|
23
78
|
))}
|
|
79
|
+
{showThinking && (
|
|
80
|
+
<div className={`${styles.messageBubble} ${styles.agentMessage}`}>
|
|
81
|
+
<div className={styles.thinkingDots}>
|
|
82
|
+
<span />
|
|
83
|
+
<span />
|
|
84
|
+
<span />
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
24
88
|
</div>
|
|
25
89
|
);
|
|
26
90
|
}
|
package/src/PolymorphWidget.tsx
CHANGED
|
@@ -1,14 +1,52 @@
|
|
|
1
|
+
import { LiveKitRoom } from "@livekit/components-react";
|
|
1
2
|
import { MantineProvider } from "@mantine/core";
|
|
2
3
|
import "@mantine/core/styles.css";
|
|
3
|
-
import { useState } from "react";
|
|
4
|
+
import { useRef, useState } from "react";
|
|
5
|
+
import { RoomHandler } from "./RoomHandler";
|
|
4
6
|
import styles from "./styles.module.css";
|
|
5
7
|
import { buildWidgetTheme } from "./theme";
|
|
6
8
|
import type { WidgetConfig } from "./types";
|
|
7
9
|
import { usePolymorphSession } from "./usePolymorphSession";
|
|
8
10
|
import { WidgetPanel } from "./WidgetPanel";
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
function ChatIcon() {
|
|
13
|
+
return (
|
|
14
|
+
<svg
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
width="24"
|
|
17
|
+
height="24"
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
fill="none"
|
|
20
|
+
stroke="currentColor"
|
|
21
|
+
strokeWidth="2"
|
|
22
|
+
strokeLinecap="round"
|
|
23
|
+
strokeLinejoin="round"
|
|
24
|
+
>
|
|
25
|
+
<title>Chat</title>
|
|
26
|
+
<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" />
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function FabCloseIcon() {
|
|
32
|
+
return (
|
|
33
|
+
<svg
|
|
34
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
35
|
+
width="24"
|
|
36
|
+
height="24"
|
|
37
|
+
viewBox="0 0 24 24"
|
|
38
|
+
fill="none"
|
|
39
|
+
stroke="currentColor"
|
|
40
|
+
strokeWidth="2"
|
|
41
|
+
strokeLinecap="round"
|
|
42
|
+
strokeLinejoin="round"
|
|
43
|
+
>
|
|
44
|
+
<title>Close</title>
|
|
45
|
+
<path d="M18 6 6 18" />
|
|
46
|
+
<path d="m6 6 12 12" />
|
|
47
|
+
</svg>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
12
50
|
|
|
13
51
|
export function PolymorphWidget(props: WidgetConfig) {
|
|
14
52
|
const { position = "bottom-right", branding } = props;
|
|
@@ -16,26 +54,60 @@ export function PolymorphWidget(props: WidgetConfig) {
|
|
|
16
54
|
const session = usePolymorphSession(props);
|
|
17
55
|
const primaryColor = branding?.primaryColor || "#171717";
|
|
18
56
|
const theme = buildWidgetTheme(primaryColor);
|
|
57
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
19
58
|
|
|
20
59
|
return (
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
60
|
+
<div
|
|
61
|
+
ref={rootRef}
|
|
62
|
+
className={`polymorph-widget ${styles.widgetRoot} ${position === "bottom-left" ? styles.bottomLeft : styles.bottomRight}`}
|
|
63
|
+
style={{ colorScheme: props.darkMode ? "dark" : "light" }}
|
|
64
|
+
>
|
|
65
|
+
<MantineProvider
|
|
66
|
+
theme={theme}
|
|
67
|
+
forceColorScheme={props.darkMode ? "dark" : "light"}
|
|
68
|
+
cssVariablesSelector=".polymorph-widget"
|
|
69
|
+
getRootElement={() => rootRef.current ?? undefined}
|
|
70
|
+
>
|
|
71
|
+
<WidgetPanel
|
|
72
|
+
config={props}
|
|
73
|
+
session={session}
|
|
74
|
+
onClose={() => setOpen(false)}
|
|
75
|
+
hidden={!open}
|
|
76
|
+
/>
|
|
30
77
|
<button
|
|
31
78
|
type="button"
|
|
32
|
-
className={styles.fab}
|
|
79
|
+
className={`${styles.fab} ${open ? styles.fabOpen : ""}`}
|
|
33
80
|
style={{ backgroundColor: primaryColor }}
|
|
34
|
-
onClick={() => setOpen(prev => !prev)}
|
|
81
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
35
82
|
>
|
|
36
|
-
<ChatIcon />
|
|
83
|
+
{open ? <FabCloseIcon /> : <ChatIcon />}
|
|
37
84
|
</button>
|
|
38
|
-
|
|
39
|
-
|
|
85
|
+
{session.roomConnection && (
|
|
86
|
+
<LiveKitRoom
|
|
87
|
+
token={session.roomConnection.token}
|
|
88
|
+
serverUrl={session.roomConnection.livekitUrl}
|
|
89
|
+
connect={true}
|
|
90
|
+
audio={
|
|
91
|
+
session.isVoiceEnabled
|
|
92
|
+
? {
|
|
93
|
+
echoCancellation: true,
|
|
94
|
+
noiseSuppression: true,
|
|
95
|
+
autoGainControl: true,
|
|
96
|
+
}
|
|
97
|
+
: false
|
|
98
|
+
}
|
|
99
|
+
video={false}
|
|
100
|
+
style={{ display: "none" }}
|
|
101
|
+
onDisconnected={session.disconnect}
|
|
102
|
+
>
|
|
103
|
+
<RoomHandler
|
|
104
|
+
setRoom={session.setRoom}
|
|
105
|
+
addMessage={session.addMessage}
|
|
106
|
+
isVoiceEnabled={session.isVoiceEnabled}
|
|
107
|
+
/>
|
|
108
|
+
</LiveKitRoom>
|
|
109
|
+
)}
|
|
110
|
+
</MantineProvider>
|
|
111
|
+
</div>
|
|
40
112
|
);
|
|
41
113
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { RoomAudioRenderer, useRoomContext } from "@livekit/components-react";
|
|
2
|
+
import type { Room } from "livekit-client";
|
|
3
|
+
import {
|
|
4
|
+
type DataPacket_Kind,
|
|
5
|
+
type Participant,
|
|
6
|
+
ParticipantKind,
|
|
7
|
+
RoomEvent,
|
|
8
|
+
type TranscriptionSegment,
|
|
9
|
+
} from "livekit-client";
|
|
10
|
+
import { useEffect } from "react";
|
|
11
|
+
|
|
12
|
+
export function RoomHandler({
|
|
13
|
+
setRoom,
|
|
14
|
+
addMessage,
|
|
15
|
+
isVoiceEnabled,
|
|
16
|
+
}: {
|
|
17
|
+
setRoom: (room: Room | null) => void;
|
|
18
|
+
addMessage: (
|
|
19
|
+
role: "user" | "agent",
|
|
20
|
+
text: string,
|
|
21
|
+
source: "chat" | "voice",
|
|
22
|
+
) => void;
|
|
23
|
+
isVoiceEnabled: boolean;
|
|
24
|
+
}) {
|
|
25
|
+
const room = useRoomContext();
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setRoom(room);
|
|
29
|
+
return () => {
|
|
30
|
+
setRoom(null);
|
|
31
|
+
};
|
|
32
|
+
}, [room, setRoom]);
|
|
33
|
+
|
|
34
|
+
// Send initial voice state to agent once connected
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const sendVoiceState = () => {
|
|
37
|
+
const payload = new TextEncoder().encode(
|
|
38
|
+
JSON.stringify({ voice_enabled: isVoiceEnabled }),
|
|
39
|
+
);
|
|
40
|
+
room.localParticipant.publishData(payload, {
|
|
41
|
+
reliable: true,
|
|
42
|
+
topic: "voice_mode",
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (room.state === "connected") {
|
|
47
|
+
sendVoiceState();
|
|
48
|
+
} else {
|
|
49
|
+
const onConnected = () => sendVoiceState();
|
|
50
|
+
room.on(RoomEvent.Connected, onConnected);
|
|
51
|
+
return () => {
|
|
52
|
+
room.off(RoomEvent.Connected, onConnected);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}, [room, isVoiceEnabled]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const handleDataReceived = (
|
|
59
|
+
payload: Uint8Array,
|
|
60
|
+
_participant?: Participant,
|
|
61
|
+
_kind?: DataPacket_Kind,
|
|
62
|
+
topic?: string,
|
|
63
|
+
) => {
|
|
64
|
+
if (topic === "chat_message") {
|
|
65
|
+
try {
|
|
66
|
+
const data = JSON.parse(new TextDecoder().decode(payload));
|
|
67
|
+
if (data.type === "chat_response") {
|
|
68
|
+
addMessage("agent", data.text, "chat");
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
/* ignore malformed */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleTranscription = (
|
|
77
|
+
segments: TranscriptionSegment[],
|
|
78
|
+
participant?: Participant,
|
|
79
|
+
) => {
|
|
80
|
+
const finalSegments = segments.filter((s) => s.final);
|
|
81
|
+
if (finalSegments.length > 0) {
|
|
82
|
+
const text = finalSegments.map((s) => s.text).join(" ");
|
|
83
|
+
const isAgent =
|
|
84
|
+
participant?.kind === ParticipantKind.AGENT ||
|
|
85
|
+
participant?.identity?.includes("agent");
|
|
86
|
+
addMessage(isAgent ? "agent" : "user", text, "voice");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
room.on(RoomEvent.DataReceived, handleDataReceived);
|
|
91
|
+
room.on(RoomEvent.TranscriptionReceived, handleTranscription);
|
|
92
|
+
return () => {
|
|
93
|
+
room.off(RoomEvent.DataReceived, handleDataReceived);
|
|
94
|
+
room.off(RoomEvent.TranscriptionReceived, handleTranscription);
|
|
95
|
+
};
|
|
96
|
+
}, [room, addMessage]);
|
|
97
|
+
|
|
98
|
+
return <RoomAudioRenderer />;
|
|
99
|
+
}
|
package/src/VoiceOverlay.tsx
CHANGED
|
@@ -3,7 +3,17 @@ import type { SessionStatus } from "./types";
|
|
|
3
3
|
|
|
4
4
|
function VolumeIcon() {
|
|
5
5
|
return (
|
|
6
|
-
<svg
|
|
6
|
+
<svg
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
width="14"
|
|
9
|
+
height="14"
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
strokeWidth="2"
|
|
14
|
+
strokeLinecap="round"
|
|
15
|
+
strokeLinejoin="round"
|
|
16
|
+
>
|
|
7
17
|
<path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z" />
|
|
8
18
|
<path d="M16 9a5 5 0 0 1 0 6" />
|
|
9
19
|
<path d="M19.364 18.364a9 9 0 0 0 0-12.728" />
|
|
@@ -13,7 +23,17 @@ function VolumeIcon() {
|
|
|
13
23
|
|
|
14
24
|
function VolumeOffIcon() {
|
|
15
25
|
return (
|
|
16
|
-
<svg
|
|
26
|
+
<svg
|
|
27
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
28
|
+
width="14"
|
|
29
|
+
height="14"
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
fill="none"
|
|
32
|
+
stroke="currentColor"
|
|
33
|
+
strokeWidth="2"
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
strokeLinejoin="round"
|
|
36
|
+
>
|
|
17
37
|
<path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z" />
|
|
18
38
|
<line x1="22" x2="16" y1="9" y2="15" />
|
|
19
39
|
<line x1="16" x2="22" y1="9" y2="15" />
|