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.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: "_widgetRoot_e2sak_1",
17684
- bottomRight: "_bottomRight_e2sak_12",
17685
- bottomLeft: "_bottomLeft_e2sak_18",
17686
- fab: "_fab_e2sak_25",
17687
- panel: "_panel_e2sak_47",
17688
- header: "_header_e2sak_62",
17689
- chatThread: "_chatThread_e2sak_70",
17690
- messageBubble: "_messageBubble_e2sak_81",
17691
- agentMessage: "_agentMessage_e2sak_90",
17692
- userMessage: "_userMessage_e2sak_96",
17693
- voiceLabel: "_voiceLabel_e2sak_101",
17694
- voiceOverlay: "_voiceOverlay_e2sak_107",
17695
- voiceBars: "_voiceBars_e2sak_117",
17696
- voiceBar: "_voiceBar_e2sak_117",
17697
- voiceToggle: "_voiceToggle_e2sak_154",
17698
- voiceToggleActive: "_voiceToggleActive_e2sak_173",
17699
- inputBar: "_inputBar_e2sak_183",
17700
- inputField: "_inputField_e2sak_191",
17701
- iconButton: "_iconButton_e2sak_207",
17702
- iconButtonActive: "_iconButtonActive_e2sak_232",
17703
- connectButton: "_connectButton_e2sak_236",
17704
- statusBadge: "_statusBadge_e2sak_257",
17705
- errorText: "_errorText_e2sak_264",
17706
- thinkingDots: "_thinkingDots_e2sak_270",
17707
- thinkingDot: "_thinkingDot_e2sak_270"
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), [v, y] = useState([]), [b, x] = useState(c), [S, C] = useState(c), [w, E] = useState(null), O = useRef(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
- x(c), C(c), O.current && O.current.localParticipant.setMicrophoneEnabled(c);
17770
+ S(c), w(c), j.current && j.current.localParticipant.setMicrophoneEnabled(c);
17763
17771
  }, [c]);
17764
- let j = useCallback((t, a, s) => {
17765
- y((c) => [...c, {
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: v,
17777
- isVoiceEnabled: b,
17778
- isMicActive: S,
17779
- error: w,
17780
- connect: useCallback(async () => {
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
- O.current?.disconnect(), O.current = null, g(null), d("idle");
17835
+ j.current?.disconnect(), j.current = null, g(null), d("idle");
17823
17836
  }, []),
17824
- addMessage: j,
17837
+ addMessage: V,
17825
17838
  sendMessage: useCallback((t) => {
17826
- let a = O.current;
17827
- if (!a || !t.trim()) return;
17828
- j("user", t.trim(), "chat");
17829
- let s = new TextEncoder().encode(JSON.stringify({ text: t.trim() }));
17830
- a.localParticipant.publishData(s, {
17831
- reliable: !0,
17832
- topic: "chat_message"
17833
- });
17834
- }, [j]),
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 = O.current;
17852
+ let t = j.current;
17837
17853
  if (!t) return;
17838
- let a = !S;
17839
- await t.localParticipant.setMicrophoneEnabled(a), C(a);
17840
- }, [S]),
17854
+ let a = !C;
17855
+ await t.localParticipant.setMicrophoneEnabled(a), w(a);
17856
+ }, [C]),
17841
17857
  toggleVoice: useCallback(async () => {
17842
- let t = O.current;
17858
+ let t = j.current;
17843
17859
  if (!t) return;
17844
- let a = !b;
17845
- await t.localParticipant.setMicrophoneEnabled(a), x(a), C(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
- }, [b]),
17867
+ }, [x]),
17852
17868
  setRoom: useCallback((t) => {
17853
- O.current = t, t && d("connected");
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: s }) {
17858
- let c = useRef(null), u = useRef(null), d = t.filter((t) => t.role === "agent").length;
17859
- s === "connecting" && u.current === null && (u.current = d), (s === "idle" || s === "error") && (u.current = null);
17860
- let p = (s === "connecting" || s === "connected") && u.current !== null && d <= u.current, m = s === "connected" && t.length > 0 && t[t.length - 1].role === "user", y = p || m;
17861
- return useEffect(() => {
17862
- c.current?.scrollTo({
17863
- top: c.current.scrollHeight,
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
- }, []), /* @__PURE__ */ jsxs("div", {
17867
- ref: c,
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)), y && /* @__PURE__ */ jsx("div", {
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: [/* @__PURE__ */ jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), /* @__PURE__ */ jsx("path", { d: "M22 2 11 13" })]
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: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
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 [l, u] = useState(""), d = t.branding?.primaryColor || "#171717", f = (t.agentName || "").toLowerCase().includes("chat-agent"), m = useCallback(() => {
18044
- l.trim() && (a.sendMessage(l), u(""));
18045
- }, [l, a]), y = useCallback((t) => {
18046
- t.key === "Enter" && !t.shiftKey && (t.preventDefault(), m());
18047
- }, [m]);
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: d,
18122
+ primaryColor: y,
18076
18123
  status: a.status
18077
18124
  }),
18078
- !f && /* @__PURE__ */ jsx(VoiceOverlay, {
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
- a.status === "idle" || a.status === "error" ? /* @__PURE__ */ jsx("button", {
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("input", {
18138
+ /* @__PURE__ */ jsx("textarea", {
18139
+ ref: m,
18104
18140
  className: styles_module_default.inputField,
18105
- placeholder: "Type a message...",
18106
- value: l,
18107
- onChange: (t) => u(t.target.value),
18108
- onKeyDown: y
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: m,
18114
- disabled: !l.trim(),
18152
+ onClick: x,
18153
+ disabled: !u.trim() || a.status === "connecting",
18115
18154
  children: /* @__PURE__ */ jsx(SendIcon, {})
18116
18155
  }),
18117
- a.isVoiceEnabled && !f && /* @__PURE__ */ jsx("button", {
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__ */ jsx("svg", {
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
- c && /* @__PURE__ */ jsx(WidgetPanel, {
18211
+ /* @__PURE__ */ jsx(WidgetPanel, {
18155
18212
  config: t,
18156
18213
  session: u,
18157
- onClose: () => {
18158
- u.disconnect(), l(!1);
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
- c && u.disconnect(), l((t) => !t);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polymorph-sdk",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "src/index.ts",
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef } from "react";
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
- useEffect(() => {
37
- scrollRef.current?.scrollTo({
38
- top: scrollRef.current.scrollHeight,
39
- behavior: "smooth",
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}
@@ -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
- {open && (
52
- <WidgetPanel
53
- config={props}
54
- session={session}
55
- onClose={() => {
56
- session.disconnect();
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}