polymorph-sdk 0.2.0 → 0.2.2
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 +5 -5
- package/dist/index.js +3313 -3243
- package/package.json +6 -2
- package/src/ChatThread.tsx +50 -6
- package/src/PolymorphWidget.tsx +70 -8
- package/src/RoomHandler.tsx +99 -0
- package/src/VoiceOverlay.tsx +22 -2
- package/src/WidgetPanel.tsx +129 -95
- package/src/__tests__/exports.test.ts +13 -0
- package/src/index.ts +6 -1
- package/src/styles.module.css +92 -28
- package/src/types.ts +5 -5
- package/src/usePolymorphSession.ts +110 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polymorph-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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,13 +1,44 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
import type { ChatMessage } from "./types";
|
|
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 agentCountAtConnect = useRef<number | null>(null);
|
|
16
|
+
const agentMessageCount = messages.filter((m) => m.role === "agent").length;
|
|
17
|
+
|
|
18
|
+
// Snapshot agent message count when connecting starts
|
|
19
|
+
if (status === "connecting" && agentCountAtConnect.current === null) {
|
|
20
|
+
agentCountAtConnect.current = agentMessageCount;
|
|
21
|
+
}
|
|
22
|
+
if (status === "idle" || status === "error") {
|
|
23
|
+
agentCountAtConnect.current = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const initialConnectThinking =
|
|
27
|
+
(status === "connecting" || status === "connected") &&
|
|
28
|
+
agentCountAtConnect.current !== null &&
|
|
29
|
+
agentMessageCount <= agentCountAtConnect.current;
|
|
30
|
+
const waitingForAgentReply =
|
|
31
|
+
status === "connected" &&
|
|
32
|
+
messages.length > 0 &&
|
|
33
|
+
messages[messages.length - 1].role === "user";
|
|
34
|
+
const showThinking = initialConnectThinking || waitingForAgentReply;
|
|
7
35
|
|
|
8
36
|
useEffect(() => {
|
|
9
|
-
scrollRef.current?.scrollTo({
|
|
10
|
-
|
|
37
|
+
scrollRef.current?.scrollTo({
|
|
38
|
+
top: scrollRef.current.scrollHeight,
|
|
39
|
+
behavior: "smooth",
|
|
40
|
+
});
|
|
41
|
+
}, []);
|
|
11
42
|
|
|
12
43
|
return (
|
|
13
44
|
<div ref={scrollRef} className={styles.chatThread}>
|
|
@@ -15,12 +46,25 @@ export function ChatThread({ messages, primaryColor }: { messages: ChatMessage[]
|
|
|
15
46
|
<div
|
|
16
47
|
key={msg.id}
|
|
17
48
|
className={`${styles.messageBubble} ${msg.role === "user" ? styles.userMessage : styles.agentMessage}`}
|
|
18
|
-
style={
|
|
49
|
+
style={
|
|
50
|
+
msg.role === "user" ? { backgroundColor: primaryColor } : undefined
|
|
51
|
+
}
|
|
19
52
|
>
|
|
20
53
|
{msg.text}
|
|
21
|
-
{msg.source === "voice" &&
|
|
54
|
+
{msg.source === "voice" && (
|
|
55
|
+
<div className={styles.voiceLabel}>voice</div>
|
|
56
|
+
)}
|
|
22
57
|
</div>
|
|
23
58
|
))}
|
|
59
|
+
{showThinking && (
|
|
60
|
+
<div className={`${styles.messageBubble} ${styles.agentMessage}`}>
|
|
61
|
+
<div className={styles.thinkingDots}>
|
|
62
|
+
<span />
|
|
63
|
+
<span />
|
|
64
|
+
<span />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
24
68
|
</div>
|
|
25
69
|
);
|
|
26
70
|
}
|
package/src/PolymorphWidget.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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";
|
|
@@ -8,7 +10,23 @@ import { usePolymorphSession } from "./usePolymorphSession";
|
|
|
8
10
|
import { WidgetPanel } from "./WidgetPanel";
|
|
9
11
|
|
|
10
12
|
// Chat bubble icon for FAB
|
|
11
|
-
function ChatIcon() {
|
|
13
|
+
function ChatIcon() {
|
|
14
|
+
return (
|
|
15
|
+
<svg
|
|
16
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
17
|
+
width="24"
|
|
18
|
+
height="24"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
fill="none"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
strokeWidth="2"
|
|
23
|
+
strokeLinecap="round"
|
|
24
|
+
strokeLinejoin="round"
|
|
25
|
+
>
|
|
26
|
+
<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" />
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
12
30
|
|
|
13
31
|
export function PolymorphWidget(props: WidgetConfig) {
|
|
14
32
|
const { position = "bottom-right", branding } = props;
|
|
@@ -16,26 +34,70 @@ export function PolymorphWidget(props: WidgetConfig) {
|
|
|
16
34
|
const session = usePolymorphSession(props);
|
|
17
35
|
const primaryColor = branding?.primaryColor || "#171717";
|
|
18
36
|
const theme = buildWidgetTheme(primaryColor);
|
|
37
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
19
38
|
|
|
20
39
|
return (
|
|
21
|
-
<
|
|
22
|
-
|
|
40
|
+
<div
|
|
41
|
+
ref={rootRef}
|
|
42
|
+
className={`polymorph-widget ${styles.widgetRoot} ${position === "bottom-left" ? styles.bottomLeft : styles.bottomRight}`}
|
|
43
|
+
style={{ colorScheme: props.darkMode ? "dark" : "light" }}
|
|
44
|
+
>
|
|
45
|
+
<MantineProvider
|
|
46
|
+
theme={theme}
|
|
47
|
+
forceColorScheme={props.darkMode ? "dark" : "light"}
|
|
48
|
+
cssVariablesSelector=".polymorph-widget"
|
|
49
|
+
getRootElement={() => rootRef.current ?? undefined}
|
|
50
|
+
>
|
|
23
51
|
{open && (
|
|
24
52
|
<WidgetPanel
|
|
25
53
|
config={props}
|
|
26
54
|
session={session}
|
|
27
|
-
onClose={() =>
|
|
55
|
+
onClose={() => {
|
|
56
|
+
session.disconnect();
|
|
57
|
+
setOpen(false);
|
|
58
|
+
}}
|
|
28
59
|
/>
|
|
29
60
|
)}
|
|
30
61
|
<button
|
|
31
62
|
type="button"
|
|
32
63
|
className={styles.fab}
|
|
33
64
|
style={{ backgroundColor: primaryColor }}
|
|
34
|
-
onClick={() =>
|
|
65
|
+
onClick={() => {
|
|
66
|
+
if (open) {
|
|
67
|
+
session.disconnect();
|
|
68
|
+
}
|
|
69
|
+
setOpen((prev) => !prev);
|
|
70
|
+
}}
|
|
35
71
|
>
|
|
36
72
|
<ChatIcon />
|
|
37
73
|
</button>
|
|
38
|
-
|
|
39
|
-
|
|
74
|
+
{/* LiveKit room lives outside WidgetPanel so it mounts before the panel renders */}
|
|
75
|
+
{session.roomConnection && (
|
|
76
|
+
<LiveKitRoom
|
|
77
|
+
token={session.roomConnection.token}
|
|
78
|
+
serverUrl={session.roomConnection.livekitUrl}
|
|
79
|
+
connect={true}
|
|
80
|
+
audio={
|
|
81
|
+
session.isVoiceEnabled
|
|
82
|
+
? {
|
|
83
|
+
echoCancellation: true,
|
|
84
|
+
noiseSuppression: true,
|
|
85
|
+
autoGainControl: true,
|
|
86
|
+
}
|
|
87
|
+
: false
|
|
88
|
+
}
|
|
89
|
+
video={false}
|
|
90
|
+
style={{ display: "none" }}
|
|
91
|
+
onDisconnected={session.disconnect}
|
|
92
|
+
>
|
|
93
|
+
<RoomHandler
|
|
94
|
+
setRoom={session.setRoom}
|
|
95
|
+
addMessage={session.addMessage}
|
|
96
|
+
isVoiceEnabled={session.isVoiceEnabled}
|
|
97
|
+
/>
|
|
98
|
+
</LiveKitRoom>
|
|
99
|
+
)}
|
|
100
|
+
</MantineProvider>
|
|
101
|
+
</div>
|
|
40
102
|
);
|
|
41
103
|
}
|
|
@@ -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" />
|
package/src/WidgetPanel.tsx
CHANGED
|
@@ -1,71 +1,87 @@
|
|
|
1
|
-
import { LiveKitRoom, RoomAudioRenderer, useRoomContext } from "@livekit/components-react";
|
|
2
|
-
import { useCallback, useEffect, useState } from "react";
|
|
3
1
|
import type { Room } from "livekit-client";
|
|
4
|
-
import {
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
5
3
|
import { ChatThread } from "./ChatThread";
|
|
6
4
|
import styles from "./styles.module.css";
|
|
7
5
|
import type { ChatMessage, SessionStatus, WidgetConfig } from "./types";
|
|
8
6
|
import { VoiceOverlay } from "./VoiceOverlay";
|
|
9
7
|
|
|
10
8
|
// Internal SVG icons (no @tabler dependency)
|
|
11
|
-
function MicIcon() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
9
|
+
function MicIcon() {
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
width="18"
|
|
14
|
+
height="18"
|
|
15
|
+
viewBox="0 0 24 24"
|
|
16
|
+
fill="none"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
strokeWidth="2"
|
|
19
|
+
strokeLinecap="round"
|
|
20
|
+
strokeLinejoin="round"
|
|
21
|
+
>
|
|
22
|
+
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
|
|
23
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
|
24
|
+
<line x1="12" x2="12" y1="19" y2="22" />
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
function MicOffIcon() {
|
|
29
|
+
return (
|
|
30
|
+
<svg
|
|
31
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
32
|
+
width="18"
|
|
33
|
+
height="18"
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
fill="none"
|
|
36
|
+
stroke="currentColor"
|
|
37
|
+
strokeWidth="2"
|
|
38
|
+
strokeLinecap="round"
|
|
39
|
+
strokeLinejoin="round"
|
|
40
|
+
>
|
|
41
|
+
<line x1="2" x2="22" y1="2" y2="22" />
|
|
42
|
+
<path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2" />
|
|
43
|
+
<path d="M5 10v2a7 7 0 0 0 12 5" />
|
|
44
|
+
<path d="M15 9.34V5a3 3 0 0 0-5.68-1.33" />
|
|
45
|
+
<path d="M9 9v3a3 3 0 0 0 5.12 2.12" />
|
|
46
|
+
<line x1="12" x2="12" y1="19" y2="22" />
|
|
47
|
+
</svg>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
function SendIcon() {
|
|
51
|
+
return (
|
|
52
|
+
<svg
|
|
53
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
54
|
+
width="18"
|
|
55
|
+
height="18"
|
|
56
|
+
viewBox="0 0 24 24"
|
|
57
|
+
fill="none"
|
|
58
|
+
stroke="currentColor"
|
|
59
|
+
strokeWidth="2"
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
strokeLinejoin="round"
|
|
62
|
+
>
|
|
63
|
+
<path d="m22 2-7 20-4-9-9-4Z" />
|
|
64
|
+
<path d="M22 2 11 13" />
|
|
65
|
+
</svg>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
function CloseIcon() {
|
|
69
|
+
return (
|
|
70
|
+
<svg
|
|
71
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
72
|
+
width="18"
|
|
73
|
+
height="18"
|
|
74
|
+
viewBox="0 0 24 24"
|
|
75
|
+
fill="none"
|
|
76
|
+
stroke="currentColor"
|
|
77
|
+
strokeWidth="2"
|
|
78
|
+
strokeLinecap="round"
|
|
79
|
+
strokeLinejoin="round"
|
|
80
|
+
>
|
|
81
|
+
<path d="M18 6 6 18" />
|
|
82
|
+
<path d="m6 6 12 12" />
|
|
83
|
+
</svg>
|
|
84
|
+
);
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
interface WidgetPanelProps {
|
|
@@ -79,7 +95,11 @@ interface WidgetPanelProps {
|
|
|
79
95
|
error: string | null;
|
|
80
96
|
connect: () => Promise<void>;
|
|
81
97
|
disconnect: () => void;
|
|
82
|
-
addMessage: (
|
|
98
|
+
addMessage: (
|
|
99
|
+
role: "user" | "agent",
|
|
100
|
+
text: string,
|
|
101
|
+
source: "chat" | "voice",
|
|
102
|
+
) => void;
|
|
83
103
|
sendMessage: (text: string) => void;
|
|
84
104
|
toggleMic: () => void;
|
|
85
105
|
toggleVoice: () => void;
|
|
@@ -91,6 +111,9 @@ interface WidgetPanelProps {
|
|
|
91
111
|
export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
|
|
92
112
|
const [inputValue, setInputValue] = useState("");
|
|
93
113
|
const primaryColor = config.branding?.primaryColor || "#171717";
|
|
114
|
+
const isChatOnlyAgent = (config.agentName || "")
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
.includes("chat-agent");
|
|
94
117
|
|
|
95
118
|
const handleSend = useCallback(() => {
|
|
96
119
|
if (!inputValue.trim()) return;
|
|
@@ -98,37 +121,58 @@ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
|
|
|
98
121
|
setInputValue("");
|
|
99
122
|
}, [inputValue, session]);
|
|
100
123
|
|
|
101
|
-
const handleKeyDown = useCallback(
|
|
102
|
-
|
|
103
|
-
e.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
const handleKeyDown = useCallback(
|
|
125
|
+
(e: React.KeyboardEvent) => {
|
|
126
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
handleSend();
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
[handleSend],
|
|
132
|
+
);
|
|
107
133
|
|
|
108
134
|
return (
|
|
109
135
|
<div className={styles.panel}>
|
|
110
136
|
{/* Header */}
|
|
111
137
|
<div className={styles.header}>
|
|
112
138
|
<div>
|
|
113
|
-
<div style={{ fontWeight: 500, fontSize: 16 }}>
|
|
139
|
+
<div style={{ fontWeight: 500, fontSize: 16 }}>
|
|
140
|
+
{config.branding?.title || "Hi there"}
|
|
141
|
+
</div>
|
|
114
142
|
{config.branding?.subtitle && (
|
|
115
|
-
<div
|
|
143
|
+
<div
|
|
144
|
+
style={{
|
|
145
|
+
fontSize: 13,
|
|
146
|
+
color: "var(--mantine-color-dimmed)",
|
|
147
|
+
marginTop: 2,
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
{config.branding.subtitle}
|
|
151
|
+
</div>
|
|
116
152
|
)}
|
|
117
153
|
</div>
|
|
118
|
-
<button type="button" className={styles.iconButton} onClick={onClose}
|
|
154
|
+
<button type="button" className={styles.iconButton} onClick={onClose}>
|
|
155
|
+
<CloseIcon />
|
|
156
|
+
</button>
|
|
119
157
|
</div>
|
|
120
158
|
|
|
121
159
|
{/* Chat Thread */}
|
|
122
|
-
<ChatThread
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<VoiceOverlay
|
|
126
|
-
isVoiceEnabled={session.isVoiceEnabled}
|
|
127
|
-
isMicActive={session.isMicActive}
|
|
160
|
+
<ChatThread
|
|
161
|
+
messages={session.messages}
|
|
162
|
+
primaryColor={primaryColor}
|
|
128
163
|
status={session.status}
|
|
129
|
-
onToggleVoice={session.toggleVoice}
|
|
130
164
|
/>
|
|
131
165
|
|
|
166
|
+
{/* Voice Overlay */}
|
|
167
|
+
{!isChatOnlyAgent && (
|
|
168
|
+
<VoiceOverlay
|
|
169
|
+
isVoiceEnabled={session.isVoiceEnabled}
|
|
170
|
+
isMicActive={session.isMicActive}
|
|
171
|
+
status={session.status}
|
|
172
|
+
onToggleVoice={session.toggleVoice}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
|
|
132
176
|
{/* Error */}
|
|
133
177
|
{session.error && <div className={styles.errorText}>{session.error}</div>}
|
|
134
178
|
|
|
@@ -143,7 +187,12 @@ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
|
|
|
143
187
|
Start conversation
|
|
144
188
|
</button>
|
|
145
189
|
) : session.status === "connecting" ? (
|
|
146
|
-
<button
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
className={styles.connectButton}
|
|
193
|
+
style={{ backgroundColor: primaryColor }}
|
|
194
|
+
disabled
|
|
195
|
+
>
|
|
147
196
|
Connecting...
|
|
148
197
|
</button>
|
|
149
198
|
) : (
|
|
@@ -163,7 +212,7 @@ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
|
|
|
163
212
|
>
|
|
164
213
|
<SendIcon />
|
|
165
214
|
</button>
|
|
166
|
-
{session.isVoiceEnabled && (
|
|
215
|
+
{session.isVoiceEnabled && !isChatOnlyAgent && (
|
|
167
216
|
<button
|
|
168
217
|
type="button"
|
|
169
218
|
className={`${styles.iconButton} ${session.isMicActive ? styles.iconButtonActive : ""}`}
|
|
@@ -174,21 +223,6 @@ export function WidgetPanel({ config, session, onClose }: WidgetPanelProps) {
|
|
|
174
223
|
)}
|
|
175
224
|
</div>
|
|
176
225
|
)}
|
|
177
|
-
|
|
178
|
-
{/* Hidden LiveKit Room */}
|
|
179
|
-
{session.roomConnection && (
|
|
180
|
-
<LiveKitRoom
|
|
181
|
-
token={session.roomConnection.token}
|
|
182
|
-
serverUrl={session.roomConnection.livekitUrl}
|
|
183
|
-
connect={true}
|
|
184
|
-
audio={session.isVoiceEnabled ? { echoCancellation: true, noiseSuppression: true, autoGainControl: true } : false}
|
|
185
|
-
video={false}
|
|
186
|
-
style={{ display: "none" }}
|
|
187
|
-
onDisconnected={session.disconnect}
|
|
188
|
-
>
|
|
189
|
-
<RoomHandler setRoom={session.setRoom} addMessage={session.addMessage} />
|
|
190
|
-
</LiveKitRoom>
|
|
191
|
-
)}
|
|
192
226
|
</div>
|
|
193
227
|
);
|
|
194
228
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("polymorph-sdk exports", () => {
|
|
4
|
+
it("exports PolymorphWidget component", async () => {
|
|
5
|
+
const mod = await import("../index");
|
|
6
|
+
expect(mod.PolymorphWidget).toBeDefined();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("exports usePolymorphSession hook", async () => {
|
|
10
|
+
const mod = await import("../index");
|
|
11
|
+
expect(mod.usePolymorphSession).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export { PolymorphWidget } from "./PolymorphWidget";
|
|
2
|
+
export type {
|
|
3
|
+
ChatMessage,
|
|
4
|
+
SessionStatus,
|
|
5
|
+
WidgetBranding,
|
|
6
|
+
WidgetConfig,
|
|
7
|
+
} from "./types";
|
|
2
8
|
export { usePolymorphSession } from "./usePolymorphSession";
|
|
3
|
-
export type { ChatMessage, SessionStatus, WidgetBranding, WidgetConfig } from "./types";
|