polymorph-sdk 0.2.2 → 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/dist/index.css +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +202 -148
- package/package.json +1 -1
- package/src/ChatThread.tsx +28 -8
- package/src/PolymorphWidget.tsx +30 -20
- package/src/WidgetPanel.tsx +50 -47
- package/src/styles.module.css +41 -304
- package/src/types.ts +13 -0
- package/src/usePolymorphSession.ts +57 -18
package/dist/index.d.ts
CHANGED
|
@@ -48,9 +48,13 @@ export declare interface WidgetBranding {
|
|
|
48
48
|
export declare interface WidgetConfig {
|
|
49
49
|
apiBaseUrl: string;
|
|
50
50
|
apiKey?: string;
|
|
51
|
+
/** Server-side widget config ID. When set, metadata is ignored and config is resolved server-side. */
|
|
52
|
+
configId?: string;
|
|
51
53
|
/** LiveKit dispatched agent name (default: "custom-voice-agent"). */
|
|
52
54
|
agentName?: string;
|
|
53
55
|
metadata?: Record<string, string | string[]>;
|
|
56
|
+
/** Pre-filled user identity. When provided, the agent skips asking for these fields. */
|
|
57
|
+
user?: WidgetUser;
|
|
54
58
|
branding?: WidgetBranding;
|
|
55
59
|
position?: "bottom-right" | "bottom-left";
|
|
56
60
|
/** Enable voice call (default: true). When false, widget is chat-only. */
|
|
@@ -61,4 +65,13 @@ export declare interface WidgetConfig {
|
|
|
61
65
|
darkMode?: boolean;
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
declare interface WidgetUser {
|
|
69
|
+
/** Display name (e.g. "Jane Smith") */
|
|
70
|
+
name?: string;
|
|
71
|
+
/** Email address */
|
|
72
|
+
email?: string;
|
|
73
|
+
/** Phone number */
|
|
74
|
+
phone?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -17680,31 +17680,33 @@ function RoomHandler({ setRoom: t, addMessage: a, isVoiceEnabled: s }) {
|
|
|
17680
17680
|
}, [c, a]), /* @__PURE__ */ jsx(ba, {});
|
|
17681
17681
|
}
|
|
17682
17682
|
var styles_module_default = {
|
|
17683
|
-
widgetRoot: "
|
|
17684
|
-
bottomRight: "
|
|
17685
|
-
bottomLeft: "
|
|
17686
|
-
fab: "
|
|
17687
|
-
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
17683
|
+
widgetRoot: "_widgetRoot_n856n_1",
|
|
17684
|
+
bottomRight: "_bottomRight_n856n_2",
|
|
17685
|
+
bottomLeft: "_bottomLeft_n856n_3",
|
|
17686
|
+
fab: "_fab_n856n_4",
|
|
17687
|
+
fabOpen: "_fabOpen_n856n_6",
|
|
17688
|
+
panel: "_panel_n856n_8",
|
|
17689
|
+
panelHidden: "_panelHidden_n856n_9",
|
|
17690
|
+
header: "_header_n856n_10",
|
|
17691
|
+
chatThread: "_chatThread_n856n_11",
|
|
17692
|
+
messageBubble: "_messageBubble_n856n_12",
|
|
17693
|
+
messageAppear: "_messageAppear_n856n_1",
|
|
17694
|
+
agentMessage: "_agentMessage_n856n_14",
|
|
17695
|
+
userMessage: "_userMessage_n856n_15",
|
|
17696
|
+
voiceLabel: "_voiceLabel_n856n_16",
|
|
17697
|
+
voiceOverlay: "_voiceOverlay_n856n_17",
|
|
17698
|
+
voiceBars: "_voiceBars_n856n_18",
|
|
17699
|
+
voiceBar: "_voiceBar_n856n_18",
|
|
17700
|
+
voiceToggle: "_voiceToggle_n856n_24",
|
|
17701
|
+
voiceToggleActive: "_voiceToggleActive_n856n_26",
|
|
17702
|
+
inputBar: "_inputBar_n856n_28",
|
|
17703
|
+
inputField: "_inputField_n856n_29",
|
|
17704
|
+
iconButton: "_iconButton_n856n_31",
|
|
17705
|
+
iconButtonActive: "_iconButtonActive_n856n_34",
|
|
17706
|
+
statusBadge: "_statusBadge_n856n_35",
|
|
17707
|
+
errorText: "_errorText_n856n_36",
|
|
17708
|
+
thinkingDots: "_thinkingDots_n856n_37",
|
|
17709
|
+
thinkingDot: "_thinkingDot_n856n_37"
|
|
17708
17710
|
};
|
|
17709
17711
|
function hexToMantineTuple(t) {
|
|
17710
17712
|
let a = Number.parseInt(t.slice(1, 3), 16), s = Number.parseInt(t.slice(3, 5), 16), c = Number.parseInt(t.slice(5, 7), 16), l = (t) => `#${[
|
|
@@ -17757,115 +17759,143 @@ async function ensureCsrf(t, a) {
|
|
|
17757
17759
|
}
|
|
17758
17760
|
}
|
|
17759
17761
|
function usePolymorphSession(t) {
|
|
17760
|
-
let a = (t.agentName || "").toLowerCase().includes("chat-agent"), c = t.enableVoice !== !1 && !a, [u, d] = useState("idle"), [m, g] = useState(null),
|
|
17762
|
+
let a = (t.agentName || "").toLowerCase().includes("chat-agent"), c = t.enableVoice !== !1 && !a, [u, d] = useState("idle"), [m, g] = useState(null), v = t.branding?.greeting, [y, b] = useState(() => v ? [{
|
|
17763
|
+
id: "greeting",
|
|
17764
|
+
role: "agent",
|
|
17765
|
+
text: v,
|
|
17766
|
+
source: "chat",
|
|
17767
|
+
timestamp: Date.now()
|
|
17768
|
+
}] : []), [x, S] = useState(c), [C, w] = useState(c), [E, O] = useState(null), j = useRef(null), R = useRef([]);
|
|
17761
17769
|
useEffect(() => {
|
|
17762
|
-
|
|
17770
|
+
S(c), w(c), j.current && j.current.localParticipant.setMicrophoneEnabled(c);
|
|
17763
17771
|
}, [c]);
|
|
17764
|
-
let
|
|
17765
|
-
|
|
17772
|
+
let V = useCallback((t, a, s) => {
|
|
17773
|
+
b((c) => [...c, {
|
|
17766
17774
|
id: crypto.randomUUID(),
|
|
17767
17775
|
role: t,
|
|
17768
17776
|
text: a,
|
|
17769
17777
|
source: s,
|
|
17770
17778
|
timestamp: Date.now()
|
|
17771
17779
|
}]);
|
|
17772
|
-
}, [])
|
|
17780
|
+
}, []), H = useCallback(async () => {
|
|
17781
|
+
if (!(u === "connecting" || u === "connected")) {
|
|
17782
|
+
d("connecting"), O(null);
|
|
17783
|
+
try {
|
|
17784
|
+
let { headers: a,...s } = t.fetchOptions ?? {}, c = {};
|
|
17785
|
+
if (t.apiKey) c.Authorization = `Bearer ${t.apiKey}`;
|
|
17786
|
+
else if (s.credentials === "include") {
|
|
17787
|
+
await ensureCsrf(t.apiBaseUrl, s);
|
|
17788
|
+
let a = sessionStorage.getItem(csrfStorageKey);
|
|
17789
|
+
a && (c["x-csrf-token"] = a);
|
|
17790
|
+
}
|
|
17791
|
+
let l = "polymorph_widget_session", u = localStorage.getItem(l) ?? void 0;
|
|
17792
|
+
u || (u = `widget-session-${crypto.randomUUID().slice(0, 12)}`, localStorage.setItem(l, u));
|
|
17793
|
+
let d = await fetch(`${t.apiBaseUrl}/voice-rooms/start`, {
|
|
17794
|
+
method: "POST",
|
|
17795
|
+
headers: {
|
|
17796
|
+
"Content-Type": "application/json",
|
|
17797
|
+
...c,
|
|
17798
|
+
...a instanceof Headers ? Object.fromEntries(a.entries()) : a
|
|
17799
|
+
},
|
|
17800
|
+
body: JSON.stringify({
|
|
17801
|
+
agent_name: t.agentName || "custom-voice-agent",
|
|
17802
|
+
config_id: t.configId ?? void 0,
|
|
17803
|
+
metadata: t.configId ? void 0 : {
|
|
17804
|
+
...t.metadata,
|
|
17805
|
+
...t.branding?.greeting && { greeting: t.branding.greeting },
|
|
17806
|
+
...t.user?.name && { user_name: t.user.name },
|
|
17807
|
+
...t.user?.email && { user_email: t.user.email },
|
|
17808
|
+
...t.user?.phone && { user_phone: t.user.phone }
|
|
17809
|
+
},
|
|
17810
|
+
user_name: t.user?.name,
|
|
17811
|
+
external_user_id: u
|
|
17812
|
+
}),
|
|
17813
|
+
...s
|
|
17814
|
+
});
|
|
17815
|
+
if (d.status === 403 && sessionStorage.removeItem(csrfStorageKey), !d.ok) throw Error(await d.text() || "Failed to start session");
|
|
17816
|
+
let f = await d.json();
|
|
17817
|
+
g({
|
|
17818
|
+
token: f.token,
|
|
17819
|
+
livekitUrl: f.livekit_url
|
|
17820
|
+
});
|
|
17821
|
+
} catch (t) {
|
|
17822
|
+
O(t instanceof Error ? t.message : "Something went wrong"), d("error");
|
|
17823
|
+
}
|
|
17824
|
+
}
|
|
17825
|
+
}, [t, u]);
|
|
17773
17826
|
return {
|
|
17774
17827
|
status: u,
|
|
17775
17828
|
roomConnection: m,
|
|
17776
|
-
messages:
|
|
17777
|
-
isVoiceEnabled:
|
|
17778
|
-
isMicActive:
|
|
17779
|
-
error:
|
|
17780
|
-
connect:
|
|
17781
|
-
if (!(u === "connecting" || u === "connected")) {
|
|
17782
|
-
d("connecting"), E(null);
|
|
17783
|
-
try {
|
|
17784
|
-
let { headers: a,...s } = t.fetchOptions ?? {}, c = {};
|
|
17785
|
-
if (t.apiKey) c.Authorization = `Bearer ${t.apiKey}`;
|
|
17786
|
-
else if (s.credentials === "include") {
|
|
17787
|
-
await ensureCsrf(t.apiBaseUrl, s);
|
|
17788
|
-
let a = sessionStorage.getItem(csrfStorageKey);
|
|
17789
|
-
a && (c["x-csrf-token"] = a);
|
|
17790
|
-
}
|
|
17791
|
-
let l = "polymorph_widget_session", u = localStorage.getItem(l) ?? void 0;
|
|
17792
|
-
u || (u = `widget-session-${crypto.randomUUID().slice(0, 12)}`, localStorage.setItem(l, u));
|
|
17793
|
-
let d = await fetch(`${t.apiBaseUrl}/voice-rooms/start`, {
|
|
17794
|
-
method: "POST",
|
|
17795
|
-
headers: {
|
|
17796
|
-
"Content-Type": "application/json",
|
|
17797
|
-
...c,
|
|
17798
|
-
...a instanceof Headers ? Object.fromEntries(a.entries()) : a
|
|
17799
|
-
},
|
|
17800
|
-
body: JSON.stringify({
|
|
17801
|
-
agent_name: t.agentName || "custom-voice-agent",
|
|
17802
|
-
metadata: {
|
|
17803
|
-
...t.metadata,
|
|
17804
|
-
...t.branding?.greeting && { greeting: t.branding.greeting }
|
|
17805
|
-
},
|
|
17806
|
-
external_user_id: u
|
|
17807
|
-
}),
|
|
17808
|
-
...s
|
|
17809
|
-
});
|
|
17810
|
-
if (d.status === 403 && sessionStorage.removeItem(csrfStorageKey), !d.ok) throw Error(await d.text() || "Failed to start session");
|
|
17811
|
-
let f = await d.json();
|
|
17812
|
-
g({
|
|
17813
|
-
token: f.token,
|
|
17814
|
-
livekitUrl: f.livekit_url
|
|
17815
|
-
});
|
|
17816
|
-
} catch (t) {
|
|
17817
|
-
E(t instanceof Error ? t.message : "Something went wrong"), d("error");
|
|
17818
|
-
}
|
|
17819
|
-
}
|
|
17820
|
-
}, [t, u]),
|
|
17829
|
+
messages: y,
|
|
17830
|
+
isVoiceEnabled: x,
|
|
17831
|
+
isMicActive: C,
|
|
17832
|
+
error: E,
|
|
17833
|
+
connect: H,
|
|
17821
17834
|
disconnect: useCallback(() => {
|
|
17822
|
-
|
|
17835
|
+
j.current?.disconnect(), j.current = null, g(null), d("idle");
|
|
17823
17836
|
}, []),
|
|
17824
|
-
addMessage:
|
|
17837
|
+
addMessage: V,
|
|
17825
17838
|
sendMessage: useCallback((t) => {
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
let s =
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17839
|
+
if (!t.trim()) return;
|
|
17840
|
+
let a = t.trim();
|
|
17841
|
+
V("user", a, "chat");
|
|
17842
|
+
let s = j.current;
|
|
17843
|
+
if (s) {
|
|
17844
|
+
let t = new TextEncoder().encode(JSON.stringify({ text: a }));
|
|
17845
|
+
s.localParticipant.publishData(t, {
|
|
17846
|
+
reliable: !0,
|
|
17847
|
+
topic: "chat_message"
|
|
17848
|
+
});
|
|
17849
|
+
} else R.current.push(a), H();
|
|
17850
|
+
}, [V, H]),
|
|
17835
17851
|
toggleMic: useCallback(async () => {
|
|
17836
|
-
let t =
|
|
17852
|
+
let t = j.current;
|
|
17837
17853
|
if (!t) return;
|
|
17838
|
-
let a = !
|
|
17839
|
-
await t.localParticipant.setMicrophoneEnabled(a),
|
|
17840
|
-
}, [
|
|
17854
|
+
let a = !C;
|
|
17855
|
+
await t.localParticipant.setMicrophoneEnabled(a), w(a);
|
|
17856
|
+
}, [C]),
|
|
17841
17857
|
toggleVoice: useCallback(async () => {
|
|
17842
|
-
let t =
|
|
17858
|
+
let t = j.current;
|
|
17843
17859
|
if (!t) return;
|
|
17844
|
-
let a = !
|
|
17845
|
-
await t.localParticipant.setMicrophoneEnabled(a),
|
|
17860
|
+
let a = !x;
|
|
17861
|
+
await t.localParticipant.setMicrophoneEnabled(a), S(a), w(a);
|
|
17846
17862
|
let s = new TextEncoder().encode(JSON.stringify({ voice_enabled: a }));
|
|
17847
17863
|
t.localParticipant.publishData(s, {
|
|
17848
17864
|
reliable: !0,
|
|
17849
17865
|
topic: "voice_mode"
|
|
17850
17866
|
});
|
|
17851
|
-
}, [
|
|
17867
|
+
}, [x]),
|
|
17852
17868
|
setRoom: useCallback((t) => {
|
|
17853
|
-
|
|
17869
|
+
if (j.current = t, t) {
|
|
17870
|
+
d("connected");
|
|
17871
|
+
let a = R.current.splice(0);
|
|
17872
|
+
for (let s of a) {
|
|
17873
|
+
let a = new TextEncoder().encode(JSON.stringify({ text: s }));
|
|
17874
|
+
t.localParticipant.publishData(a, {
|
|
17875
|
+
reliable: !0,
|
|
17876
|
+
topic: "chat_message"
|
|
17877
|
+
});
|
|
17878
|
+
}
|
|
17879
|
+
}
|
|
17854
17880
|
}, [])
|
|
17855
17881
|
};
|
|
17856
17882
|
}
|
|
17857
|
-
function ChatThread({ messages: t, primaryColor: a, status:
|
|
17858
|
-
let
|
|
17859
|
-
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
|
|
17863
|
-
|
|
17883
|
+
function ChatThread({ messages: t, primaryColor: a, status: c }) {
|
|
17884
|
+
let l = useRef(null), u = useRef(!0), d = useRef(0), p = useRef(null), m = t.filter((t) => t.role === "agent").length, y = t.length > 0 && t[t.length - 1].role === "user", b = useCallback(() => {
|
|
17885
|
+
let t = l.current;
|
|
17886
|
+
t && (u.current = t.scrollHeight - t.scrollTop - t.clientHeight < 100);
|
|
17887
|
+
}, []);
|
|
17888
|
+
c === "connecting" && p.current === null && (p.current = m), (c === "idle" || c === "error") && (p.current = null);
|
|
17889
|
+
let x = (c === "connecting" || c === "connected") && p.current !== null && m <= p.current, S = c === "connected" && t.length > 0 && t[t.length - 1].role === "user", C = x || S, w = t.length + (C ? 1 : 0);
|
|
17890
|
+
return w !== d.current && (d.current = w, (u.current || y) && queueMicrotask(() => {
|
|
17891
|
+
l.current?.scrollTo({
|
|
17892
|
+
top: l.current.scrollHeight,
|
|
17864
17893
|
behavior: "smooth"
|
|
17865
17894
|
});
|
|
17866
|
-
}
|
|
17867
|
-
ref:
|
|
17895
|
+
})), /* @__PURE__ */ jsxs("div", {
|
|
17896
|
+
ref: l,
|
|
17868
17897
|
className: styles_module_default.chatThread,
|
|
17898
|
+
onScroll: b,
|
|
17869
17899
|
children: [t.map((t) => /* @__PURE__ */ jsxs("div", {
|
|
17870
17900
|
className: `${styles_module_default.messageBubble} ${t.role === "user" ? styles_module_default.userMessage : styles_module_default.agentMessage}`,
|
|
17871
17901
|
style: t.role === "user" ? { backgroundColor: a } : void 0,
|
|
@@ -17873,7 +17903,7 @@ function ChatThread({ messages: t, primaryColor: a, status: s }) {
|
|
|
17873
17903
|
className: styles_module_default.voiceLabel,
|
|
17874
17904
|
children: "voice"
|
|
17875
17905
|
})]
|
|
17876
|
-
}, t.id)),
|
|
17906
|
+
}, t.id)), C && /* @__PURE__ */ jsx("div", {
|
|
17877
17907
|
className: `${styles_module_default.messageBubble} ${styles_module_default.agentMessage}`,
|
|
17878
17908
|
children: /* @__PURE__ */ jsxs("div", {
|
|
17879
17909
|
className: styles_module_default.thinkingDots,
|
|
@@ -17969,6 +17999,7 @@ function MicIcon() {
|
|
|
17969
17999
|
strokeLinecap: "round",
|
|
17970
18000
|
strokeLinejoin: "round",
|
|
17971
18001
|
children: [
|
|
18002
|
+
/* @__PURE__ */ jsx("title", { children: "Microphone on" }),
|
|
17972
18003
|
/* @__PURE__ */ jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
|
|
17973
18004
|
/* @__PURE__ */ jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
|
|
17974
18005
|
/* @__PURE__ */ jsx("line", {
|
|
@@ -17992,6 +18023,7 @@ function MicOffIcon() {
|
|
|
17992
18023
|
strokeLinecap: "round",
|
|
17993
18024
|
strokeLinejoin: "round",
|
|
17994
18025
|
children: [
|
|
18026
|
+
/* @__PURE__ */ jsx("title", { children: "Microphone off" }),
|
|
17995
18027
|
/* @__PURE__ */ jsx("line", {
|
|
17996
18028
|
x1: "2",
|
|
17997
18029
|
x2: "22",
|
|
@@ -18022,7 +18054,11 @@ function SendIcon() {
|
|
|
18022
18054
|
strokeWidth: "2",
|
|
18023
18055
|
strokeLinecap: "round",
|
|
18024
18056
|
strokeLinejoin: "round",
|
|
18025
|
-
children: [
|
|
18057
|
+
children: [
|
|
18058
|
+
/* @__PURE__ */ jsx("title", { children: "Send" }),
|
|
18059
|
+
/* @__PURE__ */ jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }),
|
|
18060
|
+
/* @__PURE__ */ jsx("path", { d: "M22 2 11 13" })
|
|
18061
|
+
]
|
|
18026
18062
|
});
|
|
18027
18063
|
}
|
|
18028
18064
|
function CloseIcon() {
|
|
@@ -18036,17 +18072,28 @@ function CloseIcon() {
|
|
|
18036
18072
|
strokeWidth: "2",
|
|
18037
18073
|
strokeLinecap: "round",
|
|
18038
18074
|
strokeLinejoin: "round",
|
|
18039
|
-
children: [
|
|
18075
|
+
children: [
|
|
18076
|
+
/* @__PURE__ */ jsx("title", { children: "Close" }),
|
|
18077
|
+
/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
|
|
18078
|
+
/* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
|
|
18079
|
+
]
|
|
18040
18080
|
});
|
|
18041
18081
|
}
|
|
18042
|
-
function WidgetPanel({ config: t, session: a, onClose: c }) {
|
|
18043
|
-
let [
|
|
18044
|
-
|
|
18045
|
-
|
|
18046
|
-
|
|
18047
|
-
|
|
18082
|
+
function WidgetPanel({ config: t, session: a, onClose: c, hidden: l }) {
|
|
18083
|
+
let [u, d] = useState(""), m = useRef(null), y = t.branding?.primaryColor || "#171717", b = (t.agentName || "").toLowerCase().includes("chat-agent"), x = useCallback(() => {
|
|
18084
|
+
if (!u.trim()) return;
|
|
18085
|
+
a.sendMessage(u), d("");
|
|
18086
|
+
let t = m.current;
|
|
18087
|
+
t && (t.style.height = "auto");
|
|
18088
|
+
}, [u, a]), S = useCallback((t) => {
|
|
18089
|
+
t.key === "Enter" && !t.shiftKey && (t.preventDefault(), x());
|
|
18090
|
+
}, [x]), C = useCallback(() => {
|
|
18091
|
+
let t = m.current;
|
|
18092
|
+
t && (t.style.height = "auto", t.style.height = `${t.scrollHeight}px`);
|
|
18093
|
+
}, []);
|
|
18048
18094
|
return /* @__PURE__ */ jsxs("div", {
|
|
18049
|
-
className: styles_module_default.panel
|
|
18095
|
+
className: `${styles_module_default.panel} ${l ? styles_module_default.panelHidden : ""}`,
|
|
18096
|
+
"aria-hidden": l,
|
|
18050
18097
|
children: [
|
|
18051
18098
|
/* @__PURE__ */ jsxs("div", {
|
|
18052
18099
|
className: styles_module_default.header,
|
|
@@ -18072,10 +18119,10 @@ function WidgetPanel({ config: t, session: a, onClose: c }) {
|
|
|
18072
18119
|
}),
|
|
18073
18120
|
/* @__PURE__ */ jsx(ChatThread, {
|
|
18074
18121
|
messages: a.messages,
|
|
18075
|
-
primaryColor:
|
|
18122
|
+
primaryColor: y,
|
|
18076
18123
|
status: a.status
|
|
18077
18124
|
}),
|
|
18078
|
-
!
|
|
18125
|
+
!b && /* @__PURE__ */ jsx(VoiceOverlay, {
|
|
18079
18126
|
isVoiceEnabled: a.isVoiceEnabled,
|
|
18080
18127
|
isMicActive: a.isMicActive,
|
|
18081
18128
|
status: a.status,
|
|
@@ -18085,36 +18132,28 @@ function WidgetPanel({ config: t, session: a, onClose: c }) {
|
|
|
18085
18132
|
className: styles_module_default.errorText,
|
|
18086
18133
|
children: a.error
|
|
18087
18134
|
}),
|
|
18088
|
-
|
|
18089
|
-
type: "button",
|
|
18090
|
-
className: styles_module_default.connectButton,
|
|
18091
|
-
style: { backgroundColor: d },
|
|
18092
|
-
onClick: a.connect,
|
|
18093
|
-
children: "Start conversation"
|
|
18094
|
-
}) : a.status === "connecting" ? /* @__PURE__ */ jsx("button", {
|
|
18095
|
-
type: "button",
|
|
18096
|
-
className: styles_module_default.connectButton,
|
|
18097
|
-
style: { backgroundColor: d },
|
|
18098
|
-
disabled: !0,
|
|
18099
|
-
children: "Connecting..."
|
|
18100
|
-
}) : /* @__PURE__ */ jsxs("div", {
|
|
18135
|
+
/* @__PURE__ */ jsxs("div", {
|
|
18101
18136
|
className: styles_module_default.inputBar,
|
|
18102
18137
|
children: [
|
|
18103
|
-
/* @__PURE__ */ jsx("
|
|
18138
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
18139
|
+
ref: m,
|
|
18104
18140
|
className: styles_module_default.inputField,
|
|
18105
|
-
placeholder: "Type a message...",
|
|
18106
|
-
value:
|
|
18107
|
-
onChange: (t) =>
|
|
18108
|
-
|
|
18141
|
+
placeholder: a.status === "connecting" ? "Connecting..." : "Type a message...",
|
|
18142
|
+
value: u,
|
|
18143
|
+
onChange: (t) => d(t.target.value),
|
|
18144
|
+
onInput: C,
|
|
18145
|
+
onKeyDown: S,
|
|
18146
|
+
disabled: a.status === "connecting",
|
|
18147
|
+
rows: 1
|
|
18109
18148
|
}),
|
|
18110
18149
|
/* @__PURE__ */ jsx("button", {
|
|
18111
18150
|
type: "button",
|
|
18112
18151
|
className: styles_module_default.iconButton,
|
|
18113
|
-
onClick:
|
|
18114
|
-
disabled: !
|
|
18152
|
+
onClick: x,
|
|
18153
|
+
disabled: !u.trim() || a.status === "connecting",
|
|
18115
18154
|
children: /* @__PURE__ */ jsx(SendIcon, {})
|
|
18116
18155
|
}),
|
|
18117
|
-
a.isVoiceEnabled && !
|
|
18156
|
+
a.isVoiceEnabled && !b && a.status === "connected" && /* @__PURE__ */ jsx("button", {
|
|
18118
18157
|
type: "button",
|
|
18119
18158
|
className: `${styles_module_default.iconButton} ${a.isMicActive ? styles_module_default.iconButtonActive : ""}`,
|
|
18120
18159
|
onClick: a.toggleMic,
|
|
@@ -18126,7 +18165,7 @@ function WidgetPanel({ config: t, session: a, onClose: c }) {
|
|
|
18126
18165
|
});
|
|
18127
18166
|
}
|
|
18128
18167
|
function ChatIcon() {
|
|
18129
|
-
return /* @__PURE__ */
|
|
18168
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
18130
18169
|
xmlns: "http://www.w3.org/2000/svg",
|
|
18131
18170
|
width: "24",
|
|
18132
18171
|
height: "24",
|
|
@@ -18136,7 +18175,25 @@ function ChatIcon() {
|
|
|
18136
18175
|
strokeWidth: "2",
|
|
18137
18176
|
strokeLinecap: "round",
|
|
18138
18177
|
strokeLinejoin: "round",
|
|
18139
|
-
children: /* @__PURE__ */ jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" })
|
|
18178
|
+
children: [/* @__PURE__ */ jsx("title", { children: "Chat" }), /* @__PURE__ */ jsx("path", { d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" })]
|
|
18179
|
+
});
|
|
18180
|
+
}
|
|
18181
|
+
function FabCloseIcon() {
|
|
18182
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
18183
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
18184
|
+
width: "24",
|
|
18185
|
+
height: "24",
|
|
18186
|
+
viewBox: "0 0 24 24",
|
|
18187
|
+
fill: "none",
|
|
18188
|
+
stroke: "currentColor",
|
|
18189
|
+
strokeWidth: "2",
|
|
18190
|
+
strokeLinecap: "round",
|
|
18191
|
+
strokeLinejoin: "round",
|
|
18192
|
+
children: [
|
|
18193
|
+
/* @__PURE__ */ jsx("title", { children: "Close" }),
|
|
18194
|
+
/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
|
|
18195
|
+
/* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
|
|
18196
|
+
]
|
|
18140
18197
|
});
|
|
18141
18198
|
}
|
|
18142
18199
|
function PolymorphWidget(t) {
|
|
@@ -18151,21 +18208,18 @@ function PolymorphWidget(t) {
|
|
|
18151
18208
|
cssVariablesSelector: ".polymorph-widget",
|
|
18152
18209
|
getRootElement: () => y.current ?? void 0,
|
|
18153
18210
|
children: [
|
|
18154
|
-
|
|
18211
|
+
/* @__PURE__ */ jsx(WidgetPanel, {
|
|
18155
18212
|
config: t,
|
|
18156
18213
|
session: u,
|
|
18157
|
-
onClose: () =>
|
|
18158
|
-
|
|
18159
|
-
}
|
|
18214
|
+
onClose: () => l(!1),
|
|
18215
|
+
hidden: !c
|
|
18160
18216
|
}),
|
|
18161
18217
|
/* @__PURE__ */ jsx("button", {
|
|
18162
18218
|
type: "button",
|
|
18163
|
-
className: styles_module_default.fab
|
|
18219
|
+
className: `${styles_module_default.fab} ${c ? styles_module_default.fabOpen : ""}`,
|
|
18164
18220
|
style: { backgroundColor: d },
|
|
18165
|
-
onClick: () =>
|
|
18166
|
-
|
|
18167
|
-
},
|
|
18168
|
-
children: /* @__PURE__ */ jsx(ChatIcon, {})
|
|
18221
|
+
onClick: () => l((t) => !t),
|
|
18222
|
+
children: jsx(c ? FabCloseIcon : ChatIcon, {})
|
|
18169
18223
|
}),
|
|
18170
18224
|
u.roomConnection && /* @__PURE__ */ jsx(W, {
|
|
18171
18225
|
token: u.roomConnection.token,
|
package/package.json
CHANGED
package/src/ChatThread.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
2
|
import styles from "./styles.module.css";
|
|
3
3
|
import type { ChatMessage, SessionStatus } from "./types";
|
|
4
4
|
|
|
@@ -12,8 +12,19 @@ export function ChatThread({
|
|
|
12
12
|
status: SessionStatus;
|
|
13
13
|
}) {
|
|
14
14
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
const isNearBottom = useRef(true);
|
|
16
|
+
const prevItemCount = useRef(0);
|
|
15
17
|
const agentCountAtConnect = useRef<number | null>(null);
|
|
16
18
|
const agentMessageCount = messages.filter((m) => m.role === "agent").length;
|
|
19
|
+
const lastMessageIsUser =
|
|
20
|
+
messages.length > 0 && messages[messages.length - 1].role === "user";
|
|
21
|
+
|
|
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
|
+
}, []);
|
|
17
28
|
|
|
18
29
|
// Snapshot agent message count when connecting starts
|
|
19
30
|
if (status === "connecting" && agentCountAtConnect.current === null) {
|
|
@@ -33,15 +44,24 @@ export function ChatThread({
|
|
|
33
44
|
messages[messages.length - 1].role === "user";
|
|
34
45
|
const showThinking = initialConnectThinking || waitingForAgentReply;
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
42
62
|
|
|
43
63
|
return (
|
|
44
|
-
<div ref={scrollRef} className={styles.chatThread}>
|
|
64
|
+
<div ref={scrollRef} className={styles.chatThread} onScroll={handleScroll}>
|
|
45
65
|
{messages.map((msg) => (
|
|
46
66
|
<div
|
|
47
67
|
key={msg.id}
|
package/src/PolymorphWidget.tsx
CHANGED
|
@@ -9,7 +9,6 @@ import type { WidgetConfig } from "./types";
|
|
|
9
9
|
import { usePolymorphSession } from "./usePolymorphSession";
|
|
10
10
|
import { WidgetPanel } from "./WidgetPanel";
|
|
11
11
|
|
|
12
|
-
// Chat bubble icon for FAB
|
|
13
12
|
function ChatIcon() {
|
|
14
13
|
return (
|
|
15
14
|
<svg
|
|
@@ -23,11 +22,32 @@ function ChatIcon() {
|
|
|
23
22
|
strokeLinecap="round"
|
|
24
23
|
strokeLinejoin="round"
|
|
25
24
|
>
|
|
25
|
+
<title>Chat</title>
|
|
26
26
|
<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" />
|
|
27
27
|
</svg>
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
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
|
+
}
|
|
50
|
+
|
|
31
51
|
export function PolymorphWidget(props: WidgetConfig) {
|
|
32
52
|
const { position = "bottom-right", branding } = props;
|
|
33
53
|
const [open, setOpen] = useState(false);
|
|
@@ -48,30 +68,20 @@ export function PolymorphWidget(props: WidgetConfig) {
|
|
|
48
68
|
cssVariablesSelector=".polymorph-widget"
|
|
49
69
|
getRootElement={() => rootRef.current ?? undefined}
|
|
50
70
|
>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
setOpen(false);
|
|
58
|
-
}}
|
|
59
|
-
/>
|
|
60
|
-
)}
|
|
71
|
+
<WidgetPanel
|
|
72
|
+
config={props}
|
|
73
|
+
session={session}
|
|
74
|
+
onClose={() => setOpen(false)}
|
|
75
|
+
hidden={!open}
|
|
76
|
+
/>
|
|
61
77
|
<button
|
|
62
78
|
type="button"
|
|
63
|
-
className={styles.fab}
|
|
79
|
+
className={`${styles.fab} ${open ? styles.fabOpen : ""}`}
|
|
64
80
|
style={{ backgroundColor: primaryColor }}
|
|
65
|
-
onClick={() =>
|
|
66
|
-
if (open) {
|
|
67
|
-
session.disconnect();
|
|
68
|
-
}
|
|
69
|
-
setOpen((prev) => !prev);
|
|
70
|
-
}}
|
|
81
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
71
82
|
>
|
|
72
|
-
<ChatIcon />
|
|
83
|
+
{open ? <FabCloseIcon /> : <ChatIcon />}
|
|
73
84
|
</button>
|
|
74
|
-
{/* LiveKit room lives outside WidgetPanel so it mounts before the panel renders */}
|
|
75
85
|
{session.roomConnection && (
|
|
76
86
|
<LiveKitRoom
|
|
77
87
|
token={session.roomConnection.token}
|