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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polymorph-sdk",
3
- "version": "0.2.0",
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": ["dist", "src", "README.md"],
11
+ "files": [
12
+ "dist",
13
+ "src",
14
+ "README.md"
15
+ ],
12
16
  "publishConfig": {
13
17
  "access": "public",
14
18
  "main": "dist/index.js",
@@ -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({ messages, primaryColor }: { messages: ChatMessage[]; primaryColor: string }) {
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({ top: scrollRef.current.scrollHeight, behavior: "smooth" });
10
- }, [messages]);
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={msg.role === "user" ? { backgroundColor: primaryColor } : undefined}
49
+ style={
50
+ msg.role === "user" ? { backgroundColor: primaryColor } : undefined
51
+ }
19
52
  >
20
53
  {msg.text}
21
- {msg.source === "voice" && <div className={styles.voiceLabel}>voice</div>}
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
  }
@@ -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() { return <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>; }
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
- <MantineProvider theme={theme} defaultColorScheme="light">
22
- <div className={`${styles.widgetRoot} ${position === "bottom-left" ? styles.bottomLeft : styles.bottomRight}`}>
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={() => setOpen(false)}
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={() => setOpen(prev => !prev)}
65
+ onClick={() => {
66
+ if (open) {
67
+ session.disconnect();
68
+ }
69
+ setOpen((prev) => !prev);
70
+ }}
35
71
  >
36
72
  <ChatIcon />
37
73
  </button>
38
- </div>
39
- </MantineProvider>
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
+ }
@@ -3,7 +3,17 @@ import type { SessionStatus } from "./types";
3
3
 
4
4
  function VolumeIcon() {
5
5
  return (
6
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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 xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
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" />
@@ -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 { type DataPacket_Kind, type Participant, ParticipantKind, RoomEvent, type TranscriptionSegment } from "livekit-client";
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() { return <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>; }
12
- function MicOffIcon() { return <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="2" x2="22" y1="2" y2="22"/><path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2"/><path d="M5 10v2a7 7 0 0 0 12 5"/><path d="M15 9.34V5a3 3 0 0 0-5.68-1.33"/><path d="M9 9v3a3 3 0 0 0 5.12 2.12"/><line x1="12" x2="12" y1="19" y2="22"/></svg>; }
13
- function SendIcon() { return <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>; }
14
- function CloseIcon() { return <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>; }
15
-
16
- function RoomHandler({
17
- setRoom,
18
- addMessage,
19
- }: {
20
- setRoom: (room: Room | null) => void;
21
- addMessage: (role: "user" | "agent", text: string, source: "chat" | "voice") => void;
22
- }) {
23
- const room = useRoomContext();
24
-
25
- useEffect(() => {
26
- setRoom(room);
27
- return () => { setRoom(null); };
28
- }, [room, setRoom]);
29
-
30
- useEffect(() => {
31
- const handleDataReceived = (
32
- payload: Uint8Array,
33
- _participant?: Participant,
34
- _kind?: DataPacket_Kind,
35
- topic?: string,
36
- ) => {
37
- if (topic === "chat_message") {
38
- try {
39
- const data = JSON.parse(new TextDecoder().decode(payload));
40
- if (data.type === "chat_response") {
41
- addMessage("agent", data.text, "chat");
42
- }
43
- } catch { /* ignore malformed */ }
44
- }
45
- };
46
-
47
- const handleTranscription = (
48
- segments: TranscriptionSegment[],
49
- participant?: Participant,
50
- ) => {
51
- const finalSegments = segments.filter(s => s.final);
52
- if (finalSegments.length > 0) {
53
- const text = finalSegments.map(s => s.text).join(" ");
54
- const isAgent = participant?.kind === ParticipantKind.AGENT ||
55
- participant?.identity?.includes("agent");
56
- addMessage(isAgent ? "agent" : "user", text, "voice");
57
- }
58
- };
59
-
60
- room.on(RoomEvent.DataReceived, handleDataReceived);
61
- room.on(RoomEvent.TranscriptionReceived, handleTranscription);
62
- return () => {
63
- room.off(RoomEvent.DataReceived, handleDataReceived);
64
- room.off(RoomEvent.TranscriptionReceived, handleTranscription);
65
- };
66
- }, [room, addMessage]);
67
-
68
- return <RoomAudioRenderer />;
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: (role: "user" | "agent", text: string, source: "chat" | "voice") => void;
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((e: React.KeyboardEvent) => {
102
- if (e.key === "Enter" && !e.shiftKey) {
103
- e.preventDefault();
104
- handleSend();
105
- }
106
- }, [handleSend]);
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 }}>{config.branding?.title || "Hi there"}</div>
139
+ <div style={{ fontWeight: 500, fontSize: 16 }}>
140
+ {config.branding?.title || "Hi there"}
141
+ </div>
114
142
  {config.branding?.subtitle && (
115
- <div style={{ fontSize: 13, color: "#737373", marginTop: 2 }}>{config.branding.subtitle}</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}><CloseIcon /></button>
154
+ <button type="button" className={styles.iconButton} onClick={onClose}>
155
+ <CloseIcon />
156
+ </button>
119
157
  </div>
120
158
 
121
159
  {/* Chat Thread */}
122
- <ChatThread messages={session.messages} primaryColor={primaryColor} />
123
-
124
- {/* Voice Overlay */}
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 type="button" className={styles.connectButton} style={{ backgroundColor: primaryColor }} disabled>
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";